我以为我实现了bind

巴斯光年

上一次跳槽面试的时候,一次面试接近尾声,进行的特别顺利,直到面试官提出一个问题,“请你实现一下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 实现的经历。

阅读 1.2k
228 声望
20 粉丝
0 条评论
228 声望
20 粉丝
文章目录
宣传栏