1. 程式人生 > >bind函式作用、應用場景以及模擬實現

bind函式作用、應用場景以及模擬實現

bind函式

bind 函式掛在 Function 的原型上

Function.prototype.bind

建立的函式都可以直接呼叫 bind,使用:


    function func(){
        console.log(this)
    }
    func.bind(); // 用函式來呼叫

bind 的作用:

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函式才會被執行

從以上得到如下資訊:

  1. bind被函式呼叫
  2. 返回一個新函式
  3. 能改變函式this指向
  4. 可以傳入引數

深入bind 使用

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

  1. 改變函式執行時this指向
  2. 傳遞引數
  3. 返回的新函式被當成建構函式

改變函式執行時this指向

當呼叫 bind 函式後,bind 函式的第一個引數就是原函式作用域中 this 指向的值。


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 函式時如何使用的,學會了使用,要把它放在業務場景中來解決一些現實問題。

場景一

先來一個佈局:


&lt;ul id="list"&gt;
    &lt;li&gt;1&lt;/li&gt;
    &lt;li&gt;1&lt;/li&gt;
    &lt;li&gt;1&lt;/li&gt;
&lt;/ul&gt;

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


let lis = document.querySelectorAll('#list li');
for(var i = 0; i &lt; 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 函式原始碼已實現完成,希望對你有幫助。

如有偏差歡迎指正學習,謝謝。

來源:https://segmentfault.com/a/1190000017543088