什麼是作用域?
幾乎所有編程語言最基本的功能之一,就是能夠儲存變量當中的值,並且能在之後對這個 值進行訪問或修改。事實上,正是這種儲存和訪問變量的值的能力將狀態帶給了程序。
若沒有了狀態這個概念,程序雖然也能夠執行一些簡單的任務,但它會受到高度限制,做 不到非常有趣。
但是將變量引入程序會引起幾個很有意思的問題,這些變量住在 哪裏?換句話説,它們儲存在哪裏?最重要的是,程序需要時如何找到它們?
這些問題説明需要一套設計良好的規則來存儲變量,並且之後可以方便地找到這些變量。 這套規則被稱為作用域。
簡而言之,作用域是根據名稱查找變量的一套規則。
(以上摘錄自《你不知道的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.foo,foo()就變成在全局環境執行?
這篇文章從內存的數據結構以及函數如何存儲方法進行了解釋。
下面簡要總結下:
JavaScript 中函數也是對象。上面代碼中foo函數有一塊獨立的內存來保存函數,obj.foo保存的是這塊內存的地址。由於函數是一個單獨的值,所以它可以在不同的環境(上下文)執行(即被任意對象調用)。
比如obj.foo() foo是在obj內部的指向函數的屬性,並通過 obj.foo 間接引用了函數,所以函數調用位置上下文對象是obj,所以,this 指代的是 foo函數的調用者obj,打印1
foo() 是直接使用不帶任何修飾的函數引用進行調用, 相當於獨立調用,而非由其他對象所調用,這種情況在非嚴格環境下,this會默認綁定到全局對象window, 所以打印2
注:嚴格環境下會是undefined
下面再來看幾個例子:
例一:
通過上述解釋,funs通過obj.funs()調用,所以funs內this是obj,雖然fun2定義在funs內部,但是上面説過:this指的是函數運行時所在的環境(上下文對象)。 和定義在哪裏無關。funs() 通過函數引用直接調用,所以會使用默認綁定到window
上述例子我們知道了this綁定的兩種情況:
- 隱式綁定:由指定對象間接調用函數,綁定到指定對象
- 默認綁定:非嚴格模式下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的值:
- 函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。 var bar = new foo()
- 函數是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是 指定的對象。 var bar = foo.call(obj2)
- 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上 下文對象。 var bar = obj1.foo()
- 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 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的模擬實現 -- 冴羽