iT邦幫忙

2022 iThome 鐵人賽

DAY 11
2

前言

這篇將會介紹 bind() 的一些使用範例和實作一個簡單版本的 bind()。


bind() 語法 & 範例

語法: fn.bind(thisArg, arg1, arg2..., argn)

傳入參數和 call() 一樣,但是 bind() 會回傳一個新的函式。

使用範例:

const character = {
  name: "Simon",
  getIntro(introSentence) {
    return `${introSentence} ${this.name}.`;
  }
};

const getBindChar = character.getIntro.bind(character, 'My name is');
console.log(getBindChar()); // Simon

另外 bind() 也蠻適合搭配 currying、Partial application 等觀念一起使用,不過這內容較多,將會放在明天的文章做解說。


實作 bind()

透過實作 bind(),幫助自己更了解這個函式的運作及特性。

第一個版本的 myBind() 函式

首先會針對兩個重點去思考:

  1. bind() 能改變 this 的指向,可以使用 call() 去做這件事
  2. bind() 會回傳一個新的函式

像以下使用了原生的 bind(),this 就指向了 person 物件,並且回傳一個函式 result;

function showName(...args) {
  console.log(`${args} ${this.name}.`);
}

const person = { name: 'Harry' };

const result = showName.bind(person, 'My name is');

result(); // 'My name is Harry.'

根據上面思考的兩點,我們可以寫出初步版本的 bind():

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;
  return function () {
    return that.call(thisArg, ...args);
    // 等於 showName.call(person, "My name is");,this 指向 thisArg,也就是 person 物件
  };
};

同時這個版本的 bind() 也能處理下面兩點:

  1. 第一個參數若傳入 null 或是 undefined,this 指向 window
  2. 能處理原始型別,例如數字
const result = showName.myBind(null, 'My name is');
result(); // My name is .
// 在 Codepen 上則是出現 "My name is CodePen."

const isNumber = (obj) => Object.prototype.toString.myBind(obj)() === '[object Number]';
console.log(isNumber(2)); // true

第一版本的 bind()我寫成一個 Codepen 範例在此連結: bind() v1

第二個版本的 myBind() 函式

接著要考慮的是因為 bind() 會回傳一個函式,若那個回傳的函式被當作建構函式使用時,bind 綁定的 this 會依照 new 關鍵字調用 this(Day 8 文章中 this 指向情境的第四點)。

以下是用原生 bind() 執行後的結果:

const person = { name: "Harry" };

function createPerson(...args) {
  this.skill = "programming";
  console.log(...args); // 'My name is' 24 172
  console.log(this); // { skill: 'programming' }
}

createPerson.prototype.race = "Asian";

const createPersonConstructor = createPerson.bind(person, "My name is");

const member = new createPersonConstructor(24, 172);

console.log(member); // { skill: 'programming' }
console.log(member.race); // 'Asian'

但是當前第一個版本的 myBind() 函式印出的結果如下:

const person = { name: "Harry" };

function createPerson(...args) {
  this.skill = "programming";
  console.log(...args); // 'My name is'
  console.log(this); // { name: 'Harry', skill: 'programming' }
}

createPerson.prototype.race = "Asian";

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;
  return function () {
    return that.call(thisArg, ...args);
  };
};

const createPersonConstructor = createPerson.myBind(person, "My name is");

const member = new createPersonConstructor(24, 172);

console.log(member); // {}
console.log(member.race); // undefined

這個結果顯然和原生 bind() 不一樣!所以要進行一些調整。

這個版本將 returnFn 函式回傳的程式碼改寫成 return that.call(this instanceof returnFn ? this : thisArg, ...args);,透過 instanceof 去判斷回傳的函式是被當作建構函式還是一般的函式。

  1. 如果是被當作建構函式,因為 this 會指向建構函式的實體,所以 this instanceof returnFn 為 true,所以就維持原樣,this 做為第一個參數
  2. 如果是被當作普通函式,this instanceof returnFn 為 false,將傳入 bind() 的第一個參數 thisArg 做為 this 的指向

調整後如下:

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;
  
  const returnFn = function () {
    return that.call(this instanceof returnFn ? this : thisArg, ...args);
  };

  return returnFn;
};

第二版本的 bind() Codepen 範例: bind() v2

第三個版本的 myBind() 函式

經過上個版本的調整後,會發現印出來的值不太一樣了,createPerson 函式內的 this 指向和使用 bind() 時一樣,console.log(member); 印出的結果也和使用 bind() 時一樣。

不過 createPerson 函式內 console.log(...args); 不一樣,原因是因為沒有處理到建構函式傳入的參數,以範例來說就是 new createPersonConstructor(24, 172); 裡面的兩個參數。

const person = { name: "Harry" };

function createPerson(...args) {
  this.skill = "programming";
  console.log(...args); // 'My name is'
  console.log(this); // { skill: 'programming' }
}

createPerson.prototype.race = "Asian";

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;
  
  const returnFn = function () {
    return that.call(this instanceof returnFn ? this : thisArg, ...args);
  };

  return returnFn;
};

const createPersonConstructor = createPerson.myBind(person, "My name is");

const member = new createPersonConstructor(24, 172);

console.log(member); // { skill: 'programming' }
console.log(member.race); // undefined

所以我們來做些調整,使用 concat 將 bind() 的參數和建構函式的參數做合併。

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;

  const returnFn = function (...constructorArgs) {
    return that.call(
      this instanceof returnFn ? this : thisArg,
      ...args.concat([...constructorArgs])
    );
  };

  return returnFn;
};

這樣在 createPerson 就可以取到兩個函式的參數。

第三版本的 bind() Codepen 範例: bind() v3

第四個版本的 myBind() 函式

最後就是處理原型的問題,因為返回函式的原型 createPersonConstructor 不是 createPerson,所以在 member 物件查找 race 屬性會找不到。

createPerson.prototype.race = "Asian";

// 第三個版本的 myBind() 函式
console.log(member.race); // undefined

// 原生 bind():
console.log(member.race); // 'Asian'

這裡將 returnFn 的 prototype 修改為綁定函式 createPerson 的 prototype,returnFn 就可以透過原型鏈找到 race 屬性。

最終版本:

Function.prototype.myBind = function (thisArg, ...args) {
  const that = this;

  const returnFn = function (...constructorArgs) {
    return that.call(
      this instanceof returnFn ? this : thisArg,
      ...args.concat([...constructorArgs])
    );
  };

  returnFn.prototype = Object.create(this.prototype);
  return returnFn;
};

第四版本的 bind() Codepen 範例: bind() v4

bind() 的實作就到這邊,不過實作完我相信還有可以更優化的地方,所以也歡迎讀者不吝的提出,謝謝!

這篇也提到了一些繼承的觀念,包括 instanceof 和 Object.create() 函式,在後面的篇章也會介紹到~敬請期待!


參考資料 & 推薦閱讀

Implement your own — call(), apply() and bind() method in JavaScript

『面試的底氣』—— 手寫call、apply、bind

JavaScript深入之bind的模拟实现

尤雨溪寫的 bind()


上一篇
Day10-call()、apply() 函式介紹 & 實作
下一篇
Day12-介紹 Currying、Partial Application
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
lagagain
iT邦新手 2 級 ‧ 2022-10-16 08:48:59

英雄所見略同? 你可能不知道的Function.prototype.bind()

我也有嘗試實現一個myBind()或許可以相互參考~

harry xie iT邦研究生 1 級 ‧ 2022-10-16 09:43:59 檢舉

我來看看哩~

我要留言

立即登入留言