【面试题解】你了解call,apply,bind吗?那你可以手写一个吗?

8,068 阅读4分钟

本系列面试题旨在学会相关知识点,从而轻松应对面试题的各种形式,本文讲解了 JavaScript 的基础 callapplybind 的作用。

感觉有帮助的小伙伴请点赞👍鼓励一下 ~

call/apply

作用

用来改变函数内部 this 的指向。

特点

任何函数都可以调用这两个方法,说明它们是添加在函数原型上的方法(Function.prototype)。

console.dir(Function.prototype)

image.png

调用 callapply 的函数会立即执行。

callapply 的返回值就是函数的返回值。

var name = '一尾流莺'
var obj = {
  name: 'warbler',
}
function foo() {
  console.log(this.name);
  return 'success'
}
foo.call(obj) //=> warbler
console.log(foo.call(obj)); // => success

调用 callapply 指向 undefined 或者 null ,会将 this 指向 window

function foo() {
  console.log(this)
}
foo.call(undefined)
foo.call(null)
foo.apply(undefined)
foo.apply(null)

image.png

调用 callapply 指向一个值类型, 会将 this 指向由它们的构造函数创建的实例。

function foo() {
  console.log(this)
}
foo.call(11)
foo.call('11')
foo.call(true)

image.png

调用 callapply 指向一个引用类型, 会将 this 指向这个对象。

我们声明了一个全局变量 name 和一个全局作用域下的函数 foo

var name = '一尾流莺'
var obj = {
  name: 'warbler',
}
function foo() {
  console.log(this.name)
}
foo() //=> 一尾流莺

这段代码很好理解,name 等价于 window.namefoo() 等价于 window.foo() ,我们打印出this.name,当前的 this 指向它的调用者 window, 也就是 window.name 得到 一尾流莺

但是如果我想打印出 warbler 该怎么办呢?在 obj 里面再定义一个 obj.fn 么? 当然不需要, 我们只需要调用 call/apply 改变 this 的指向,指向 obj 这个对象就可以了。这个时候 this.name 等价于 obj.name ,就得到了 warbler

foo.call(obj) //=> warbler
foo.apply(obj) //=> warbler

call 和 apply的区别

除了传参的形式不同没什么区别。

传给fn的参数写法不同:

  • call 接收多个参数,第一个为函数上下文也就是 this ,后边参数为函数本身的参数。
  • apply 接收两个参数,第一个参数为函数上下文 this,第二个参数为函数参数只不过是通过一个 数组 的形式传入的。

只要记住 apply 是以 a 开头,它传给 fun 的参数是 Array,也是以 a 开头的,就可以很好的分别这两个函数了。

手写call/apply

手写call

var name = '一尾流莺'
var obj = {
  name: 'warbler',
}
function foo() {
  console.dir(this);
  return 'success'
}

/**
* Object()方法
* 如果传入的是值类型 会返回对应类型的构造函数创建的实例
* 如果传入的是对象 返回对象本身
* 如果传入 undefined 或者 null 会返回空对象
*/
Function.prototype._call = function(ctx, ...args) {
  // 判断上下文类型 如果是undefined或者 null 指向window
  // 否则使用 Object() 将上下文包装成对象
  const o = ctx == undefined ? window : Object(ctx)
  // 如何把函数foo的this 指向 ctx这个上下文呢
  // 把函数foo赋值给对象o的一个属性  用这个对象o去调用foo  this就指向了这个对象o
  // 下面的this就是调用_call的函数foo  我们把this给对象o的属性fn 就是把函数foo赋值给了o.fn
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  o[key] = this
  // 立即执行一次
  const result = o[key](...args)
  // 删除这个属性
  delete o[key]
  // 把函数的返回值赋值给_call的返回值
  return result
}

开始验证

foo._call(undefined) // window
foo._call(null) // window
foo._call(1) // Number
foo._call('11') // String
foo._call(true) // Boolean
foo._call(obj) // {name: 'warbler'}
console.log(foo._call(obj)); // success

可以看到我们手写的 _call 方法和原生的 call 方法得到的结果是一样的。

image.png

手写 apply

之前讲过,callapply 的唯一区别就是传递参数的不同,所以我们只需要改一下对参数的处理,其它的和 call 一致就可以了。

var age = 10
var obj = {
  age: 20,
}
function foo(a, b) {
  console.dir(this.age + a + b);
}
// 只需要把第二个参数改成数组形式就可以了。
Function.prototype._apply = function(ctx, array = []) {
  const o = ctx == undefined ? window : Object(ctx)
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  o[key] = this
  const result = o[key](...array)
  delete o[key]
  return result
}
foo(3, 4) // => 17
foo._apply(obj, [3, 4]) //=> 27

bind

作用

也是用来改变函数内部 this 的指向。

bind 和 call/apply 的区别

是否立刻执行

  • call/apply 改变了函数的 this 上下文后 马上 执行该函数。
  • bind 则是返回改变了上下文后的函数, 不执行该函数

返回值的区别:

  • call/apply 返回 fun 的执行结果。
  • bind 返回 fun 的拷贝,并指定了 funthis 指向,保存了 fun 的参数。
var name = '一尾流莺'
var obj = {
  name: 'warbler',
}

// this 指向调用者document
document.onclick = function() {
  console.dir(this); // => #document
}

// this 指向 obj
document.onclick = function() {
  console.dir(this); // => #Object{name:'warbler}
}.bind(obj)

手写bind

Function.prototype._bind = function(ctx, ...args) {
  // 下面的this就是调用_bind的函数,保存给_self
  const _self = this
  // bind 要返回一个函数, 就不会立即执行了
  const newFn = function(...rest) {
    // 调用 call 修改 this 指向
    return _self.call(ctx, ...args, ...rest)
  }
  if (_self.prototype) {
    // 复制源函数的prototype给newFn 一些情况下函数没有prototype,比如箭头函数
    newFn.prototype = Object.create(_self.prototype);
  }
  return newFn
}