博客 / 詳情

返回

深入理解this

什麼是作用域?

幾乎所有編程語言最基本的功能之一,就是能夠儲存變量當中的值,並且能在之後對這個 值進行訪問或修改。事實上,正是這種儲存和訪問變量的值的能力將狀態帶給了程序。
若沒有了狀態這個概念,程序雖然也能夠執行一些簡單的任務,但它會受到高度限制,做 不到非常有趣。
但是將變量引入程序會引起幾個很有意思的問題,這些變量住在 哪裏?換句話説,它們儲存在哪裏?最重要的是,程序需要時如何找到它們?
這些問題説明需要一套設計良好的規則來存儲變量,並且之後可以方便地找到這些變量。 這套規則被稱為作用域。
簡而言之,作用域是根據名稱查找變量的一套規則。
(以上摘錄自《你不知道的JS》)

什麼是this?

JavaScript有兩種作用域:詞法(靜態)作用域和動態作用域(即this)。
詞法作用域即函數和變量的作用域在定義的時候就確定了(即聲明的位置)。如:

var value = 1;

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

function bar() {
    var value = 2;
    foo();
}

bar();

// 結果是 ???

答案是1. foo定義在全局對象,所以作用域即window, foo內部沒有找到value,就往作用域鏈上面找,即window,window對象有聲明value,則打印1.
注:瀏覽器的全局對象默認是window

在看一個例子(摘取《你不知道的JS》):

function identify() {
return this.name.toUpperCase(); 
} 
 
function speak() {
var greeting = "Hello, I'm " + identify.call( this );    
console.log( greeting ); 
} 
 
var me = {
name: "Kyle" 
}; 
 
var you = {  
name: "Reader" 
}; 
 
identify.call( me ); // KYLE 
identify.call( you ); // READER 
 
speak.call( me ); // Hello, 我是 KYLE 
speak.call( you ); // Hello, 我是 READER

這段代碼可以在不同的上下文對象(me 和 you)中重複使用函數 identify() 和 speak(), 不用針對每個對象編寫不同版本的函數。
如果不使用 this,那就需要給 identify() 和 speak() 顯式傳入一個上下文對象(context)。

function identify(context) {
    return context.name.toUpperCase(); 
} 
 
function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting ); 
}
var me = {
    name: "Kyle" 
}; 
 
var you = {  
    name: "Reader" 
}; 

identify(you); // READER 
speak(me); //Hello, I'm KYLE

隨着你的使用模式越來越複雜,顯式傳遞上下文對象會讓代碼變得越來越混亂,使用 this 提供了一種更優雅的方式來隱式“傳遞”一個對象引用。也就是動態作用域。this指的是函數運行時所在的環境(上下文對象)。

瞭解了作用域和this,接下來進入本文的主題:

如何確定函數的運行環境?

var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1 
foo() // 2
同樣都是foo函數,為什麼結果不一樣呢?這種差異的原因,就在於函數體內部使用了this關鍵字。很多教科書會告訴你,this指的是函數運行時所在的環境。對於obj.foo()來説,foo運行在obj環境,所以this指向obj;對於foo()來説,foo運行在全局環境,所以this指向全局環境。所以,兩者的運行結果不一樣。

這種解釋沒錯,但是教科書往往不告訴你,為什麼會這樣?也就是説,函數的運行環境到底是怎麼決定的?舉例來説,為什麼obj.foo()就是在obj環境執行,而一旦var foo = obj.foofoo()就變成在全局環境執行?

這篇文章從內存的數據結構以及函數如何存儲方法進行了解釋。
下面簡要總結下:
JavaScript 中函數也是對象。上面代碼中foo函數有一塊獨立的內存來保存函數,obj.foo保存的是這塊內存的地址。由於函數是一個單獨的值,所以它可以在不同的環境(上下文)執行(即被任意對象調用)
比如obj.foo() foo是在obj內部的指向函數的屬性,並通過 obj.foo 間接引用了函數,所以函數調用位置上下文對象是obj,所以,this 指代的是 foo函數的調用者obj,打印1
foo() 是直接使用不帶任何修飾的函數引用進行調用, 相當於獨立調用,而非由其他對象所調用,這種情況在非嚴格環境下,this會默認綁定到全局對象window, 所以打印2
注:嚴格環境下會是undefined

