bind函數做用、應用場景以及模擬實現

bind函數

bind 函數掛在 Function 的原型上面試

Function.prototype.bind

建立的函數均可以直接調用 bind,使用:瀏覽器

function func(){
        console.log(this)
    }
    func.bind(); // 用函數來調用

bind 的做用:app

bind() 方法調用後會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲新函數運行時的 this的值,以後的序列參數將會在傳遞的實參前傳入做爲新函數的參數。<MDN>

bind 接收的參數函數

func.bind(thisArg[,arg1,arg2...argN])
  • 第一個參數thisArg,當 func 函數被調用時,該參數會做爲 func 函數運行時的 this 指向。當使用 new 操做符調用綁定函數時,該參數無效。
  • [,arg1,arg2...argN] 做爲實參傳遞給 func 函數。

bind 返回值佈局

返回一個新函數

注意:這和函數調用 call/apply 改變this指向有所不一樣。調用call/apply 會把原函數直接執行了。學習

舉個例子說明:測試

function func(){
    console.log(this)
}

// 用call
func.call({a:1});  // func函數被執行了,打印:{a:1}

// 用bind
let newFunc = func.bind({});   // 返回新函數

newFunc(); // 只有當返回的新函數執行,func函數纔會被執行

從以上獲得以下信息:this

  1. bind被函數調用
  2. 返回一個新函數
  3. 能改變函數this指向
  4. 能夠傳入參數

深刻bind 使用

以上知道了 bind 函數的做用以及使用方式,接下深刻到 bind 函數的使用中,具體介紹三個方面的使用,這也是以後模擬實現 bind 函數的要點。prototype

  1. 改變函數運行時this指向
  2. 傳遞參數
  3. 返回的新函數被當成構造函數

改變函數運行時this指向

當調用 bind 函數後,bind 函數的第一個參數就是原函數做用域中 this 指向的值。rest

function func(){
    console.log(this); 
}

let newFunc = func.bind({a:1});
newFunc(); // 打印:{a:1}

let newFunc2 = func.bind([1,2,3]);
newFunc2(); // 打印:[1,2,3]

let newFunc3 = func.bind(1);
newFunc3(); // 打印:Number:{1}

let newFunc4 = func.bind(undefined/null);
newFunc4(); // 打印:window

以上要注意,當傳入爲 null 或者 undefined 時,在非嚴格模式下,this 指向爲 window

當傳入爲簡單值時,內部會將簡單的值包裝成對應類型的對象,數字就調用 Number 方法包裝;字符串就調用 String 方法包裝;true/false 就調用 Boolean 方法包裝。要想取到原始值,能夠調用 valueOf 方法。

Number(1).valueOf(); // 1
String("hello").valueOf(); // hello
Boolean(true).valueOf(); // true

當屢次調用 bind 函數時,以第一次調用 bind 函數的改變 this 指向的值爲準。

function func(){
    console.log(this);
}

let newFunc = func.bind({a:1}).bind(1).bind(['a','b','c']);
newFunc(); // 打印:{a: 1}

傳遞的參數

bind 的第二個參數開始,是向原函數傳遞的實參。bind 返回的新函數調用時也能夠向原函數傳遞實參,這裏就涉及順序問題。

function func(a,b,c){
    console.log(a,b,c); // 打印傳入的實參
}

let newFunc = func.bind({},1,2);

newFunc(3)

打印結果爲1,2,3。
能夠看到,在 bind 中傳遞的參數要先傳入到原函數中。

返回的新函數被當成構造函數

調用 bind 函數後返回的新函數,也能夠被當作構造函數。經過新函數建立的實例,能夠找到原函數的原型上。

// 原函數
function func(name){
console.log(this); // 打印:經過{name:'wy'}
this.name = name;
}
func.prototype.hello = function(){
    console.log(this.name)
}
let obj = {a:1}
// 調用bind,返回新函數
let newFunc = func.bind(obj);

// 把新函數做爲構造函數,建立實例

let o = new newFunc('seven');

console.log(o.hello()); // 打印:'seven'
console.log(obj); // 打印:{a:1}

