2021-11-17 JavaScript 的 this 原理是什麼?

場景 1:全局環境下的 this

這種情況相對簡單直接,函數在瀏覽器全局環境中被簡單調用,非嚴格模式下 this 指向 window; 在 use strict 指明嚴格模式的情況下就是 undefined:

function f1() {
    console.log(this);
}
function f2() {
    "use strict";
    console.log(this);
}
f1(); // window
f2(); // undefined

這樣的題目比較基礎,但是如果你是在面試,那麼需要候選人格外注意其變種(爲什麼總有這種變態無聊面試題),請再看:

const foo = {
    bar: 10,
    fn: function () {
        console.log(this);
        console.log(this.bar);
    }
};
var fn1 = foo.fn;
fn1();

這裏 this 仍然指向的是 window。雖然 fn 函數在 foo 對象中作爲方法被引用,但是在賦值給 fn1 之後,fn1 的執行仍然是在 window 的全局環境中。因此輸出 windowundefined,還是上面這道題目,如果調用改變爲:

const foo = {
    bar: 10,
    fn: function () {
        console.log(this);
        console.log(this.bar);
    }
};
foo.fn();

將會輸出:

{bar: 10, fn: ƒ}
10

這其實屬於第二種情況了,因爲這個時候 this 指向的是最後調用它的對象,在 foo.fn() 語句中 this 指向 foo 對象。請記住:在執行函數時,如果函數中的 this 是被上一級的對象所調用,那麼 this 指向的就是上一級的對象;否則指向全局環境。

場景 2:上下文對象調用中的 this

我們直接來看“難”一點的:當存在更復雜的調用關係時,

const person = {
    name: "Lucas",
    brother: {
        name: "Mike",
        fn: function () {
            return this.name;
        }
    }
};
console.log(person.brother.fn());

在這種嵌套的關係中,this 指向最後調用它的對象,因此輸出將會是:Mike

我們再看一道更復雜的題目,請跟我一起做好“應試”的準備:

const o1 = {
    text: "o1",
    fn: function () {
        return this.text;
    }
};
const o2 = {
    text: "o2",
    fn: function () {
        return o1.fn();
    }
};
const o3 = {
    text: "o3",
    fn: function () {
        var fn = o1.fn;
        return fn();
    }
};

console.log(o1.fn());
console.log(o2.fn());
console.log(o3.fn());

答案是:o1o1undefined,你答對了嗎?

我們來一一分析。

  • 第一個 console 最簡單,o1 沒有問題。難點在第二個和第三個上面,關鍵還是看調用 this 的那個函數。
  • 第二個 consoleo2.fn(),最終還是調用 o1.fn(),因此答案仍然是 o1
  • 最後一個,在進行 var fn = o1.fn 賦值之後,是“裸奔”調用,因此這裏的 this 指向 window,答案當然是 undefined

如果是在面試中,我作爲面試官,就會追問:如果我們需要讓 console.log(o2.fn()) 輸出 o2,該怎麼做?

一般開發者可能會想到使用 bind/call/apply 來對 this 的指向進行干預,這確實是一種思路。但是我接着問,如果不能使用 bind/call/apply,有別的方法嗎?

const o1 = {
    text: "o1",
    fn: function () {
        return this.text;
    }
};
const o2 = {
    text: "o2",
    fn: o1.fn
};

console.log(o2.fn());

還是應用那個重要的結論:this 指向最後調用它的對象,在 fn 執行時,掛到 o2 對象上即可,我們提前進行了類似賦值的操作。

場景 3:bind/call/apply 改變 this 指向

上文提到 bind/call/apply

const foo = {
    name: "lucas",
    logName: function () {
        console.log(this.name);
    }
};
const bar = {
    name: "mike"
};
console.log(foo.logName.call(bar));

將會輸出 mike,這不難理解。但是對 call/apply/bind 的高級考察往往會結合構造函數以及組合式實現繼承。實現繼承的話題,我們會單獨講到。構造函數的使用案例,我們結合下面的場景進行分析。

場景 4:構造函數和 this

function Foo() {
    this.bar = "Lucas";
}
const instance = new Foo();
console.log(instance.bar);

答案將會輸出 Lucas。但是這樣的場景往往伴隨着下一個問題:new 操作符調用構造函數,具體做了什麼?以下供參考:

  • 創建一個新的對象;
  • 將構造函數的 this 指向這個新對象;
  • 爲這個對象添加屬性、方法等;
  • 最終返回新對象。

