关于JavaScript的bind相信作为一名FEer都不会感到陌生,由于JavaScript中有着一个特殊的值–this的存在,一个函数可以在不同的上下文中被调用,所以关于this我们要学会去分辨不同的场景,这样才能更好的去运用JavaScript这门灵活的语言

call && apply && bind

在真正的开始之前我们先来复习一些基本知识.

1
2
3
Function.prototype.call(thisToBind[,arg1, arg2, ...]);
Function.prototype.apply(thisToBind[,rest arguments]);
Function.prototype.bind(thisToBind[,arg1,arg2,...]);

共同点:

传入的第一个参数都能作为该函数运行时候的this值。(都能显示的改变某个函数运行时候的this)

不同点:

call 返回一个函数的运行结果,接受改函数的参数以明确的个数传入。
apply返回一个函数的运行结果,接受的参数可以是数组或者是类数组对象。(什么是类数组对象?可以通过obj[0]这样数字属性访问,但是没有数组的方法(就是不能使用类似obj.push())
bind 返回一个函数,该函数运行时内部的this值将会指向bind方法调用时候传入的第一个值,并且同一个函数不能同时bind2次或更多,这点等下我会和大家说到。

函数柯里化

什么是函数柯里化?这里引用wikipedia上的定义:

柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

这里说的很清楚了,函数柯里化其实就是将函数一次性接受所有参数进行调用的方式改变了一下而已,它可能是这个样子的:

1
methodA(a, b, c) === methodACurrying(a)(b, c);

就是一个函数珂里化之后返回一个新的函数,这个函数保存着之前首次调用传入的参数a,并且在该函数执行的时候将之前的参数a加上b,c作为参数一起调用,也就相当于以methodA(a, b, c)的调用形式
有点不懂?没关系,我们来看下面一个简单的函数柯里化(currying)的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

function add( ) { // 定义一个简单的加函数
var result = 0;
for(var i =0, l = arguments.length; i < l; i++) {
result += arguments[i];
}
return result;
}
function count() { // count函数实现add函数的柯里化 既count(a)(b ,c) <==> add(a, b, c)
var args = [].slice.call(arguments);
return function () {
return add.apply(window, args.concat(Array.prototype.slice.apply(arguments)));
}
}
log(add.bind(window,1)(2,3)); // 6
log(count(1)(2, 3)); // 6
log(count(1, 2)(3)); // 6
log(count(1, 2, 3)()); // 6

可以看到,通过使用强大的闭包我们实现了函数柯里化。

  • 我们定义了一个count函数,在这个函数内部我们县通过callcount函数传入的参数进行了数组化(保存在变量args中),并且最后它返回的是一个函数,这个函数包含着对args变量的引用,所以能够保证在count函数运行返回的匿名函数中依旧能够访问到args
  • 这个变量,这个知识属于JavaScirpt中的闭包知识,详情可以查看这里
  • 现在我们在返回的匿名函数内部对add函数使用apply方法 add函数如何能调用apply?因为add是Function类的一个实例所以能够访问到Function.prototype.apply实例公共方法,巧妙的通过args变量(在count函数中已经被转化为数组了)的concat方法将后面传入的参数先数组化然后再进行合并。

简直就是一气呵成的啊^-^ my god !!!
别急!!! 但是这个和bind有关系么???当然有啦,我们可以看到在log第一行我们是使用了add.bind(window, 1)(2, 3)的形式来调用的,它比我们上面的count函数多了一个优点,那就是this值的绑定,使用bind传入的第一个参数会成为后面新函数运行的this值就是函数运行的上下文,而我上面定义的count方法是没有绑定this值的功能的.
现在我们可以看看MDN上面关于bind的解释:

bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

这个定义是不是很熟悉?答案是是的,它和我们上面提到的函数柯里化定义几乎一样,除了一个地方,那就是bind有绑定this的功能而我们上面提到的柯里化是没有包含绑定this这一项的

优化

上面我们简单实现的count函数其实离bind还有一些距离,比如:

  • 返回的函数没有绑定this
  • 只能通过count()对它进行调用

那么如何让所有的function类都能使用count方法呢?我们来改装一下count函数,并把它挂载在Funcition.prototypeFuncition这个原型对象上

1
2
3
4
5
6
7
8
9
10
function count() {
var args = [].slice.call(arguments, 1);
var self = this; // save this
return function () {
return add.apply(self, args.concat(Array.prototype.slice.apply(arguments)));
}
}
if(!Function.prototype.count) {
Function.prototype.count = count;
};

好了现在看起来好像是添加了绑定this功能并且每个funcition类都能访问了。但是有一天我们发现这样还是太麻烦了,设想一下,我们这里只是让每个函数绑定count方法,那么如果还有减法运算和乘法运算呢?我们难道一个个的去添加到Function的原型上?(这里举例的加减乘法只是例子,实际的项目需求当然会有很多不同功能的函数),伟大的开发者也想到了,这个根本不是办法啊?于是乎,bind方法就这样出现了。

bind 知多少

分分钟实现一个bind方法啊有木有~

1
2
3
4
5
6
7
function bind(ObjToBind) { // ObjToBind => 在self调用的时候函数内部this的指向
var self = this; // save this
var args = [].slice.call(arguments, 1); // 数组化 去掉传入的第一个参数
return function () { // 返回匿名函数(闭包)
self.apply(ObjToBind, args.concat([].slice.call(arguments))); // 合并参数执行函数
}
}

由于Function.prototype.bind方法是在ECMAScript5中才被添加进去的,对于IE8下这种远古浏览器来说是无法理解的,但是可能我们实际的项目中用到了bind方法同时我们也需要去兼容IE8,这个时候就要人为手动的去扩展Function.prototype对象上的方法了,我们来优化一下上面的bind方法

1
2
3
4
5
6
7
8
9
(function() {
Function.prototype.bind = Function.prototype.bind || function (ObjToBind ) {
var self = this; // save this
var args = [].slice.call(arguments, 1); // 数组化
return function () {
self.apply(ObjFnToBind, args.concat([].slice.call(arguments)));
}
}
})

上面的代码做了几件事情:

  • 使用立即执行匿名表达式(IIFE) --- Immediately-Invoked Function Expression 避免全局污染
  • 只需要运行一次匿名函数即可解决bind问题 无需每次调用bind检查是否存在Function.prototype上面
  • 返回的函数可以接受任意个数的参数来进行调用,这得益于apply的使用

这就是bind的好处,能够绑定函数运行的上下文。试想一下,一些事件处理程序里面,我们往往要注意this的取值,因为在这些事件处理程序里面的this值会有所不同,这个时候如果我们能对这些事件处理函数来进行bind的话,那就可以放心的在事件handle里面使用this啦~~~~但是我们上面只是简单的实现了bind,在想一下我们通过执行bind方法返回的匿名函数还是有缺陷的

  • 比如说如果我们绑定的函数原型上有许多我们定义好的方法,但是这个返回的匿名函数只是简单的包裹了一下,在这个匿名函数下调用在前面被绑定函数对象原型上面定义的方法属性并不能够被访问。

啥?没听懂?抱歉我语言表达能力还是有待加强,那我们还是来上代码吧~小儿,给我来一壶上好的代码~~

MDN Bind polyfill

那么如何更加优雅的实现原生的bind函数呢? 让我们一起来看看MDN上的关于bind方法的Polifill吧
具体的例子可以看下面这段代码,摘抄自MDN Bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
};
}

