> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/web/js/data-type/object/function/this.md).

# 1.4.3 函数的 this 关键字

相比其它语言，JavaScript 中函数的 `this` 关键字的行为略有不同。此外，它还区分[严格模式和非严格模式](https://anjia1.gitbook.io/web/js/data-type/object/function/pages/bz7iXXziPw1vfcELYTti#2.4.1-this-zhi)。

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

1. [`bind()` 方法](/web/js/data-type/object/function/bind.md)可以设置函数的 `this` 值，而不管它是如何被调用的
2. [箭头函数](/web/js/data-type/object/function/arrow-function.md)不提供自己的 `this` 绑定，而是保留了封闭词法上下文的 `this` 值

值得注意的是，在代码执行期间，`this` 的值是不能通过赋值来设置的。要想在调用函数的时候设置 `this` 值，可以使用 [`call()` 和 `apply()` 方法](/web/js/data-type/object/function/call-and-apply.md)。

## 1. 执行上下文

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

> a property of an execution context

* Global context，全局上下文
* Function context，函数上下文
* Class context，类上下文
* Derived classes context，派生类上下文

### 1.1 全局

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

```javascript
// 浏览器环境
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` 的值取决于函数的调用方式。

{% code lineNumbers="true" %}

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

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

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

{% endcode %}

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

### 1.3 类

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

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

```javascript
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` 值，以确保它始终指向特定类的实例。比如：

```javascript
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()`。比如，执行以下代码会报错：

```javascript
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` 是继承的父类。

当然，如果派生类中根本就没有构造函数，或是[构造函数自己就返回了一个对象](https://anjia1.gitbook.io/web/js/data-type/object/function/pages/H1VIPMJGa3jJSbMV1Elk#2.-han-shu)，那么不写 `super()` 也是不会报错的。比如，以下代码就可以正常运行：

```javascript
class Base { }

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

new Child1();
new Child2();
```

## 2. 调用方式

### 2.1 作为对象方法

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

{% code lineNumbers="true" %}

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

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

{% endcode %}

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

#### 2.1.1 先单独定义函数

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

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

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

#### 2.1.2 嵌套对象的方法

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

```javascript
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` 指向的依然是调用该方法的特定对象（就像该方法就在那个对象上一样）。

```javascript
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` 会绑定到获取和设置属性的对象上。

```javascript
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` 值会绑定到正在构造的新对象上。

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

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

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

{% code lineNumbers="true" %}

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

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

{% endcode %}

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

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

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

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

### 2.3 箭头函数

箭头函数没有绑定自己的 `this` 值，它会直接保留封闭词法上下文的 `this`。关于箭头函数的基本介绍详见[《箭头函数》](/web/js/data-type/object/function/arrow-function.md)，这里只重点介绍一些交叉类信息，同时作为对箭头函数的一个补充。

#### 2.3.1 在函数内嵌套

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

{% code lineNumbers="true" %}

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

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

{% endcode %}

以上代码的执行结果，如下：

![](/files/a1hSbatoBAbt4vPigBPp)

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

#### 2.3.2 无法设置的 `this`

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

```javascript
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  元素。

```html
<!-- 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. 主要参考

* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this>
