bind方法的javascript實現及函數柯里化

這是一道面試題,題目給出了使用bind方法的樣例,要求用javascript實現這個方法,面試官還很善意的提醒我函數柯里化,然而,我仍是不會這道題目,因此回來這會《javacript權威指南》和《javacript 高級教程》開始學習相關知識。javascript

1、javacript實現bind方法

bind()是在ECMAScript5中新增的方法,可是在ECMAScript3中能夠輕易的模擬bind()php

版本一

這部分參考了《javacript權威指南》權威指南的p191ECMAScript3版本的Function.bind()方法的實現。html

if(!Function.prototype.bind){
    Function.prototype.bind = function(o){
        // 將`this`和`arguments`的值保存在變量中,以便在後面嵌套的函數中可使用它們
        var self = this,
            boundArgs = arguments;
        //bind方法的返回值是一個函數
        return function(){
            var args = [],//建立一個實參列表,將傳入的bind()的第二個及後續的實參都傳入這個函數。
                i;
            for(i=1;i<boundArgs.length;i++){
                args.push(boundArgs[i]);
            }
            for(i=0;i<arguments.length;i++){
                args.push(arguments[i]);
            }
            //如今將self做爲o的方法來調用,傳入這些實參
            return self.apply(o,args);
        }
    }
}

版本一存在的問題

上述ECMAScript3版本的Function.bind()方法和ECMAScript5中定義的bind()有些出入,主要有如下三個方面。java

  • 真正的bind()方法(ECMAScript5中定義的bind())返回一個函數對象,這個函數對象的length屬性是綁定函數的形參個數減去綁定實參的個數。而模擬的bind()方法返回的函數對象的length屬性的值爲0.node

圖片描述

  • 真正的bind()方法能夠順帶用做構造函數,此時將忽略傳入bind()this,原始函數就會以構造函數的形式調用,其實參也已經綁定。而模擬的bind()方法返回的函數用做構造函數時,生成的對象爲Object()面試

圖片描述

  • 真正的bind()方法所返回的函數並不包含prototype屬性(普通函數固有的prototype屬性是不能刪除的),而且將這些綁定的函數用做構造函數時所建立的對象從原始的未綁定的構造函數中繼承prototype。一樣,使用instanceof運算符時,綁定構造函數和未綁定構造函數並沒有兩樣。數組

圖片描述

版本二

針對上述ECMAScript3版本的Function.bind()方法存在的問題,《JavaScript Web Application》一書中給出的版本有針對性的修復了這些問題。閉包

Function.prototype.bind = function(context){
    var args = Array.prototype.slice.call(arguments,1),//要點3
        self = this,
        F = function(){};//要點1
        bound = function(){
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(innerArgs);
            return self.apply((this instanceof F ? this : context),finalArgs);//要點2
        };
        F.prototype = self.prototype;
        bound.prototype = new F();
        return bound;
}
  • 要點1,解釋app

以下這段代碼,實際上用到了原型式繼承。這跟ECMAscript5中的Object.creat()方法只接受一個參數時是同樣的。函數

F = function(){};//要點1
...
F.prototype = self.prototype;
bound.prototype = new F();
  • 要點2,解釋

以下這段代碼,是要判斷經過bind方法綁定獲得的函數,是直接調用仍是用做構造函數經過new來調用的。

this instanceof F ? this : context

爲了分析這段代碼的具體含義,須要知道經過構造函數生成對象時,new操做符都幹了啥。好比以下代碼:

var a = new B()

(1).首先建立一個空對象,var a = {};
(2).將構造函數的做用域賦給新對象(所以,this就指向了這個新對象);
(3).執行構造函數中的代碼(爲這個新對象添加屬性), B.call(a);
(4).繼承被構造函數的原型,a._proto_ = B.prototype;
(5).返回這個新對象。

標準的bind方法

建立一個新函數(稱爲綁定函數),新函數與被調函數(綁定函數的目標函數)具備相同的函數體(在 ECMAScript 5 規範中內置的call屬性)。當目標函數被調用時this值綁定到bind()的第一個參數,該參數不能被重寫。綁定函數被調用時,bind() 也接受預設的參數提供給原函數。一個綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器。提供的this值被忽略,同時調用時的參數被提供給模擬函數。

