理解JS中的this

王玉略發表於2018-03-15

一、開始

最近閱讀《你不知道的javascript》,裡面有關於 this 的詳細介紹,王二受益匪淺,於是在這裡做一個分享。

關於JS中的 this 到底是什麼,知乎中輪子哥這樣說到:

this 在js的函式裡面只是一個引數,是通過 Fuck.Shit(Bitches) 這種語法來傳遞的,點號前面的表示式就算 this

輪子哥說的沒錯,通常來說,想要確定 this 就是尋找“函式被呼叫的位置”,但是這做起來並沒有這麼簡單,因為某些程式設計模式可能會隱藏真正的呼叫位置。

劃分到具體,js中有四條繫結規則來確定 this 的繫結物件。

二、四條繫結規則

假設我們已經找到函式的被呼叫位置,我們還要確定用下面四條繫結規則中的哪一條,來確定 this 的繫結物件。在這裡,王二首先會分別解釋這四條規則,然後解釋多條規則都可用時它們的優先順序如何排列。

第一條規則:預設繫結

1、預設繫結下 this 會指向全域性物件

function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
foo(); // 2
複製程式碼

2、但是如果使用嚴格模式(strict mode),那麼全域性物件將無法使用預設繫結,this 會繫結到 undefined,因此以上的程式碼會報錯:

"use strict";
function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
foo(); // TypeError: this is undefined
複製程式碼

3、但是如果我們顯式地用 window 呼叫 foo 函式,則以上程式碼不會報錯:

"use strict";
function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
window.foo(); // 2
複製程式碼

這是因為我們應用了第二條規則——隱式繫結

第二條規則:隱式繫結

如果一個函式中有 this ,這個函式有被上一級的物件所呼叫,那麼 this 指向的就是上一級的物件;this 是在執行時被確定,而不是在定義時被確定。

1、參考如下程式碼:

function foo() {
    console.log( this.a ); 
} 
var obj = {
    a: 2,     
    foo: foo 
};
obj.foo(); // 2
複製程式碼

2、this 指向的是被呼叫方法的上一級物件,而不是它的最外層物件,

參考如下程式碼:

function foo() {
    console.log( this.a ); 
} 
var obj2 = {
    a: 22,     
    foo: foo 
}; 
var obj1 = {     
    a: 12,     
    obj2: obj2 
}; 
obj1.obj2.foo(); // 22
複製程式碼

3、this 是在執行時被確定,而不是在定義時被確定,

參考如下程式碼:

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var bar = obj.foo; // 函式別名! 
var a = "oops, global"; // a是全域性物件的屬性
bar(); // "oops, global"
複製程式碼

4、在方法的引數中傳入函式時也需要特別注意,傳入函式的 this 也指向其方法被呼叫的上一級物件

參考如下程式碼:

function foo() {
    console.log( this.a ); 
}
function doFoo(fn) {     // fn其實引用的是foo     
    fn(); // <-- 呼叫位置! 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全域性物件的屬性
doFoo( obj.foo ); // "oops, global"  
複製程式碼

再例如:

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全域性物件的屬性
setTimeout( obj.foo, 100 ); // "oops, global"
複製程式碼

在上面的的程式碼片段中,有時候我們就想列印 obj 中的 a 屬性,這時候我們應該怎麼修改呢?

這就需要我們應用第三條規則——顯式繫結

第三條規則:顯式繫結

以上的程式碼可以如下修改來訪問到 obj 中的 a 屬性:

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全域性物件的屬性
setTimeout( obj.foo.bind(obj), 100 ); // 2
複製程式碼

這個程式碼片段中用了 bind() 方法來顯式修改 this 的指向,與 bind() 方法有類似功能的還有 call() 方法和 apply() 方法,他們都可以改變 this的指向;

但是它們之間也有重要的區別:bind() 是返回對應函式,便於稍後呼叫;call()apply() 則是立即呼叫 。關於這三個方法更詳細的介紹,感興趣的同學可以參考王二之前寫過的一篇文章——JS中apply、call、bind的用法

第四條規則:new繫結

參考如下程式碼:

function foo(a) {
    this.a = a; 
} 
var bar = new foo(2);
console.log( bar.a ); // 2
複製程式碼

使用 new 來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面的操作。

  1. 建立(或者說構造)一個全新的物件。
  2. 這個新物件會被執行[[原型]]連線。
  3. 這個新物件會繫結到函式呼叫的 this 上。
  4. 如果函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件。

其中第二步操作,王二這裡暫不討論。

更具第一步和第三部操作,我們可以知道使用 new 來呼叫 foo(..) 時,我們會構造一個新物件並把它繫結到 foo(..) 呼叫中的 this 上。

關於第四步操作,我們需要額外注意,王二接下來提供一些示例程式碼以供參考(此程式碼來自'追夢子'的部落格):

示例程式碼一:

function fn()  
{  
    this.user = '追夢子';  
    return {};  
}
var a = new fn;  
console.log(a.user); //undefined
複製程式碼

示例程式碼二:

function fn()  
{  
    this.user = '追夢子';  
    return function(){};
}
var a = new fn;  
console.log(a.user); //undefined
複製程式碼

示例程式碼三:

function fn()  
{  
    this.user = '追夢子';  
    return 1;
}
var a = new fn;  
console.log(a.user); //追夢子
複製程式碼

示例程式碼四:

function fn()  
{  
    this.user = '追夢子';  
    return undefined;
}
var a = new fn;  
console.log(a.user); //追夢子
複製程式碼

也就是說:如果返回值是一個物件,那麼 this 指向的就是那個返回的物件,如果返回值不是一個物件那麼 this 還是指向函式的例項。

三、四條繫結規則的優先順序

現在我們可以根據優先順序來判斷函式在某個呼叫位置應用的是哪條規則。可以按照下面的順序來進行判斷:

1、 函式是否在 new 中呼叫(new繫結)?如果是的話this繫結的是新建立的物件。

var bar = new foo()
複製程式碼

2、 函式是否通過 callapplybind(顯式繫結)呼叫?如果是的話,this 繫結的是指定的物件。

var bar = foo.call(obj)
複製程式碼

3、函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this 繫結的是那個上下文物件。

var bar = obj.foo()
複製程式碼

4、如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結到 undefined ,否則繫結到全域性物件。

var bar = foo()
複製程式碼

四、箭頭函式裡的this

箭頭函式不使用 this 的四種標準規則,而是根據外層(函式或者全域性)作用域來決定 this

參考如下程式碼:

function foo() {      // 返回一個箭頭函式     
    return () => {   //this繼承自foo()       
        console.log( this.a );     
    }
} 
var obj1 = {
    a:2 
}; 
var obj2 = {
    a:3 
}; 
var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 2, 不是3!
複製程式碼

如果將箭頭函式換為普通函式,則列印的是3:

function foo() {   
    return function () {       
        console.log( this.a );     
    }
} 
var obj1 = {
    a:2 
}; 
var obj2 = {
    a:3 
}; 
var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 3
複製程式碼

五、參考閱讀

你不知道的Javascript(上)(中文版) 密碼:x7ge

"追夢子"的部落格

知乎關於this的問題

原文地址:王玉略的個人網站

相關文章