简单搞定call/apply/bind应付面试

64 阅读3分钟

前端面试经常会被问到的this指向相关问题,比如

  • js怎么改变this指向?
  • call/apply/bind的区别?
  • 手写call/apply/bind方法

要回答上面的这些问题,只需要搞懂其方法内部实现的原理

call

我们先来看看call方法相关的使用例子

var person = {
    fullName: function() {
        return this.firstName + " " + this.lastName;
    },
    describe: function(city, country) {
        return this.firstName + " " + this.lastName + "," + city + "," + country;
    }
}
var person1 = {
    firstName:"Bill",
    lastName: "Gates",
}
var person2 = {
    firstName:"Steve",
    lastName: "Jobs",
}
person.fullName.call(person1);  // 将返回 "Bill Gates"
person.fullName.call(person2);  // 将返回 "Steve Jobs"
person.describe.call(person1, "Seattle", "USA"); // 将返回 "Bill Gates,Seattle,USA"

使用call方法后,person1person2可以调用personfullName 方法,可想而知,其作用就是改变this指向

call方法接收的参数分为两个部分,第一个参数为this的指向,后续则是参数列表,当没有参数的时候this指向window

思路

    1. 先获取call前面的对象方法
    1. 将该对象方法塞给需要改变this的对象里
    1. 调用方法并删除

通俗点讲就是将方法给指向的对象,让指向对象自己调用,其this自然指向本身

Function.prototype.myCall = function (target, ...args) {
  target = target || window
  const funKey = Symbol()
  target[funKey] = this
  const res = target[funKey](...args)
  delete target[funKey]
  return res
}
person.fullName.myCall(person1);  // 将返回 "Bill Gates"
person.fullName.myCall(person2);  // 将返回 "Steve Jobs"
person.describe.myCall(person1, "Seattle", "USA"); // 将返回 "Bill Gates,Seattle,USA"

Symbol防止覆盖掉target里同名方法

apply

func.apply(thisArg, [argsArray])

applycall没有什么很大的差异就是在传参上不一样,call 接收的是参数列表,apply 接收的是参数数组

// 例子
var person = {
    fullName: function() {
        return this.firstName + " " + this.lastName;
    },
    describe: function(city, country) {
        return this.firstName + " " + this.lastName + "," + city + "," + country;
    }
}
var person1 = {
    firstName:"Bill",
    lastName: "Gates",
}
var person2 = {
    firstName:"Steve",
    lastName: "Jobs",
}
person.fullName.apply(person1);  // 将返回 "Bill Gates"
person.fullName.apply(person2);  // 将返回 "Steve Jobs"
person.describe.apply(person1, ["Seattle", "USA"]); // 将返回 "Bill Gates,Seattle,USA"

根据传参的不同,我们调整一下上面call的方法

Function.prototype.myApply = function (target, args=[]) {
  target = target || window
  const funKey = Symbol()
  target[funKey] = this
  const res = target[funKey](...args)
  delete target[funKey]
  return res
}

person.fullName.apply(person1);  // 将返回 "Bill Gates"
person.fullName.apply(person2);  // 将返回 "Steve Jobs"
person.describe.apply(person1, ["Seattle", "USA"]); // 将返回 "Bill Gates,Seattle,USA"

bind

我们直接来看例子,并分析一下bind的用法和特点

var person = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  },
  describe: function(city, country) {
    return this.firstName + " " + this.lastName + "," + city + "," + country;
  }
}
var person1 = {
  firstName:"Bill",
  lastName: "Gates",
}
var person2 = {
  firstName:"Steve",
  lastName: "Jobs",
}
const person1Name = person.fullName.bind(person1)
const person2Name = person.fullName.bind(person2)
const person1Desc = person.describe.bind(person1,"Seattle")
person1Name() // 将返回 "Bill Gates"
person2Name() // 将返回 "Steve Jobs"
person1Desc("USA") // 将返回 "Bill Gates,Seattle,USA"
person1Desc.call(person2,"USA") // 返回 "Bill Gates,Seattle,USA"
const newDesc = new person1Desc("USA") // 返回 "undefined undefined,Seattle,USA"

bind函数并不是立即执行,而是返回一个新函数,当我们想通过call来改变bind的this指向时,发现无效。根据输出结果。

MDN有相关描述

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

通俗点就是:在new运算符构造过程中,已经确定的this会被忽略,且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。

可以总结bind函数的一下特点

  • 可以修改this指向
  • 返回一个绑定了this的新函数
  • 绑定后的this指向无法被修改
  • 使用new运算符构造,this会被忽略,实例继承属性

根据以上特点我们来手写一下

Function.prototype.myBind = function (target, ...args) {
  const _fn = this
  let bound = function(...remainArg) {
    // 使用 instanceof 判断 this 是否为当前函数的实例,true则表示在通过 new 构建实例
    let newTarget = this instanceof bound ? this : target
    const funKey = Symbol()
    newTarget[funKey] = _fn
    const res = newTarget[funKey](...args,...remainArg)
    delete newTarget[funKey]
    return res
  }
  bound.prototype = _fn.prototype; // 原型链继承
  return bound
}


const person1Name = person.fullName.myBind(person1)
const person2Name = person.fullName.myBind(person2)
const person1Desc = person.describe.myBind(person1,"Seattle")
person1Name() // 将返回 "Bill Gates"
person2Name() // 将返回 "Steve Jobs"
person1Desc("USA") // 将返回 "Bill Gates,Seattle,USA"
person1Desc.call(person2,"USA") // 返回 "Bill Gates,Seattle,USA"
const newDesc = new person1Desc("USA") // 返回 "undefined undefined,Seattle,USA"