趣談js的bind牌膠水

前言

今天聊一聊js中的bind方法,主要從三個維度來闡述:why——>what——>how。文章雖經我的屢次校驗,對語言表述、代碼書寫等進行了認真審覈,但仍免不了有疏漏之處,如若發現,還望指出,鄙人將審而改之,如如有不爽之處,還望輕噴,理性交流,共同進步也。html

Why ???——> 爲何會誕生bind?

1. 背景講解bind誕生的緣由:

bind是ECMAscript5新增的一個方法,ECMAscript是js的編程語言實現(詳情可閱相關資料),ECMAscript5是當前主流瀏覽器的通用支持版本,這個版本的出現很大程度上是爲了解決js這門語言在誕生到發展過程當中出現的大量問題而提出的解決方案版本,在早期,js定位爲「網頁小助手」語言,只負責作簡單的校驗表單字段小活,一度還淪爲廣告彈框專屬語言,由於其尷尬的定位,因此js充滿各類意想不到的坑,你們一直也不怎麼重視它,直到基於Ajax技術的Gmail項目誕生(Gmail項目不是直接緣由,這裏只是藉機聊下js歷史),你們才發現利用js能夠作出這麼多牛逼的交互,一時間,各大公司蜂擁而至,大公司的項目每每預示着項目的複雜和多人協做,當項目一複雜後你們發現js的缺點就暴露出來了,js雖然在其名裏面包含了Java,但其命名只屬於取巧沾光,Java面向對象編程的特性可沒被js吸取,js語言更具函數式編程特性,函數爲js語言的一等公民,當函數越寫越多之時,管理他們的藝術就被提上了檯面,爲了複雜項目開發的規範化、統一化,js迫切須要引入面向對象的相關思想,但面向對象屬於語言靈魂層次,js做爲函數式編程使用了這麼多年,不可能想改就改靈魂層次的東西,爲了兼顧函數式編程的靈活和麪向對象編程的規範,js開發的相關組織作了不少努力,其中一個努力就是創造出了bind、call、apply三個媒婆,這三個媒婆的共同做用就是爲js的一等公民Function函數找個門當戶對的人家(指明Function函數的this指向)。node

2. 代碼講解bind誕生的緣由:

我定義了一個類:編程

var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }
複製代碼

若是我想使用這個類的sayHi功能,一開始,我想到的是直接拿來就用:瀏覽器

var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var sayHi = Xiaoming.sayHi;
  sayHi(); // hello 
複製代碼

不出意外,將會輸出hello(在嚴格模式下將會直接報出cannot read property 'name'錯誤),緣由就是若是直接拿來用,這裏的this將會隱式指向到全局window對象,而全局對象中並無name屬性。在js中,當沒有明確指定this的狀況下,置於全局環境下的函數的this將會是window(注:瀏覽器環境下爲window,node環境下爲global,其它宿主環境本篇不作解釋,本篇文章涉及的宿主環境都是瀏覽器)。app

function func() {
  console.log(this.toString());  // [object Window]
}

func();
複製代碼

window是全局環境下this的最終歸屬(若是你無家可歸,你的家就是這片天地),若是咱們想給這些無家可歸的可憐函數找一個歸屬,咱們須要一箇中介來牽線搭橋,bind就是那個中介之一,bind在js中充當粘合劑的做用,他負責把指定的類和Function函數強力的粘貼在一塊兒:編程語言

var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var Jack = {
    name: '傑克'
  };

  var sayHello = Xiaoming.sayHi.bind(Jack);
  sayHello(); // hello 傑克
複製代碼

當咱們用bind粘貼劑把sayHi方法和Jack類粘貼在一塊兒時,sayHello函數的this就指向Jack類了,因此輸出的結果就是hello 傑克ide

What ???——> 什麼是bind?

1. 漢語釋義:

vt. 綁;約束;裝訂;包紮;凝固 vi. 結合;裝訂;有約束力;過緊函數式編程

