重拾JS-手寫bind(延伸作用域理解,有助於面試)

coding-老王發表於2024-11-26

簡言

最近在做前端知識的複習和整理,有了一些自己新的體會。更多在於記錄,透過反覆的溫習,寫筆記改變自己以前學習知識點的誤區

關於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,這是為什麼呢?

對比上述例子我們可以得到結論:

  1. 如果在全域性作用域下沒有使用var宣告變數,則當程式執行前掃描,會將沒有使用宣告識別符號的變數直接繫結到window上,而在程式執行時執行的console.log(a)相當於執行console.log(window.a)
  2. 如果在函式作用域下沒有使用var宣告的變數,則變數不會進行變數提升,也不會被繫結在window上,因為函式已經將b限制在函式的作用域內了,外部無法訪問。因此當函式o執行時,呼叫console.log(b),當前函式作用域下沒有找到b的宣告,那麼就會根據作用域鏈,向函式作用域外查詢,發現全域性作用域中也沒有找到b的宣告,則認定b未被定義

那麼這就是在靜態情況下,程式幫開發者做的預掃描,根據作用域範圍,提升作用域中的變數以及函式

動態 - 執行上下文(this)

那麼在動態情況下,this又是怎麼表現的呢。大家也都知道this有幾個指向,那也到這裡就結束了,一做起面試題來就跪了,this指向又分為隱式指向顯式指向

隱式指向

這裡有個兒歌啟蒙法:

家門口前有條河,上面有座橋,裡面有群鴨

如果程式沒有執行的時候,程式肯定靜態的按家門口前有條河上面有座橋裡面有群鴨,來一條條解析語句,那麼這裡有個問題,靜態解析的時候,上面有座橋,裡面有群鴨,在我們不知道的前提下,怎麼知道上面裡面代指是什麼地方呢,因此我們就需要唱出家門口前有條河,那麼這個家門口前的河就是thisthis的上面有座橋,this的裡面有群鴨

總結就是:

  1. this是在程式執行時讀取上下文獲取的
  2. this指向呼叫(this)所在函式的那個物件

當程式執行時,函式真正被呼叫的時候,this就會指向那個呼叫函式的物件

第一點就是大家平時忽略最多的,因此總是做面試題時,找不到this指向

顯式指向

顯式改變this指向,這裡大家都知道使用call,bind,apply

call,apply,bind的區別

  1. 傳參方式不同
  2. 執行結果不同

傳參方式不同

call和bind從第二個引數開始使用的是依次傳入,而apply第二個引數接收的是一個引數ArrayList

執行結果不同

call和bind會將this指向到新物件後,立即執行原函式的函式體內容,而bind則會將這個原函式執行過程封裝成一個函式作為結果返回,因此bind最後會返回一個函式

接下來我們就講講怎麼去手寫一個Bind

手寫Bind

首先我們要確定以下需求

  1. bind要被掛載到什麼地方 - Function.prototypes
  2. bind的輸入(第一個引數為this所指向的新物件,第二個引數~最後一個引數依次傳入)
  3. bind的輸出(返回一個函式,這個函式中返回原函式改變this指向後的執行結果)
  4. 原函式改變this指向需要構建新的apply
  5. apply掛載到什麼地方 - Function.prototypes
  6. apply的輸入(第一個引數為新物件,也就是bind的第一個引數, 第二個引數為傳入的引數列表-來源於bind第二個引數~最後一個引數)
  7. apply的輸出(執行原函式-呼叫bind的那個函式)並返回函式執行結果
  8. 做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;
};

結語

後面一段時間我會持續更新,希望多多三連

相關文章