上一次跳槽面试的时候,一次面试接近尾声,进行的特别顺利,直到面试官提出一个问题,“请你实现一下bind”。
“什么!!实现bind?为什么不问call、apply、bind的使用及区别,这些我都倒背如流”。
因为那时的段位还很低,对知识的掌握还停留在使用层面,所以被问到的时候是特别懵的。好在面试官人很好,经过多次提示还是写出了一个初级实现。
代码如下:
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this // this就是调用的函数
// 将arguments转换为数组
var argsArray = Array.prototype.slice.call(arguments)
// 返回一个函数,符合bind的特性
return function () {
// 返回的函数中执行调用的函数,并通过apply改变this指向,传递参数
return me.apply(that, argsArray.slice(1))
}
}
// 验证一下
function aa(p1, p2) {
console.log(this.a + "|" + p1 + "|" + p2)
}
var fn = aa.bind({ a: 2 }, "p1")
fn("p2") // 2|p1|undefined
这就是一个最最基础的实现,我一度认为bind的实现也不过如此。不过在使用的时候发现一个问题,注意上面的验证代码输出结果,undefined
是什么鬼?不应该是输出 2|p1|p2
。这时因为如此实现的bind只能通过调用bind的时候给函数传参,无法在调用bind返回的函数时传参。
有了上面的实现,解决这个问题也不太难。
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this
var args = Array.prototype.slice.call(arguments, 1)
return function () {
// 获取调用返回的函数时传递的参数,并将两次参数合并
var innerArgs = Array.prototype.slice.call(arguments)
var totalArgs = args.concat(innerArgs)
return me.apply(that, totalArgs)
}
}
// 验证一下
function aa(p1, p2) {
console.log(this.a + "|" + p1 + "|" + p2)
}
var fn = aa.bind({ a: 2 }, "p1")
fn("p2") // 2|p1|p2
验证通过,心想这回应该没有问题了吧。直到有一天在总结 this 指向问题的时候。遇到了一个 new 和 bind 同时出现的情况。也就是说当 bind 返回的函数作为构造函数调用时。那么通过 bind 绑定的this就需要被忽略,很明显 this 要绑定到创建的实例上。
从改变 this 指向的角度来说,new 的优先级要高于 bind 的绑定。
如果对this的指向问题感兴趣可以参考《this到底指向谁》一文。
知道真相的我赶紧翻出代码,完善我的 bind。这次的进展不如上次顺利,因为又要用到继承的相关知识。抽出时间又将js的继承简单总结了下,《永不过时的面向对象——继承》。这下算是豁然开朗,噼里啪啦……代码如下:
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this
var args = Array.prototype.slice.call(arguments, 1)
var F = function () { }
// F的原型继承调用函数的原型,利用空对象方式实现原型链继承
F.prototype = this.prototype
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments)
var totalArgs = args.concat(innerArgs)
return me.apply(this instanceof F ? this : that, totalArgs)
}
// 将 bound 的 prototype 对象指向一个 F 的实例
bound.prototype = new F()
return bound
}
核心在于通过创建空对象的方式,实现了 bound 继承调用函数。
为何要继承原函数?
因为 new 调用 bind 后返回的函数,也是相当于将原函数作为构造函数调用,创建实例,如果不继承原函数,那么创建的实例与原函数没有任何关系。
另一个关键点在于对 new 的理解,new 的时候都做了些什么操作,在上面分享的《继承》一文中有详细解答。
由于通过 new 调用返回的函数时,bound 内的 this 指向自身实例。并且 bound 的原型指向 F 的实例,又因为 F 的原型继承调用函数的原型,所以有 this instanceof F
为 true,自然三目表达式的结果为 this。因此创建的实例也是调用函数的实例。this instanceof me
也为 true。
验证代码:
function Animal(a) {
this.a = a
}
const o1 = {}
// 普通调用
var a1 = Animal.bind(o1)
a1(2)
console.log(o1.a) // 2
// 作为构造函数调用
var a2 = new (Animal.bind(o1))(5);
console.log(a2) // Animal {a: 5}
a2.__proto__.constructor === Animal // true
console.log(a2.a) // 5
这次我不敢说我实现了 bind,只能说这次的实现比之前更完善。为了看下 bind 的完美实现方式,翻出了es5-shim.js中的 bind 源码。
function bind(that) {
var target = this;
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
var args = array_slice.call(arguments, 1);
var bound;
var binder = function () {
if (this instanceof bound) {
// 构造函数调用情况
var result = apply.call(
target,
this,
array_concat.call(args, array_slice.call(arguments))
);
if ($Object(result) === result) {
return result;
}
return this;
} else {
// 正常调用情况
return apply.call(
target,
that,
array_concat.call(args, array_slice.call(arguments))
);
}
};
var boundLength = max(0, target.length - args.length);
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
array_push.call(boundArgs, '$' + i);
}
bound = $Function(
'binder',
'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }'
)(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
return bound;
}
比我想象的要复杂一些,但是实现的核心部分是相似的。其中有一点是特别容易被忽略的,就是每个函数都有像数组和字符串那样的 length 属性,用于表示函数的形参个数。并且函数的 length 属性值是不可重写的。es5-shim 是为了最大限度地进行兼容,包括对返回函数 length 属性的还原。
一次 bind 实现的经历。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。