分析Bind的实现

可能大家对于上面的代码会有一些疑惑,我当时是看了好好多遍,自己想了好几次才全部明白这其中的原理,真的不得不佩服前辈们的思维深度和思维广度,把一切问题都想到了!我们来一步步分析吧

1
2
3
4
5
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");
}

检测调用bind方法的 是不是函数,如果不是函数的话那还玩个球啊。直接抛出错误并返回

1
2
3
4
var aArgs = Array.prototype.slice.call(arguments, 1), // 参数数组化 make to array
fToBind = this, // save this 保存调用bind的函数对象
fNOP = function () {}, // 定义一个空函数 后面会讲到他的意义 这里就知道有这么一个空的函数就好了。
fBound = function () {} ... // bind运行返回的函数

这一步定义了一些变量,包括保存调用的数组aArgs,调用bind的函数fToBind,返回的函数fBound,还有一个神秘的空函数fNOP,对于fNOP我们打个问号一?

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

这里就是定义返回函数内部的逻辑了,我和我们上面自己实现的简单bind函数返回的匿名函数还是有些区别的,比如说第一个参数这里是用了条件运算符this instanceof fNOP ? this : oThis || this 这里打个问号二?

1
2
3
fNOP.prototype = this.prototype; // 让空函数的原型执行this原型
fBound.prototype = new fNOP(); // 返回的函数原型为空函数的实例 保存着指向fNOP的原型的指针
return fBound;

看到这里终于恍然大悟了,这个不是继承么,通过一个空函数作为中介,让返回的fBound函数的原型链上包含this也就是调用bind的函数。这个也就解决了我们上面所说的bind后的函数原型链的中断的情况。问号一解决!!!

谢谢大家阅读,再见~
哎,等等,卧槽,还有问题二呢!!!! 泥TM是在逗我啊,挖坑不填充,菊花万人捅org
回归原意,this instanceof fNOP ? this : oThis || this 判断this的原型链上有fNOP么,如果包含就说明了fNOP函数是被作为函数构造器调用的,所以这里的this就不能够是oThis啦,而应该正确的指向实例化的新的对象。如果函数不是作为函数构造器调用,正确的传入oThis就好了。还有一点必须要提醒大家的就是,一个函数只能被bind一次,一次,一次 因为调用bind函数返回的函数里面已经指定死了this的值了,我们无法再去改变fToBind.apply(oThis)中的oThis,因为他访问的是外层闭包的取值。

现在再去回过头细细品味上面的bind polyfill 你应该会有不一样的收获吧? Enjoy it!

(完)