JavaScript中对this的进一步理解

之前在学习this的时候,总是通过死记硬背来区分各种情况下this的指向,最近在看了 《You Dont Know JS》 后,对其有了进一步的理解

Call-site 和 Call stack

即调用点和调用栈。二者并不是一个东西,有一些时机上的差别。
调用栈 CallStack 指的是一个记录到当前执行函数为止,记录其顺序的堆栈。
调用点 Call-site 指的是当前执行函数之前的调用。

举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
function baz() {
// 调用栈是: `baz`
// 调用点是 global scope(全局作用域)
console.log( "baz" );
bar(); // <-- `bar` 的调用点
}

function bar() {
// 调用栈是: `baz` -> `bar`
// 我们的调用点位于 `baz`
console.log( "bar" );
foo(); // <-- `foo` 的 call-site
}

调用点是影响this绑定的唯一因素

默认绑定

当我们在全局作用域下直接执行函数的时候,this 会进行默认绑定。在非严格模式下,绑定的对象是全局对象,在严格模式下绑定的对象是 undefined
但是需要注意一个细节,即:如果函数的调用点环境是严格模式,即使函数内部没有声明严格模式,全局对象也是唯一合法的。

严格模式下的细节
1
2
3
4
5
6
7
8
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

有时你可能会引用与你的 Strict 模式不同的第三方包,所以对这些微妙的兼容性细节要多加小心

隐含绑定

当函数被声明作为引用属性添加到obj上时,并不代表这个对象真正拥有或包含这个函数,但调用点通过obj环境来引用函数,所以可以说obj对象在函数被调用的事件点上拥有或包含这个函数引用。

当一个方法引用存在一个环境对象时,隐含绑定 规则会说:是这个对象应当被用于这个函数调用的 this 绑定。因为 obj 是 foo() 调用的 this,所以 this.a 就是 obj.a 的同义词,同时,只有对象属性引用链的最后一层是影响调用点的

隐式绑定
1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隐含丢失

下面的代码会造成绑定的丢失
1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数引用!
var a = "oops, global"; // `a` 也是一个全局对象的属性
bar(); // "oops, global"

尽管 bar 似乎是 obj.foo 的引用,但实际上它只是另一个 foo 本身的引用而已。起到绑定作用的是 默认绑定。
上面的情况在我们将函数当作参数传递时,也会发生。因为传递函数的时候,是一个隐含的引用赋值。

明确绑定

明确绑定指的是通过 call、apply 调用函数。二者除了参数格式不同,没什么太大区别,都允许我们传入this要绑定的对象。

如果传递一个简单基本类型值(string,boolean,或 number 类型)作为 this 绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..),new Boolean(..),或 new Number(..))。这通常称为“封箱(boxing)”。

硬绑定

单独使用 明确绑定 无法解决三方框架覆盖this、丢失自己原本的this绑定等问题。但是有一个 明确绑定 的变种确实可以实现这个技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2

用 硬绑定 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道,通过 apply 绑定this,并将 arguments 参数列表返回。

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 = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

也可以实现bind函数

简单的bind函数
1
2
3
4
5
6
// 简单的 `bind` 帮助函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}

在 ES6 中,bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)

new 绑定 this

需要明确一点,JavaScript通过new运算符操作的函数并不是严格意义上的构造器,它仅仅是一个函数。可以说是我们通过构造器的方法去调用了这个函数,改变了函数的行为。例如Number()函数,在直接调用和new时会有不同的行为

当在函数前面被加入 new 调用时,也就是构造器调用时,下面这些事情会自动完成:

  • 一个全新的对象会凭空创建(就是被构建)
  • 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  • 这个新构建的对象被设置为函数调用的 this 绑定
  • 除非函数返回一个它自己的其他 对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象。

上述几种规则的优先顺序

当调用点同时满足上述几种规则时,会有如下优先级

new > 硬绑定(明确绑定) > 隐含绑定 > 默认绑定

当然我们无法同时使用 new 和 call/apply ,但是我们能够通过硬绑定去测试二者的优先级

通过硬绑定比较优先级
1
2
3
4
5
6
7
8
9
10
11
12
13
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

可以看到,new覆盖了硬绑定的this

一些特列

null 和 undefined

当传递 null 或 undefined 作为 call、apply 或 bind 的 this 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用

间接

当创建函数引用时,默认绑定规则也会适用

赋值
1
2
3
4
5
6
7
8
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的 结果值 是一个刚好指向底层函数对象的引用

箭头函数

箭头函数从封闭它的(函数或全局)作用域采用 this 绑定。一个箭头函数的词法绑定是不能被覆盖的(就连 new 也不行!)

作者

徐云飞

发布于

2022-09-05

更新于

2023-02-05

许可协议

评论