bind方法的javascript实现及函数柯里化

这是一道面试题,题目给出了使用bind方法的样例,要求用javascript实现这个方法,面试官还很善意的提醒我函数柯里化,然而,我还是不会这道题目,所以回来这会《javacript权威指南》和《javacript 高级教程》开始学习相关知识。

一、javacript实现bind方法

bind()是在ECMAScript5中新增的方法,但是在ECMAScript3中可以轻易的模拟bind()

版本一

这部分参考了《javacript权威指南》权威指南的p191ECMAScript3版本的Function.bind()方法的实现。

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

版本一存在的问题

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

  • 真正的bind()方法(ECMAScript5中定义的bind())返回一个函数对象,这个函数对象的length属性是绑定函数的形参个数减去绑定实参的个数。而模拟的bind()方法返回的函数对象的length属性的值为0.

bind方法的javascript实现及函数柯里化_第1张图片

  • 真正的bind()方法可以顺带用作构造函数,此时将忽略传入bind()this,原始函数就会以构造函数的形式调用,其实参也已经绑定。而模拟的bind()方法返回的函数用作构造函数时,生成的对象为Object()

bind方法的javascript实现及函数柯里化_第2张图片

  • 真正的bind()方法所返回的函数并不包含prototype属性(普通函数固有的prototype属性是不能删除的),并且将这些绑定的函数用作构造函数时所创建的对象从原始的未绑定的构造函数中继承prototype。同样,使用instanceof运算符时,绑定构造函数和未绑定构造函数并无两样。

bind方法的javascript实现及函数柯里化_第3张图片

版本二

针对上述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,解释

如下这段代码,实际上用到了原型式继承。这跟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
    bind方法的javascript实现及函数柯里化_第4张图片

可见,版本二并没有解决版本一的问题1和3

  • 测试2
    bind方法的javascript实现及函数柯里化_第5张图片

可见版本二解决了版本一的问题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;
}
  • 测试结果如下:
    bind方法的javascript实现及函数柯里化_第6张图片

二、bind函数应用

关于bind函数的应用这里只提两点在我使用这个方法的时候,遇到的让我刚开始比较懵逼仔细一想还真是这么回事的问题。

一段神奇的代码

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

bindSlice(arguments);
  • 测试一下
    bind方法的javascript实现及函数柯里化_第7张图片

这段代码的作用就是将一个类数组对象转化为真正的数组,是下面这段代码的另一种写法而已

Array.prototype.slice.call(arguments);

将一个函数对象作为bindcontext,这种写法的作用是,为需要特定this值的函数创造捷径。

bind函数只创建一个新函数而不执行

私以为这是bindcallapply方法的一个重要差别,callapply这两个方法都会立即执行函数,返回的是函数执行后的结果。而bind函数只创建一个新函数而不执行。

之前看过一段错误的代码,就是用apply改变一个构造函数的this,紧接着又用这个构造函数创建新对象,毫无疑问这是错误的,遗憾的是找不到这段错误代码的出处了。

三、函数柯里化

函数柯里化是与函数绑定紧密相关的一个主题,它用于创建已经设置好了一个或者多个参数的函数。函数柯里化的基本方法与函数绑定是一样的:使用一个闭包返回一个函数。

  • 柯里化函数通常创建步骤如下:调用另一个函数并为它传入要柯里化的函数和必要参数。同样方式如下:

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];
    };
}

四、参考文献

1.Javascript中bind()方法的使用与实现.
2.javascript原生一步步实现bind分析.
3.JS中的bind方法与函数柯里化.

你可能感兴趣的:(bind方法的javascript实现及函数柯里化)