下面再來看幾個例子:
例一:
image.png
通過上述解釋,funs通過obj.funs()調用,所以funs內this是obj,雖然fun2定義在funs內部,但是上面説過:this指的是函數運行時所在的環境(上下文對象)。 和定義在哪裏無關。funs() 通過函數引用直接調用,所以會使用默認綁定到window
上述例子我們知道了this綁定的兩種情況:

  1. 隱式綁定:由指定對象間接調用函數,綁定到指定對象
  2. 默認綁定:非嚴格模式下window,嚴格模式下undefined

那麼如何讓例一中fun2()的this也綁定到obj呢?
改造下:

let obj = {
    funs() {
        let fun2 = function() { console.log(this) } //obj
        fun2.call(this)
    }
}
obj.funs()

通過call, apply, bind等方法可以顯示指定this綁定到哪個對象。這便是this的顯示綁定。

new

當我們使用new來調用函數的時候,此時函數被當作構造函數來處理,具體new的實現可以參考這篇文章,構造函數內的this綁定到新生成的對象。

function foo(a) {     
    this.a = a; 
}  
 
var bar = new foo(2); 
console.log( bar.a ); // 2

this綁定的四種規則

至此,上述內容可以總結出四種規則來確定this的值:

  1. 函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。 var bar = new foo()
  2. 函數是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是 指定的對象。 var bar = foo.call(obj2)
  3. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上 下文對象。 var bar = obj1.foo()
  4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined,否則綁定到 全局對象。 var bar = foo()

以上出自《你不知道的JS》

this詞法

我們之前介紹的四條規則已經可以包含所有正常的函數。但是 ES6 中介紹了一種無法使用 這些規則的特殊函數類型:箭頭函數。
箭頭函數並不是使用 function 關鍵字定義的,而是使用被稱為“胖箭頭”的操作符 => 定 義的。箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)作用域來決 定 this。

將上面的例一改造下:

let obj = {
    funs() {
        let fun2 = () => { console.log(this) } //obj
        fun2()
    }
}
obj.funs()

箭頭函數使用詞法作用域(在定義時確定),fun2定義在函數funs內,所以函數funs環境決定了fun2的環境。
obj.funs()調用後funs的環境是obj, 所以fun2的環境也是obj, 後面調用時打印出obj。
注:箭頭函數this值無法被修改

綁定例外

在某些場景下 this 的綁定行為會出乎意料,你認為應當應用其他綁定規則時,實際上應用 的可能是默認綁定規則。

function foo() {   
console.log( this.a ); 
} 
 
var a = 2; 
 
foo.call( null ); // 2

如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值 在調用時會被忽略,實際應用的是默認綁定規則。

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

另一個需要注意的是,你有可能(有意或者無意地)創建一個函數的“間接引用”,在這 種情況下,調用這個函數會應用默認綁定規則。
如示例3 賦值表達式 foo.bar = foo.bar 的返回值是目標函數的引用,函數實際是直接使用不帶任何修飾的(匿名)函數引用進行調用, 類似與通過 foo() 調用(注:這裏若調用foo()會報not function錯誤哦,因為全局未聲明) 而不是 foo.bar()調用 。根據我們之前説過的,這裏會應用默認綁定。示例4,5 分別使用 邏輯運算和逗號運算符返回了目標函數的引用,因此結果同示例3。

注意:對於默認綁定來説,決定 this 綁定對象的並不是調用位置是否處於嚴格模式,而是 函數體是否處於嚴格模式。如果函數體處於嚴格模式,this 會被綁定到 undefined,否則 this 會被綁定到全局對象。

這裏是關於this的測試題

參考來源

書籍《你不知道的JavaScript》
JavaScript 的 this 原理 -- 阮一峯
JavaScript深入之從ECMAScript規範解讀this -- 冴羽
JavaScript深入之new的模擬實現 -- 冴羽

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.