手写 call、apply、bind | 刷题打卡

215 阅读6分钟

手写 call、apply、bind

闲时要有吃紧的心思,忙时要有悠闲的趣味

  • 掘金团队号上线,助你 Offer 临门! 点击 查看详情

目录

前言

返回目录

 我们都知道 call apply bind 都可以改变函数调用的 this 指向。那么它们三者有什么区别,什么时候该用哪个呢?

 然后我们尝试一下自己实现这三个函数

正文

call

返回目录

详情请参考:Function.prototype.call() | MDN

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。 x  注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。

语法:

function.call(thisArg, arg1, arg2, ...)

参数:

  • thisArg:可选的。在 function 函数运行时使用的 this 值。请注意,
    • 非严格模式:如果不传参数,或者第一个参数是 null 或 nudefined,this 都指向window
    • 严格模式:第一个参数是谁,this 就指向谁,包括 null 和 undefined,如果不传参数 this 就是undefined
  • arg1, arg2, ...:指定的参数列表

返回值:

 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

使用:

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

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

const food = new Food('cheese', 5)
console.log(food.name) // 'cheese'

apply

返回目录

详情请参考:Function.prototype.apply() | MDN

 apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

语法:

function.apply(thisArg, [argsArray])

参数:

  • thisArg:必选的。在 function 函数运行时使用的 this 值,和 call 基本一致。
  • [argsArray]:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。

返回值:

 调用有指定 this 值和参数的函数的结果。

使用:

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

Math.min.apply(null, 1)

bind

返回目录

详情请参考:Function.prototype.bind() | MDN

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

语法:

function.bind(thisArg, arg1, arg2, ...)

参数:

  • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者 thisArg 是 null 或 undefined,执行作用域的 this 将被视为新函数的 thisArg。

  • arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值:

一个原函数的拷贝,并拥有指定的 this 值和初始参数。我们如果需要使用的话,需要去主动调用这个函数。

使用:

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

const unboundGetX = module.getX
console.log(unboundGetX()) // undefined
// 谁调用指向谁,这里 unboundGetX = module.getX
// 让 getX 里面的 this 指向了 window
// 而 window 里面并没有 x 方法
// 当然,在前面加上 window.x = 43 就有了

const boundGetX = unboundGetX.bind(module)
console.log(boundGetX()) // 42
// 通过 bind,将 this 指向 module

实现 myCall

返回目录

考虑两点:

  • 第一个参数为 undefined 或 null 的时候,那么会转变为 window
  • 改变了 this 执行,让新的对象可以执行该函数。

代码实现:

Function.prototype.myCall = function (context, ...args) {
  // 首先context为可选参数,如果不传的话默认上下文是window
  context = context || window
  // 接下来给content创建一个_fn属性,并将值设置为需要调用的函数
  context._fn = this
  // 调用函数,将这个执行结果传给 result
  let result = context._fn(...args)
  // 将对象上的函数删除
  delete context._fn
  // 返回 result 结果
  return result
}

实现 myApply

返回目录

apply 和 call 实现类似,不同的就是对参数的判断及处理

代码实现:

Function.prototype.myApply = function (context, args) {
  // 首先context为可选参数,如果不传的话默认上下文是window
  context = context || window
  // 接下来给content创建一个_fn属性,并将值设置为需要调用的函数
  context._fn = this
  // 判断参数数组是否存在,不存在直接返回
  if (!args) {
    return context._fn()
  }
  // 如果参数不是数组,则抛出错误
  if (!(args instanceof Array)) {
    throw new Error('params must be array')
  }
  // 调用函数,将这个执行结果传给 result
  let result = context._fn(...args)
  // 将对象上的函数删除
  delete context._fn
  // 返回 result 结果
  return result
}

实现 myBind

返回目录

因为 bind 转换后的函数可以作为构造函数使用,此时 this 应该指向构造出的实例,而不是 bind 绑定的第一个参数

代码实现:

Function.prototype.myBind = function (context, ...args1) {
  // 首先context为可选参数,如果不传的话默认上下文是window
  context = context || window
  ///返回一个绑定this的函数,这里我们需要保存this指向
  let that = this
  // 返回的一个新函数
  return function (...args2) {
    // 接下来给content创建一个_fn属性,并将值设置为需要调用的函数
    context._fn = that
    // 调用函数,将这个执行结果传给 result
    let result = context._fn(...[...args1, ...args2])
    // 将对象上的函数删除
    delete context._fn
    // 返回 result 结果
    return result
  }
}

验证我们的实现

我们可以验证一下自己实现的 myCall、myApply 、myBind 函数:

function add(c, d) {
  return this.a + this.b + c + d
}
const obj = {
  a: 1,
  b: 2,
}
console.log('===============myCall================')
console.log(add.myCall(obj, 3, 4)) // 10
console.log(add.myCall({ a: 3, b: 9 }, 3, 4)) // 19
console.log(add.myCall({ a: 3, b: 9 }, { xx: 1 }, 4)) // 12[object Object]4

console.log('==============myApply=================')
console.log(add.myApply(obj)) // NaN
console.log(add.myApply(obj, [3, 4])) // 10
console.log(add.myApply(obj, [1, 'abc', '2'])) // 4abc

console.log('===============myBind================')
console.log(add.myBind(obj, 3, 4)()) // 10
console.log(add.myBind({ a: 3, b: 9 }, 3, 4)()) // 19
console.log(add.myBind({ a: 3, b: 9 }, { xx: 1 }, 4)()) // 12[object Object]4

总结

返回目录

 我们对比一下call / apply / bind

区别

  • call 跟 apply 的用法几乎一样,唯一的不同就是传递的参数不同
    • call 只能一个参数一个参数的传入
    • apply 则只支持传入一个数组,哪怕是一个参数也要是数组形式。最终调用函数时候这个数组会拆成一个个参数分别传入。
  • bind 参数方式跟 call 方法一致。不过 bind 是直接改变这个函数的 this 指向并且返回一个新的函数,之后再次调用这个函数的时候 this 都是指向 bind 绑定的第一个参数

使用场景

  • 当我们使用一个函数需要改变 this 指向的时候才会用到 call / apply / bind
  • 如果你要传递的参数不多,则可以使用 fn.call(thisObj, arg1, arg2 ...)
  • 如果你要传递的参数很多,则可以用数组将参数整理好调用 fn.apply(thisObj, [arg1, arg2 ...])
  • 如果你想生成一个新的函数长期绑定某个函数给某个对象使用,则可以使用 const newFn = fn.bind(thisObj); newFn(arg1, arg2...)

参考文献

后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址

文档协议

知识共享许可协议
db 的文档库db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。