boxboxs
技术

js中call、apply和bind

浏览 16最近编辑于
js中call、apply和bind

在this的文章中提到,可以显式绑定函数的this。使用call、apply、bind就可以实现,但是它们之间还是有一些差别,值得我们继续学习下。

call:立即调用,参数一个一个传递

基本语法

fn.call(thisArg, arg1, arg2, ...)
  • thisArg:函数要指定的this
  • 后面的参数,按顺序传递给函数

示例

function greet(city, country) {
  console.log(`我叫 ${this.name},来自 ${city},${country}`);
}

const user = { name: '墨墨' };

greet.call(user, '杭州', '中国');
// 我叫 墨墨,来自 杭州,中国

apply: 立即调用,参数以数组形式调用

基本语法

fn.apply(thisArg, [arg1, arg2, ...])
  • thisArg:函数要指定的this
  • 数组或者类数组参数

示例

function greet(city, country) {
  console.log(`我叫 ${this.name},来自 ${city},${country}`);
}

const user = {name: '墨墨'}
greet.apply(user, ['杭州', '中国']);
// 我叫 墨墨,来自 杭州,中国

bind:不立即调用,返回一个新函数,参数一个一个传递

基本语法

const newFn =  fn.bind(thisArg, arg1, arg2, ...)

bind和前面两个最大的区别就是:它不会立即执行,而是返回一个绑定好的this函数

示例

const user = {name: '墨墨'}

function greet() {
  console.log(`你好,我是 ${this.name}`)
}

const newGreet = greet.bind(user)
newGreet()
// 你好,我是 墨墨

bind还有一个特点就是:可以预设参数

function add(a, b) {
  return a + b
}

const newAdd = add.bind(null, 10)
const sum = newAdd(5)
console.log(sum) 
// 15

上面相当于把参数a预设成了10, 后续新函数调用,只需传递b即可。这类方法也叫做偏函数

所以我们可以把bind记成:

bind = 不立即执行 + 返回新函数 + 可预置参数

开发中实际可能遇到的场景

  • 方法借用
// 1. 一个对象借用另一个对象方法
const obj1 = {
  name: "Tom",
  say() {
    console.log(this.name)
  }
}

const obj2 = {
  name: "Jerry"
}

obj1.say.call(obj2) // Jerry,相当于obj2自己有say方法一样,将this指向自己

// 2. 借用数组方法,讲类数组转为数组
function test() {
  const args = Array.prototype.slice.call(arguments)
}
// 因为arguments是类数组对象,不是真正的数组,它没有slice方法。所以通过call借用了数组原型上的slice方法
// 当然还有其他写法; Array.form(arguments) [...arguments]

// 3. 借用构造函数继承
function Animal (name) {
  this.name = name
}

function Dog(name, breed) {
   Animal.call(this, name)
   this.breed = breed
}
// 这里的Animal.call(this, name),意思就是在Dog的构造过程中,借用Animal来给当前实例赋值

  • 参数预置和函数封装

bind常用于创建返回一个带固定参数的新函数

function multiply (a, b) {
  return a * b
}

const double = multiply.bind(2)

console.log(double(8)) // 16
  • 解决回调中this丢失问题
const counter = {
  count: 0,
  inc() {
    this.count++
    console.log(this.count)
  }
}

setTimeout(counter.inc, 1000)

上面代码看上去没有问题,1秒后执行counter.inc,打印1。但实际上是有问题。因为传给setTimeout的只是一个函数引用,不再是通过counter.inc() 形式调用,所以this丢失了。

正确写法:

setTimeout(counter.inc.bind(counter), 1000)

这样无论谁来调用这个函数,this都会指向counter。new调用除外,new调用的权重更高,后面讲。

优先级问题

我们知道,显示绑定是高于隐式绑定和默认绑定的。但是显示绑定内部和new绑定,谁的优先级更高呢?

  • bind绑定后this,通常不能更改,即bind高于call、appy
function show() {
  console.log(this.name)
}

const obj1 = {name: "A"}
const obj2 = {name: "B"}

const newShow = show.bind(obj1)
newShow() // A
newShow.call(obj2) // A 还是打印A,bind的优先级高于call
newShow.apply(obj2) // A 还是打印A,bind的优先级高于apply
  • new绑定优先级又高于bind
function Person(name) {
  this.name = name
}
const obj = {name: "oldName"}
const BoundPerson = Person.bind(obj)

const p = new BoundPerson("newName")

console.log(p.name) // newName
console.log(obj.name) // oldName

很多人以为bind之后,this永远就是obj,但是这里的obj打印的还是oldName。因为在new调用时,情况就变了。

也就是说:bind指定的this会失效,this会指向新创建的实例对象。

箭头函数不能使用call、apply、bind

因为箭头函数没有自己的this,他的this来自它的外层作用域。所以他恰恰相反,它的this是词法绑定,并不是和普通函数一样由调用的方式决定的。

手写call、apply、bind

  • call
Function.prototype.myCall = function(context, ...args) {
  // 兼容this,如果没有传递,使用全局this
  context = context ?? globalThis
  // 创建一个独一无二的key,防止覆盖
  const fnKey = Symbol('fn')
  // 将原函数挂载在指定的context对象上
  context[fnKey] = this 
  // 使用默认绑定方式调用函数,此时原函数的this,变成了context,指定成功
  const result = context[fnKey](...args)
  // 删除挂载
  delete context[fnKey]
  return result
}
  • apply
Function.prototype.myApply = function(context, args) {
  context = context ?? globalThis
  const fnKey = Symbol('fn')
  context[fnKey] = this
  const result = args ? context[fnKey](...args) : context[fnKey]()
  delete context[fnKey]
  return result
}
  • bind
Function.prototype.myBind = function(context, ...presetArgs) {
  if (typeof this !== 'function') {
    throw new typeError('myBind must be called on a function')
  }

  const targeFn = this

  function boundFn(...laterArgs) {
    const isNewCall = this instanceof boundFn
    const thisArg = isNewCall
      ? this
      : (context == null ? globalThis : Object(context))

    return targeFn.apply(thisArg, presetArgs.concat(laterArgs))
  }

  // 让new boundFn() 的实例能继承targetFn.prototype
  if (targeFn.prototype) {
    boundFn.prototype = Object.create(targeFn.prototype)
    boundFn.prototype.constructor = boundFn
  }
 
  rturn boundFn
}

评论 (0)

为了减少垃圾评论,请先使用 GitHub 登录后再发表评论。

使用 GitHub 登录评论

暂无评论,成为第一个评论者!