js中this指向的問題與聯絡深入探究

LiuWango發表於2021-02-23

前言

JavaScript 中最大的一個安全問題,也是最令人困惑的一個問題,就是在某些情況下this的值是如何確定的。有js基礎的同學面對這個問題基本可以想到:this的指向和函式呼叫的方式相關。這當然是正確的,然而,這幾種方式有什麼聯絡嗎?這是我接下來要說明的問題。

this從哪裡來

this 是js的一個關鍵字,和arguments類似,它是函式執行時,在函式體內部自動生成的一個物件,只能在函式體內部使用。這句話似乎與認知不同,我們在函式體外部即全域性作用域下也能使用this

// 直接在全域性作用域下輸出this
console.log(this);
// 輸出window

但是不要忘記,即便是全域性作用域,依舊是執行在window下的,我們寫的程式碼都在window的某個函式中。而這也催生了一種理解this指向的方法:this永遠指向呼叫者(非箭頭函式中)。

作為普通函式呼叫

函式作為普通函式直接呼叫(也稱為自執行函式)的時候,無論函式在全域性還是在另一個函式中,this都是指向window

function fn() {
    this.author = 'Wango';
}

fn();
console.log(author);
// Wango

這很好理解,但又不是很好理解,因為在程式碼中省略了window,補全後就好理解了:this指向的是呼叫者。

function fn() {
    this.author = 'Wango';
}

window.fn();
console.log(window.author);
// Wango

而在內部函式中,自執行函式中的this依舊指向全域性作用域,我們無法通過window.foo()呼叫函式,但並不妨礙我們先這樣理解(具體參見本文最後一部分this的強制轉型)。

function fn() {
    function foo() {
        console.log(this);
    }
    foo();
    // Window
    window.foo();
    // TypeError
}

fn();

作為建構函式呼叫

在建構函式中,this指向new生成的新物件,即建構函式是通過new呼叫的,建構函式內部的this當然就應該指向new出來的物件。

function Person(name, age) {
    this.name = name;
    this.age = age;
    console.log(this);
    // Person { name: 'Wango', age: 24 }
}

new Person('Wango', 24);

建構函式中的this與建構函式的返回值型別無關,下列程式碼中p指向了建構函式返回的物件,而不是new出來的物件。當然,這是建構函式的特性,與本主題關係不大。

function Person(name, age) {
    console.log(this);
    // Person {}
    this.name = name;
    this.age = age;
    console.log(this);
    // Person { name: 'Wango', age: 24 }

    return {
        name: 'Lily',
        age: 25
    }
}

Person.prototype.sayName = function() {
    return this.name + ' ' + this.age
}

const p = new Person('Wango', 24);
console.log(p.sayName());
// TypeError: p.sayName is not a function

作為物件方法呼叫

通過物件方法呼叫時,this指向應該是最明晰的了。與其他面嚮物件語言的this行為相同,指向該方法的呼叫者。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.sayName = fn;

function fn() {
    return this.name + ' ' + this.age
}

const p = new Person('Wango', 24);
console.log(p);
// Person { name: 'Wango', age: 24 }
console.log(p.sayName());
// Wango 24

通過[]呼叫物件方法

通常,我們對於物件方法是通過.語法呼叫,但通過[]也可以呼叫物件方法,在這種情況下的this指向常常會被我們混淆、忽略。

function fn() {
    console.log(this);
}

const arr = [fn, 1];

arr[0]();
// [Function: fn, 1]

function fn2() {
    arguments[0]();
}

fn2(fn, 1);
// [Arguments] { '0': [Function: fn], '1': 1 }

在上例中,無論是陣列還是偽陣列,其本質上都是物件,在通過[]獲取函式元素並呼叫的時候,會改變函式中的this指向,this指向這個陣列或偽陣列,與物件呼叫函式的行為一致。

通過call、apply呼叫

function fn() {
    console.log(this.name);
}

const author = {
    name: 'Wango'
}

fn.call(author);
// Wango

這似乎與this永遠指向呼叫者相違背,但一旦我們明白了call函式的實現機制就會明白,這不僅不是違背,反而是佐證。對callapplybind實現機制不熟悉的同學可以參考我另一篇文章,下面擷取call簡要說明。

// 儲存一個全域性變數作為預設值
const root = this;

Function.prototype.myCall = function(context, ...args) {
    if (typeof context === 'object') {
        // 如果引數是null,使用全域性變數
        context = context || root;
    } else {
        // 引數不是物件的建立一個空物件
        context = Object.create(null);
    }
    // 使用Symbol建立唯一值作為函式名
    let fn = Symbol();
    context[fn] = this;
    context[fn](...args);
    delete context[fn];
}

call 函式最核心的實現在於context[fn] = this;context[fn](...args);這兩行。實際上就是將沒有函式呼叫者的普通函式掛載到指定的物件上,這時this指向與物件呼叫方法的一致。而delete context[fn];是在呼叫後立即解除物件與函式之間的關聯。

嚴格模式下的不同表現

this強制轉型

使用函式的apply()call()方法時,在非嚴格模式下nullundefined值會被強制轉型為全域性物件。在嚴格模式下,則始終以指定值作為函式this的值,無論指定的是什麼值。這也是為何在嚴格模式下,自執行函式的this不再指向window,而是指向undefined的根本原因。

// 定義一個全域性變數
color = "red";
function displayColor() {
    console.log(this.color);
}
// 在非嚴格模式下使用call修改this指向,並指定null,或undefined,
displayColor.call(null);
displayColor.call();
// red
// 修改指向無效,傳入null或undefined被轉換為了window

實際上,我們也可以將自執行函式,如fn(),看作是fn.call()的語法糖,在普通模式下,第一個引數預設為undefined,但被強制轉換為window。這也就解釋了為何所有自執行函式中this都指向window但無法通過window呼叫的問題(函式在call函式中掛載到window物件上,執行後被立即刪除,所以無法再次通過window訪問)。

apply()call()方法在嚴格模式下傳入簡單資料型別作為第一個引數時,該簡單資料型別會被轉換為相應的包裝類,而非嚴格模式不會如此轉換。

function foo() {
    console.log(this);
}

foo.call(); // Window {}
foo.call(2); // Number {2}


function foo() {
    console.log(this);
}

foo.call(); // undefined
foo.call(2); // 2

箭頭函式的this指向

在箭頭函式中, this引用的是定義箭頭函式的上下文。即箭頭函式中的this不會隨著函式呼叫方式的改變而改變。

function Person(name) {
    this.name = name;

    this.getName = () => console.log(this.name);
}

const p = new Person('Wango');

p.getName();
// Wango

const getName = p.getName;

getName();
// Wango
getName.call({name: 'Lily'});
// Wango

參考資料:

Javascript 的 this 用法

Javascript高階程式設計(第四版)

相關文章