本文用于总结 JavaScript bind 函数的机制,分析其源码以及应用。
介绍
根据MDN
上的介绍:
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
就是说 bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
bind 应用
创建硬绑定的this
使用MDN
上的例子:
1 | this.x = 9; // this refers to global "window" object here in the browser |
可以看到,在一个函数上使用bind
,其在调用时的this
始终会指向给bind
传递的第一个参数,这就是硬绑定。
硬绑定还可以用于setTimeout
、事件处理函数。在setTimeout
中,this
的值默认是 window,事件处理函数中的this
通常情况下是事件发生的目标DOM
节点。在很多时候不注意就会产生 BUG,此时就可以使用 bind 来显示指定this
的值。MDN
例子:
1 | function LateBloomer() { |
如果写成
1 | window.setTimeout(this.declare, 1000); |
那么运行declare
函数时,this
的值指向window
,而window
中没有petalCount
,就会报错。
bind
在事件处理函数中的应用与setTimeout
类似,这里就不赘述了。
函数柯里化
柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
也可以理解为如果一个函数接收多个参数,那么柯里化会把函数的前几个参数固定为特定的值,之后的参数会在调用时再指定。还是来看MDN
上的例子
1 | function list() { |
可以看到,list
函数在没有使用柯里化时,所有的参数都需要在调用时指定。在进行柯里化之后的leadingThirtysevenList
函数,其第一个参数被固定为了37
,在真正调用leadingThirtysevenList
时传递的参数都会在这个37
之后。
这里注意,调用bind
时,若第一个参数是null
,并不是说柯里化之后的函数的this
被绑定为了null
,而是undefined
或者window
,取决于是否在严格模式下。
延迟执行
这是参考的前端开发者进阶之函数柯里化 Currying这篇博客。
柯里化还可以用于延迟执行:不断的柯里化,累积传入的参数,最后执行。例如:
1 | var add = function() { |
通用的写法:
1 | var curry = function(fn) { |
源码分析
先贴上MDN
上的源码:
1 | if (!Function.prototype.bind) { |
使用
1
if (!Function.prototype.bind)
是兼容不支持
bind
的浏览器,bind
是在 ES5 中才出现的,在较新的浏览器上都已经有了原生的bind
实现,所以不需要使用垫片。函数最开始的判断是因为
bind
只能在函数上调用,非函数上调用需要报错。aArgs
是调用bind
时参入的固定参数,可以看到最终的fBound
在调用时的实参是综合了固定参数和后来调用fBound
传递的参数,这就实现了柯里化。为什么不直接使
fBound
的原型和this
的原型相同,即1
fBound.prototype = this.prototype;
因为这样就会导致在改变
fBound
时连带着改变了调用bind
的那个函数,即fToBind
。
所以使用一个空函数fNOP
作为中转。此时原型链的示意为fBound.prototype -----> new fNOP() ---> this.prototype
fToBind.apply(this instanceof fNOP? this: oThis, ...)
的作用:
这是因为当bind
返回的函数作为构造函数的时候,bind
指定的this
值会失效,此时的this
会指向构造出来的那个对象实例。具体可以参见MDN
给的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function 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'
// not supported in the polyfill below,
// works fine with native bind:
var YAxisPoint = Point.bind(null, 0 /*x*/);var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 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
`
软绑定
此节内容参考你不懂 JS:this 豁然开朗
硬绑定 是一种通过强制函数绑定到特定的 this 上,来防止函数调用在不经意间退回到 默认绑定 的策略(除非你用 new 去覆盖它!)。问题是,硬绑定 极大地降低了函数的灵活性,阻止我们手动使用 隐式绑定 或后续的 明确绑定 尝试来覆盖 this。
为默认绑定 提供不同的默认值(不是 global 或 undefined),同时保持函数可以通过 隐式绑定 或 明确绑定 技术来手动绑定 this。
模拟代码:
这里提供的 softBind(..)工具的工作方式和 ES5 内建的 bind(..)工具很相似,除了我们的 软绑定 行为。他用一种逻辑将指定的函数包装起来,这个逻辑在函数调用时检查 this,如果它是 global 或 undefined,就使用预先指定的 默认值 (obj),否则保持 this 不变。它也提供了可选的柯里化行为。
来看看它的用法: