手写bind,call,apply,改变this指向的本质原来这么易懂!

154 阅读3分钟

bind,call,apply手写作为一个经典的前端面试题,很多人都会被问到,通常面试者回答的话一般都会回答以下三点:

  • call传递的第一个参数是this指向的对象,后面以参数队列传递参数
  • apply传递的第一个参数是this指向的对象,后面以数组传递参数
  • bind只有一个参数(绑定的对象),返回的是一个可执行的函数,函数调用后完成this指向的改变

但其中的原理还需要一一道来

首先分别来手写call,apply,bind

call实现:

Function.prototype.myCall = function (context) {
  // 判断调用myCall的是否是函数
  if (typeof this !== 'function') {
    throw new Error('not function')
  }
  // 关键点 需要挂载的对象,如果第一个参数为null,undefined等其它会自动转换为false的对象时,挂载的对象为window
  context = context || window
  // 这里改变this的指向,将this(执行call的函数的执行环境)绑定到了context上
  context.fn = this 
  // 取参数
  const arg = [...arguments].slice(1)
  const result = context.fn(...arg)  // 执行
  delete context.fn // 用完删掉
  return result
}

apply实现:

Function.prototype.myApply = function (context) {
  // 判断调用myCall的是否是函数
  if (typeof this !== 'function') {
    throw new Error('not function')
  }
 if (!Array.isArray(arguments[1])) {
    throw new Error('arg not a array') 
  }
  // 关键点 需要挂载的对象,如果第一个参数为null,undefined等其它会自动转换为false的对象时,挂载的对象为window
  context = context || window
  // 这里改变this的指向,将this(执行call的函数的执行环境)绑定到了context上
  context.fn = this 
  const result = context.fn(...arguments[1])  // 执行
  delete context.fn // 用完删掉
  return result
}

由上面可以看出,改变this指向实质上是将函数的this通过对象绑定的形式绑定到第一个参数上,同时执行。 bind实现:

Function.prototype.mybind = function(thisArg) {
  if (typeof this !== 'function') {
    throw TypeError("Bind must be called on a function")
  }
  const args = Array.prototype.slice.call(arguments, 1)
  const self = this
    // 构建一个干净的函数,用于保存原函数的原型
  const nop = function() {}
    // 绑定的函数
  const bound = function() {
      // this instanceof nop, 判断是否使用 new 来调用 bound
      // 如果是 new 来调用的话,this的指向就是其实例,
      // 如果不是 new 调用的话,就改变 this 指向到指定的对象thisArg
      return self.apply(
        this instanceof nop ? this : thisArg,
        args.concat(Array.prototype.slice.call(arguments)) 
      );
    };
  // 箭头函数没有 prototype,箭头函数this永远指向它所在的作用域
  if (this.prototype) {
    nop.prototype = this.prototype;
  }
  // 关键点 如果有this 将通过原型链继承this 确保在返回的函数仍然可以使用初始调用bind函数的方法
  // 同时如果通过new 创建对象 对象的原型链会指向nop, 如果直接返回bound,bound只有prototype指向nop
  bound.prototype = new nop();
  return bound;
}

为了更好理解bind的过程,再熟悉一下new的过程

  • 创建一个新对象
  • 新对象的__proto__指向构造函数的prototype
  • 将构造函数的this指向这个新对象
  • 执行构造函数 如果执行构造函数返回值为对象则直接返回对象,如果不为对象则返回创建的新对象

本质上就时new创建了一个新的对象,构造了一个原型链关系,同时将this指向了这个对象

bind内部借助new判断了是否bind过后为new调用,同时内部通过apply或者call改变this指向,将执行环境绑定到指定的对象上。这便是bind的核心点。

如果觉得有用,谢谢大家点赞,六冰感激不尽