以上過程,也可以用代碼表述:

var obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj);

當然,這裏對 new 的模擬是一個簡單基本版的,更復雜的情況這個問題下我不會贅述。

需要指出的是,如果在構造函數中出現了顯式 return 的情況,那麼需要注意分爲兩種場景:

function Foo() {
    this.user = "Lucas";
    const o = {};
    return o;
}
const instance = new Foo();
console.log(instance.user);

將會輸出 undefined,此時 instance 是返回的空對象 o

function Foo() {
    this.user = "Lucas";
    return 1;
}
const instance = new Foo();
console.log(instance.user);

將會輸出 Lucas,也就是說此時 instance 是返回的目標對象實例 this

結論:如果構造函數中顯式返回一個值,且返回的是一個對象,那麼 this 就指向這個返回的對象;如果返回的不是一個對象,那麼 this 仍然指向實例。

場景 5:箭頭函數中的 this 指向

箭頭函數使用 this 不適用以上標準規則,而是根據外層(函數或者全局)上下文作用域來決定。

const foo = {
    fn: function () {
        setTimeout(function () {
            console.log(this);
        });
    }
};
console.log(foo.fn());

這道題中,this 出現在 setTimeout() 中的匿名函數裏,因此 this 指向 window 對象。如果需要 this 指向 foo 這個 object 對象,可以巧用箭頭函數解決:

const foo = {
    fn: function () {
        setTimeout(() => {
            console.log(this);
        });
    }
};
console.log(foo.fn());

// {fn: ƒ}

單純箭頭函數中的 this 非常簡單,但是綜合所有情況,結合 this 的優先級考察,這時候 this 指向並不好確定。請繼續閱讀。

終極場景 6:this 優先級相關

我們常常把通過 callapplybindnewthis 綁定的情況稱爲顯式綁定;根據調用關係確定的 this 指向稱爲隱式綁定。

那麼顯式綁定和隱式綁定誰的優先級更高呢?

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

const obj1 = {
    a: 1,
    foo: foo
};

const obj2 = {
    a: 2,
    foo: foo
};

obj1.foo.call(obj2);
obj2.foo.call(obj1);

輸出分別爲 2、1,也就是說 callapply 的顯式綁定一般來說優先級比隱式綁定更高。

function foo(a) {
    this.a = a;
}

const obj1 = {};

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);

上述代碼通過 bind,將 bar 函數中的 this 綁定爲 obj1 對象。執行 bar(2) 後,obj1.a 值爲 2。即經過 bar(2) 執行後,obj1 對象爲:{a: 2}。

當再使用 bar 作爲構造函數時:

var baz = new bar(3);
console.log(baz.a);

將會輸出 3。我們看 bar 函數本身是通過 bind 方法構造的函數,其內部已經對將 this 綁定爲 obj1,它再作爲構造函數,通過 new 調用時,返回的實例已經與 obj1 解綁。 也就是說:

new 綁定修改了 bind 綁定中的 this,因此 new 綁定的優先級比顯式 bind 綁定更高。

function foo() {
    return (a) => {
        console.log(this.a);
    };
}

const obj1 = {
    a: 2
};

const obj2 = {
    a: 3
};

const bar = foo.call(obj1);
console.log(bar.call(obj2));

將會輸出 2。由於 foo()this 綁定到 obj1bar(引用箭頭函數)的 this 也會綁定到 obj1,箭頭函數的綁定無法被修改。

如果將 foo 完全寫成箭頭函數的形式:

var a = 123;
const foo = () => (a) => {
    console.log(this.a);
};

const obj1 = {
    a: 2
};

const obj2 = {
    a: 3
};

var bar = foo.call(obj1);
console.log(bar.call(obj2));

將會輸出 123。

這裏我再“抖個機靈”,僅僅將上述代碼的第一處變量 a 的賦值改爲:

const a = 123;
const foo = () => (a) => {
    console.log(this.a);
};

const obj1 = {
    a: 2
};

const obj2 = {
    a: 3
};

var bar = foo.call(obj1);
console.log(bar.call(obj2));

答案將會輸出爲 undefined,原因是因爲使用 const 聲明的變量不會掛載到 window 全局對象當中。

因此 this 指向 window 時,自然也找不到 a 變量了。關於 const 或者 let 等聲明變量的方式不再本課的主題當中,我們後續也將專門進行介紹。

原文鏈接:JavaScript 的 this 原理是什麼?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章