JavaScript this 總結(含 ES6)
本文主要總結自《JavaScript 語言精粹》、部分總結自《JavaScript 高階程式設計》以及自己的經驗
四種呼叫模式
在 JavaScript 中,this 的值取決於呼叫模式,有四種呼叫模式,分別是方法呼叫模式、函式呼叫模式、構造器呼叫模式、Apply、call 呼叫模式。
方法呼叫模式
當一個函式被儲存為物件的一個屬性時,我們稱它為一個方法。當方法被呼叫時(通過 . 表示式或 [subscript] 下標表達式),this 繫結到該物件。
var name = "window", lzh = { name: "lzh", sayName: function(){ alert(this.name); // 輸出 "lzh" } } lzh.sayName();
函式呼叫模式
當一個函式並非一個物件的屬性時,那麼它就是被當做一個函式來呼叫的,以此模式呼叫函式時,this 被繫結到全域性物件。
這是語言設計上的一個錯誤。倘若語言設計正確,那麼當內部函式被呼叫時,this 應該仍然繫結到外部函式的 this 變數。
這個設計錯誤的後果是方法不能利用內部函式來幫助它工作。
ECMAScript6 的箭頭函式(注意只是箭頭函式)基本糾正了這個設計上的錯誤(注意只是基本上,但不是徹底地糾正了錯誤)
var name = "window", lzh = { name: "lzh", sayName: function(){ innerFunction(); function innerFunction(){ alert(this.name); } return function(){ alert(this.name); } } } lzh.sayName()();
上面這段程式碼 alert 的均是 window,從上面可以看出,不管外部環境的 this 是不是 window,通過函式呼叫模式呼叫的函式,this 指向 window。
來看一段 ES6 箭頭函式中的 this (上面提到箭頭函式基本糾正了設計上的錯誤)
var name = 'window'; var lzh = { name: 'lzh', sayName: function(){ return ()=> { console.log(this.name); } } } var iny = { name: 'iny' } lzh.sayName().apply(iny); // 輸出 lzh
其實轉換成 ES5 是這麼幹的:
var name = 'window';
var lzh = {
name: 'lzh',
sayName: function(){
var _this = this;
return function(){
console.log(_this.name);
}
}
}
var iny = {
name: 'iny'
}
lzh.sayName().apply(iny); // lzh
但如果ES6 中這麼寫
var name = "window";
var lzh = {
name: 'lzh',
sayName: () => {
console.log(this.name)
}
}
var iny = {
name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window
轉換成 ES5 卻是這樣的
var name = "window";
var _this = this;
var lzh = {
name: 'lzh',
sayName: function() {
console.log(_this.name)
}
};
var iny = {
name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window
// 有點失望
構造器呼叫模式
JavaScript 是一門基於原型繼承的語言。這意味著物件可以直接從其他物件繼承屬性。該語言是無型別的。
當今(書的中文版第一版出版時間是2009年)大多數語言都是基於類的語言。儘管原型繼承極富表現力,但它未被廣泛理解。
JavaScript 本身對它原型的本質也缺乏信心,所以它提供了一套和基於類的語言類似的物件構建語法。
如果在一個函式前面帶上 new 來呼叫,那麼背地裡將會建立一個連線到該函式的 prototype 成員的新物件,同時 this 會繫結到那個新物件上
如果建構函式返回的不是物件,則通過 new 呼叫建構函式返回背地裡建立的物件。
var Person = function(name){
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
var lzh = new Person("lzh");
console.log(lzh.getName()); // lzh
Apply、call 呼叫模式
因為 JavaScript 是一門函式式的面向物件程式語言,所以函式可以擁有方法。
每個函式都包含兩個非繼承而來的方法:apply()和 call()。這兩個方法的用途都是在特定的作用域中呼叫函式,實際上等於設定函式體內 this 物件的值。首先,apply()方法接收兩個引數:一個是在其中執行函式的作用域,另一個是引數陣列。其中,第二個引數可以是 Array 的例項,也可以是arguments 物件。call()方法與 apply()方法的作用相同,它們的區別僅在於接收引數的方式不同。對於 call()方法而言,第一個引數是 this 值沒有變化,變化的是其餘引數都直接傳遞給函式。換句話說,在使用call()方法時,傳遞給函式的引數必須逐個列舉出來
function sum(num1, num2){
console.log(this);
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 傳入 arguments 物件
}
function callSum2(num1, num2){
return sum.apply(null, [num1, num2]); // 傳入陣列
}
function callSum3(num1, num2){
return sum.call(null, num1, num2); // 一個一個地傳遞引數
}
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
alert(callSum3(10,10)); //20
從上面的程式碼可以看出,apply 的第一個引數是一個改變 sum 函式的 this 的值,但這裡不論傳進去的是 window 還是 null,內部 console.log 出來的都是 window 物件,還可以看出,apply 的第二個引數要麼是 arguments、要麼是一個數組。call 從第二個引數開始,就要一個一個的傳遞引數,而不能傳遞陣列或arguments。
單從上面的程式碼,不能很好的看出 apply、call 的長處,既然 call 能設定 this,那麼就能複用其它物件的方法,比如下面這個:
var lzh = {
name: 'lzh',
say: function(something){
alert(something + this.name);
}
}
var iny = {
name: 'iny'
}
lzh.say.apply(iny, ['hi, I am ']); // 輸出 hi I am iny
- iny 物件沒有 say 方法,但是又希望複用 lzh 的 say 方法,那麼就可以用 apply 或 call
- 這樣還是不能很明顯的看出 call、apply 的優越性,舉個現實點的例子,如何把 arguments 轉換成陣列,因為 arguments 不是陣列,是一個 Array like 的物件,就是有下標元素,可以通過 arguments[0]、arguments[1] 來訪問它的元素,但它沒有陣列的各種方法,比如
pop
,shift
,slice
等,操作 arguments 會不大方便,所以我們希望把 arguments 轉換成 陣列。如果我們大概明白 Array.prototype.slice 的實現原理的話,我們可以利用這個方法將 arguments 轉換成陣列。 - 第一步,講一下 Array.prototype.slice 簡易版的大概實現原理(原版應該是使用 C++ 實現的,功能和效能上都比這個簡易版的要好):
Array.prototype.slice = function(start, end){
var newArray = [];
if(start >= 0 && end <= this.length){
for(var i = start; i < end; i++){
newArray.push(this[i]);
}
}
return newArray;
}
var testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(testArray.slice(0, 2)); // [1, 2]
- 從上面我們可以看出,假如我們用 Array.prototype.slice.apply(arguments, 0, arguments.length),就相當於把 slice 內部的 this 換成了 arguments; 就可以把 arguments[0]、arguments[1]...等 push 到一個新陣列,這樣就可以成功的把 arguments 轉換成陣列了,於是就可以利用陣列的各種方法操作引數。當然,這裡只是簡易地重寫了一遍 slice,真實的 slice 可以不傳遞這裡的第三個引數,預設從 0 擷取到末尾。
- apply、call 在實現函式柯里化、物件繼承上也有很大的作用,這裡不詳細展開。
匿名函式中的 this
- 在網上看了很多關於 this 的部落格,都有介紹到匿名函式中的 this 指向 window 物件,但這種說法是不正確的,關鍵還是要看怎麼呼叫(就是前面介紹的4中呼叫方式),比如下面的程式碼
var name = "window",
lzh = function(){
return function(){
//這裡是匿名函式,但是 this 的值只有在呼叫的時候才能確定
alert(this.name);
}
}
var iny = {
name: 'iny',
sayName: lzh()
}
lzh()(); // window
iny.sayName(); // iny
從上面可以看出,this 的值在呼叫的時候決定
- 還有就是事件處理程式裡面的 this
在 DOM0 級、DOM2 級的事件處理程式中(onclick/addEventListener),this 指向繫結事件的那個元素,雖然不知道瀏覽器內部的具體實現,但可以猜測它是由 DOM 物件以方法呼叫的,屬於 DOM 物件的方法,而在 IE 舊版本的實現中,attachEvent 指定的事件處理程式的呼叫模式應該是函式呼叫模式,所以 this 指向 window。如有錯誤,還請指出。 - setTimeout、setInterval 裡面的 this 也指向 window,這個應該還是由呼叫模式決定的。
下面看幾道題目
- 題目1
var name = "window",
lzh = {
name: "lzh",
sayName: function(){
alert(this.name);
alert(name);
}
}
lzh.sayName();
題目1先 alert lzh
,再 alert window
,alert window 的原因是:sayName 實際上是一個閉包,它的活動物件裡有 this(指向 lzh 物件),但沒有 name,所以它往父級作用域鏈尋找 name
, 於是找到了全域性作用域的變數物件中的 windw
,所以 alert window
,如果想了解閉包的更多內容,可以點這裡。
- 題目2
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
滑動檢視答案:alert 的是 "The Window"
- 題目3
var name = "window",
person = {
name: 'lzh',
getName: function(){
return this.name;
}
}
console.log(person.getName());
console.log((person.getName)());
console.log((person.getName = person.getName)());
滑動檢視答案:輸出順序:lzh、lzh、window