Javascript的函数柯里化与apply、call、bind

摘要:记录学习Node过程中对Javascript基础的掌握与复习

一、Javascript中的apply()、call()、bind()

1.1 作为值的函数


 在JavaScript中,函数不仅是一种语法,也是值,也就是说,可以将函数赋值给变量,存储在对象的属性或数组的元素中, 作为参数传入另外一个函数等。
 每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称作“原型对象”。 每一个函数都包含不同的原型对象。当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。
 JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

1.2 apply()与call()


 在 Javascript 中,cal()apply() 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
 首先举例说明:

1
2
3
4
5
6
7
8
var Person = {
hairColor : "red",
sayHi : function() {
console.log("My Hair Color is " + this.hairColor);
}
}
var XiaoLi = new Person();
XiaoLi.sayHi();//My Hair Color is red

 如果我们有一个对象XiaoZhang= { hairColor : "black"} ,我们不想对它重新定义 sayHi() 方法,那么我们可以通过 call()apply() 用 XiaoLi 的 sayHi() 方法:

1
2
3
4
5
XiaoZhang = {
hairColor : "black"
}
XiaoLi.sayHi.call(XiaoZhang);//My Hair Color is black
XiaoLi.sayHi.apply(XiaoZhang);//My Hair Color is black

 可以看出 call()apply() 是为了动态改变 this 而出现的,当一个 object 没有某个方法,但是其他的有,我们可以借助call()apply()用其它对象的方法来操作。
 它们的第一个参数是要调用函数的母对象,它是调用上下文,在函数体内通过 this 来获得对它的引用。 apply()方法和call()方法的作用相同,只不过函数传递的方式不一样,它的实参都放入在一个数组中。
 举个例子,以对象o的方法的形式调用函数f(),并传入两个参数,可以使用这样的代码:

1
2
3
4
5
6
7
var o = {};
function f(a, b) {
return a + b;
}

f.call(o, 1, 2); // 将函数f作为o的方法,实际上就是重新设置函数f的上下文
f.apply(o, [1, 2]);

 再举一个来自MDN的例子, 使用call()方法调用匿名函数:
 在下例中的for循环体内,我们创建了一个匿名函数,然后通过调用该函数的call()方法,将每个数组元素作为指定的this值执行了那个匿名函数。 这个匿名函数的主要目的是给每个数组元素对象添加一个print方法

1
2
3
4
5
6
7
8
9
10
11
12
13
var animals = [
{species: 'Lion', name: 'King'},
{species: 'Whale', name: 'Fail'}
];

for (var i = 0; i < animals.length; i++) {
(function (i) {
this.print = function () {
console.log('#' + i + ' ' + this.species + ': ' + this.name);
}
this.print();
}).call(animals[i], i);
}

1.2.1 apply()与call()的区别


 对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:

1
2
3
var func = function(arg1, arg2) {

};

 就可以通过如下方式来调用:

1
2
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])

 其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。
 JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call
 而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。

1.2.2 call()与apply()的常用用法


  • 1、数组之间追加

    1
    2
    3
    4
    var array1 = [12 , "foo" , {name "Joe"} , -2458]; 
    var array2 = ["Doe" , 555 , 100];
    Array.prototype.push.apply(array1, array2);
    /* array1 值为 [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] */
  • 2、获取数组中的最大值和最小值
     number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。

    1
    2
    3
    var  numbers = [5, 458 , 120 , -215 ]; 
    var maxInNumbers = Math.max.apply(Math, numbers), //458
    maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
  • 3、类(伪)数组使用数组方法

    1
    2
    3
    4
    var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
    function getArgs(){
    return Array.prototype.slice.call(arguments);
    }

 Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。
 但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。

1.2.3 应用


 要求是给每一个 log 消息添加一个”(app)”的前辍,比如:

1
log("hello world");    //(app)hello world

 想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift(),像这样:

1
2
3
4
5
6
function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(app)');

console.log.apply(console, args);
};

1.3 bind()

1.3.1 bind()的语法和描述


 以下摘自MDN上对Function.prototype.bind()的描述:

bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

 bind()方法会创建一个新函数,当这个新函数被调用时,它的this值是传递给bind()的第一个参数, 它的参数是bind()的其他参数和其原本的参数。也是可以改变函数体内 this 的指向。
 看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:

