技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

回味 JS 基础:call、apply 与 bind

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/90-回味JS基础 call apply 与 bind
本文目录8 个章节
  1. 先理解 thisArg
  2. call()
  3. apply()
  4. bind()
  5. 应用场景:构造器初始化与继承
  6. 借用数组原型方法,而不是修改原生原型
  7. 小结
  8. 参考资料

回味 JS 基础:callapplybind

Category(分类): JavaScript Status: 已整理

原文:回味 JS 基础:call apply 与 bind(历史来源)

callapplybind 都可以从 Function.prototype 使用。它们的共同主题是:调用一个函数时,如何提供 this 值和参数。但三者并不等价:call/apply 立即调用,bind 返回一个新的绑定函数;它们也不能把普通函数调用伪装成完整的 new

本文默认代码使用现代 JavaScript。涉及浏览器 DOM 的代码会单独标注环境。

先理解 thisArg

对普通函数而言,this 不是函数定义时固定的“所属对象”,而是由调用形式和函数类型决定的。callapply 传入的第一个参数叫 thisArg

  • 普通非严格函数收到 nullundefined 时,通常会把它替换为被调用函数所在 realm 的全局对象;收到原始值时,通常会进行对象包装。
  • 严格普通函数会保留传入的 nullundefined 和原始值。
  • 箭头函数没有自己的 thiscallapplybind 都不能改写它捕获的词法 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)

applycall 一样立即调用函数,区别是第二个参数通过 array-like 读取后作为参数列表:

  • nullundefined 表示空参数列表;
  • 数组当然可以使用;
  • 普通类数组对象也可以使用,不要求实现 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, ...)

绑定函数普通调用时:

  1. 使用保存的 thisArg
  2. 把预置参数放在调用时传入的参数之前;
  3. 返回目标函数的返回值。
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)

绑定函数会让 eventClickthis 保持为 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 extendssuper()

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) 更安全。

小结

  • callapply 立即调用函数;call 接收逐个参数,apply 接收数组或类数组。
  • bind 延迟调用并返回 bound function,预置参数会与调用参数合并。
  • 严格函数、非严格函数、箭头函数、类和 bound function 对 this 的处理不同。
  • call/apply 不是 new;构造需求优先使用 newReflect.construct
  • Parent.call(this) 只初始化实例属性;完整 ES5 继承还需要设置原型链,现代代码可使用 class extends
  • Array.prototype.forEach.call(arguments, fn) 是方法借用示例,不是修改原生原型。

参考资料

作者:小弟调调 原文链接:https://juejin.cn/post/6844903444348665870 来源:稀土掘金。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS