手写 bind 的实现

222 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情

核心描述

  • bind 方法的作用:创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被制定为 bind() 的第一个参数,而其余参数作为新函数的参数,供调用时使用。
  • bind 方法的核心注意事项
    • 返回一个方法,并绑定第一个入参为新方法的 this 指向
    • 新方法支持绑定时传入默认参数,函数柯里化
    • 新方法可以通过 new 关键字,作为构造函数进行实例化,会忽略传入的 this 参入
    • 支持 Function 的 prototype 原型链拓展,即每个 function 对象都可以直接调用
    • 需要继承目标方法的 prototype 原型链
    • 异常处理:如果调用的目标不是 function ,则抛出异常
  • 手写 bind 的实现
/**
 * 模拟 bind 的实现
 * 说明1:拓展 Function.prototype 的原因是为了使所有 function 对象都可以使用该方法,符合 ES5 中 bind 的用法
 * 说明2:此时的 this 为调用 xxx._bind 方法的 xxx 函数对象
 * 说明3:调用 _bind 方法的对象不是一个 function,模拟非 function 对象调用 bind 时的报错输出,可以通过 Function._bind.apply(xxx),来模拟此场景
 * 说明4:获取 targetFunc 传入的其余参数,第一个参数为要改变的 this 对象,剩余参数为函数执行时接收的参数
 * 说明5:新创建一个函数对象,用于继承 targetFunc 的 prototype 原型对象
 * 说明6:最终返回的结果函数,在调用这个函数时,获取到的入参,在执行时,需要和说明4中的初始参数做合并
 * 说明7:最终返回的结果函数,默认其为 undefined
 * 说明8:通过 instanceof 来判断是否为 new 关键词调用,因为如果用 new 关键词调用,则结果方法在执行时的 this 的类型的是说明5的函数对象,注意此处的 this 并非是 targetFunc
 * 说明9:如果不是通过 new 调用,则赋值为传入 _bind 方法的第一个参数
 * 说明10:利用调用 _bind 方法基于 apply 来实现最终的执行,此处也可以使用 call 来实现,区别在于入参的类型是数组还是多个对象
 * 说明11:原型链的继承,作用是切断结果函数和 targetFunc 函数的关联,使其后续不再互相影响
 * @param {*} targetThis 
 */
Function.prototype._bind = function (targetThis) {
  const targetFunc = this; // 说明2

  if(typeof targetFunc !== 'function') { // 说明3
    throw new Error('_Bind must be called on a function')
  }
  // 说明4
  const targetFuncArgs = Array.prototype.slice.call(arguments,1)

  // 说明5
  const fNOP = function (){}
  const resultFunc = function (){
    // 说明6
    const resultFuncArgs = Array.prototype.slice.call(arguments)
    // 说明7
    let resultFuncThis = undefined
    // 说明8
    if(this instanceof fNOP){
      resultFuncThis = this
    } // 说明9
    else {
      resultFuncThis = targetThis
    }
    // 说明10
    return targetFunc.apply(resultFuncThis, [...targetFuncArgs, ...resultFuncArgs])
  }
  // 说明11
  fNOP.prototype = targetFunc.prototype
  resultFunc.prototype = new fNOP()
  return resultFunc
}

// 测试
const testObj = {
  value: '123'
}
const getValue = function(){
  return this.value
}
// 基础调用
const getValueByBind = getValue._bind(testObj)
console.log(getValueByBind()) // 输出:123
// 接受参数
const getArgs = function(...rest){
  console.log(this.value,rest)
}
const getArgsByBind = getArgs._bind(testObj,1)
getArgsByBind(2) // 输出:123 [1,2]
// new 关键词
const newFunc = function(initValue){
  console.log(this.value, initValue)
}
const newFuncByBind = newFunc._bind(testObj)
const obj = new newFuncByBind(1) // 输出:undefined 1 ,可以看出当使用 new 关键词时,指定的 this 并没有生效


知识拓展

  • 完整的 bind 实现,来源于 es5-shim
function bind(that) { // .length is 1
    // 1. Let Target be the this value.
    var target = this;
    // 2. If IsCallable(Target) is false, throw a TypeError exception.
    if (!isCallable(target)) {
        throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    // 3. Let A be a new (possibly empty) internal list of all of the
    //   argument values provided after thisArg (arg1, arg2 etc), in order.
    // XXX slicedArgs will stand in for "A" if used
    var args = array_slice.call(arguments, 1); // for normal call
    // 4. Let F be a new native ECMAScript object.
    // 11. Set the [[Prototype]] internal property of F to the standard
    //   built-in Function prototype object as specified in 15.3.3.1.
    // 12. Set the [[Call]] internal property of F as described in
    //   15.3.4.5.1.
    // 13. Set the [[Construct]] internal property of F as described in
    //   15.3.4.5.2.
    // 14. Set the [[HasInstance]] internal property of F as described in
    //   15.3.4.5.3.
    var bound;
    var binder = function () {

        if (this instanceof bound) {
            // 15.3.4.5.2 [[Construct]]
            // When the [[Construct]] internal method of a function object,
            // F that was created using the bind function is called with a
            // list of arguments ExtraArgs, the following steps are taken:
            // 1. Let target be the value of F's [[TargetFunction]]
            //   internal property.
            // 2. If target has no [[Construct]] internal method, a
            //   TypeError exception is thrown.
            // 3. Let boundArgs be the value of F's [[BoundArgs]] internal
            //   property.
            // 4. Let args be a new list containing the same values as the
            //   list boundArgs in the same order followed by the same
            //   values as the list ExtraArgs in the same order.
            // 5. Return the result of calling the [[Construct]] internal
            //   method of target providing args as the arguments.

            var result = apply.call(
                target,
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;

        }
        // 15.3.4.5.1 [[Call]]
        // When the [[Call]] internal method of a function object, F,
        // which was created using the bind function is called with a
        // this value and a list of arguments ExtraArgs, the following
        // steps are taken:
        // 1. Let boundArgs be the value of F's [[BoundArgs]] internal
        //   property.
        // 2. Let boundThis be the value of F's [[BoundThis]] internal
        //   property.
        // 3. Let target be the value of F's [[TargetFunction]] internal
        //   property.
        // 4. Let args be a new list containing the same values as the
        //   list boundArgs in the same order followed by the same
        //   values as the list ExtraArgs in the same order.
        // 5. Return the result of calling the [[Call]] internal method
        //   of target providing boundThis as the this value and
        //   providing args as the arguments.

        // equiv: target.call(this, ...boundArgs, ...args)
        return apply.call(
            target,
            that,
            array_concat.call(args, array_slice.call(arguments))
        );

    };

    // 15. If the [[Class]] internal property of Target is "Function", then
    //     a. Let L be the length property of Target minus the length of A.
    //     b. Set the length own property of F to either 0 or L, whichever is
    //       larger.
    // 16. Else set the length own property of F to 0.

    var boundLength = max(0, target.length - args.length);

    // 17. Set the attributes of the length own property of F to the values
    //   specified in 15.3.5.1.
    var boundArgs = [];
    for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }

    // XXX Build a dynamic function with desired amount of arguments is the only
    // way to set the length property of a function.
    // In environments where Content Security Policies enabled (Chrome extensions,
    // for ex.) all use of eval or Function costructor throws an exception.
    // However in all of these environments Function.prototype.bind exists
    // and so this code will never be executed.
    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();
        // Clean up dangling references.
        Empty.prototype = null;
    }

    // TODO
    // 18. Set the [[Extensible]] internal property of F to true.

    // TODO
    // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3).
    // 20. Call the [[DefineOwnProperty]] internal method of F with
    //   arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]:
    //   thrower, [[Enumerable]]: false, [[Configurable]]: false}, and
    //   false.
    // 21. Call the [[DefineOwnProperty]] internal method of F with
    //   arguments "arguments", PropertyDescriptor {[[Get]]: thrower,
    //   [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false},
    //   and false.

    // TODO
    // NOTE Function objects created using Function.prototype.bind do not
    // have a prototype property or the [[Code]], [[FormalParameters]], and
    // [[Scope]] internal properties.
    // XXX can't delete prototype in pure-js.

    // 22. Return F.
    return bound;
}
  • new 关键词的核心原理
    • 作用:new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
    • 背后的操作:
      • 创建一个空的简单 JavaScript 对象(即{});
      • 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
      • 将步骤 1 新创建的对象作为this的上下文 ;
      • 如果该函数没有返回对象,则返回this。
    • 简单实现:
      function New(fn) {
          const obj = Object.create(fn.prototype)
          const result = fn.apply(obj, [...arguments].slice(1))
          return typeof result === 'object' ? result : obj
      }
      

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。