在漢語釋義中,bind的大致意思就是綁定、結合,我我的給其在js中的定義爲膠水(注意膠水二字!)。當我想給一個函數換一個新宿主之時,我就取出「bind牌膠水」把想用的函數和它的新宿主粘貼在一塊兒,而後再調用這個用「bind牌膠水」粘貼的擁有新宿主的新函數。函數

2. MDN釋義:
  • English:

The bind( ) method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.post

  • 中文:

bind( )方法建立了一個新函數,當新函數被調用之時,將其this指向到指定的值,同時會經過bind傳入一串預設參數序列供新函數使用。

在這段定義中我抽出了幾個細節:

1. 建立了一個新函數 ——> 這句話很關鍵,這也是我把bind定義爲膠水的緣由,這句話如下幾點須注意:
  • 使用bind,它不會破壞原先的宿主(意即:不是把函數從原先的宿主中刪除掉):
var Xiaoming = {
    name: '小明',
    sayHi() {
      console.log('hello ' + this.name);
    }
  }

  var Jack = {
  name: '傑克'
};

  var sayHello = Xiaoming.sayHi.bind(Jack);
  console.log(xiaoming); // {name: "小明", sayHi: ƒ}
複製代碼
  • 使用bind,會建立一個新函數,意即:把指定的函數從原先的宿主中「複製」一份成新函數,而後經過「bind牌膠水」把指定的宿主和新函數粘貼在一塊兒。這個新函數的this將會指向到指定宿主,並且和以前的舊宿主撇清了關係,實現了和指定宿主的結合。注意:一、不是把函數綁定到指定宿主上;二、這裏的綁定是「按址綁定」,不是copy了一份指定宿主,因此當這個粘貼的指定宿主發生改變時,使用「bind牌膠水」粘貼的新函數也會受影響:
var Xiaoming = {
  name: '小明',
  sayHi() {
    console.log('hello ' + this.name);
  }
}

var Jack = {
  name: '傑克'
};

var sayHello = Xiaoming.sayHi.bind(Jack);
sayHello(); // hello 傑克
Jack.name = '皆可'; // 改變新宿主的name屬性
sayHello(); // hello 皆可 <—— 當新宿主發生改變時,對應的輸出也會受影響

/* 並無把函數綁定到新宿主上 */
Jack.sayHi(); // error: Uncaught TypeError: Jack.sayHi is not a function
Jack.sayHello(); // error: Uncaught TypeError: Jack.sayHello is not a function
複製代碼
2. 一串預設參數序列供新函數使用

「bind牌膠水」的主要做用是給指定函數綁定指定this,第一個參數即指定的新宿主,其後的剩餘參數爲預設參數,既然第一個參數已經達到了目的,爲何還要在其後加一些預設參數呢?這裏要注意參數的預設二字,預設表示預先設定給新函數的參數,經過bind預設的參數將會比新函數本身設定的參數預先使用。看代碼:

var obj1 = {
      name: 'han',
      sayHi(word1, word2) {
        console.log('hello' + this.name + ',' + word1 + ',' + word2);
      }
    };
    
    var obj2 = {
      name: '李'
    };
    var func = obj1.sayHi.bind(obj2, '早上好');
    func('good morning'); // hello 李,早上好, good morning
    func(); // hello 李,早上好, undefined
複製代碼

經過代碼咱們發現這個預設參數和默認參數有點相似(但其實徹底不是一回事!),由於經過bind預設的參數老是先被調用,而使用新函數時自定義的參數老是等預設參數調用後再被調用,相似的概念(先進先出)。這個預設參數的設計,我我的以爲略顯尷尬,多是由於js的函數以前沒有默認參數的設定致使的吧(不甚瞭解)?這裏用默認參數我的以爲會更合適。

How ???——> 怎麼使用bind?

1. 在事件綁定中使用:

