Sanakey

你不知道的JavaScript--this解析
前言this是JavaScript里面老生常谈的问题,因为其诡异的机制,基本每个初学者都会迷惑于this的指向问题...
扫描右侧二维码阅读全文
08
2018/08

你不知道的JavaScript--this解析

前言

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

this解析

调用位置

  • 什么是调用位置?

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

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

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 的调用位置

绑定规则

默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。

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

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

注意一个细节

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

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

隐式绑定

思考下面的代码:

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 是一样的。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:

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 上,取决于是否是严格模式。

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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

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"

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

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

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 的绑定对象,因此我们称之为显式绑定。
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对象,后面则是多个参数。
硬绑定
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

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

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 表达式中的函数调用会自动返回这个新对象。
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

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 也不行!)

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

function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2

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

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 和箭头函数。

优先级

显示绑定>隐式绑定

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绑定>隐式绑定

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绑定>显示绑定

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,在非严格模式下指向全局对象。
Last modification:November 3rd, 2019 at 04:04 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment

lamu.png