經過原型鏈的繼承能夠判斷綁定函數是否用做了構造函數,經過new操做符來調用。假設目標函數爲funObj,綁定函數爲funBind.即

var funBind = funObj.bind(context);
var obj = new funBind();

上面代碼具備以下繼承關係(這裏畫出繼承關係圖更容易理解):

obj instanceof funBind // true

funBind.prototype instanceof F //true

F.prototype = self.prototyep

a instanceof B原理,是判斷B.prototype是否存在於a的原型鏈中。所以有

obj instanceof F // true

此外,要點2這裏還用到了借用構造函數來實現繼承,以下代碼

self.apply(this,finalArgs)
  • 要點3,解釋

這裏其實是將類數組對象轉化爲數組,由於類數組對象,好比argumentsnodelist;雖然很像數組,好比具備length屬性,可是不是數組,好比,沒有concatslice這些方法.

經常使用的將類數組對象轉爲數組的方法有

(1).Array.prototype.slice.call
(2).擴展運算符...,好比[...arguments]
(3). Array.from();

版本二測試

  • 測試1
    圖片描述

可見,版本二並無解決版本一的問題1和3

  • 測試2
    圖片描述

可見版本二解決了版本一的問題2

版本二的精簡版

版本二中要點1和要點2看着很不爽,因而,我給精簡了一下,測試結果與版本二相同。

Function.prototype.bind = function(context){
    var args = Array.prototype.slice.call(arguments,1),//要點3
        self = this,
        //F = function(){};//要點1
        bound = function(){
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(innerArgs);
            //return self.apply((this instanceof F ? this : context),finalArgs);//要點2
            return self.apply((this instanceof self ? this : context),finalArgs);//要點2
        };
        //F.prototype = self.prototype;
        //bound.prototype = new F();
        bound.prototype = self.prototype;
        return bound;
}
  • 測試結果以下:
    圖片描述

2、bind函數應用

關於bind函數的應用這裏只提兩點在我使用這個方法的時候,遇到的讓我剛開始比較懵逼仔細一想還真是這麼回事的問題。

一段神奇的代碼

var unBindSlice = Array.prototype.slice;
var bindSlice = Function.prototype.call.bind(unBindSlice);
...

bindSlice(arguments);
  • 測試一下
    圖片描述

這段代碼的做用就是將一個類數組對象轉化爲真正的數組,是下面這段代碼的另外一種寫法而已

Array.prototype.slice.call(arguments);

將一個函數對象做爲bindcontext,這種寫法的做用是,爲須要特定this值的函數創造捷徑。

bind函數只建立一個新函數而不執行

私覺得這是bindcallapply方法的一個重要差異,callapply這兩個方法都會當即執行函數,返回的是函數執行後的結果。而bind函數只建立一個新函數而不執行。

以前看過一段錯誤的代碼,就是用apply改變一個構造函數的this,緊接着又用這個構造函數建立新對象,毫無疑問這是錯誤的,遺憾的是找不到這段錯誤代碼的出處了。

3、函數柯里化

函數柯里化是與函數綁定緊密相關的一個主題,它用於建立已經設置好了一個或者多個參數的函數。函數柯里化的基本方法與函數綁定是同樣的:使用一個閉包返回一個函數。

  • 柯里化函數一般建立步驟以下:調用另外一個函數併爲它傳入要柯里化的函數和必要參數。一樣方式以下:

function curry(fn){
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

有沒有感到很熟悉,其實上面bind方法的兩個實現版本都用到了函數柯里化,區別在於,這裏的通用函數沒用考慮到執行環境。

  • 曾經看過一段相似函數柯里化的代碼,私覺得很巧妙,以下:

假若有一個對象數組,想要根據對象的某個屬性來對其進行排序。而傳遞給sort方法的比較函數只能接受兩個參數,即比較的值,這樣就沒法指定排序的對象屬性了。如何將須要三個參數的函數轉化爲知足要求的僅須要兩個參數?要解決這個問題,能夠定義一個函數,它接收一個屬性名,而後根據這個屬性名建立並返回一個比較函數,以下:

function createComparisionFunction(property){
    return function(obj1,obj2){
        return obj1[property]-obj2[property];
    };
}

4、參考文獻

1.Javascript中bind()方法的使用與實現.
2.javascript原生一步步實現bind分析.
3.JS中的bind方法與函數柯里化.

相關文章
相關標籤/搜索