簡言
最近在做前端知識的複習和整理,有了一些自己新的體會。更多在於記錄,透過反覆的溫習,寫筆記改變自己以前學習知識點的誤區
關於Bind,Apply,Call
大家本能知道,當函式呼叫他們的時候就會將函式中的this,顯示指向他們的第一個引數(新物件),那麼為什麼大家在面試或者其他場景下仍然會因為這個this指向而發愁呢?
答案是,大家沒理解this在什麼場景下才會作用
作用域,上下文
因為js是一門指令碼語言,因此它不具備靜態編譯的特徵,很多程式碼需要在程式執行過程中才會正確理解他的含義
但是js神奇之處在於,js有著靜態(作用域)
和動態(執行上下文)
,兩種表現。
靜態 - 程式執行之前
當程式執行之前,程式會掃描所有的程式碼,會先以函式
為範圍劃分作用域(全域性作用域,函式作用域)
作用域:以一個函式區域為邊界的範圍
然後各個作用域會將各自的變數和函式做提升,提升到當前作用域的頂層,當然變數提升也僅僅只侷限於使用var
宣告的變數,且只提升變數的宣告
console.log(a); // undefined,因為只提升了a變數的宣告,相當於 var a; console.log(a); a = 1;
var a = 1;
function o (){
console.log(b); // undefined,只提升了b變數的宣告
var b = 2;
}
o();
接下來我們再看看下面這個例子
console.log(a);
a = 1;
function o (){
console.log(b);
b = 2;
}
o();
執行結果後,a輸出1,b輸出b is not defined
,這是為什麼呢?
對比上述例子我們可以得到結論:
- 如果在
全域性作用域
下沒有使用var
宣告變數,則當程式執行前掃描,會將沒有使用宣告識別符號的變數直接繫結到window
上,而在程式執行時執行的console.log(a)相當於執行console.log(window.a)
- 如果在
函式作用域
下沒有使用var
宣告的變數,則變數不會進行變數提升,也不會被繫結在window上,因為函式已經將b限制在函式的作用域內了,外部無法訪問。因此當函式o執行時,呼叫console.log(b),當前函式作用域下沒有找到b的宣告,那麼就會根據作用域鏈
,向函式作用域外查詢,發現全域性作用域中也沒有找到b的宣告,則認定b未被定義
那麼這就是在靜態情況下,程式幫開發者做的預掃描,根據作用域範圍,提升作用域中的變數以及函式
動態 - 執行上下文(this)
那麼在動態情況下,this又是怎麼表現的呢。大家也都知道this有幾個指向,那也到這裡就結束了,一做起面試題來就跪了,this指向又分為隱式指向和顯式指向
隱式指向
這裡有個兒歌啟蒙法:
家門口前有條河,上面有座橋,裡面有群鴨
如果程式沒有執行的時候,程式肯定靜態的按家門口前有條河
,上面有座橋
,裡面有群鴨
,來一條條解析語句,那麼這裡有個問題,靜態解析的時候,上面有座橋,裡面有群鴨
,在我們不知道河的前提下,怎麼知道上面
,裡面
代指是什麼地方呢,因此我們就需要唱出家門口前有條河
,那麼這個家門口前的河就是this
,this的上面有座橋,this的裡面有群鴨
總結就是:
- this是在程式執行時讀取上下文獲取的
- this指向呼叫(this)所在函式的那個物件
當程式執行時,函式真正被呼叫的時候,this就會指向那個呼叫函式的物件
第一點就是大家平時忽略最多的,因此總是做面試題時,找不到this指向
顯式指向
顯式改變this指向,這裡大家都知道使用call,bind,apply
call,apply,bind的區別
- 傳參方式不同
- 執行結果不同
傳參方式不同
call和bind從第二個引數開始使用的是依次傳入,而apply第二個引數接收的是一個引數ArrayList
執行結果不同
call和bind會將this指向到新物件後,立即執行原函式的函式體內容,而bind則會將這個原函式執行過程封裝成一個函式作為結果返回,因此bind最後會返回一個函式
接下來我們就講講怎麼去手寫一個Bind
手寫Bind
首先我們要確定以下需求
- bind要被掛載到什麼地方 - Function.prototypes
- bind的輸入(第一個引數為this所指向的新物件,第二個引數~最後一個引數依次傳入)
- bind的輸出(返回一個函式,這個函式中返回原函式改變this指向後的執行結果)
- 原函式改變this指向需要構建新的apply
- apply掛載到什麼地方 - Function.prototypes
- apply的輸入(第一個引數為新物件,也就是bind的第一個引數, 第二個引數為傳入的引數列表-來源於bind第二個引數~最後一個引數)
- apply的輸出(執行原函式-呼叫bind的那個函式)並返回函式執行結果
- 做apply的最佳化
// 1. bind要被掛載到 Function.prototypes
Function.prototype.myBind = function () {
// 8. 做apply的最佳化
if (typeof this !== "function") {
// 8-1. 如果this不是函式,則丟擲錯誤
throw new TypeError("this is not a function");
}
// 2. bind的輸入(第一個引數為this所指向的新物件,第二個引數~最後一個引數依次傳入)
const args = Array.prototype.slice.call(arguments); // arguments是一個類陣列,並不是一個真正的陣列,因此需要轉換成真正的陣列
// 3-1. 獲取新的this, 同時確保newThis一定有值
const newThis = args.shift() || window;
// 3-2, 獲取原函式
const _this = this; // 當原函式呼叫mybind的時候,this就指向原函式
// 3. bind的輸出(返回一個函式,這個函式中返回原函式改變this指向後的執行結果)
return function () {
// 3-3. 返回原函式改變this指向後的執行結果
return _this.myApply(newThis, args);
};
};
// 4. 原函式改變this指向需要構建新的apply
// 5. apply掛載到什麼地方 - Function.prototypes
Function.prototype.myApply = function (context) {
// 8. 做apply的最佳化
if (typeof this !== "function") {
// 8-1. 如果this不是函式,則丟擲錯誤
throw new TypeError("this is not a function");
}
// 6. apply的輸入(第一個引數為新物件,也就是bind的第一個引數, 第二個引數為傳入的引數列表-來源於bind第二個引數~最後一個引數)
// 6-1 傳入的物件一定有值
context = context || window;
// 6-2 將呼叫myApply的函式(this)隱式新增到context物件上,這裡的context就是上面的newThis
// 6-3 為了防止context上已經有fn,因此使用Symbol取唯一值
const fn = Symbol();
context[fn] = this;
// 7. apply的輸出(執行原函式-呼叫bind的那個函式)並返回函式執行結果
let result = arguments[1] ? context[fn](...arguments[1]) : context[fn]();
delete context[fn];
return result;
};
結語
後面一段時間我會持續更新,希望多多三連