Skip to content

call、apply、bind 有什么区别,用在哪? ​

三个方法都用来指定函数里的 this。call 和 apply 会立刻调用函数,差别只是参数一个个传还是放进数组。bind 不调用,它返回一个新函数,把 this 和一部分参数固定下来,适合交给以后再执行的回调。

原理 ​

普通函数的 this 由调用方式决定。方法从对象上拆出来单独调用时,this 会丢。call、apply、bind 就是在调用前把 this 交回去。

js
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`
}

const user = { name: "Ada" }

greet.call(user, "Hi", "!") // "Hi, Ada!"
greet.apply(user, ["Hi", "!"]) // "Hi, Ada!"

const sayHi = greet.bind(user, "Hi")
sayHi("!") // "Hi, Ada!"

call(thisArg, ...args) 从第二个参数开始逐个传入。apply(thisArg, argsArray) 的第二个参数是参数列表,没有更多参数时传 undefined 或空数组。bind(thisArg, ...preset) 返回新函数;之后再传入的参数接在预先绑定的参数后面。

thisArg 是 null 或 undefined 时,非严格模式会落到全局对象,严格模式就保持 null / undefined。传入数字、字符串这些原始值时,会被包装成对象,所以能读到对应的方法。

箭头函数没有自己的 this。对它使用这三个方法可以固定参数,但不能改掉它的 this。

应用 ​

借用别的对象上的方法。 类数组没有数组方法时,可以用 call 或 apply 借过来。现在更常见的是先转成数组。

js
function sum() {
  const list = Array.prototype.slice.call(arguments)
  return list.reduce((total, n) => total + n, 0)
}

sum(1, 2, 3) // 6

const nodeList = document.querySelectorAll("p")
Array.prototype.forEach.call(nodeList, (node) => {
  node.classList.add("ready")
})

把数组展开成参数。 有些 API 只接收逐个参数。apply 能把数组交进去。参数很多时,展开运算符更直接,也避开了引擎对 apply 参数个数的限制。

js
const scores = [3, 10, 7]
Math.max.apply(null, scores) // 10
Math.max(...scores) // 10

继承时借用父构造函数。 子构造函数里用 call,让父构造函数把实例属性写到子实例上。

js
function Parent(name) {
  this.name = name
}
function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

new Child("Ada", 20) // { name: "Ada", age: 20 }

把方法交给回调,同时保住 this。 setTimeout、事件监听调用函数时,不会按原来的对象去调用。bind 先把 this 固定好再传出去。

js
const counter = {
  n: 0,
  add() {
    this.n += 1
    return this.n
  }
}

setTimeout(counter.add.bind(counter), 0)
button.addEventListener("click", counter.add.bind(counter))

固定一部分参数。 bind 的后续参数会排在调用时传入的参数前面,用来做部分应用。

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

const double = multiply.bind(null, 2)
double(5) // 10

判断类型时也常用 call,避免对象自己改过 toString:

js
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(null) // "[object Null]"

追问 ​

三者怎么选?

马上要执行、参数已经逐个在手里,用 call。参数已经是数组,用 apply 或展开。现在不执行、要交给别人稍后调用,用 bind。

bind 之后还能 new 吗?

可以。new 的优先级更高,实例的 this 指向新对象,绑定时传入的 this 被忽略,预先绑定的参数仍然生效。实现见 手写 bind。

多次 bind 会怎样?

原生 bind 的 this 只认第一次。后面的 bind 还能继续往前追加参数,但不能把 this 改成另一个对象。

易错点 ​

  • bind 不会执行原函数。忘记再调用返回值,函数体一次都不会跑。
  • apply 的第二个参数必须是类数组。把参数一个个塞给 apply,它们不会变成实参。
  • 箭头函数上看不到 this 被改掉,会误以为 call 失效。
  • 每次渲染都 fn.bind(this) 会制造新函数。传给子组件或当成依赖时,回调身份一直在变。

相关 ​