1.4.3 函数的 this 关键字

相比其它语言,JavaScript 中函数的 this 关键字的行为略有不同。此外,它还区分严格模式和非严格模式

在大多数情况下,this 的值都取决于函数的调用方式(运行时绑定),这就意味着即便是同一个函数,随着它调用方式的不同,函数里面的 this 值就有可能不一样。但有两种特殊情况,分别是:

  1. bind() 方法可以设置函数的 this 值,而不管它是如何被调用的

  2. 箭头函数不提供自己的 this 绑定,而是保留了封闭词法上下文的 this

值得注意的是,在代码执行期间,this 的值是不能通过赋值来设置的。要想在调用函数的时候设置 this 值,可以使用 call()apply() 方法

1. 执行上下文

this 是执行上下文的一个属性。在严格模式下它可以是任意值(原始值或对象),在非严格模式下它始终是对象的引用。

a property of an execution context

  • Global context,全局上下文

  • Function context,函数上下文

  • Class context,类上下文

  • Derived classes context,派生类上下文

1.1 全局

在全局执行上下文中,this 指的是全局对象。我们可以使用全局属性 globalThis 来获取全局对象,而不管当前代码的执行上下文。

// 浏览器环境
console.log(this === window);     // true
console.log(this === globalThis); // true

// Node 环境
console.log(this === globalThis); // true

a = 'hello';
this.b = 'world';
window.c = '!';

console.log(a, b, c); // hello world !
console.log(this.a, this.b, this.c); // hello world !
console.log(window.a, window.b, window.c); // hello world !

1.2 函数

在函数内部,this 的值取决于函数的调用方式。

const obj = {
    prop: 33,
    method: function () {
        return this.prop;
    },
};

console.log(obj.method()); // 33

let fn = obj.method;
console.log(fn()); // undefined

在上面的代码中,fn 是对象方法的一个引用。fn() 是作为一个函数被直接调用的,而不是作为对象的方法,比如第 8 行里的 obj.method()

1.3 类

class 中 this 的行为和函数中的类似(毕竟类的底层依然是函数),但是它们之间还是有一些区别的。

在类的构造函数中,this 是一个常规对象,类里面的所有非静态方法都会添加到这个 this 的原型上。而类里面的所有静态方法都不是这个 this 的属性,而是类本身的属性。

class Obj {
    constructor() {
        this.a = 'inside';
        const proto = Object.getPrototypeOf(this);
        console.log(Object.getOwnPropertyNames(proto)); // ['constructor', 'method1']
    }
    method1() {
        console.log(this.a);
    }
    static method2() {
        console.log(this.a);
    }
}

const o = new Obj();
o.method1(); // 'inside'
o.method2(); // TypeError: o.method2 is not a function
Obj.method2();  // undefined

有时可能需要手动修改类方法里的 this 值,以确保它始终指向特定类的实例。比如:

class Car {
    constructor() {
        this.name = 'Car';
        this.sayBye = this.sayBye.bind(this); // sayBye() 方法将始终指向 Car 的实例
    }
    sayHi() {
        console.log('Hi,', this.name);
    }
    sayBye() {
        console.log('Bye,', this.name);
    }
}

class Bird {
    constructor() {
        this.name = 'Bird';
    }
}

const car = new Car();
const bird = new Bird();
bird.sayHi = car.sayHi;
bird.sayHi(); // Hi, Bird

bird.sayBye = car.sayBye;
bird.sayBye(); // Bye, Car

1.4 派生类

和基类的构造函数不同,派生类的构造函数没有初始的 this 绑定,所以就需要开发人员在构造函数中手动调用 super()。比如,执行以下代码会报错:

class Base { }
class Child extends Base {
    constructor() { }
}

// ReferenceError: 
//    Must call super constructor in derived class
//    before accessing 'this' or returning from derived constructor
new Child();

在构造函数中调用 super() 的时候,会创建一个 this 绑定。这本质上就和执行了语句 this = new Base() 的效果类似,其中 Base 是继承的父类。

当然,如果派生类中根本就没有构造函数,或是构造函数自己就返回了一个对象,那么不写 super() 也是不会报错的。比如,以下代码就可以正常运行:

class Base { }

class Child1 extends Base { } // 没有 constructor() 
class Child2 extends Base {
    constructor() {
        return {}; // constructor() 自己返回一个对象
    }
}

new Child1();
new Child2();

2. 调用方式

2.1 作为对象方法

当一个函数作为对象的方法被调用时,它的 this 值会被设置成调用该方法的对象。

const o = {
    a: 34,
    f() {
        return this.a;
    }
};

console.log(o.f()); // 输出 34,因为当执行 f() 时,其内部的 this 会被绑定到 o 对象

只要函数是作为对象的方法被调用的,那就符合这样的逻辑:函数的 this 值会被设置成调用该方法的对象,而不管函数是怎么定义的或是在哪里定义的。

2.1.1 先单独定义函数

也可以先定义函数,然后再将其附加到对象的属性上,最终效果都是一样的。

function fn() {
    return this.a;
}
const o = {
    a: 34
};
o.f = fn;

console.log(o.f()); // 依然是输出 34,因为 f() 还是作为对象 o 的方法被调用的

2.1.2 嵌套对象的方法

也可以将函数赋值给嵌套 n 层的对象的属性,此时 this 值的原理也是一样的。

function fn() {
    return this.a;
}
const o = {
    a: 34
};
o.b = {
    a: 40,
    m: fn
};

console.log(o.b.m()); // 会输出 40,因为方法 fn 是作为对象 o.b 的 m 属性被调用的