新函數被當成了構造函數,原函數func 中的 this 再也不指向傳入給 bind 的第一個參數,而是指向用 new 建立的實例。在經過實例 o 找原型上的方法 hello 時,可以找到原函數 func 原型上的方法。

在模擬實現 bind 特別要注意這一塊的實現,這也是面試的重點,會涉及到繼承。

bind函數應用場景

以上只是說了 bind 函數時如何使用的,學會了使用,要把它放在業務場景中來解決一些現實問題。

場景一

先來一個佈局:

<ul id="list">
    <li>1</li>
    <li>1</li>
    <li>1</li>
</ul>

需求:點擊每個 li 元素,延遲1000ms後,改變 li 元素的顏色,

let lis = document.querySelectorAll('#list li');
for(var i = 0; i < lis.length; i++){
    lis[i].onclick = function(){
        setTimeout(function(){
            this.style.color = 'red'
        },1000)
    }
}

以上代碼點擊每個 li,並不會改變顏色,由於定時器回調函數的 this 指向的不是點擊的 li,而是window,(固然你也可使用箭頭函數,let之類來解決,這裏討論的主要是用bind來解決)。此時就須要改變回調函數的 this 指向。能改變函數 this 指向的有:call、apply、bind。那麼選擇哪個呢?根據場景來定,這裏的場景是在1000ms以後才執行回調函數,因此不能選擇使用call、apply,由於它們會當即執行函數,因此這個場景應該選擇使用 bind解決。

setTimeout(function(){
    this.style.color = 'red'
}.bind(this),1000)

場景二

有時會使用面向對象的方式來組織代碼,涉及到把事件處理函數拆分在原型上,而後把這些掛在原型上的方法賦值給事件,此時的函數在事件觸發時this都指向了元素,進而須要在函數中訪問實例上的屬性時,便不能找到成。

function Modal(options){
    this.options = options;
}

Modal.prototype.init = function(){
    this.el.onclick = this.clickHandler; // 此方法掛載原型上
}
Modal.prototype.clickHandler = function(){
    console.log(this.left);  // 此時點擊元素執行該函數,this指向元素,不能找到left
}

let m = new Modal({
    el: document.querySelector('#list'),
    left: 300
})

m.init(); // 啓動應用

以上代碼,在 init 函數中,給元素綁定事件,事件處理函數掛在原型上,使用 this 來訪問。當點擊元素時,在 clickHandler 函數中須要拿到實例的 left 屬性,但此時 clickHandler 函數中的 this 指向的是元素,而不是實例,因此拿不到。要改變 clickHandler 函數 this 的指向,此時就須要用到 bind

Modal.prototype.init = function(){
    this.el.onclick = this.clickHandler.bind(this)
}

以上場景只是 bind 使用的冰山一角,它本質要作的事情是改變 this 的指向,達到預期目的。掌握了 bind 的做用以及應用的場景,在腦海中就會樹立一個印象:當須要改變this指向,並不當即執行函數時,就能想到 bind

模擬實現

爲何要本身去實現一個bind函數呢?

bind()函數在 ECMA-262 第五版才被加入;它可能沒法在全部瀏覽器上運行(ie8如下)。
面試用,讓面試官找不到拒絕你的理由

抓住 bind 使用的幾個特徵,把這些點一一實現就OK,具體的點:

  1. 被函數調用
  2. 返回新函數
  3. 傳遞參數
  4. 改變函數運行時this指向
  5. 新函數被當作構造函數時處理

被函數調用,能夠直接掛在Function的原型上,爲了補缺那些不支持的瀏覽器,不用再爲支持的瀏覽器添加,能夠作以下判斷:

if(!Function.prototype.bind) {
    Function.prototype.bind = function(){
        
    }
}

這種行爲也叫做 polyfill,爲不支持的瀏覽器添加某項功能,以達到抹平瀏覽器之間的差距。

注意:若是瀏覽器支持,方便本身測試,能夠把 if 條件去掉,或者把 bind 改一個名字。在下文準備更名字爲 bind2,方便測試。

調用 bind 後會返回一個新的函數,當新函數被調用,原函數隨之也被調用。