1
2
3
4
5
6
7
8
9
10
var foo = {
bar : 1,
eventBind: function(){
var _this = this;
$('.someClass').on('click',function(event) {
/* Act on the event */
console.log(_this.bar); //1
});
}
}

 由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ }过渡到$(‘.someClass’).on(‘click’,function(event) { })发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind()可以更加优雅的解决这个问题:

1
2
3
4
5
6
7
8
9
var foo = {
bar : 1,
eventBind: function(){
$('.someClass').on('click',function(event) {
/* Act on the event */
console.log(this.bar); //1
}.bind(this));
}
}

 在上述代码里,bind()创建了一个函数,当这个click事件绑定在被调用的时候,它的 this关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。
 同理,在举一个例子:

1
2
3
4
5
6
7
8
9
var foo = {
x:1
};
var bar = function(){
console.log(this.x);
};
bar();//undefined
var func = bar.bind(foo);
func();//1

 当使用 bind() 创建一个绑定函数之后,相当于我们重新创建了一个函数,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。
 如果有兴趣想知道 Function.prototype.bind() 内部长什么样以及是如何工作的,这里有个非常简单的例子:

1
2
3
4
5
6
Function.prototype.bind = function (scope) {
var fn = this;
return function () {
return fn.apply(scope);
};
}

1.3.2 bind()浏览器支持


浏览器 版本
Chrome 7
Firefox (Gecko) 4.0 (2)
Internet Explorer 9
Opera 11.60
Safari 5.1.4

 Function.prototype.bind 在IE8及以下的版本中不被支持,所以如果你没有一个备用方案的话,可能在运行时会出现问题。

bind 函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。你可以部份地在脚本开头加入以下代码,就能使它运作,让不支持的浏览器也能使用 bind() 功能。

以下时解决代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP
? this
: oThis || this,
aArgs.concat(Array.prototype.slice.call(arguments)));
};

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

return fBound;
};
}

 以下代码是《JavaScript Web Application》一书中对bind()的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof,因此这是最严谨的bind()实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.bind = function(context){  
var args = Array.prototype.slice(arguments, 1),
F = function(){},
self = this,
bound = function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply((this instanceof F ? this : context), finalArgs);
};

F.prototype = self.prototype;
bound.prototype = new F();
return bound;
};

1.3.3 应用示例

1.3.3.1 点击处理函数


 一个用途是记录点击事件(或者在点击之后执行一个操作),这可能需要我们在一个对象中存入一些信息,比如:

1
2
3
4
5
6
7
var logger = {
x: 0,
updateCount: function(){
this.x++;
console.log(this.x);
}
}

 我们可能会以下面的方式来指定点击处理函数,随后调用 logger 对象中的 updateCount() 方法。

1
2
3
document.querySelector('button').addEvenListener('click',function(){
logger.updateCount();
});

 我们需要创建一个匿名函数,来确保updateCount()方法中的this的正确指向。
 但是我们还能使用bind()函数,以更优雅的形式完成这个功能。

1
document.querySelector('button').addEvenListener('click',logger.updateCount.bind(logger));

1.3.3.2 setTimeOut函数


 关于在渲染模板之后立即访问新的DOM节点时会遇到的问题。例如工作中遇到一个使用easyui的combobox组件进行输入框自动联想提示功能,但是出现了在初始化总是失败。大体上程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$('#divId').html('<input type=''text" id="inputId">');
$('#inputId').combobox({
prompt:'输入关键字自动搜索',
valueField:'Name',
textField:'Name',
mode:'remote',
editable:true,
onBeforeLoad:beforeLoad,
loader:loader,
});
function beforeLoad(){
//doSomeThing
}
function loader(){
//doSomeThing
}

 你或许发现它能正常工作——但并不是每次都行,因为里面存在着问题。这是一个竞争的问题:只有先到达的才能获胜。有时候是渲染先到,而有时候是插件的实例化先到。如果渲染过程还没有完成(DOM Node还没有被添加到DOM树上),那么find(‘select’)将无法找到相应的节点来执行实例化。
 我们可以使用基于 setTimeout()slight hack来解决问题。一般情况下setTimeout()的this指向windowglobal对象。当使用类的方法时需要this指向类实例,就可以使用bind()将this绑定到回调函数来管理实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var box = {
init:function(){
$('#inputId').combobox({
prompt:'输入关键字自动搜索',
valueField:'Name',
textField:'Name',
mode:'remote',
editable:true,
onBeforeLoad:beforeLoad,
loader:loader,
});
}
render:function(){
$('#divId').html('<input type=''text" id="inputId">');
setTimeout(this.init.bind(this) , 0);
}
}

 现在,我们的 init() 函数就能够在正确的上下文环境中执行了。

