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

場景 1:全局環(huán)境下的 this

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

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

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

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

這里 this 仍然指向的是 window。雖然 fn 函數(shù)在 foo 對象中作為方法被引用,但是在賦值給 fn1 之后,fn1 的執(zhí)行仍然是在 window 的全局環(huán)境中。因此輸出 windowundefined,還是上面這道題目,如果調(diào)用改變?yōu)椋?/p>

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

將會輸出:

{bar: 10, fn: ?}
10

這其實屬于第二種情況了,因為這個時候 this 指向的是最后調(diào)用它的對象,在 foo.fn() 語句中 this 指向 foo 對象。請記?。?strong>在執(zhí)行函數(shù)時,如果函數(shù)中的 this 是被上一級的對象所調(diào)用,那么 this 指向的就是上一級的對象;否則指向全局環(huán)境。

場景 2:上下文對象調(diào)用中的 this

我們直接來看“難”一點的:當存在更復雜的調(diào)用關系時,

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

在這種嵌套的關系中,this 指向最后調(diào)用它的對象,因此輸出將會是: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());

答案是:o1、o1undefined,你答對了嗎?

我們來一一分析。

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

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

一般開發(fā)者可能會想到使用 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 指向最后調(diào)用它的對象,在 fn 執(zhí)行時,掛到 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 的高級考察往往會結合構造函數(shù)以及組合式實現(xiàn)繼承。實現(xiàn)繼承的話題,我們會單獨講到。構造函數(shù)的使用案例,我們結合下面的場景進行分析。

場景 4:構造函數(shù)和 this

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

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

  • 創(chuàng)建一個新的對象;
  • 將構造函數(shù)的 this 指向這個新對象;
  • 為這個對象添加屬性、方法等;
  • 最終返回新對象。

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

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

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

需要指出的是,如果在構造函數(shù)中出現(xiàn)了顯式 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。

結論:如果構造函數(shù)中顯式返回一個值,且返回的是一個對象,那么 this 就指向這個返回的對象;如果返回的不是一個對象,那么 this 仍然指向?qū)嵗?/p>

場景 5:箭頭函數(shù)中的 this 指向

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

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

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

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

// {fn: ?}

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

終極場景 6:this 優(yōu)先級相關

我們常常把通過 callapply、bindnewthis 綁定的情況稱為顯式綁定;根據(jù)調(diào)用關系確定的 this 指向稱為隱式綁定。

那么顯式綁定和隱式綁定誰的優(yōu)先級更高呢?

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 的顯式綁定一般來說優(yōu)先級比隱式綁定更高。

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

const obj1 = {};

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

上述代碼通過 bind,將 bar 函數(shù)中的 this 綁定為 obj1 對象。執(zhí)行 bar(2) 后,obj1.a 值為 2。即經(jīng)過 bar(2) 執(zhí)行后,obj1 對象為:{a: 2}。

當再使用 bar 作為構造函數(shù)時:

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

將會輸出 3。我們看 bar 函數(shù)本身是通過 bind 方法構造的函數(shù),其內(nèi)部已經(jīng)對將 this 綁定為 obj1,它再作為構造函數(shù),通過 new 調(diào)用時,返回的實例已經(jīng)與 obj1 解綁。 也就是說:

new 綁定修改了 bind 綁定中的 this,因此 new 綁定的優(yōu)先級比顯式 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(引用箭頭函數(shù))的 this 也會綁定到 obj1,箭頭函數(shù)的綁定無法被修改。

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

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 等聲明變量的方式不再本課的主題當中,我們后續(xù)也將專門進行介紹。

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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容