日期:2021年11月26日标签:JavaScript

全面解析JS的this关键字 #

this问题是JavaScript中最复杂的问题之一,即使是有经验的程序员也会在不经意间掉进this关键字的坑里。每当定义一个函数时,this会被自动定义在这个函数里面(即使在函数内部没有去使用this关键字)。

函数调用位置 #

理解this之前,先了解一下函数的调用位置函数的调用位置是函数被调用时所处的位置,不是函数声明的位置。为了理解调用位置,重要的是分析一个函数的调用栈,调用栈是函数执行的一个链表,可以通过以下几个例子,理解调用栈和调用位置。

    function f1() {
        // 当前调用栈:f1
        
        f2(); // f2的调用位置

        console.log("f1");
    }

    function f2() {
        // 当前调用栈为 f1 -- >f2

        f3(); // f3的调用位置

        console.log("f2");
    }

    function f3() {
        // 当前调用栈为 f1 --> f2 --> f3

        console.log("f3");
    }

    f1();

我们定义了三个函数f1f2f3,执行f1(),因为在f1内部调用了f2f2内部又调用了f3,所以程序会相继执行f2()f3(),在执行f1时调用栈为f1,在f2执行时,调用栈为f1 --> f2,在f3执行时,调用栈为f1 --> f2 --> f3,所以调用栈类似一个单链表,从表头依次执行到表尾。

一些调试工具,会有专门的调用栈面板,显示当前程序执行的调用栈。在谷歌浏览器中,你可以按F12打开开发者工具,切换到Sources页面,你可以看到call stack面板,这里会显示程序执行时的调用栈。

绑定规则 #

下面,我总结了四条关于this的绑定规则,在应用以下规则之前,需要先分析调用栈,找到函数的调用位置,然后判断应用下面的四条规则之一。

默认绑定 #

这是this绑定的默认规则,在无法应用其他规则时,使用该规则。

function f() {
    console.log(this.a);
}

var a = 2;

f(); // 输出:2

在JavaScript中声明在全局作用域中的变量就是全局对象的一个同名属性。在浏览器环境中,这个全局对象就是window,所以var a = 2等价于window.a = 2

当使用默认绑定时,this会被绑定到全局对象,上面的例子中函数f()执行时,this会被默认绑定到全局对象,即此时this指向全局对象,所以输出结果为2。

注意,当程序声明为"use strict"时,则不能将全局对象用于默认绑定。

function f() {
    "use strict";

    // 此时this绑定到undefined
    console.log(this.a);
}

var a = 2;

f();

注意:尽量在代码中使用严格模式,非严格模式容易写出难以维护的代码。更不要混合使用严格和非严格模式。

隐式绑定 #

当函数执行时,它属于某个对象Object,那么此时this被绑定到该对象。

function f() {
    console.log(this.a);
}

var obj = {
    a: 3,
    f: f
};

var a = 2;

obj.f(); // 输出:3

虽然函数f定义在全局作用域中,但是它被当做引用(reference)添加到obj中,当执行obj.f()时,此时f'属于'obj,所以输出3。

将上述例子更改一下。

var obj = {
    a: 3,
    f: function() {
        console.log(this.a);
    }
};

var f = obj.f;

var a = 2;

f(); // 输出:2

此时,输出结果为2,为什么呢?因为严格来说函数并不属于任意一个对象,上例中的obj.ff仅仅是函数的一个引用,所以fobj.f没有直接关系,仅仅是所引用的函数相同,所以执行f(),此时执行环境在全局作用域,会自动应用默认绑定规则,将this绑定到全局对象中。第二个例子,是一个比较常见的this绑定问题,叫做this的隐式丢失

在回调函数中经常会遇到隐式丢失的问题。

function f1() {
    console.log(this.a);
}

function f2(f) {
    // f引用的是f1
    f();
}

var obj = {
    a: "obj",
    f: f1
};

var a = "global"

f2(obj.f); // 输出:"global"

在执行f2(obj.f)时,实际上试将f1赋值给f2的参数f,它实际上也只是函数的引用,所以结果是"global"

显示绑定 #

可以通过callapplybind函数显示绑定this值。

function f() {
    console.log(this.a);
}

var obj = {
    a: 2
};
f.call(obj); // 输出:2

通过call方法,显示指定this绑定到obj对象,此时输出结果为2。可以给call传递多个参数,第一个参数指定this绑定对象,后续参数指定函数执行所需要的的参数。

apply方法与call类似,只不过call方法接受的是参数列表,而apply方法接受多个参数的数组。

bind方法创建一个新的函数,在bind被调用时,这个新函数的this被指定为bind的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

const module = {
    x: 42,
    getX: function() {
        return this.x;
    }
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

new绑定 #

如果你没有接触过JavaScript之外的语言,那么你可能很少会用到new关键字,因为JavaScript中实际并不存在(Es6中的类底层其实也是函数)。JavaScript中的“构造函数”,只是使用new操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类,就是被new操作符调用的普通函数。

使用new调用函数(构造函数),会自动执行下面的步骤。

  1. 创建一个全新的空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向函数的的prototype属性。
  3. 将这个空对象绑定到函数内部的this关键字。
  4. 执行函数内部的代码(一般通过给this属性赋值,初始化对象)。

上述第三步,就是new绑定。

完整步骤 #

讲述了四种绑定规则,接下来讲述下完整的步骤,确定几种方式的优先级。

  1. new绑定,如果函数在new中调用,那么this绑定的是新创建的对象。

  2. 显示绑定,如果函数通过callapply或者bind调用,那么this绑定的是指定对象。

  3. 隐式绑定,函数是否在某个上下文对象中调用,如果是的,那么this绑定的是该上下文对象。

  4. 以上都不符合的话,使用默认绑定,严格模式下绑定到undefined,非严格模式下绑定到全局对象。

例外 #

凡事总有例外,这里介绍一个比较常见的例外——箭头函数=>

箭头函数不使用this的四种绑定规则,而是根据外层(函数或者全局)作用域来确定this的绑定对象。

const myObject = {
    myMethod: () => {
        console.log(this);
    }
}

myObject.myMethod(); // this === global object

为什么上述myMethod()执行的this是全局对象呢?因为箭头函数的this继承自它的外层(函数或者全局)作用域。

继续看一个例子。

const myObject = {
    myArrowFunction: null,
    myMethod: function() {
        this.myArrowFunction = () => {console.log(this)};
    }
};

myObject.myMethod(); // this === myObject

myObject.myArrowFunctoin(); // this === myObject

const myArrowFunction = myObject.myArrowFunction;
myArrowFunction(); // this === myObject

(完)

目录