浅谈 javascript 中的bind
关于JavaScript的bind
相信作为一名FEer都不会感到陌生,由于JavaScript
中有着一个特殊的值–this
的存在,一个函数可以在不同的上下文中被调用,所以关于this
我们要学会去分辨不同的场景,这样才能更好的去运用JavaScript这门灵活的语言
call && apply && bind
在真正的开始之前我们先来复习一些基本知识.1
2
3Function.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
方法调用时候传入的第一个值,并且同一个函数不能同时bind
2次或更多,这点等下我会和大家说到。
函数柯里化
什么是函数柯里化?这里引用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
函数,在这个函数内部我们县通过call
将count
函数传入的参数进行了数组化(保存在变量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.prototype
Funcition这个原型对象上
1 | function count() { |
好了现在看起来好像是添加了绑定this
功能并且每个funcition类都能访问了。但是有一天我们发现这样还是太麻烦了,设想一下,我们这里只是让每个函数绑定count方法,那么如果还有减法运算和乘法运算呢?我们难道一个个的去添加到Function的原型上?(这里举例的加减乘法只是例子,实际的项目需求当然会有很多不同功能的函数),伟大的开发者也想到了,这个根本不是办法啊?于是乎,bind方法就这样出现了。
bind 知多少
分分钟实现一个bind方法啊有木有~1
2
3
4
5
6
7function 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 Bind1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23if (!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 | if (typeof this !== "function") { |
检测调用bind
方法的 是不是函数,如果不是函数的话那还玩个球啊。直接抛出错误并返回
1 | var aArgs = Array.prototype.slice.call(arguments, 1), // 参数数组化 make to array |
这一步定义了一些变量,包括保存调用的数组aArgs
,调用bind
的函数fToBind
,返回的函数fBound
,还有一个神秘的空函数fNOP
,对于fNOP
我们打个问号一?
1 | fBound = function () { |
这里就是定义返回函数内部的逻辑了,我和我们上面自己实现的简单bind
函数返回的匿名函数还是有些区别的,比如说第一个参数这里是用了条件运算符this instanceof fNOP ? this : oThis || this
这里打个问号二?1
2
3fNOP.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!
(完)