回味 JS 基础:call、apply 与 bind
Category(分类): JavaScript Status: 已整理
原文:回味 JS 基础:call apply 与 bind(历史来源)
call、apply 和 bind 都可以从 Function.prototype 使用。它们的共同主题是:调用一个函数时,如何提供 this 值和参数。但三者并不等价:call/apply 立即调用,bind 返回一个新的绑定函数;它们也不能把普通函数调用伪装成完整的 new。
本文默认代码使用现代 JavaScript。涉及浏览器 DOM 的代码会单独标注环境。
先理解 thisArg
对普通函数而言,this 不是函数定义时固定的“所属对象”,而是由调用形式和函数类型决定的。call、apply 传入的第一个参数叫 thisArg。
- 普通非严格函数收到
null或undefined时,通常会把它替换为被调用函数所在 realm 的全局对象;收到原始值时,通常会进行对象包装。 - 严格普通函数会保留传入的
null、undefined和原始值。 - 箭头函数没有自己的
this,call、apply、bind都不能改写它捕获的词法this。 - 类体天然是严格模式,类构造器和方法不能像普通函数一样脱离
new随意调用。
不要把浏览器 classic script 中的 window 结论推广到模块、Node.js CommonJS、Node.js ESM 或其他宿主。示例:
function sloppyThis() {
return this
}
function strictThis() {
'use strict'
return this
}
console.log(sloppyThis.call(null) === globalThis) // classic script 中通常为 true
console.log(strictThis.call(null)) // null
console.log(strictThis.call(1)) // 1
thisArg 是传给目标函数的值,不是函数的词法作用域,也不等同于“调用了这个函数的函数”。
call()
语法
fun.call(thisArg, arg1, arg2, ...)
call 以给定的 thisArg 和逐个列出的参数立即调用目标函数,返回目标函数的返回值。
基础示例
function showThis(label) {
return `${label}: ${String(this?.name ?? this)}`
}
const object = { name: '对象' }
console.log(showThis.call(object, 'object'))
console.log(showThis.call({ name: '另一个对象' }, 'other'))
把函数对象作为 thisArg 传入,只表示目标函数内部的 this 是那个函数对象;它与该函数对象稍后被调用时的 this 没有直接关系:
function inspect(value) {
return typeof value
}
function anotherFunction() {}
console.log(inspect.call(anotherFunction)) // function
方法借用
call 常用于借用一个 generic 方法。下面不是“Array 继承了 Object”,而是把 Object.prototype.toString 作为函数调用,并显式把数组传给它的 this:
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })) // ['a', 'b']
Object.prototype.toString 的标签可以被 Symbol.toStringTag 影响,不应把它当作不可伪造的类型品牌。检测数组优先使用 Array.isArray()。
call 的参数是普通参数列表
function add(a, b, c) {
return a + b + c
}
console.log(add.call(null, 1, 2, 3)) // 6
call 不会把一个数组自动拆成多个参数;需要数组/类数组参数时,应考虑 apply、展开语法或 Reflect.apply:
const values = [1, 2, 3]
console.log(add.apply(null, values))
console.log(add(...values))
console.log(Reflect.apply(add, null, values))
apply()
语法与参数
fun.apply(thisArg, argsArray)
apply 与 call 一样立即调用函数,区别是第二个参数通过 array-like 读取后作为参数列表:
null或undefined表示空参数列表;- 数组当然可以使用;
- 普通类数组对象也可以使用,不要求实现 iterable;
- 展开语法
fn(...value)要求value可迭代,这与apply不同。
function joinValues(a, b, c) {
return [a, b, c].join('-')
}
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
console.log(joinValues.apply(null, arrayLike)) // a-b-c
console.log(joinValues.apply(null, null)) // --
旧文章中关于 Chrome 14、Internet Explorer 9 的兼容提示属于历史资料,不能当作当前浏览器结论。现代代码应根据目标浏览器矩阵和特性检测决定方案。
使用 apply 借用内置函数
const numbers = [5, 6, 2, 3, 7]
const max = Math.max.apply(null, numbers)
const min = Math.min.apply(null, numbers)
console.log(max, min) // 7 2
对超长数组直接使用 apply 或展开语法可能超过引擎的参数数量限制,ECMAScript 没有统一规定这个上限。大数组应使用循环、reduce 或分块:
function minOfArray(array) {
if (array.length === 0) return Infinity
let min = Infinity
const quantum = 32_768
for (let index = 0; index < array.length; index += quantum) {
const chunk = array.slice(index, index + quantum)
min = Math.min(min, ...chunk)
}
return min
}
console.log(minOfArray([5, 6, 2, 3, 7])) // 2
32_768只是一个保守的教学分块值,不是规范保证的最大参数数量;性能敏感代码仍应基准测试。
不要用 apply 连接构造器
历史上可以把构造器通过 apply 包装起来,但这只适用于非常简单的 ES5 函数,并不能保留 new.target、类、私有字段、构造器返回对象等语义。现代代码应使用 Reflect.construct:
function construct(target, args, newTarget = target) {
return Reflect.construct(target, args, newTarget)
}
class Person {
constructor(name) {
this.name = name
}
}
const person = construct(Person, ['Ada'])
console.log(person instanceof Person, person.name) // true Ada
不要为了这个用途给 Function.prototype 添加 construct,全局原型扩展会影响其他代码并造成命名冲突。
bind()
bind 返回一个新的 bound function,不会立即执行目标函数:
const bound = fun.bind(thisArg, arg1, arg2, ...)
绑定函数普通调用时:
- 使用保存的
thisArg; - 把预置参数放在调用时传入的参数之前;
- 返回目标函数的返回值。
const user = { name: 'Ada' }
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const greetAda = greet.bind(user, 'Hello')
console.log(greetAda('!')) // Hello, Ada!
再次 bind 不能覆盖已经绑定的 this,但会继续追加参数:
function values(a, b, c) {
return [this.name, a, b, c]
}
const first = values.bind({ name: 'first' }, 1)
const second = first.bind({ name: 'second' }, 2)
console.log(second(3)) // ['first', 1, 2, 3]
bind 与 DOM 事件
下面是浏览器代码。addEventListener 需要一个稍后执行的函数,所以不能把 call 当作 bind 使用;call 会在注册时立即执行。保存绑定后的引用,移除监听器时才能传入同一个函数:
const obj = { name: 'JSLite.io' }
function eventClick(first, second, event) {
console.log(this.name, first, second, event.type)
}
const handler = eventClick.bind(obj, 'p1', 'p2')
document.addEventListener('click', handler)
// 需要移除时:
// document.removeEventListener('click', handler)
绑定函数会让 eventClick 的 this 保持为 obj;浏览器仍会把事件对象作为后续参数传入。
bind 与构造调用
如果目标函数本身可构造,绑定函数也可以用 new 调用。此时绑定的 thisArg 会被忽略,但预置参数仍然保留:
class Person {
constructor(firstName, lastName) {
this.name = `${firstName} ${lastName}`
}
}
const BoundPerson = Person.bind({ ignored: true }, 'Ada')
const person = new BoundPerson('Lovelace')
console.log(person.name) // Ada Lovelace
console.log(person instanceof Person) // true
箭头函数、普通 async 函数等不可构造,不能通过 new 变成构造器。原生 bound function 也没有自己的 prototype 属性;不要用一个简单的 this instanceof fNOP 判断就宣称实现了完整 bind。
一个明确标注的教学版
下面的 bindLike 只展示普通调用时的参数预置和 this 传递,不是原生 Function.prototype.bind 的 polyfill,也不支持构造调用:
function bindLike(fn, thisArg, ...preset) {
if (typeof fn !== 'function') {
throw new TypeError('bindLike target must be callable')
}
return function boundFunction(...later) {
return Reflect.apply(fn, thisArg, [...preset, ...later])
}
}
const say = bindLike(function (word) {
return `${this.name}: ${word}`
}, { name: 'Ada' }, 'hello')
console.log(say()) // Ada: hello
生产代码直接使用原生 bind。历史兼容实现若确有需要,应使用经过维护的 polyfill,不要覆盖原生方法或把简化版本发布为规范实现。
应用场景:构造器初始化与继承
Parent.call(this, value) 可以在子构造器中初始化父构造器写入的实例属性,但它不会建立 Parent.prototype 原型链,因此不是完整继承:
function Animal(name, weight) {
this.name = name
this.weight = weight
}
function Cat(name, weight) {
Animal.call(this, name, weight)
}
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
Cat.prototype.say = function () {
return `I am ${this.name}, my weight is ${this.weight}`
}
const cat = new Cat('cat', 50)
console.log(cat.say())
console.log(cat instanceof Animal) // true
上面的 Animal.call 只负责初始化属性;Object.create(Animal.prototype) 才建立了原型链。现代代码通常使用 class extends 和 super():
class ModernAnimal {
constructor(name, weight) {
this.name = name
this.weight = weight
}
}
class ModernCat extends ModernAnimal {
say() {
return `I am ${this.name}, my weight is ${this.weight}`
}
}
console.log(new ModernCat('cat', 50).say())
如果父类是 class,不能用 ModernAnimal.call(this, ...) 代替 super();类构造器只能通过构造调用执行。
借用数组原型方法,而不是修改原生原型
arguments 是类数组对象,不是数组。可以借用 Array.prototype.forEach,但现代代码通常更推荐 rest 参数或 Array.from:
function printArguments() {
console.log(arguments instanceof Array) // false
console.log(Array.isArray(arguments)) // false
Array.prototype.forEach.call(arguments, (item, index) => {
console.log(index, item)
})
for (const item of arguments) {
console.log('iterable:', item)
}
}
printArguments(1, 2, 3, 4)
不要把“在 Array 上扩展一个 forEach”理解成修改 Array.prototype。内建原型扩展会污染全局环境,借用已有方法或直接使用 Array.from(arguments) 更安全。
小结
call与apply立即调用函数;call接收逐个参数,apply接收数组或类数组。bind延迟调用并返回 bound function,预置参数会与调用参数合并。- 严格函数、非严格函数、箭头函数、类和 bound function 对
this的处理不同。 call/apply不是new;构造需求优先使用new或Reflect.construct。Parent.call(this)只初始化实例属性;完整 ES5 继承还需要设置原型链,现代代码可使用class extends。Array.prototype.forEach.call(arguments, fn)是方法借用示例,不是修改原生原型。
参考资料
- ECMAScript:Function.prototype.call
- ECMAScript:Function.prototype.apply
- ECMAScript:Bound Function Exotic Objects
- MDN:Function.prototype.call
- MDN:Function.prototype.apply
- MDN:Function.prototype.bind
- MDN:Reflect.construct
- MDN:this
- MDN:Classes
- MDN:arguments
作者:小弟调调 原文链接:https://juejin.cn/post/6844903444348665870 来源:稀土掘金。