深入一點 - 使用bind的時候發生了什麼呢?

從規範來看,Function.prototype.bind 是如何工作,以及如何來模擬bind操作。

簡單示例

如下簡單示例,普通對象 testObj 內部有一個b函數,接受一個普通參數,若參數爲空則輸出 this.a

const testObj = {
  a: 3,
  b: function(args) {
    console.log(args || this.a);
  },
};
testObj.b()
testObj.b(23)
const c = testObj.b
c()
c(23)
const c1 = testObj.b.bind(testObj, 50)
c1(70)

查看結果:
-w568

testObj.b 被重新賦值給 c 後,函數的的執行上下文已經改變,導致輸出爲 undefined。通過上面例子,如果採用 bind 後,則可以改變 testObj的執行上下文,並可以把默認值傳遞到參數函數列表.

倘若在 testObj.b內,加入 console.log(arguments), 則可以看到如下輸出:

50 70

bind 函數

bind函數是,Function 原型鏈上的函數,主要是改變函數執行上下文的同時,可以傳入函數參數值,並返回新的函數


如圖是mdn上的定義,

bind產生的函數就一個偏函數,就是說使用bind可以參數一個函數,然後接受新的參數。

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
詳細可看: https://github.com/mqyqingfeng/Blog/issues/43

規範定義

在EcmaScript的規範 15.3.4.5 中如下截圖:

bind 方法需要一個或更多參數,thisArg 和(可選的)arg1, arg2, 等等,執行如下步驟返回一個新函數對象:

1. 令 Target 爲 this 值 .
2. 如果 IsCallable(Target) 是 false, 拋出一個 TypeError 異常 .
3. 令 A 爲一個(可能爲空的)新內部列表,它包含按順序的 thisArg 後面的所有參數(arg1, arg2 等等)。
4. 令 F 爲一個新原生 ECMAScript 對象。
5. 依照 8.12 指定,設定 F 的除了 [[Get]] 之外的所有內部方法。
6. 依照 15.3.5.4 指定,設定 F 的 [[Get]] 內部屬性。
7. 設定 F 的 [[TargetFunction]] 內部屬性爲 Target。
8. 設定 F 的 [[BoundThis]] 內部屬性爲 thisArg 的值。
9. 設定 F 的 [[BoundArgs]] 內部屬性爲 A。
10. 設定 F 的 [[Class]] 內部屬性爲 "Function"。
11. 設定 F 的 [[Prototype]] 內部屬性爲 15.3.3.1 指定的標準內置 Function 的 prototype 對象。
12. 依照 15.3.4.5.1 描述,設定 F 的 [[Call]] 內置屬性。
13. 依照 15.3.4.5.2 描述,設定 F 的 [[Construct]] 內置屬性。
14. 依照 15.3.4.5.3 描述,設定 F 的 [[HasInstance]] 內置屬性。
15. 如果 Target 的 [[Class]] 內部屬性是 "Function", 則
    a. 令 L 爲 Target 的 length 屬性減 A 的長度。
    b. 設定 F 的 length 自身屬性爲 0 和 L 中更大的值。
16. 否則設定 F 的 length 自身屬性爲 0.
17. 設定 F 的 length 自身屬性的特性爲 15.3.5.1 指定的值。
18. 設定 F 的 [[Extensible]] 內部屬性爲 true。
19. 令 thrower 爲 [[ThrowTypeError]] 函數對象 (13.2.3)。
20. 以 "caller", 屬性描述符 {[[Get]]: thrower, [[Set]]: thrower,[[Enumerable]]: false, [[Configurable]]: false}, 和 false 作爲參數調用 F 的 [[DefineOwnProperty]] 內部方法。
21. 以 "arguments", 屬性描述符 {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, 和 false 作爲參數調用 F 的 [[DefineOwnProperty]] 內部方法。
22. 返回 F.
 bind 方法的 length 屬性是 1。

 Function.prototype.bind 創建的函數對象不包含 prototype 屬性或 [[Code]], [[FormalParameters]], [[Scope]] 內部屬

規範表達過程簡述如下:

  • 第1步,創建Target,並把 this 值給Target 【this爲當前執行環境, 可以理解爲當前函數】
  • 第3步,內部創建一個參數空的參數列表A,包含了bind參數中除了thisArg 之外的其他函數
  • 第4步,創建一個對象 F
  • 第6,7,8,9,10,11步,設置函數內部屬性,這裏第7步中的 [[TargetFunction]] 僅僅只有使用 bind 纔會生成,第8步設置 thisArg 新的函數的 this.
  • 第12,13,14步,需要設置對象F內部方法 [[Call]], [[Construct]], [[HasInstance]], 讓對象可以被調用
  • 第15步,讓對象F變成 [[Function]], 並設置新的函數 length, 新的length值 範圍是 0 ~ Target.length
  • 第18步,設置返回新函數可以任意添加屬性
  • 第20步,設置函數描述符號 caller屬性,
  • 第21步,設置函數參數描述符號, arguments
  • 最後返回對象F,也就是新執行函數。
在bind過程中,會重新設置 [[Call]] 相關函數內部方法,詳細可以看規範。

isCallable 定義

Object 內部屬性以及方法

用途

  • 創建綁定函數,例如react事件參數傳遞
  • 偏函數
  • 定時器修改this
  • 構造函數使用綁定函數
  • 快捷調用

最後

知道了過程,倘若不支持,該如何呢?來,搞一個手工的:

Function.prototype.bind2 = function bind2(thisArgs) {
  const aArgs = Array.prototype.slice.call(arguments, 1);
  const fThis = this;
  const fNOP = function() {};
  const fBound = function() {
    // 這段判斷是不是使用bind返回函數繼續bind
    return fThis.apply(this instanceof fBound ? this : fThis, aArgs.concat(Array.prototype.slice.call(arguments)));
  };
  // this === Function.prototype, 保證創建函數的原型鏈也爲undefined
  if (this.prototype) fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

我們實現方法依賴一些屬性方法:

  • Fucntion.prototype.apply
  • Function.prototype.call
  • Array.prototype.slice

而且我們實現方法有一個問題在於:

  • length 始終返回爲0,並沒有計算
  • 返回函數具有 prototype, 不符合規範

查看更規範的實現,點擊這裏

歡迎交流
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章