1.3.3.3. 基于 querySelectorAll的事件绑定


 如今的DOM API引入了很多非常有用的方法,比如 querySelector, querySelectorAllclassList接口,这些方法给DOM API带来了非常显著的进步。于是我们最终从 Array.prototype中剽窃了forEach方法来完成遍历,例如:

1
2
3
Array.prototype.forEach.call(document.querySelectorAll('klasses'), function(el){
el.addEventListener('click', someFunction);
});

 通过使用.bind()可以做到更好。

1
2
3
4
5
6
var unboundForEach = Array.prototype.forEach,
forEach = Function.prototype.call.bind(unboundForEach);

forEach(document.querySelectorAll('klasses'), function (el) {
el.addEventListener('click', someFunction);
});

1.3.3.4 绑定函数作为构造函数


 绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Point(x, y) {  
this.x = x;
this.y = y;
}

Point.prototype.toString = function() {
return this.x + ',' + this.y;
};

var p = new Point(1, 2);
p.toString(); // '1,2'


var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 实现中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/);

var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(17, 42) instanceof YAxisPoint; // true

上面例子中Point和YAxisPoint共享原型,因此使用instanceof运算符判断时为true。

1.3.3.5 注意bind()函数作构造函数


 当bind()所返回的函数用作构造函数的时候, 传入bind()的this将被忽略,实参会全部传入原函数。以下时举例:

1
2
3
4
5
6
7
8
9
10
function original(x){
this.a=1;
this.b =function(){return this.a + x}
}
var obj={
a:10
}
var newObj = new (original.bind(obj,2)) //传入了一个实参2
console.log(newObj.a) //输出 1, 说明返回的函数用作构造函数时obj(this的值)被忽略了
console.log(newObj.b()) //输出3 ,说明传入的实参2传入了原函数original

1.4 apply()、call()、bind()比较

  • apply() 、 call() 、bind() 三者都是用来改变函数的this对象的指向的;
  • apply() 、 call() 、bind() 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
  • apply() 、 call() 、bind() 三者都可以利用后续参数传参;
  • bind() 是返回对应函数,便于稍后调用;apply() 、 call() 则是立即调用 。

二、初识Javascript函数柯里化

2.1 柯里化(currying)概念


 柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。
 当我们这么说的时候,我想柯里化听起来相当简单。JavaScript中是怎么实现的呢?
 假设我们要写一个函数,接受3个参数。

1
2
3
var sendMsg = function (from, to, msg) {
alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
};

 现在,假定我们有柯里化函数,能够把传统的JavaScript函数转换成柯里化后的函数:

1
2
3
4
5
var sendMsgCurried = curry(sendMsg); // returns function(a,b,c)

var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob"); // returns function(c)

sendMsgFromJohnToBob("Come join the curry party!"); //=> "Hello Bob, Come join the curry party! Sincerely, - John"

2.2 实现柯里化函数

 思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。理论上我们期望可以有一个方便的方式转换普通老式的JavaScript函数(多个参数)到完全柯里化的函数。首先让我们来实现一个脏柯里化函数:

1
2
3
4
5
6
7
8
var curryIt = function(uncurried) {
var parameters = Array.prototype.slice.call(arguments, 1);
return function() {
return uncurried.apply(this, parameters.concat(
Array.prototype.slice.call(arguments, 0)
));
};
};

三、this关键字、上下文

四、函数原型


4.1 基本概念


 我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。那么,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

 使用原型的好处是可以让对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中添加定义对象信息,而是可以直接将这些信息添加到原型中。使用构造函数的主要问题就是每个方法都要在每个实例中创建一遍。

 在JavaScript中,一共有两种类型的值,原始值和对象值。每个对象都有一个内部属性prototype ,我们通常称之为原型。原型的值可以是一个对象,也可以是null如果它的值是一个对象,则这个对象也一定有自己的原型。这样就形成了一条线性的链,我们称之为原型链。

1
2
3
4
5
6
7
var Browser = function(){};
Browser.prototype.run = function(){
alert("I'm Gecko,a kernel of firefox");
}

var Bro = new Browser();
Bro.run();

 当我们调用Bro.run()方法时,由于Bro中没有这个方法,所以,他就会去他的__proto__中去找,也就是Browser.prototype,所以最终执行了该run()方法.

 当调用构造函数创建一个实例的时候,实例内部将包含一个内部指针__proto__指向构造函数的prototype,这个连接存在于实例和构造函数的prototype之间,而不是实例与构造函数之间。
