手写系列之 call、apply、bind 的实现

发布于 2023-05-06 17:43:22 字数 4612 浏览 69 评论 0

这三个货,都是用来绑定 this 的。区别如下:

  • call()apply() 都返回函数执行结果,区别在于参数不一样,前者接收参数列表,后者接收一个数组参数(也可以是类数组)。
  • bind() 返回一个新函数,但注意对使用 new 关键字调用时,绑定无效。其使用方法与 call() 一致,接受一个参数列表。

一、call 实现

假设没有 Function.prototype.call() 方法,我们利用 this 默认绑定的特性处理即可。

Function.prototype.myCall() {
  // 获取执行函数
  const fn = this

  // 获取绑定对象,及参数列表
  const [context, ...args] = arguments

  // 避免与绑定对象属性冲突,采用 Symbol 值作为属性键,并将执行函数赋予该属性。
  const key = Symbol()
  context[key] = fn

  // 执行函数并返回结果
  const res = context[key](...args)

  // 移除临时属性
  delete context[key]

  // 返回结果
  return res
}

还没完,请注意以下两种情况:

  • contextundefinednull 的情况。若处于非严格模式,context 应指向顶层对象 globalThis,在浏览器为 window 对象。
  • context 为其他原始值(不是 undefinednull),应该要将其转换为对应的引用值。
Function.prototype.myCall = function () {
  const fn = this
  const [ctx, ...args] = arguments
  const context = ctx == undefined ? window : Object(ctx)

  const key = Symbol()
  context[key] = fn

  const res = context[key](...args)
  delete context[key]

  return res
}

再简化一下:

Function.prototype.myCall = function (ctx, ...args) {
  const key = Symbol()
  const context = ctx == undefined ? window : Object(ctx)

  context[key] = this
  const res = context[key](...args)
  delete context[key]

  return res
}

二、apply 实现

我们知道,Function.prototype.apply()Function.prototype.call() 的区别仅在接收参数的形式不同,前者接收一个数组。

基于上面的实现,简单修改下函数执行的参数即可。

Function.prototype.myApply = function (ctx, ...args) {
  const key = Symbol()
  const context = ctx == undefined ? window : Object(ctx)

  context[key] = this
  const res = context[key](args) // 与 call 方法的区别点
  delete context[key]

  return res
}

三、bind 实现

我们知道,Function.prototype.bind() 参数形式与 Function.prototype.call() 相同,区别在于 bind() 返回一个绑定了 this 的新函数。

其实,我们可以很快就写出以下方法:

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this
  return function (...newArgs) {
    return fn.apply(ctx, [...args, ...newArgs])
  }
}

但是,这是不完全正确的...

需要注意的是,bind() 方法返回的新函数,若通过 new 关键字进行调用,那么 this 绑定则不生效。

举个例子:

const person = {
  name: 'Frankie'
}

function Foo(name) {
  this.name = name
}

const Bar = Foo.bind(person)
const bar = new Bar('Mandy')

console.log(person.name) // "Frankie"
console.log(bar.name) // "Mandy"

假设 Foo.bind(person) 生效的话,那么 new Bar('Mandy')this.name 应该是 person.name = 'Mandy',修改的应该是 person 对象的 name 属性,但事实并非如此。

顺道总结一下 this 绑定的优先级:

  1. 只要使用 new 关键字调用,无论是否含有 bind 绑定,this 总指向实例化对象。
  2. 通过 callapply 或者 bind 显式绑定,this 指向该绑定对象。若第一个参数缺省时,则根据是否为严格模式,来确定 this 指向全局对象或者 undefined
  3. 函数通过上下文对象调用,this 指向(最后)调用它的对象。
  4. 如以上均没有,则会默认绑定。严格模式下,this 指向 undefined,否则指向全局对象。

因此,我们来完善一下 myBind() 方法:

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this
  return function newFn(...newArgs) {
    // 若通过 new 关键字调用,有几种方式可以判断:
    // 1. this instanceof newFn
    // 2. this.__proto__.constructor === newFn
    // 3. new.target 不为 undefined
    if (new.target) {
      return new newFn(...args, ...newArgs)
    }

    return fn.apply(ctx, [...args, ...newArgs])
  }
}

去掉注释,就长这样:

Function.prototype.myBind = function (ctx, ...args) {
  const fn = this
  return function newFn(...newArgs) {
    if (new.target) {
      return new newFn(...args, ...newArgs)
    }

    return fn.apply(ctx, [...args, ...newArgs])
  }
}

总的来说,其实并不能,真正了解 this 原理,要手写这几个常考的面试题,其实很简单哈。

插个话,我认为对于初学者来说,千万不要把作用域(链)和 this 混为一谈,其实它们完全就是两回事。作用域(链)与闭包相关,它在函数被定义时就已“确定”,不会再变了。而 this 则与函数调用相关。

References

这里将此前写过关于 this 的文章列举出来:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

清眉祭

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

懂王

文章 0 评论 0

清秋悲枫

文章 0 评论 0

niceone-tech

文章 0 评论 0

小伙你站住

文章 0 评论 0

刘涛

文章 0 评论 0

南街九尾狐

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文