在改變this指向的方法中,存在着三個方法:bind、call、apply,bind屬於「靜態綁定」,做爲膠水,bind只負責粘貼函數,不負責粘貼以後的函數的運行,但call和apply卻不是,他們給函數綁定this後還把綁定後的函數給當場運行了。由於這個特性,咱們在給事件綁定函數時只能使用bind來進行this的綁定(由於給事件綁定的函數不須要咱們手動執行,它是在事件被觸發時由JS 內部自動執行的),看代碼:

<button id="btn">Click Me</button>
複製代碼
var obj = {
    thing: '搞點事情'
  };
  function onBtnClick() {
    console.dir('我被點擊了,我想' + this.thing);
  }
  var btn = document.getElementById('btn');
  btn.addEventListener('click', onBtnClick.bind(obj)); // 我想搞點事情 
複製代碼

2. 給迷失的函數找回自我:

在js編程中,常常會出現使用var that = this;的hack黑魔法來給函數找回自我,具體場景以下:

var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      var that = this;
      this.datas.forEach(function(val) {
        that.name = val;
        console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
        console.log(that); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      });
      console.log(this.name); // jack
    }
  }
  obj.resolveDatas();
複製代碼

在上面的代碼中,當在內部函數內部使用匿名函數時,this將會指向到全局window對象,爲了不這個問題,在函數內部經過var that = this聲明瞭一個變量,而後在forEach的匿名函數中調用,爲何要這樣使用?這是由於在沒出現ES6的箭頭函數以前,js存在着一個「任性this」,關於js中this的複雜度,在《你不知道的js(上卷)》中寫到:

this 關鍵字是js 中最複雜的機制之一。它是一個很特別的關鍵字,被自動定義在 全部函數的做用域中。可是即便是很是有經驗的js 開發者也很難說清它到底指向 什麼。 任何足夠先進的技術都和魔法無異。 ——Arthur C. Clarke

關於js中this的指向黑魔法問題這裏只略提,具體可查閱相關權威資料。在上面的代碼中,咱們發現一個哭笑不得的現實:好好的一個函數,咋包了一層函數後就找不到設想中的那個this了呢?本覺得本身把this指向到了當前的obj對象,一到用的時候就直接「認賊做父」了,把this指向到了window對象,what the hell?,如何避免這種悲劇?讓咱們有請「bind牌膠水」隆重登場,做爲專業的「this硬綁定」方法,bind用起來妥妥的:

var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      this.datas.forEach((function(val) {
        this.name = val;
        console.log(this); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      }).bind(this));
      console.log(this.name); // jack
    }
  }
  obj.resolveDatas();
複製代碼

在上面的代碼中,咱們經過「bind牌膠水」把真正想用的對象粘貼給了匿名函數,從而讓匿名函數可以堅持自我,但在這裏我的以爲這種硬綁定是一種笨拙的hack方法,由於針對這種詭異問題居然要用膠水進行「修補」,我的以爲其實很low,因此不予提倡,Es6提出箭頭函數纔是專業應對該問題的合適方案:

var obj = {
    datas: ['jack'],
    resolveDatas: function() {
      this.datas.forEach((val) => {
        this.name = val;
        console.log(this); // {datas: Array(1), resolveDatas: ƒ, name: "jack"}
      });
      console.log(this.name); // un
    }
  }
  obj.resolveDatas();
複製代碼

關於bind的使用方,我這裏只列舉出了兩個,更多的使用場景還有不少,能夠查閱相關資料。

後語

花了好幾天時間終於寫完了這篇文章,但願相關內容能給你們帶來一些啓發和感悟。通常講bind的時候都會把call和apply放在一塊兒聊,我本有此意,但考慮到本身的囉嗦話語,內容過長,因此仍是分開講解,下一篇文章我來聊聊apply和call方法(由於這兩個方法就是孿生兄弟,因此一塊兒講再合適不過了),而後到下篇再對bind、apply、call三者進行對比來闡述,若知後事如何,且聽下回分解!

相關文章
相關標籤/搜索