你不知道的JavaScript--this解析

前言

thisJavaScript里面老生常谈的问题,因为其诡异的机制,基本每个初学者都会迷惑于this的指向问题,而this又是面向对象编程的基础,所以弄明白this的指向是提高自己编程能力的重要一环。《你不知道的JavaScript》上册第二部分详细讲解了this的几种指向问题,如果你还不懂this的指向,下面的文章也许能帮你解决这个难题。

this解析

调用位置

  • 什么是调用位置?

    调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
    要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。

什么是调用栈和调用位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
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 = 2;
foo(); // 2
  • 声明在全局作用域中的变量(比如 var a = 2 )就是全局对象的一个同名属性。等价于window.a = 2
  • 它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。

当调用 foo() 时, this.a 被解析成了全局变量 a 。为什么?因为在本例中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。

那么我们怎么知道这里应用了默认绑定呢?

可以通过分析调用位置来看看 foo() 是如何调用的。在代码中, foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

严格模式下,this会绑定到undefined

1
2
3
4
5
6
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

注意一个细节

虽然 this 的绑定规则完全取决于调用位置,但是只有 foo()运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo() 的调用位置无关:

1
2
3
4
5
6
7
8
9
function foo() {
// 如果在此处声明"use strict",下面自执行函数内foo()运行结果为undefined;
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})()

隐式绑定

思考下面的代码:

1
2
3
4
5
6
7
8
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
  • 无论是直接在 obj 中定义还是先定义再添加为引用属性, foo() 这个函数严格来说都不属于 obj 对象。
  • 然而,调用位置会使用 obj 上下文来引用函数,当 foo() 被调用时,它的落脚点确实指向 obj 对象。

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo()this 被绑定到 obj ,因此 this.aobj.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

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

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"

虽然 barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

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"

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

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

//JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}

回调函数丢失 this 绑定是非常常见的。调用回调函数的函数也可能会修改 this

显示绑定

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

  • JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
  • 它们的第一个参数是一个对象,它们会把这个对象绑定到this ,接着在调用函数时指定这个 this 。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。
1
2
3
4
5
6
7
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2

通过 foo.call(..) ,我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..) 、 new Boolean(..)或者new Number(..) )。这通常被称为“装箱”。

this 绑定的角度来说, call(..)apply(..) 是一样的,它们的区别体现在其他的参数上:

  • apply最多只能有两个参:新this对象和一个数组Array
  • call它可以接受多个参数,第一个参数也是新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 );
};
var a = 3;
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2

我们创建了函数 bar() ,并在它的内部手动调用了 foo.call(obj) ,因此强制把 foothis 绑定到了 obj 。无论之后如何调用函数 bar ,它总会手动在 obj 上调用 foo 。这种显示强制绑定,我们称之为硬绑定

由于硬绑定十分常用,所以在 ES5 中提供了内置的方法 Function.prototype.bind

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

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

API调用的“上下文”

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this

1
2
3
4
5
6
7
8
9
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定。

new绑定

先了解一个事实,JavaScriptnew 的机制实际上和面向类的语言完全不同。

  • 在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
  • 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this 。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
1
2
3
4
5
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this上。 new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

ES6中的this词法

ES6 中介绍了一种无法使用上述四种规则的特殊函数类型:箭头函数。

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this

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 绑定到 obj1bar (引用箭头函数)的 this 也会绑定到 obj1 ,箭头函数的绑定无法被修改。( new 也不行!)

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

1
2
3
4
5
6
7
8
9
10
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

1
2
3
4
5
6
7
8
9
10
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2

实际工作中,为了确保编程风格统一,便于后期维护,你或许应当:

  1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(..) ,尽量避免使用 self = 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
12
13
14
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 );
console.log( obj1.a ); // 2
var baz = new bar("new");
console.log( obj1.a ); // 2
console.log( baz.a ); // new

至此,我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。

  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()

总结

判断this绑定对象的四条规则:

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

四种绑定规则的优先级:new绑定>显式绑定>隐式绑定>默认绑定

例外

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

额外说明

默认绑定

  • this的指向,是在函数被调用的时候确定的
  • 如果函数独立调用,那么该函数内部的this,在严格模式中指向undefined。在非严格模式中,指向全局对象。
  • 例如fn()是独立调用,fn函数里面的this在非严格模式下指向全局对象,严格模式下会指向undefined

隐式绑定

  • 如果函数被某个对象调用,那么函数中的this,指向的就是这个对象。
  • 例如obj.fn()fn()obj调用,this指向的就是obj
  • 如果出现obj1.obj2.foo();这种情况,this指向的是obj2,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

关于属性调用,比如obj.a

  • obj在全局环境中声明时,无论obj.a在什么地方调用,这里的this都指向全局对象window
  • obj在函数环境中声明时,在严格模式下this指向undefined,在非严格模式下指向全局对象。

感谢打赏,错误之处欢迎指正交流(`・ω・´) !~~



文章目录
  1. 1. 前言
  2. 2. this解析
    1. 2.1. 调用位置
    2. 2.2. 绑定规则
      1. 2.2.1. 默认绑定
      2. 2.2.2. 隐式绑定
      3. 2.2.3. 显示绑定
        1. 2.2.3.1. 硬绑定
        2. 2.2.3.2. API调用的“上下文”
      4. 2.2.4. new绑定
      5. 2.2.5. ES6中的this词法
    3. 2.3. 优先级
      1. 2.3.1. 显示绑定>隐式绑定
      2. 2.3.2. new绑定>隐式绑定
      3. 2.3.3. new绑定>显示绑定
  3. 3. 总结
    1. 3.1. 额外说明
      1. 3.1.1. 默认绑定
      2. 3.1.2. 隐式绑定
|