揭開 this & that 之迷
新手在入門 JavaScript 的過程中,一定會踩很多關于 this 的坑,出現問題的本質就是 this 指針的指向和自己想的不一樣。筆者在入門學習的過程中,也踩了很多坑,于是便寫下本篇文章記錄自己“踩坑”歷程。
一. this 在哪里?
在上篇 《從 JavaScript 作用域說開去》 分析中,我們知道,在 Execution Context 中有一個屬性是 this,這里的 this 就是我們所說的 this 。this 與上下文中可執行代碼的類型有直接關系, * this 的值在進入執行上下文時確定,并且在執行上下文運行期間永久不變。 *
this 到底取何值?this 的取值是動態的,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。因為 this 的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境。
所以 this 的作用就是用來指明執行上下文是在哪個上下文中被觸發的對象。令人迷惑的地方就在這里,同一個函數,當在不同的上下文進行調用的時候,this 的值就可能會不同。也就是說,this 的值就是函數調用表達式(也就是函數被調用的方式)的 caller。
二. this & that 具體值得是誰?
目前接觸的有以下14種情況,筆者打算一一列舉出來,以后如果遇到了更多的情況,還會繼續增加。
既然 this 是執行上下文確定的,那么從執行上下文的種類進行分類,可以分為3種:
那么接下來我們就從 Global Execution Context 全局執行上下文,Function Execution Context 函數執行上下文,Eval Execution Context Eval執行上下文 這三類,具體談談 this 究竟指的是誰。
(一). 全局執行上下文
1. 非嚴格模式下的函數調用
這是函數的最通常用法,屬于全局性調用,因此 this 就代表全局對象 Global。
var name = 'halfrost';
function test() {
console.log(this); // window
console.log(this.name); // halfrost
}
test();
在全局上下文(Global Context)中,this 總是 global object,在瀏覽器中就是 window 對象。
2. 嚴格模式下的函數調用
嚴格模式由 ECMAScript 5.1 引進,用來限制 JavaScript 的一些異常處理,提供更好的安全性和更強壯的錯誤檢查機制。使用嚴格模式,只需要將 'use strict' 置于函數體的頂部。這樣就可以將上下文環境中的 this 轉為 undefined。這樣執行上下文環境不再是全局對象,與非嚴格模式剛好相反。
在嚴格模式下,情況并不是僅僅是 undefined 這么簡單,有可能嚴格模式夾雜著非嚴格模式。
先看嚴格模式的情況:
'use strict';
function test() {
console.log(this); //undefined
};
test();
上面的這個情況比較好理解,還有一種情況也是嚴格模式下的:
function execute() {
'use strict'; // 開啟嚴格模式
function test() {
// 內部函數也是嚴格模式
console.log(this); // undefined
}
// 在嚴格模式下調用 test()
// this 在 test() 下是 undefined
test(); // undefined
}
execute();
如果嚴格模式在外層,那么在執行作用域內部聲明的函數,它會繼承嚴格模式。
接下來就看看嚴格模式和非嚴格模式混合的情況。
function nonStrict() {
// 非嚴格模式
console.log(this); // window
}
function strict() {
'use strict';
// 嚴格模式
console.log(this); // undefined
}
這種情況就比較簡單了,各個模式下分別判斷就可以了。
(二).函數執行上下文
3. 函數調用
當通過正常的方式調用一個函數的時候,this 的值就會被設置為 global object(瀏覽器中的 window 對象)。
嚴格模式和非嚴格模式的情況和上述全局執行上下文的情況一致,嚴格模式對應的 undefined ,非嚴格模式對應的 window 這里就不再贅述了。
4. 方法作為對象的屬性被調用
var person = {
name: "halfrost",
func: function () {
console.log(this + ":" + this.name);
}
};
person.func(); // halfrost
在這個例子里面的 this 調用的是函數的調用者 person,所以會輸出 person.name 。
當然如果函數的調用者是一個全局對象的話,那么這里的 this 指向又會發生變化。
var name = "YDZ";
var person = {
name: "halfrost",
func: function () {
console.log(this + ":" + this.name);
}
};
temp = person.func;
temp(); // YDZ
在上面這個例子里面,由于函數被賦值到了另一個變量中,并沒有作為 person 的一個屬性被調用,那么 this 的值就是 window。
上述現象其實可以描述為,“ 從一個類中提取方式時丟失了 this 對象 ”。針對這個現象可以再舉一個例子:
var counter = {
count: 0,
inc: function() {
this.count ++;
}
}
var func = counter.inc;
func();
counter.count; // 輸出0,會發現func函數根本不起作用
這里我們雖然把 counter.inc 函數提取出來了,但是函數里面的 this 變成了全局對象了,所以 func() 函數執行的結果是 window.count++。然而 window.count 根本不存在,且值是 undefined,對 undefined 操作,得到的結果只能是 NaN。
驗證一下,我們打印全局的 count:
count // 輸出是 NaN
那么這種情況我們應該如何解決呢?如果就是想提取出一個有用的方法給其他類使用呢?這個時候的正確做法是使用 bind 函數。
var func2 = counter.inc.bind(counter);
func2();
counter.count; // 輸出是1,函數生效了!
5. 構造函數的調用
所謂構造函數就是用來 new 對象的函數。嚴格的來說,所有的函數都可以 new 一個對象,但是有些函數的定義是為了 new 一個對象,而有些函數則不是。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object、Array、Function等。
function person() {
this.name = "halfrost";
this.age = 18;
console.log(this);
}
var ydz = new person(); // person {name: "halfrost", age: 18}
console.log(ydz.name, ydz.age); // halfrost 18
如果是構造函數被調用的話,this 其實指向的是 new 出來的那個對象。
如果不是被當做構造函數調用的話,情況有所區別:
function person() {
this.name = "halfrost";
this.age = 18;
console.log(this);
}
person(); // Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
如果不是被當做構造函數調用的話,那就變成了普通函數調用的情況,那么這里的 this 就是 window。
構造函數里面如果還定義了 prototype,this 會指向什么呢?
function person() {
this.name = "halfrost";
this.age = 18;
console.log(this);
}
person.prototype.getName = function() {
console.log(this.name); // person {name: "halfrost", age: 18} "halfrost"
}
var ydz = new person(); // person {name: "halfrost", age: 18}
ydz.getName();
在 person.prototype.getName 函數中,this 指向的是 ydz 對象。因此可以通過 this.name 獲取 ydz.name 的值。
其實,不僅僅是構造函數的 prototype,即便是在整個原型鏈中,this 代表的也都是當前對象的值。
6. 內部函數 / 匿名函數 的調用
如果在一個對象的屬性是一個方法,這個方法里面又定義了內部函數和匿名函數,那么它們的 this 又是怎么樣的呢?
var context = "global";
var test = {
context: "inside",
method: function () {
console.log(this + ":" +this.context);
function f() {
var context = "function";
console.log(this + ":" +this.context);
};
f();
(function(){
var context = "function";
console.log(this + ":" +this.context);
})();
}
};
test.method();
// [object Object]:object
// [object Window]:global
// [object Window]:global
從輸出可以看出,內部函數和匿名函數里面的 this 都是指向外面的 window。
7. call() / apply() / bind() 的方式調用
this 本身是不可變的,但是 JavaScript 中提供了 call() / apply() / bind() 三個函數來在函數調用時設置 this 的值。
這三個函數的原型如下:
// Sets obj1 as the value of this inside fun() and calls fun() passing elements of argsArray as its arguments.
fun.apply(obj1 [, argsArray])
// Sets obj1 as the value of this inside fun() and calls fun() passing arg1, arg2, arg3, ... as its arguments.
fun.call(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])
// Returns the reference to the function fun with this inside fun() bound to obj1 and parameters of fun bound to the parameters specified arg1, arg2, arg3, ....
fun.bind(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])
在這3個函數里面,this 都是對應的第一個參數。
var rabbit = { name: 'White Rabbit' };
function concatName(string) {
console.log(this === rabbit); // => true
return string + this.name;
}
// 間接調用
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'
apply() 和 call() 能夠強制改變函數執行時的當前對象,讓 this 指向其他對象。apply() 和 call() 的區別在于,apply() 的入參是一個數組,call() 的入參是一個參數列表。
apply() 和 call(),它倆都立即執行了函數,而 bind() 函數返回了一個新的函數,它允許創建預先設置好 this 的函數 ,并可以延后調用。
function multiply(number) {
'use strict';
return this * number;
}
// 創建綁定函數,綁定上下文2
var double = multiply.bind(2);
// 調用間接調用
double(3); // => 6
double(10); // => 20
bind() 函數實質其實是實現了,原始綁定函數共享相同的代碼和作用域,但是在執行時擁有不同的上下文環境。
bind() 函數創建了一個永恒的上下文鏈并不可修改。一個綁定函數即使使用 call() 或者 apply()傳入其他不同的上下文環境,也不會更改它之前連接的上下文環境,重新綁定也不會起任何作用。
只有在構造器調用時,綁定函數可以改變上下文,然而這并不是特別推薦的做法。
function getThis() {
'use strict';
return this;
}
var one = getThis.bind(1);
// 綁定函數調用
one(); // => 1
// 使用 .apply() 和 .call() 綁定函數
one.call(2); // => 1
one.apply(2); // => 1
// 重新綁定
one.bind(2)(); // => 1
// 利用構造器方式調用綁定函數
new one(); // => Object
只有 new one() 時可以改變綁定函數的上下文環境,其他類型的調用結果是 this 永遠指向 1。
8. setTimeout、setInterval 中的 this
《 javascript 高級程序設計》中寫到:“超時調用的代碼需要調用 window 對象的 setTimeout 方法”。setTimeout/setInterval 執行的時候,this 默認指向 window 對象,除非手動改變 this 的指向。
var name = 'halfrost';
function Person(){
this.name = 'YDZ';
this.sayName=function(){
console.log(this); // window
console.log(this.name); // halfrost
};
setTimeout(this.sayName, 10);
}
var person=new Person();
上面這個例子如果想改變 this 的指向,可是使用 apply/call 等,也可以使用 that 保存 this。
值得注意的是: setTimeout 中的回調函數在嚴格模式下也指向 window 而不是 undefined !
'use strict';
function test() {
console.log(this); //window
}
setTimeout(test, 0);
因為 setTimeout 的回調函數如果沒有指定的 this ,會做一個隱式的操作,將全局上下文注入進去,不管是在嚴格還是非嚴格模式下。
9. DOM event
當一個函數被當作event handler的時候,this會被設置為觸發事件的頁面元素(element)。
var body = document.getElementsByTagName("body")[0];
body.addEventListener("click", function(){
console.log(this);
});
// <body>…</body>
10. in-line 的方式調用
當代碼通過 in-line handler 執行的時候,this 同樣指向擁有該 handler 的頁面元素。
看下面的代碼:
document.write('<button onclick="console.log(this)">Show this</button>');
// <button onclick="console.log(this)">Show this</button>
document.write('<button onclick="(function(){console.log(this);})()">Show this</button>');
// window
在第一行代碼中,正如上面 in-line handler 所描述的,this 將指向 "button" 這個 element。但是,對于第二行代碼中的匿名函數,是一個上下文無關(context-less)的函數,所以 this 會被默認的設置為 window。
前面我們已經介紹過了 bind 函數,所以,通過下面的修改就能改變上面例子中第二行代碼的行為:
document.write('<button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>');
// <button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>
11. this & that
在 JavaScript 中,經常會存在嵌套函數,這是因為函數可以作為參數,并可以在合適的時候通過函數表達式創建。這會引發一些問題,如果一個方法包含一個普通函數,而你又想在后者的內部訪問到前者,方法中的 this 會被普通函數的 this 覆蓋,比如下面的例子:
var person = {
name: 'halfrost',
friends: [ 'AA', 'BB'],
loop: function() {
'use strict';
this.friends.forEach(
function(friend) { // (1)
console.log(this.name + ' knows ' + friend); // (2)
}
);
}
};
上述這個例子中,假設(1)處的函數想要在(2)這一行訪問到 loop 方法里面的 this,該怎么做呢?
如果直接去調用 loop 方法是不行的,會發現報了下面這個錯誤。
person.loop();
// Uncaught TypeError: Cannot read property 'name' of undefined
因為(1)處的函數擁有自己的 this,是沒有辦法在里面調用外面一層的 this 的。那怎么辦呢?
解決辦法有3種:
(1) that = this 我們可以把外層的 this 保存一份,一般會使用 that ,self,me,這些變量名暫存 this。
var person = {
name: 'halfrost',
friends: [ 'AA', 'BB'],
loop: function() {
'use strict';
var that = this;
this.friends.forEach(
function(friend) { // (1)
console.log(that.name + ' knows ' + friend); // (2)
}
);
}
};
person.loop();
// halfrost knows AA
// halfrost knows BB
這樣就可以正確的輸出想要的答案了。
(2) bind()
借助 bind() 函數,直接給回調函數的 this 綁定一個固定值,即函數的 this:
var person = {
name: 'halfrost',
friends: [ 'AA', 'BB'],
loop: function() {
'use strict';
var that = this;
this.friends.forEach(
function(friend) { // (1)
console.log(this.name + ' knows ' + friend); // (2)
}.bind(this)
);
}
};
person.loop();
// halfrost knows AA
// halfrost knows BB
(3) forEach() 的 thisValue
這種方法特定于 forEach()中,因為在這個方法的回調函數里面提供了第二個參數,我們可以利用這個參數,讓它來為我們提供 this:
var person = {
name: 'halfrost',
friends: [ 'AA', 'BB'],
loop: function() {
'use strict';
var that = this;
this.friends.forEach(
function(friend) { // (1)
console.log(this.name + ' knows ' + friend); // (2)
}, this );
}
};
person.loop();
// halfrost knows AA
// halfrost knows BB
12. 箭頭函數
箭頭函數是 ES6 增加的新用法。
var numbers = [1, 2];
(function() {
var get = () => {
console.log(this === numbers); // => true
return this;
};
console.log(this === numbers); // => true
get(); // => [1, 2]
// 箭頭函數使用 .apply() 和 .call()
get.call([0]); // => [1, 2]
get.apply([0]); // => [1, 2]
// Bind
get.bind([0])(); // => [1, 2]
}).call(numbers);
從上面的例子可以看出:
- 箭頭函數里面的 this 對象就是定義時候所在的對象,而不是使用時所在的對象。
-
箭頭函數不能被用來當做構造函數,于是也不能使用 new 命令。否則會報錯 TypeError: get is not a constructor 。
this 指向的固化,并不是因為箭頭函數內部有綁定 this 的機制,實際原因是箭頭函數根本就沒有自己的 this ,導致內部的 this 就是外層代碼塊的 this。正因為它沒有 this,所以也就不能作為構造函數了。
- 箭頭函數也不能使用 arguments 對象,因為 arguments 對象在箭頭函數體內不存在,如果要使用,可以用 rest 參數代替。同樣的,super,new.target 在箭頭函數里面也是不存在的。所以,arguments、super、new.target 這3個變量在箭頭函數里面都不存在。
-
箭頭函數里面也不能使用 yield 命令,因此箭頭函數也不能用作 Generator 函數。
-
由于箭頭函數沒有自己的 this ,當然就不能用 call()、apply()、bind() 這些方法去改變 this 的指向。
13. 函數綁定
雖然在 ES6 中引入了箭頭函數可以綁定 this 對象,大大減少了顯示綁定 this 對象的寫法(call、apply、bind)。鑒于箭頭函數有上述說到的4個缺點(不能當做構造函數,不能使用 arguments 對象,不能使用 yield 命令,不能使用call、apply、bind),所以在 ES7 中又提出了函數綁定運算符。用來取代 call、apply、bind 的調用。
函數綁定運算符是并排的雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動的將左邊的對象作為上下文環境(即 this 對象)綁定到右邊的函數上。
foo::bar // 等同于 bar.bind(foo)
foo::bar(...arguments) // 等同于 bar.apply(foo,arguments)
(三). Eval執行上下文
14. Eval 函數
Eval 函數比較特殊,this 指向就是當前作用域的對象。
var name = 'halfrost';
var person = {
name: 'YDZ',
getName: function(){
eval("console.log(this.name)");
}
}
person.getName(); // YDZ
var getName=person.getName;
getName(); // halfrost
這里的結果和方法作為對象的屬性被調用的結果是一樣的。
Reference:
《ECMAScript 6 Primer》
《javascript 高級程序設計》
來自:https://halfrost.com/javascript_this/