this指向 this定義 this用於指定對當前對象的引用。 this的兩種綁定方式 為什麼說是兩種?在《你不知道的JavaScript(上捲)》一書中共提到了四種綁定方式。如下: 預設綁定 隱式綁定 顯式綁定 new綁定 實際上這四種綁定方式有兩種方式重覆了(隱式綁定和new綁定)。我們在學習過 ...
目錄
this指向
this
定義
this
用於指定對當前對象的引用。
this
的兩種綁定方式
為什麼說是兩種?在《你不知道的JavaScript(上捲)》一書中共提到了四種綁定方式。如下:
-
預設綁定
-
隱式綁定
-
顯式綁定
-
new
綁定
實際上這四種綁定方式有兩種方式重覆了(隱式綁定和new
綁定)。我們在學習過程中應帶有辯證思維去看待問題。基於獺子細緻的分析與總結,實際上this
的綁定可以認為只存在兩種方式:預設綁定和顯式綁定。分析如下:
預設綁定
在嚴格模式下,全局作用域的this
對象會變為undefined
。在非嚴格模式的普通函數若不作為對象方法,它的this
綁定都會自動綁定到window
全局對象上,即預設綁定。
顯式綁定
函數可以使用call
/apply
/bind
等方法綁定this
對象。這種方式屬於強制綁定措施,this
的指向是可預見的(即綁定誰就指向誰),我們重點談談他們的用法。
基本格式:函數名稱.call(要綁定的對象, 參數列表)
call
:接受一個參數列表。會立即執行。
apply
:接受數組形式的參數。會立即執行。
bind
:接受一個參數列表。返回原函數拷貝,不會立即執行。
若需應用,一般可以這樣思考:我們想要函數的this
值指向哪個對象?
let a = { name: '小紅' }
function getName() {
console.log(this.name)
}
getName() // 這裡預設綁定全局對象
getName.call(a) // 顯式綁定對象a
new
綁定(具有顯式綁定效果)
我們來看看new
關鍵字的執行過程:
- 創建一個新的空對象
- 將構造函數的原型賦給新創建對象(實例)的隱式原型
- 利用顯式綁定將構造函數的
this
綁定到新創建對象併為其添加屬性 - 返回這個對象
很顯然,這裡的this
的指向同樣是可預見的。
基於上面的執行過程,我們可以手寫實現一下(面試題):
function myNew(fn, ...args) { // 構造函數作為參數
let obj = {}
obj.__proto__ = fn.prototype
fn.apply(obj, args)
return obj
}
一步一行代碼,是不是很簡潔。
註意:我們可以理解為new
的過程應用了顯式綁定
隱式綁定(具有顯式綁定效果)
關於隱式綁定,這裡提一下書里被翻譯過的作者原話:
另一條需要考慮的規則是調用位置是否有上下文對象
其實隱式綁定也可以理解為它應用了顯式綁定。比如我們在利用模板字面量創建對象的時候,普通函數作為對象方法擁有與顯式綁定同樣的效果,可以理解為已經執行了顯式綁定這一過程。即函數會綁定對應的實例對象。如下:
let a = {
x: 10,
y: function () { // 作為對象方法,存在顯示綁定效果。
console.log(this)
}
}
a.y()
// 輸出結果:a { x: 10 y: f } 即函數內部的this指向被綁定的實例對象
this
綁定優先順序
順序:顯式綁定 > 預設綁定
註意:箭頭函數本身沒有this
,不會應用以上規則
函數的this
指向
關於this
指向,我們會更多的關註函數內部的this
指向。一般而言會考察以下兩種類型的題目:
- 自定義對象內部函數的
this
指向 - 全局對象下函數的
this
指向
可以利用以下準則去解決this
指向問題。實際上我們只需處理這兩種函數:
-
對於非嚴格模式下的普通函數會有兩個情況:
1.1 作為對象方法,
this
會綁定對象(執行new綁定過程)。1.2 不作為對象方法,在非嚴格模式下
this
預設綁定window
。 -
箭頭函數沒有
this
。它只會繼承最近一層普通函數或全局作用域的this
。
註意:call
/apply
/bind
方法不能改變箭頭函數的this
指向,因為箭頭函數本身沒有this
。
我們儘量一次性解決所有this
指向問題。首先設計這樣兩個結構,如下:
// 普通函數結構
function a(){} // 普通函數聲明
setTimeout(function(){})// 內置函數
(function(){})() // 立即執行函數
return function(){} // 匿名函數
// 箭頭函數結構
let a = ()=>{} // 箭頭函數聲明
setTimeout(()=>{}) // 內置函數
(()=>{})() // 立即執行函數
return ()=>{} // 匿名函數
利用上面的結構。分析第一種情況。如下:
let obj = {
fun: function () { // 這裡是普通函數
console.log(this) // 普通作為對象方法定義。內部this指向obj
// 以下普通函數都不作為對象方法定義,this全部指向window
function a() { console.log(this) }; a(); // 普通函數聲明執行
setTimeout(function () { console.log(this) });// 內置函數
(function () { console.log(this) })(); // 立即執行函數
return function () { console.log(this) }; // 匿名函數
},
arr: () => { // 這裡是箭頭函數
console.log(this) // 箭頭函數的this俺規則繼承全局作用域指向window
// 以下普通函數都不作為對象方法定義,this全部指向window
function a() { console.log(this) }; a(); // 普通函數聲明執行
setTimeout(function () { console.log(this) });// 內置函數
(function () { console.log(this) })(); // 立即執行函數
return function () { console.log(this) }; // 匿名函數
}
}
obj.fun()() // 會執行普通函數內部的所有普通函數和返回的匿名函數
obj.arr()() // 會執行箭頭函數內部的所有普通函數和返回的匿名函數
輸出結果:第一個this
為obj
對象,後面九個this
全是window
對象
接下來分析第二種情況。如下:
let obj = {
fun: function () { // 這裡是普通函數
console.log(this) // 普通作為對象方法定義。this指向obj
// 以下是箭頭函數,它的this按規則繼承最近一次普通函數即全部指向obj
let a = () => { console.log(this) }; a();// 箭頭函數聲明
setTimeout(() => { console.log(this) }); // 內置函數
(() => { console.log(this) })(); // 立即執行函數
return () => { console.log(this) }; // 匿名函數
},
arr: () => { // 這裡是箭頭函數
console.log(this) // 箭頭函數的this俺規則繼承全局作用域指向window
// 以下是都是箭頭函數,它的this按規則繼承全局作用域全部指向window
let a = () => { console.log(this) }; a();// 箭頭函數聲明
setTimeout(() => { console.log(this) }); // 內置函數
(() => { console.log(this) })(); // 立即執行函數
return () => { console.log(this) }; // 匿名函數
}
}
obj.fun()() // 會執行普通函數內部的所有箭頭函數和返回的匿名函數
obj.arr()() // 會執行箭頭函數內部的所有箭頭函數和返回的匿名函數
輸出結果:前五個this
都是obj
對象,後面五個this
都是window
對象
分析第三種情況。如下:
// 箭頭函數和普通函數放在全局中聲明,全部指向window
function fun() { console.log(this) }; fun(); // 普通函數聲明執行
setTimeout(function () { console.log(this) });// 內置函數
(function () { console.log(this) })(); // 立即執行函數
let arr = () => { console.log(this) }; arr(); // 箭頭函數聲明
setTimeout(() => { console.log(this) }); // 內置函數
(() => { console.log(this) })(); // 立即執行函數
輸出結果:六個this
全是window
對象
總結解題的關鍵點:首先判斷是箭頭函數還是普通函數。箭頭函數只會按規則繼承this
指向。普通函數則要分兩種情況:作為對象方法和不作為對象方法:作為對象方法this
會綁定到對象上,不作為對象方法this
則綁定到window
(非嚴格模式)。
補充說明:普通函數作為對象方法實際上已經執行了new
綁定(可以看上面的手寫new過程)。因此普通函數作為對象方法,它的this
會指向對象。
this
綁定丟失的情況
函數別名(作為參數被傳遞或調用):例如obj.foo
或手寫Promise中的resolve
方法。我們可以理解為他是一個已經定義好的函數。它的this指向具體要看他在哪裡使用,而且要分清楚它是普通函數還是箭頭函數。
obj.foo // 是一個函數
// 等價於下麵我們定義好的普通函數或箭頭,如下
let foo = function{}{ console.log(this) }
let foo = ()=>{ console.log(this) }
補充說明:實際上this丟失只有這一種情況
手寫call
、apply
和bind
前置知識:ES6 剩餘參數、Function.prototype
原型方法定義,在調用時每個function
可通過隱式原型(原型鏈)找到此方法。
Function.prototype.myCall = function (obj, ...args) {
obj = obj === null || obj === undefined ? window : obj;
return (() => {
obj.method = this; //作為臨時方法傳遞給對象
obj.method(...args);
delete obj.method;
})();
}
call
和apply
區別,apply
的接受參數為數組形式。
Function.prototype.myApply = function (obj, ...args) {
obj = obj === null || obj === undefined ? window : obj;
return (() => {
obj.method = this; //作為臨時方法傳遞給對象
obj.method(...args[0]);
delete obj.method;
})();
}
普通版:bind
方法是硬綁定。返回值為原函數的拷貝,其this
值不可再修改。
Function.prototype.myBind = function (obj, ...args1) {
obj = obj === null || obj === undefined ? window : obj;
return (...args2) => {
this.apply(obj, args1.concat(args2));
};
}
進階版:bind
方法可支持new
關鍵字
Function.prototype.myNewBind = function (obj, ...args1) { // 函數 1
obj = obj === null || obj === undefined ? window : obj;
let self = this;
let fn = function (...args2) { // 函數 2
return self.apply(this instanceof fn ? this : obj, args1.concat(args2));
};
fn.prototype = Object.create(self.prototype); // 維持其原型
fn.prototype.constructor = fn
return fn;
}
過程解析:
- 為什麼要維持原形?
原生函數中bind
在執行new
操作時會保留其所綁定函數的原型,我們希望在執行new
關鍵字後myNewBind
函數也能擁有同樣的效果。若沒有進行維持原型這一步操作,我們的new
操作效果其實是把返回的函數 2 作為構造函數操作去生成實例,會丟失之前所綁定函數的原型,無法實現繼承。
- 為什麼使用
instanceof
?
判斷當前對象是否為返回的構造函數所生成的實例對象。若是則認為執行了new
關鍵字操作,返回的構造函數內部需要this
代表新的實例對象,而不是舊的obj
對象。
- 為什麼要使用
Object.create()
?
我們希望返回的函數也有自己的獨立原型。直接將一個構造函數原型賦給另一個構造函數原型會使兩個原型對象的數據捆綁(引用值特點)在一起,即需要保持原型對象數據的獨立性。
Object.create()
的運行過程手寫如下:
function createObject(obj) { // 參數為原型對象
let temp = function () { };
temp.prototype = obj;
return new temp();
}
參考