如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name){                             //构造函数
this.name=name;
}

Person.prototype.printName=function() //原型对象
{

alert(this.name);
}

var person1=new Person('Byron');//实例化对象
console.log(person1.__proto__);//Person
console.log(person1.constructor);//Function.Person
console.log(Person.prototype);//指向原型对象Person
var person2=new Person('Frank');

 Person的实例person1中包含了name属性,同时自动生成一个__proto__属性,该属性指向Person的prototype,可以访问到prototype内定义的printName方法,大概就是这个样子的:

 每个JavaScript函数都有prototype属性,这个属性引用了一个对象,这个对象就是原型对象。原型对象初始化的时候是空的,我们可以在里面自定义任何属性和方法,这些方法和属性都将被该构造函数所创建的对象继承。

4.2 构造函数、实例和原型对象的区别


 实例就是通过构造函数创建的。实例一创造出来就具有constructor属性(指向构造函数)和__proto__属性(指向原型对象)。
 构造函数中有一个prototype属性,这个属性是一个指针,指向它的原型对象。
 原型对象内部也有一个指针(constructor属性)指向构造函数:Person.prototype.constructor = Person;
 实例可以访问原型对象上定义的属性和方法。
 在4.1中,person1和person2就是实例,prototype是他们的原型对象。

4.2.1 函数对象实例


1
2
3
function Base() {  
this.id = "base"
}

此段代码生成Base构造函数中的函数指针prototype指向,并指向他的原型:Base.prototype
原型:Base.prototype 中的constructor指向构造函数:Function.Base

1
var obj = new Base();

生成一个obj实例,此实例的__proto__隐藏属性也指向他的原型对象:Base.prototype

new的作用可以转换为以下三句话:

1
2
3
var obj  = {};  //生成实例
obj.__proto__ = Base.prototype; //实例的隐藏属性指向它的原型对象
Base.call(obj); //调用Function.prototype.call方法,将obj实例的this指针传入Base的构造方法

4.3 基本属性


 我们看一下Function.prototype对象的属性:

1
2
3
Object.getOwnPropertyNames(Function.prototype)
//=> ["length", "name", "arguments", "caller",
// "constructor", "bind", "toString", "call", "apply"]

 我们看到一些我们感兴趣的几个属性。

  • Function.prototype.length
  • Function.prototype.call
  • Function.prototype.apply

 这些已在前文阐述过,不再赘述。

4.4 原型链


 原型链:当从一个对象那里调取属性或方法时,如果该对象自身不存在这样的属性或方法,就会去自己关联的prototype对象那里寻找,如果prototype没有,就会去prototype关联的前辈prototype那里寻找,如果再没有则继续查找Prototype.Prototype引用的对象,依次类推,直到Prototype.….PrototypeundefinedObjectPrototype就是undefined)从而形成了所谓的“原型链”。
举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};

function Bar() {}

// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 创建Bar的一个新实例

// 原型链
test //作为Bar的实例
Bar.prototype//Foo的实例
{ foo: 'Hello World' }//Bar的原型属性
Foo.prototype//Foo的原型内的方法
{method: ...};
Object.prototype//test->Bar->Foo->Object,所有原型链的尽头
{toString: ... /* etc. */};

 上面的例子中,test 对象从Bar.prototypeFoo.prototype 继承下来;因此,它能访问 Foo 的原型方法 method。同时,它也能够访问那个定义在原型上的 Foo 实例属性 value。需要注意的是 new Bar() 不会创造出一个新的 Foo 实例,而是重复使用它原型上的那个实例;因此,所有的 Bar 实例都会共享相同的 value 属性。

 以下是使用原型的一些方式:

  • 在赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式:

    1
    Calculator.prototype = function () { } ();
  • 可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果,修改后的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Calculator.prototype = function () {
    add = function (x, y) {
    return x + y;
    },

    subtract = function (x, y) {
    return x - y;
    }
    return {
    add: add,
    subtract: subtract
    }
    } ();

4.5 proto属性和prototype属性的区别

prototypefunction对象中专有的属性。
__proto__是普通对象的隐式属性,在new的时候,会指向prototype所指的对象;
__ptoto__实际上是某个实体对象的属性,而prototype则是属于构造函数的属性。__ptoto__只能在学习或调试的环境下使用。