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方法与函数柯里化.

相关文章
相关标签/搜索