这是一道面试题,题目给出了使用bind
方法的样例,要求用javascript
实现这个方法,面试官还很善意的提醒我函数柯里化
,然而,我还是不会这道题目,所以回来这会《javacript权威指南》和《javacript 高级教程》开始学习相关知识。
一、javacript实现bind方法
bind()
是在ECMAScript5
中新增的方法,但是在ECMAScript3
中可以轻易的模拟bind()
。
版本一
这部分参考了《javacript权威指南》权威指南的p191
,ECMAScript3
版本的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()
方法可以顺带用作构造函数,此时将忽略传入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,解释
如下这段代码,实际上用到了原型式继承。这跟
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,解释
这里实际上是将类数组对象转化为数组,因为类数组对象,比如
arguments
、nodelist
;虽然很像数组,比如具有length
属性,但是不是数组,比如,没有concat
、slice
这些方法.常用的将类数组对象转为数组的方法有
(1).Array.prototype.slice.call
(2).扩展运算符...
,比如[...arguments]
(3).Array.from()
;
版本二测试
可见,版本二并没有解决版本一的问题1和3
可见版本二解决了版本一的问题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
函数应用
关于bind
函数的应用这里只提两点在我使用这个方法的时候,遇到的让我刚开始比较懵逼仔细一想还真是这么回事的问题。
一段神奇的代码
var unBindSlice = Array.prototype.slice;
var bindSlice = Function.prototype.call.bind(unBindSlice);
...
bindSlice(arguments);
这段代码的作用就是将一个类数组对象转化为真正的数组,是下面这段代码的另一种写法而已
Array.prototype.slice.call(arguments);
将一个函数对象作为
bind
的context
,这种写法的作用是,为需要特定this
值的函数创造捷径。
bind
函数只创建一个新函数而不执行
私以为这是
bind
和call
与apply
方法的一个重要差别,call
和apply
这两个方法都会立即执行函数,返回的是函数执行后的结果。而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方法与函数柯里化.