目錄
- this指向
this定義this的兩種系結方式- 默認系結
- 顯式系結
new系結(具有顯式系結效果)- 隱式系結(具有顯式系結效果)
this系結優先級- 函式的
this指向 this系結丟失的情況- 手寫
call、apply和bind
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();
}
參考
你不知道的JavaScript (上卷)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540480.html
標籤:其他
下一篇:認識一下 Mobx