Function.prototype.bind2 = function(thisArg,...args){
    let funcThis = this; // 函數調用bind,this指向原函數
    // 返回新函數
    return function (...rest) {
        return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/)
    }
}

// 測試
function func(a,b,c){
    console.log(this)
    console.log(a,b,c)
}

let newFunc = func.bind2({a:1},1,2);

newFunc(3);
 // 打印:{a: 1}
 // 打印:1 2 3

以上這個函數已經可以改變原函數 this 的指向,並傳遞正確順序的參數。接下來就是比較難理解的地方,當新函數被當作構造函數的狀況。

須要做出兩個地方的改變:

  1. 新返回的函數要繼承原函數原型上的屬性
  2. 原函數改變this問題。若是用new調用,則原函數this指向應該是新函數中this的值;不然爲傳遞的thisArg的值。

先作繼承,讓新函數繼承原函數的原型,維持原來的原型關係。匿名函數沒辦法引用,因此給新函數起一個名字。

Function.prototype.bind2 = function(thisArg,...args){
    let funcThis = this; // 函數調用bind,this指向原函數
    
    // 要返回的新函數
    let fBound = function (...rest) {
        return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/)
    }
    
    // 不是全部函數都有prototype屬性,好比 Function.prototype就沒有。
    if(funcThis.prototype){
        // 使用Object.create,以原函數prototype做爲新對象的原型建立對象
        fBound.prototype = Object.create(funcThis.prototype);
    }
    return fBound;
}

// 測試
function func(name){
    console.log(this); // {a: 1}
    this.name = name;
}

func.prototype.hello = function(){
    console.log(this.name); // undefined
}

let newFunc = func.bind2({a:1});
let o = new newFunc('seven')

o.hello();
// 打印:{a: 1}
// 打印:undefined

以上代碼,新建的實例 o 可以調用到 hello 這個方法,說明繼承已經實現,可以訪問新函數上原型方法。

接下來是關於 this 指向問題,上面例子中,使用了 new 運算符調用函數,那麼原函數中,this 應該指向實例纔對。因此須要在改變 this 指向的 apply 那裏對是不是使用 new 操做符調用的作判斷。

用到的操做符是 instanceof,做用是判斷一個函數的原型是否在一個對象的原型鏈上,是的話返回true,不然返回false。測試以下:

function Person(){}
let p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log(p instanceof Array); // fasle

也能夠用 instanceof 在構造函數中判斷是不是經過 new 來調用的。若是是用 new 來調用,說明函數中 this 對象的原型鏈上存在函數的原型,會返回true。

function Person(){
    console.log(this instanceof Person); // true
}

new Person();

回到咱們的 bind2 函數上,當調用 bind2 後返回了新函數 fBound,當使用 new 調用構造函數時,實際上調用的就是 fBound 這個函數,因此只須要在 fBound 函數中利用 instanceof 來判斷是不是用 new 來調用便可。

Function.prototype.bind2 = function(thisArg,...args){
    let funcThis = this; // 函數調用bind,this指向原函數
    
    // 要返回的新函數
    let fBound = function (...rest) {
        // 若是是new調用的,原函數this指向新函數中建立的實例對象
        // 不是new調用,依然是調用bind2傳遞的第一個參數
        thisArg = this instanceof fBound ? this : thisArg;
        return funcThis.apply(thisArg,[...args,...rest]/*bind2傳遞的實參優先於新函數的實參*/)
    }
    
    // 不是全部函數都有prototype屬性,好比 Function.prototype就沒有。
    if(funcThis.prototype){
        // 使用Object.create,以原函數prototype做爲新對象的原型建立對象
        fBound.prototype = Object.create(funcThis.prototype);
    }
    return fBound;
}
// 測試
function func(name){
    console.log(this); // {a: 1}
    this.name = name;
}

func.prototype.hello = function(){
    console.log(this.name); // undefined
}


let newFunc = func.bind2({a:1});
let o = new newFunc('seven')

o.hello();
// 打印:{name:'seven'}
// 打印:'seven'

bind 函數源碼已實現完成,但願對你有幫助。

若有誤差歡迎指正學習,謝謝。

相關文章
相關標籤/搜索