深入理解this

Posted by Rimin on 2019-04-16

个人认为要行先理解了闭包,理解了js函数执行机制,作用域链之后再来理解this的指向更容易。判断es5的this指向应该按这三步:

  1. 创建时 scope
  2. 执行时 (作用域链,上下文 )
  3. 是否被显式,隐式改变

this 是一个指针,一般指向其所在函数的执行环境(作用域链)的第二个环境对象(或者说是指向[[scope]]的第一个环境对象)(作用域链:执行环境(作用域链)= 函数内部环境 + 定义环境[[scope]])(和函数作用域链不同的是,函数是按作用域链层层查找的(从里到外哪里有就返回哪里的),而this指向是唯一的。)

所以,this并不指向自身,也不一定指向包含它的外部函数。

可以先看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
function baz() {
console.log("baz"+this);
bar(); // <-- bar 的调用位置
}
function bar() {
console.log("bar"+this);
foo(); // <-- foo 的调用位置
}
function foo() {
console.log("foo"+this);
}
baz(); // <-- baz 的调用位置

运行结果:

1
2
3
baz[object Window]
bar[object Window]
foo[object Window]

我们知道,函数是词法作用域,而不是动态作用域,所以不管在哪里调用,只要this没有发生隐式或显式更改,就按其声明时的scope。

再看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 why?

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码
this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相
同。这里的this指向window.

进入正文:

this绑定规则

  1. 默认绑定:函数声明的位置
1
2
3
4
5
6
var a = 2;
!function(){
// 'use strict' Cannot read property 'a' of undefined
console.log(this.a); //2
默认绑定时,this指向全局对象。 console.log(this === window); //true
}()

默认绑定时,this指向全局对象。

注意:严格模式下(‘use strict’)this会默认绑定为undefined

  1. 隐式绑定:调用位置是否有上下文对象,或是否被某个对象拥有
1
2
3
4
5
6
7
8
9
function foo(){
console.log(this.a);
}
var obj = {
a:1,
b:2,
foo:foo
}
obj.foo();

等同于foo.apply(obj),foo被调用时,函数引用有上下文对象,隐式绑定规则会把函数调用中的this绑定到这个对象上。

但是有时候会出现隐式丢失:

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.a);
}
var obj = {
a:1,
b:2,
foo:foo
}
var goo = obj.foo; // 这里相当于指针, 被当做普通函数赋值了
goo(); // undefined

又比如

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global" // obj.foo 被隐式赋值给参数了

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

再比如:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
x:10,
fn:function(){
function f(){
console.log(this);//window
console.log(this.x);//undefined
} f();
}
};

obj.fn();

这里不是像上面那两个例子一样独立出去也不是传参,而是执行obj里面的立即执行函数,虽然fn是被绑定到obj上下文中的,但是f不是,。this没有按预想的绑定到外层函数对象上,而是绑定到了全局对象。这里普遍被认为是JavaScript语言的设计错误,因为没有人想让内部函数中的this指向全局对象。

还有一种常见的丢失

1
2
3
4
5
6
7
8
9
function foo() {
console.log( this.a );

var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined

  1. 显式绑定

(1)利用call 和 apply, bind方法

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

引申知识:call 和 apply, bind的区别以及如何手写实现。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(sth) {
console.log(this.a + sth);
return this.a + sth;
}
function connect(fn,obj) {
return function () {
return fn.apply(obj,arguments); //这里传入的arguments形成了闭包
}
}
var obj = {
a:2
}
var bar = connect(foo,obj);
let b = bar(3);
console.log(b); //5

new绑定

首先看 new一个对象的过程发生了什么:

  1. 创建(或者说构造)一个全新的对象。
1
instance = new Object();
  1. 进行原型链的链接
1
instance.__proto__ = SubType.prototype;
  1. 新对象绑定到函数调用的this,再让 SubType中的this指向instance,执行SubType的函数体内的语句(上面语句的内容就是定义两个属性并给其赋值)。

  2. 若没有return,默认返回该新对象
    这就是new 操作符的工作

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

因此,在这个过程中,就会有this指向的改变

四种绑定的优先级

new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

验证:

  • 隐式绑定和显式绑定:
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
12
13
14
15
16
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );

console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

(这个例子需要用心观察 )

  • new绑定和显实绑定:
1
2
3
4
5
6
7
8
9
10
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 ); //或者foo.call(obj1,2);
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a
修改为 3。相反,new 修改了硬绑定(到 obj1的)调用 bar(..) 中的 this。因为使用了
new 绑定,我们得到了一个名字为 baz 的新对象,并且baz.a 的值是 3

因此: 对于this的判断

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
    var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是
    指定的对象。
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上
    下文对象。
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到
    全局对象。
    var bar = foo()

ES6箭头函数的this

箭头函数的 this 的指向是不能被更改的。箭头函数完全修复了this的指向,即es6之前,this总是指向词法作用域,也就是外层调用者obj,如果使用箭头函数,以前的hack写法 var that = this就不再需要了。

例如:

1
2
3
4
5
6
7
8
9
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth;
return fn.call({birth:2000}, year); // this.birth仍是1990
}
};
obj.getAge(2015); // 25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,
bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不
行!)

还有一个之前es5被认为设计错误的例子在es6 箭头函数这里也得到了修正:

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1
var obj = {
x: 'obj',
a: function a(){
var x = 'a'
var b = ()=>{ // 注意这里
var x = 'b'
return this.x
}
return b()
}
}
obj.a() // 'obj'

箭头函数的 this 是没有办法被更改的。 因为无论显示更改还是隐式更改,都只能更改函数自身的 this,而箭头函数的 this 不是自己的,而是像普通变量一样从外部引进来的。

最后,欢迎大家围观指正