bind,call 和 apply

98 阅读2分钟

函数中(在这里说的是普通函数,非剪头函数),this 总是指向调用函数的对象。而 bind,call 和 apply 函数可以用来改变函数的执行上下文(即执行时的 this 指向)。

区别

区别bindcallapply
返回值返回一个新函数待调用立即执行,return 执行结果立即执行,return 执行结果
传参arg1, arg2, ..., argnarg1, arg2, ..., argnargsArray 数组或类数组对象

手写题

call

Function.prototype.myCall = function (context) {
  // 如果在非函数 x 上调用,在进入 myCall 之前就会报错说 x.myCall is not a function
  // 所以感觉这个校验并不需要呢?你怎么看
  // if (typeof this !== 'function') {
  //   throw new TypeError('myCall must be called on a function');
  // }
  
  // 如果上下文对象 context 为空 undefined / null,非严格模式下会指向全局上下文对象
  context = context || globalThis
  // 取传入 myCall 的除了 context 之外的其他参数
  // 为什么需要 Array.from 处理?因为 arguments 是类数组对象,不能直接用数组的 slice 方法
  const args = Array.from(arguments).slice(1); 
  const key = Symbol('key'); // 生成一个唯一的 key
  context[key] = this; // 将调用 myCall 的目标函数作为 context 新属性的 value
  const ret = context[key](...args); // 执行函数,此时调用函数的是 context,所以this自然指向了 context
  delete context[key]; // 删除 key-value
  return ret;
};

🤔 通过 Array.from(arguments).slice(1) 或者 Array.prototype.slice.call(arguments, 1) 又或者 [...arguments].slice(1) 处理 arguments 类数组对象来获得剩余参数有点麻烦呢。直接用剩余参数吧!

Function.prototype.myCall = function (context, ...args) {
  const key = Symbol('key');
  context[key] = this;
  const ret = context[key](...args);
  delete context[key];
  return ret;
};

apply

除了传参,其他和 call 一样

Function.prototype.myCall = function (context, argsArray) {
  context = context || globalThis
  const key = Symbol('key');
  context[key] = this;
  const ret = context[key](...argsArray); // 将数组展开
  delete context[key];
  return ret;
};

🤔 关于数组展开,想到一个题:怎么通过 Math.max 获得数组 const arr = [1,2,3,4,5] 中的最大值呢?

  1. Math.max.apply(null, arr)
  2. Math.max(...arr)

bind

Function.prototype.myBind = function(context, ...args) {
  context = context || globalThis;
  const fn = this // this 就是调用 myBind 方法的函数
  return function (...innerArgs) {
    // 兼容 new 关键字:当该 return 的函数被当成构造函数被 new 调用时,
    // new.target 会返回该构造函数,普通调用则返回 undefined
    // 既然决意要 new,那我们就没法管 this 指向了,
    // 因为这时 JavaScript 引擎在内部控制了 this 指向新建的对象 (参阅 new 操作符文档)
    if(new.target) {
      return new fn(...args, ...innerArgs)
    }
    return fn.apply(context, args.concat(innerArgs))
  }
}

🤔 说到 new.target,又想到一个题:怎么保证一个函数只能作为构造函数被 new 调用?

🤔 args 是个数组,所以可以用数组的 concat 方法,而在 手写 myCall 的时候要将 arguments 转成数组才能用数组的 slice 方法。