2.1.3 原型链上的方法

同样的逻辑,也适用于定义在对象原型链上的方法。如果一个方法位于对象的原型链上,那么其 this 指向的依然是调用该方法的特定对象(就像该方法就在那个对象上一样)。

const o = {
    sum() {
        return this.a + this.b;
    }
};
const p = Object.create(o);
p.a = 7;
p.b = 33;

console.log(p.sum()); // 40

虽然分配给变量 p 的对象没有自己的 sum 属性,但它可以从其原型链上继承。

p.sum()sum 的查找是从对象 p 开始的,所以 sum() 函数内部的 this 值会被设置成对象 p 的引用。也就是说因为 sum() 是作为对象 p 的方法被调用的,所以它的 this 指的就是对象 p。这是 JavaScript 原型继承中一个非常有趣的特性。

2.1.4 getter 和 setter

同样,当函数作为 getter 和 setter 被调用时,其内部的 this 会绑定到获取和设置属性的对象上。

function sum() {
    return this.a + this.b + this.c;
}

const o = {
    a: 7,
    b: 3,
    c: 5,
    get average() {
        return (this.a + this.b + this.c) / 3;
    }
};

Object.defineProperty(o, 'sum', {
    get: sum,
    enumerable: true,
    configurable: true
});

console.log(o.sum, o.average); // 15, 5

2.2 作为构造函数

当一个函数被当作构造函数使用时(使用 new 关键字),它的 this 值会绑定到正在构造的新对象上。

function Func() {
    this.a = 34;
}

let o = new Func();
console.log(o.a); // 34

然而,如果函数有自己的 return 语句,且返回的是一个对象,那么这个对象就会被作为 new 表达式的结果。如下:

function Func() {
    this.a = 34;
    return {
        a: 40
    }
}

let o = new Func();
console.log(o.a); // 40

在上面的例子中,因为函数在构造的过程中返回了一个对象(第 3 行),所以它就作为了 new 表达式的结果,因此第 9 行会输出 40,而不是 34。在这种场景下,Func() 函数里的 this 所指向的新对象并没有被暴露出去,这就意味着 this 对象只能在构造函数里使用,这在一定程度上实现了封装。

需要注意的是,一旦(构造)函数 return 的不是一个对象,那么 new 表达式的结果还依然是初始的 this 值(即正在被构造的新对象)。如下:

function Func() {
    this.a = 34;
    return 40; // 返回的是“原始值”而不是“对象”
}

let o = new Func();
console.log(o.a); // 会输出 34

2.3 箭头函数

箭头函数没有绑定自己的 this 值,它会直接保留封闭词法上下文的 this。关于箭头函数的基本介绍详见《箭头函数》,这里只重点介绍一些交叉类信息,同时作为对箭头函数的一个补充。

2.3.1 在函数内嵌套

如果是在其它函数内部创建的箭头函数,它的 this 值依然只和封闭词法上下文的 this 相关。

const obj = {
    bar() {
        const x = () => this; // this 会永久绑定到它的封闭函数的 this 上
        return x;
    }
};
const fn = obj.bar();
console.log(fn());

const fn2 = obj.bar;
console.log(fn2()());

以上代码的执行结果,如下:

对象 obj 的方法 bar() 返回的是一个箭头函数。因为是箭头函数,所以它的 this 值会永久绑定到其封闭上下文的 this 上——函数 bar()this 值。而函数 bar()this 值又是动态绑定的(和它的调用方式相关),这就反过来影响到了返回的箭头函数的 this 值,也就是说这里的箭头函数的 this 值看起来也是“动态”的了。

第 7 行 fn = obj.bar() 的执行过程大约是:执行函数 bar(),因为它是作为对象 obj 的方法被调用的,所以 bar() 函数里的 this 值会被设置成 obj 对象。函数 bar() 执行完之后,会返回箭头函数的引用 x,并将其赋值给变量 fn。此时,fn 指向箭头函数,里面的 this 值是 obj 对象,所以第 8 行代码会输出 obj 对象。

第 10 行 fn2 = obj.bar 只是引用了 objbar 方法并没有调用它,所以此时的变量 fn2 是一个指向了 bar 函数的引用。所以,当在第 11 行执行 fn2()() 时,第一个 () 相当于执行了 bar() 函数且是在全局作用域里调用的,所以 bar() 里的 this 会被设置成全局对象,而第二个括号 () 相当于是执行了返回的箭头函数,所以最终会输出全局对象 window

2.3.2 无法设置的 this

如果是通过 bind(), call()apply() 调用箭头函数,那么传过来的 thisArg 参数会直接被箭头函数忽略,所以通常会直接传个 null

const fn = () => this;
const fn2 = function () { return this; }

console.log(fn() === globalThis); // true
console.log(fn2() === globalThis); // true

const o = {};
const boundFn = fn.bind(o);
const boundFn2 = fn2.bind(o);

console.log(boundFn() === o); // false
console.log(boundFn2() === o); // true
console.log(boundFn() === globalThis); // true
console.log(boundFn2() === globalThis); // false

所以,我们通常会说箭头函数不适用于 call(), apply()bind() 这些实例方法。

2.4 事件处理器

当一个函数用作事件处理程序时,它的 this 会被设置成监听的那个 DOM 元素。

<!-- 1. 内联事件处理器 -->
<button id="btn" onclick="alert(this.tagName)">Button</button>
<script>
    // 2. DOM 事件处理器
    const btn = document.getElementById("btn");
    btn.addEventListener("click", function () {
        console.log(this.tagName);
    });
</script>

3. 主要参考

Last updated