viviier

你不知道的JavaScript this篇

调用位置

调用位置就是函数在代码中被调用的位置,而找到调用位置之前,我们还要知道调用栈(就是为了达到当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function baz() {
// 当前调用栈: baz
// baz的调用位置在全局, 因此当前调用位置是全局作用域
console.log('baz');
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈: baz -> bar
// 因此,调用位置在 baz 中
console.log('baz');
foo(); // <-- foo的调用位置
}
function foo() {
// 当前的调用栈: baz -> bar -> foo
// 因此,调用位置在bar
console.log('foo');
}
baz(); // <-- baz的调用位置

绑定规则

找到调用位置,然后判断需要应用下面四条规则中的哪一条。

默认绑定

独立函数调用,不带任何修饰的函数引用进行调用。

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

如果使用严格模式,则不能将全局对象用作默认绑定,因此this会指向 undefined

隐式绑定

当函数引用有上下文时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。对象属性引用链中只有上一层在调用位置起作用。

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

隐式丢失

隐式使用时会遇到 隐式丢失

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo; // 这里虽然使用了obj.foo 其实这只是foo的函数别名 实际上等于 => var bar = foo
var a = 'oops, global';
bar(); // 其实只是单纯的调用了foo() => window => foo 2

隐式丢失还会发生在传入回调函数的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops, global';
doFoo(obj.foo); // 这里的obj.foo也是foo的函数别名,相当于传入了doFoo(foo)执行,也就是执行了window => foo 'oops, global'

所以一定要小心别名陷阱,类似于 setTimeout(obj.foo, 10) ,里面的obj.foo参数也是一个别名陷阱,会导致隐式丢失

显示绑定

使用javascript的 call(…)apply(…) 方法,直接指定this的绑定对象,因此我们称之为显示绑定。

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

显示绑定无法解决丢失绑定问题,但是他的变种硬绑定可以。

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

硬绑定的bar不可能再修改他的this,强制把foo的this绑定给了obj,因此无论之后怎么调用bar,他总会手动的在obj上调用foo。
还可以把他创建成一个可重复使用的辅助函数:

1
2
3
4
5
6
7
8
9
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
}
}
var bar = bind(foo, obj);
var b = bar(3);
console.log(b); // 4

注*: ES5内置了bind方法,foo.bind(obj)

new绑定

首先我们要知道使用new来调用函数(发生构造函数调用)的时候,会执行哪些操作:

  • 创建一个全新的对象
  • 这个新对象会被执行[[Prototype]]连接
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

在new的时候,会在内存的某处创建一个新的对象,然后这个新对象的prototype连接到他的constructor,同时也会把他constructor上面的方法、this绑定给自己。

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

优先级

  • 显示绑定 > 隐式绑定
  • new绑定 > 隐式绑定
  • new其实是创建了一个新的对象
  • 默认绑定优先级最低

判断this

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

小结

如果要判断一个运行函数的this绑定,首先找到这个函数的调用位置,然后通过四条规则来判断this的绑定对象。

  1. 由new调用?绑定到新创建的对象
  2. 由call或者apply(或者bind)?绑定到指定的对象
  3. 由上下文调用?绑定到上下文对象
  4. 默认: 在严格模式下绑定到undefined,否则绑定到全局对象
------本文结束感谢阅读------