外观
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)会制造新函数。传给子组件或当成依赖时,回调身份一直在变。