JS 之改变 this 大法

153 阅读2分钟

在 JS 中有三种方法可以改变 this 指向,分别为 call、apply 和 bind。这 3 种方法也是面试中经常要求会手写的面试题。

call

使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

JavaScript Demo

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

console.log(new Food('cheese', 5).name); // cheese

接下来我们就实现一个 call 方法

Function.prototype.myCall = function (context, args) {
  // 不是函数不允许调用 call 方法
  if (typeof this !== 'function') {
    throw new Error('error');
  }
  
  // 如果传入的上下文是 null 或 undefined,则应该指向 window
  context = context || window;
  
  // 将要执行的函数挂到传入的上下文上,改变了 this
  context.fn = this;
  
  // 执行函数
  let result = context.fn(args);
  
  // 为了不给传入的上下文添加属性,执行完函数后需要删除
  delete context.fn;
  
  return result;
}

apply

apply 方法和 call 方法只有一个区别,就是它接受一个包含多个参数的数组

JavaScript Demo

const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);
console.log(max); // 7

const min = Math.min.apply(null, numbers);
console.log(min); // 2

实现一个 apply 方法

Function.prototype.myApply = function (context, args) {
  // 不是函数不允许调用 apply 方法
  if (typeof this !== 'function') {
    throw new Error('error');
  }
  
  // 如果传入的上下文是 null 或 undefined,则应该指向 window
  context = context || window;
  
  // 将要执行的函数挂到传入的上下文上,改变了 this
  context.fn = this;
  
  // 执行函数,这里使用了 ES6 的函数解构
  let result = context.fn(...args);
  
  // 为了不给传入的上下文添加属性,执行完函数后需要删除
  delete context.fn;
  
  return result;
}

bind

创建一个新的函数,在 bind 被调用时,这个新函数的 this 被指定为 bind 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

JavaScript Demo

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
}

const boundGetX = module.getX.bind(module);
console.log(boundGetX()); // 42

实现 bind

Function.prototype.myBind = function (context, ...args1) {
  // 不是函数不允许调用 bind 方法
  if (typeof this !== 'function') {
    throw new Error('error');
  }
  
  let fn = this;
  
  // 如果传入的上下文是 null 或 undefined,则应该指向 window
  context = context || window;
  
  let FToBind = function (...args2) {
    // 合并 2 次传入的参数
    let args = args1.concat(args2);
    
    // 这里需要判断一下
    // 应为有可能返回的函数被用作构造函数
    // 当用作构造函数的时候就不能指向 context 了,需要指向 this
    return fn.apply(this instanceof FToBind ? this : context, args);
  }
  
  // 维持原型链
  if (this.prototype) {
    FToBind.prototype = this.prototype;
  }

  return FToBind
}