在项目和框架中,最常用的三种改变this指向的方法有call
bind
apply
,其中 call
和 apply
是最早在标准中实现了的。都是通过调用了函数的内置方法 [[call]]
来实现 this
指向的改变。在仿写call
和apply
的时候,主要是通过用对象属性调用的方式来实现,只需要判断传入的参数是否符合标准即可。而 bind
方法复杂的地方是它的返回了一个函数,而没有调用它。JavaScript的函数既可以常规调用,又可以通过 new
调用。除此之外,函数还具有非常特殊的几个属性,比如 name
和 length
。在这些特性中有一部分是我们在创建的时候程序帮我们处理好的,还有一部分需要我们自己绑定。
因为[[call]]方法无法外部调用,而本身call和apply的实现就是调用了 [[call]] 这个内部方法,所以方法中涉及到修改this指向的几乎都是通过调用 call和apply方法实现的
完整的代码片段全部是来自es5-shim
- 先简单获取一些基本的属性
Function.prototype._bind = function() {
var target = this
// 要求target必须是一个可调用对象,在这里就简单的判断成一个函数
if(typeof target !== 'function'){
throw TypeError('is not a function')
}
var [thisArg, ...args] = arguments
}
这一步的关键是容易忽略this
类型的判断
- 处理
binder
函数的调用
Function.prototype._bind = function() {
// ...
var bound
var binder = function() {
if (this instanceof bound) {
var result = Function.prototype.apply.call(target, thisArg, [
...args, ...arguments
])
if (Object(result) === result)
return result
return this
}else {
return Function.prototype.apply.call(target, thisArg, [
...args, ...arguments
])
}
}
// ...
}
binder
函数最终是执行修改this
值的函数,值得注意的是如何使用 apply
和 call
来完成调用
- 绑定返回函数
bound
的自身的属性
Function.prototype._bind = function() {
// ...
var boundLength = 0
var boundArgs = []
boundLength = Math.max(0, target.length - args.length)
for(var i = 0; i < boundLength; i++) {
boundArgs.push('$' + i)
}
bound = Function('binder', `return function (${boundArgs.join(',')}) {
return binder(arguments)
}`)(binder)
bound.name = `bound ${target.name}`
// ...
}
以上这段代码我已经写过很多遍了,但是每一次写到这还是会停下来思考这些代码都做了什么,这里也是 bind
方法比较难理解的和想到的地方。获取 bound length 的地方是贴着标准实现的,这个可以参考后面的传送门,难想到的是如何设置参数。
bind
除了具有函数自身的特性之外,还可以分步的接受参数,这就是为什么大家都喜欢用 bind
来实现柯里化
var foo = function (a, b) { console.log(a, b) }
var bindFoo = foo.bind(null, 'a')
bindFoo.length // 1
bindFoo('b') // a, b
可以看到在使用bind的时候,如果如参的个数小于形参的个数,那么少的部分就会作为生成的 bindFoo
的形参。
而在JavaScript中有能力设置形参的方式就是 Function
。所以通过自调用的方式将 binder
函数传入 bound
函数内部,同时设置了 bound
函数的形参
- 完成
bound
函数的原型链绑定
Function.prototype._bind = function() {
// ...
var Noop = function() {}
var proto = target.getPropertyOf()
if (proto){
Noop.setPrototypeOf(proto)
bound.setPrototypeOf(new Noop)
Noop.setPrototypeOf(null)
}
return bound
}
最后就是设置了 bound
的原型,就结束了。
其实这是一个非常好的题,因为题目本身涉及的内容非常的广泛,而且功能的完善是分成了多个步骤,可能考验应试者对于函数特性的理解程度。