JS中的this详解

  • this是使用call方法调用函数时传递的第一个参数,它可以在函数调用时修改,在函数没有调用的时候,this的值是无法确定。
  • this是在运行的时候绑定的,不是在编写的时候绑定的,函数调用的方式不同,就可能使this`所绑定的对象不同。

快速入门

1. 纯粹的函数调用

第一种方法最常见,例子如下:

1
2
3
4
5
function test(name) {
console.log(name)
console.log(this)
}
test('Jerry') //调用函数

这种方法我们使用最多,但是这种函数调用方法只是一种简写,它完整的写法是下面这样的:

1
2
3
4
5
function test(name) {
console.log(name)
console.log(this)
}
test.call(undefined, 'Tom')

注意到上面调用函数的call方法了吗?call方法接收的第一个参数就是this,这里我们传了一个undefined。那么,依据定义,函数执行了之后打出来的this会是undefined吗?也不是。

如果你传的 context 就 null 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)。

所以这里我们打出来的this是Window对象。

2. 对象中函数的调用

直接看例子:

1
2
3
4
5
6
7
8
const obj = {
name: 'Jerry',
greet: function() {
console.log(this.name)
}
}
obj.greet() //第一种调用方法
obj.greet.call(obj) //第二种调用方法

例子里第一种调用方法只是第二种调用方法的语法糖,第二种才是完整的调用方法,而且第二种方法厉害的地方在于它可以手动指定this

手动指定this的例子:

1
2
3
4
5
6
7
const obj = {
name: 'Jerry',
greet: function() {
console.log(this.name)
}
}
obj.greet.call({name: 'Spike'}) //打出来的是 Spike

从上面的例子我们看到greet函数执行时this,已经被我们改过了。

3. 构造函数中this

构造函数里的this稍微有点特殊,每个构造函数在new之后都会返回一个对象,这个对象就是this,也就是context上下文。

例子:

1
2
3
4
5
6
function Test() {
this.name = 'Tom'
}
let p = new Test()
console.log(typeof p) //object
console.log(p.name) // Tom

4. window.setTimeout()和window.setInterval()中函数的调用

window.setTimeout()和window.setInterval()的函数中的this有些特殊,里面的this默认是window对象。

总结

  • 函数完整的调用方法是使用call方法,包括test.call(context, name)obj.greet.call(context,name),这里的context就是函数调用时的上下文,也就是this,只不过这个this是可以通过call方法来修改的;
  • 构造函数稍微特殊一点,它的this直接指向new之后返回的对象;
  • window.setTimeout()window.setInterval()默认的是this是window对象。
调用方式 this指向
普通函数调用 window
构造函数调用 实例对象,原型对象里面的方法也指向实例对象
对象方式调用 该方法所属对象
事件绑定方法 绑定时间对象
定时器函数 window
立即执行函数 window

箭头函数中的this

1. 箭头函数的特性一:this是继承自父执行上下文中的this

  • 上面提到:this的值是可以用call方法修改的,而且只有在调用的时候我们才能确定this的值
  • 而当我们使用箭头函数的时候,箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的。

不使用箭头函数例子:

1
2
3
4
const obj = {
a: function() { console.log(this) }
}
obj.a() //打出的是obj对象

使用箭头函数的例子:

1
2
3
4
5
6
const obj = {
a: () => {
console.log(this)
}
}
obj.a() //打出来的是window

在使用箭头函数的例子里,因为箭头函数默认不会使用自己的this,而是会和外层的this保持一致,最外层的this就是window对象。

箭头函数本身与a平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,因此这里的this实际上表示的是window。

2. 箭头函数的特性二:不能用call方法修改里面的this

这个也很好理解,我们之前一直在说,函数的this可以用call方法来手动指定,而为了减少this的复杂性,箭头函数无法用call方法来指定this。

1
2
3
4
5
6
const obj = {
a: () => {
console.log(this)
}
}
obj.a.call('123') //打出来的结果依然是window对象

因为上文我们说到window.setTimeout()中函数里的this默认是window,我们也可以通过箭头函数使它的this和外层的this保持一致:

window.setTimeout()的例子:

1
2
3
4
5
6
7
8
9
const obj = {
a: function() {
console.log(this)
window.setTimeout(() => {
console.log(this)
}, 1000)
}
}
obj.a.call(obj) //第一个this是obj对象,第二个this还是obj对象

想必大家明白了,函数obj.a没有使用箭头函数,因为它的this还是obj,而setTimeout里的函数使用了箭头函数,所以它会和外层的this保持一致,也是obj;如果setTimeout里的函数没有使用箭头函数,那么它打出来的应该是window对象。

3. 多层对象嵌套里箭头函数的this

箭头函数里的this是和外层保持一致的,但是如果这个外层有好多层,那它是和哪层保持一致呢?

1
2
3
4
5
6
7
8
const obj = {
a: function() { console.log(this) },
b: {
c: function() {console.log(this)}
}
}
obj.a() // 打出的是obj对象, 相当于obj.a.call(obj)
obj.b.c() //打出的是obj.b对象, 相当于obj.b.c.call(obj.b)

上面的代码都符合直觉,接下来把obj.b.c对应的函数换成箭头函数,结果如下:

1
2
3
4
5
6
7
8
const obj = {
a: function() { console.log(this) },
b: {
c: () => {console.log(this)}
}
}
obj.a() //没有使用箭头函数打出的是obj
obj.b.c() //打出的是window对象!!

obj.a调用后打出来的是obj对象,而obj.b.c调用后打出的是window对象而非obj,这表示多层对象嵌套里箭头函数里this是和最最外层保持一致的。

详细说明

1.几种绑定规则

函数调用的位置对this的指向有着很大的影响,但却不是完全取决于它。下面是几种this的绑定规则:

1.1.默认绑定

默认规则的意思就是在一般情况下,如果没有别的规则出现,就this绑定到全局对象上,我们看如下代码:

1
2
3
4
5
6
function foo() {
var a = 3;
console.log( this.a );
}
var a = 2;
foo(); //2

这段代码中,this是被默认绑定到了全局对象上,所以this.a得到的是2。我们如何判断这里应用了默认绑定呢?

foo在调用的时候直接使用不带任何修饰的函数引用,只能使用默认绑定。有人会误认为结果是3this常有的几种错误理解之一就是认为this指向当前函数的词法作用域,this与词法作用域以及作用域对象是完全不同的东西,作用域对象是在引擎内部的,js代码是无法访问的。还有本文我们不讨论严格模式下的情况,严格模式这里的this会绑定到undefined

1.2.隐式绑定

如果在调用位置有上下文对象,说简单点就是这个函数调用时是用一个对象.出来的。就像下边这样,它就遵循隐式绑定:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var a = "opps, gloabl"; //全局对象的属性
obj.foo(); //2
var bar = obj.foo;
bar(); //"opps, gloabl"
  • 9行代码,就是函数在调用的时候,是用前边的对象加上.操作符调用出来的,此时就用到了隐式绑定规则,隐式绑定规则会将函数调用中的this绑定到这个上下文对象,此时的this.aobj.a是一样的。
  • 而隐式绑定会出现一个问题,就是隐式丢失,上边的第10行代码,是新建一个foo函数的引用,即bar,在最后一行调用的时候,这个函数不具有上下文对象,此时采用默认绑定规则,得到的结果自然是opps, gloabl;

绑定丢失也会发生在函数作为参数传递的情况下,即传入回调函数时,因为参数传递就是一种隐式赋值,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn(); //在此处调用,参数传递是隐式赋值,丢失this绑定
}
var obj = {
a: 2,
foo: foo
};
var a = "opps, global";
doFoo( obj.foo ); //看似是隐式绑定,输出opps, global

javascript环境中内置的函数,在具有接受一个函数作为参数的功能的时候,也会发生像上边这种状况。例如setTimeout函数的实现就类似于下边的伪代码:

1
2
3
4
function setTimeout(fn, delay) {
//等待delay毫秒
fn();//在此处调用
}

所以回调函数丢失this绑定是非常常见的,后边我们再看如何通过固定this来修复这个问题。

1.3.显式绑定

在此之前,相信你已经用过很多次applycall函数了,使用这两个函数可以直接**为你要执行的函数指定this**,所以这种方式称为显式绑定。

1
2
3
4
5
6
7
function foo() {   //显式绑定this,这种方式依然无法解决绑定丢失的问题
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); //2

通过像上边这样调用,我们可以foothis强制绑定到obj上。如果给call传入的是一个基本类型数据,这个基本类型数据将会被转换成对应的基本包装类型。不过这种方式依然无法解决上边的丢失绑定问题。

1.3.1.硬绑定

为了解决这个问题,我们可以写像下边这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() { //创建一个包裹函数,以确保obj的绑定
foo.call( obj );
};
bar(); //2
setTimeout( bar, 100 ); //2
bar.call( window ); //2

上边这样的函数确实解决了绑定丢失的问题,每次调用bar就可以确保obj的绑定,不过还不能为函数传参,而且这种方法复用率低,所以又出现了这样的辅助绑定函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
function bind(fn, obj) { //辅助绑定函数
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a: 2
};
var bar = bind( foo, obj );
var b = bar(3);
console.log(b);

因为这种模式很常用,所以ES5内置了这个方法,就是bindbind(...)返回一个硬编码的新函数,将你指定的对象绑定到调用它的函数的this上。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj ); //bind返回一个绑定到obj上的新函数
var b = bar(3);
console.log(b);
var a = "window's a";
foo("!"); //对原来的函数不产生影响
1.3.2.API调用参数指定this

许多第三方库里的函数,以及许多语言内置的函数,都提供了一个可选的参数用来指定函数执行的this

1
2
3
4
5
6
7
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
[1, 2, 3].forEach( foo, obj ); //forEach的第二个参数就是用来设置this

1.4.new绑定

js中的所谓的构造函数,其实和一般的普通函数没有什么区别,并不具有特殊性,它们只是被new操作符调用的普通函数而已。实际上并不存在什么构造函数,只存在对于函数的构造调用

发生构造函数的调用时,会自动执行下边的操作:

  1. 创建一个全新的对象。
  2. 这个对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this
  4. 执行这个函数里的代码。
  5. 如果函数没有返回其他对象,则自动返回这个新对象。

这个在执行new操作的时候对this的绑定就叫做new绑定。

1
2
3
4
5
6
function fun() {
this.a = 1;
this.b = 2;
}
var instance = new fun();
console.log(instance.a);

1.5.箭头函数的this

ES6中的箭头函数是无法使用以上几种规则的,它是根据外层的作用域来决定this,即取决于外层的函数作用域或全局作用域,而且箭头函数的绑定无法修改,即使是new绑定也不可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
return (a) => {
console.log( this.a );
}
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.apply(obj1);
bar.apply(obj2); //2

2.绑定规则的优先级

前边我们已经说了this的几种绑定规则,当函数调用的位置可以使用多条绑定规则的时候,我们就需要确定这几种规则的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
}
obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call( obj2 ); //3
obj2.foo.call( obj1 ); //2

从上边的代码可以看出来,显式绑定的优先级要高于隐式绑定,下边再看看显式绑定和new绑定的优先级:

1
2
3
4
5
6
7
8
9
10
11
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar(2);
console.log( obj1.a ); //2

var baz = new bar(3);
console.log( obj1.a ); //2
console.log( baz.a ); //3

仔细看这段代码,barfoo绑定到obj1上返回的一个函数,对这个函数进行new操作,并传入新的a值,发现改变的是新对象baz的属性,和obj1已经脱离关系。说明new绑定的优先级高于硬绑定。

综上所述,我们在遇到this时,如果不是箭头函数,就可以以这种顺序判断它的指向:

  1. 如果函数在new中调用,绑定到新建的对象。
  2. 函数通过callapply或者硬绑定调用,this绑定到指定的对象上。
  3. 函数在某个上下文对象中调用,绑定到这个上下文对象上。
  4. 采用默认绑定规则。

3. 严格模式

  • 严格模式下,全局作用域的function中的this是undefined
  • 严格模式下,定时器函数的this还是指向window
1
2
3
4
5
6
7
8
// 因为一般函数fn绑定的this是window,一旦调用fn,sex就赋值给window
function fn() {
this.sex = "male"
}

fn();
console.log(sex) // male
console.log(window.sex) // male

严格模式:

1
2
3
4
5
6
7
8
9
10
'use strict'

function fn() {
this.sex = "male"
}

// error
// 因为严格模式下,全局作用域的function中的this是undefined,
// 给undefined赋值sex变量,自然报错
fn();
1
2
3
4
5
'use strict'

setTimeout(function() {
console.log(this) // window
},2000)