从规范来看,Function.prototype.bind
是如何工作,以及如何来模拟 bind 操作。
简单示例
如下简单示例,普通对象 testObj
内部有一个 b 函数,接受一个普通参数,若参数为空则输出 this.a
。
const testObj = {
a: 3,
b: function(args) {console.log(args || this.a);
},
};
testObj.b()
testObj.b(23)
const c = testObj.b
c()
c(23)
const c1 = testObj.b.bind(testObj, 50)
c1(70)
查看结果:
testObj.b
被重新赋值给 c
后,函数的的执行上下文已经改变,导致输出为 undefined
。通过上面例子,如果采用 bind
后,则可以改变 testObj
的执行上下文,并可以把默认值传递到参数函数列表.
倘若在 testObj.b
内,加入 console.log(arguments)
, 则可以看到如下输出:
50 70
bind 函数
bind 函数是,Function
原型链上的函数,主要是改变函数执行上下文的同时,可以传入函数参数值,并返回新的函数
如图是 mdn 上的定义,
bind 产生的函数就一个偏函数,就是说使用 bind 可以参数一个函数,然后接受新的参数。
In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
详细可看: https://github.com/mqyqingfeng/Blog/issues/43
规范定义
在 EcmaScript 的规范 15.3.4.5 中如下截图:
bind 方法需要一个或更多参数,thisArg 和(可选的)arg1, arg2, 等等,执行如下步骤返回一个新函数对象:1. 令 Target 为 this 值 .
2. 如果 IsCallable(Target) 是 false, 抛出一个 TypeError 异常 .
3. 令 A 为一个(可能为空的)新内部列表,它包含按顺序的 thisArg 后面的所有参数(arg1, arg2 等等)。4. 令 F 为一个新原生 ECMAScript 对象。5. 依照 8.12 指定,设定 F 的除了 [[Get]] 之外的所有内部方法。6. 依照 15.3.5.4 指定,设定 F 的 [[Get]] 内部属性。7. 设定 F 的 [[TargetFunction]] 内部属性为 Target。8. 设定 F 的 [[BoundThis]] 内部属性为 thisArg 的值。9. 设定 F 的 [[BoundArgs]] 内部属性为 A。10. 设定 F 的 [[Class]] 内部属性为 "Function"。11. 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 的 prototype 对象。12. 依照 15.3.4.5.1 描述,设定 F 的 [[Call]] 内置属性。13. 依照 15.3.4.5.2 描述,设定 F 的 [[Construct]] 内置属性。14. 依照 15.3.4.5.3 描述,设定 F 的 [[HasInstance]] 内置属性。15. 如果 Target 的 [[Class]] 内部属性是 "Function", 则
a. 令 L 为 Target 的 length 属性减 A 的长度。b. 设定 F 的 length 自身属性为 0 和 L 中更大的值。16. 否则设定 F 的 length 自身属性为 0.
17. 设定 F 的 length 自身属性的特性为 15.3.5.1 指定的值。18. 设定 F 的 [[Extensible]] 内部属性为 true。19. 令 thrower 为 [[ThrowTypeError]] 函数对象 (13.2.3)。20. 以 "caller", 属性描述符 {[[Get]]: thrower, [[Set]]: thrower,[[Enumerable]]: false, [[Configurable]]: false}, 和 false 作为参数调用 F 的 [[DefineOwnProperty]] 内部方法。21. 以 "arguments", 属性描述符 {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, 和 false 作为参数调用 F 的 [[DefineOwnProperty]] 内部方法。22. 返回 F.
bind 方法的 length 属性是 1。Function.prototype.bind 创建的函数对象不包含 prototype 属性或 [[Code]], [[FormalParameters]], [[Scope]] 内部属
规范表达过程简述如下:
- 第 1 步,创建 Target,并把 this 值给 Target【this 为当前执行环境,可以理解为当前函数】
- 第 3 步,内部创建一个参数空的参数列表 A,包含了 bind 参数中除了
thisArg
之外的其他函数 - 第 4 步,创建一个对象 F
- 第 6,7,8,9,10,11 步,设置函数内部属性,这里第 7 步中的 [[TargetFunction]] 仅仅只有使用 bind 才会生成,第 8 步设置
thisArg
新的函数的 this. - 第 12,13,14 步,需要设置对象 F 内部方法
[[Call]]
,[[Construct]]
,[[HasInstance]]
, 让对象可以被调用 - 第 15 步,让对象 F 变成
[[Function]]
, 并设置新的函数length
,新的 length 值 范围是0 ~ Target.length
- 第 18 步,设置返回新函数可以任意添加属性
- 第 20 步,设置函数描述符号
caller
属性, - 第 21 步,设置函数参数描述符号,
arguments
- 最后返回对象 F,也就是新执行函数。
在 bind 过程中,会重新设置
[[Call]]
相关函数内部方法,详细可以看规范。
isCallable 定义
Object 内部属性以及方法
用途
- 创建绑定函数,例如 react 事件参数传递
- 偏函数
- 定时器修改 this
- 构造函数使用绑定函数
- 快捷调用
最后
知道了过程,倘若不支持,该如何呢?来,搞一个手工的:
Function.prototype.bind2 = function bind2(thisArgs) {const aArgs = Array.prototype.slice.call(arguments, 1);
const fThis = this;
const fNOP = function() {};
const fBound = function() {
// 这段判断是不是使用 bind 返回函数继续 bind
return fThis.apply(this instanceof fBound ? this : fThis, aArgs.concat(Array.prototype.slice.call(arguments)));
};
// this === Function.prototype, 保证创建函数的原型链也为 undefined
if (this.prototype) fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
我们实现方法依赖一些属性方法:
- Fucntion.prototype.apply
- Function.prototype.call
- Array.prototype.slice
而且我们实现方法有一个问题在于:
- length 始终返回为 0,并没有计算
- 返回函数具有
prototype
, 不符合规范
查看更规范的实现,点击这里
欢迎交流