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

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 深入之 call 和 apply 的模拟实现(进阶边界)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/32-JavaScript深入之call和apply的模拟实现_2
本文目录11 个章节
  1. 一、从“模拟”转向“理解语义”
  2. 二、为什么临时属性不是等价实现
  3. 三、thisArg 的准确行为
  4. 四、apply 的参数对象
  5. 五、返回值、异常和异步函数
  6. 六、与 new 和 class 的边界
  7. 七、函数、Proxy 和可调用对象
  8. 八、常见的原生方法借用
  9. 九、测试矩阵
  10. 十、与 31 号文章的关系
  11. 十一、参考资料与原文

JavaScript 深入之 call 和 apply 的模拟实现(进阶边界)

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

当前文件的原始抓取内容与 31-JavaScript深入之call和apply的模拟实现_1.md 基本重复,并再次混入“代码解读/复制代码”和粘连换行。这里保留原文关于临时属性、参数数组、返回值和 apply 的核心思路,删除重复正文,改为补充规范边界和可验证的现代实现。历史来源仍保留在文末。

一、从“模拟”转向“理解语义”

callapply 的共同目标是:在一次调用中指定 thisArg,再把参数传给可调用对象。它们不会修改 thisArg,也不会把函数永久挂到对象上:

function describe(prefix, suffix) {
  return `${prefix}${this.name}${suffix}`
}

const user = { name: 'Ada' }

console.log(describe.call(user, '[', ']'))
console.log(describe.apply(user, ['[', ']']))
console.log(Reflect.apply(describe, user, ['[', ']']))

原文的“把函数临时放到 context.fn 上再调用”的方法,是解释隐式绑定的直观模型,不是原生实现。它会遇到对象属性冲突、不可扩展对象、Proxy、异常清理、原始值、严格函数和箭头函数等问题。

如果只是希望在变量形式下完成同样的反射调用,现代 JavaScript 已经提供了 Reflect.apply

function call2(func, thisArg, ...args) {
  if (typeof func !== 'function') {
    throw new TypeError('func 必须是可调用对象')
  }
  return Reflect.apply(func, thisArg, args)
}

function add(value) {
  return this.base + value
}

console.log(call2(add, { base: 10 }, 5)) // 15

这不是重新实现引擎内部算法,而是一个不污染原生 API 的现代封装。生产代码通常直接使用 callapplyReflect.apply

二、为什么临时属性不是等价实现

原文的简化过程是:

// 原文的教学模型
context.fn = func
context.fn(...args)
delete context.fn

即使改用 Symbol 和 try/finally,也只能得到一个受限的教学近似:

function callByTemporaryProperty(func, object, ...args) {
  const key = Symbol('temporary-call')
  Object.defineProperty(object, key, {
    configurable: true,
    value: func
  })

  try {
    return object[key](...args)
  } finally {
    delete object[key]
  }
}

限制包括:

  • object 不可扩展时无法定义临时属性;
  • Proxy 可以观察、拒绝或改变属性操作;
  • 目标对象的属性描述符、原型和访问器行为会影响结果;
  • 严格函数收到的原始 thisArg 不等于包装后的对象;
  • null/undefined、数字、字符串和 Boolean 接收者不能靠简单赋属性处理;
  • 被调用对象可能是绑定函数、Proxy 或其他可调用对象,不只是普通函数;
  • 若没有 try/finally,异常会让临时属性残留。

因此,该代码适合放在“为什么 object.method() 会有对象 this”的历史教学段落,不应称作完整的 call polyfill。

三、thisArg 的准确行为

3.1 严格函数与非严格函数

function nonStrict() {
  return this
}

function strict() {
  'use strict'
  return this
}

console.log(nonStrict.call(null) === globalThis) // 非严格函数的典型结果
console.log(strict.call(null)) // null
console.log(strict.call(undefined)) // undefined
console.log(strict.call(7)) // 7

非严格普通函数会对 null/undefined 使用运行时全局对象,并对原始值进行对象包装;严格普通函数接收原始 thisArg。这里的 globalThis 只是示例环境的全局对象,不应把结果固定写成浏览器 window

3.2 箭头函数与绑定函数

const receiver = { value: 1 }

const arrow = () => this
console.log(arrow.call(receiver) === arrow()) // true

function read() {
  return this.value
}

const bound = read.bind(receiver)
console.log(bound.call({ value: 2 })) // 1:绑定函数的 this 已固定

箭头函数没有自己的 this;绑定函数的 this 也不能再通过普通 call/apply 改写。bind 生成的函数在用 new 调用时,新实例可以优先成为 this,但预绑定参数仍会保留。

四、apply 的参数对象

apply 的第二个参数不是必须为真正的数组,而是可以转换为参数列表的 array-like 对象:

function collect(a, b, c) {
  return [this.label, a, b, c]
}

const args = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
}

console.log(collect.apply({ label: 'values' }, args))
console.log(Reflect.apply(collect, { label: 'values' }, args))

现代代码中也可以使用展开语法,但展开前需要把数据整理成真正的数组:

const values = ['a', 'b', 'c']
console.log(collect.call({ label: 'values' }, ...values))
console.log(collect.apply({ label: 'values' }, values))

nullundefined 参数列表表示没有参数:

function count(...args) {
  return args.length
}

console.log(count.apply(null, null)) // 0
console.log(count.apply(null, undefined)) // 0

数组、arguments、字符串和许多带有非负 length 的对象都可以作为参数列表;length 会经过规范的长度转换。自定义实现不应在循环条件中反复读取可能带副作用的 length,也不要用字符串拼接参数。

一个不污染原生 API 的 apply2 可以写成:

function apply2(func, thisArg, argsArray) {
  if (typeof func !== 'function') {
    throw new TypeError('func 必须是可调用对象')
  }
  return Reflect.apply(func, thisArg, argsArray == null ? [] : argsArray)
}

function multiply(a, b) {
  return this.factor * a * b
}

console.log(apply2(multiply, { factor: 2 }, [3, 4])) // 24

不要执行原文中的:

// 错误的生产实践:会覆盖整个运行时的原生方法
// Function.prototype.apply = function () { ... }

覆盖 Function.prototype.apply 会影响第三方库、框架和引擎内部依赖,调试后果远大于示例收益。

五、返回值、异常和异步函数

callapplyReflect.apply 都返回目标函数的返回值;目标函数抛出的异常会继续抛出:

function createUser(name) {
  return {
    name,
    value: this.value
  }
}

console.log(createUser.apply({ value: 1 }, ['Ada']))

try {
  function fail() {
    throw new Error('failed')
  }
  fail.call(null)
} catch (error) {
  console.log(error.message) // failed
}

如果目标函数是 async 函数,call 只负责同步地启动这次调用,返回 Promise;它不会自动等待、取消或捕获异步错误:

async function load(id) {
  return { id }
}

load.call(undefined, 1).then(result => {
  console.log(result.id) // 1
})

六、与 new 和 class 的边界

call/apply 只能调用可调用对象,不能把 class 构造器当作普通函数执行:

class User {
  constructor(name) {
    this.name = name
  }
}

try {
  User.call({}, 'Ada')
} catch (error) {
  console.log(error.name) // TypeError
}

const user = new User('Ada')
console.log(user.name) // Ada

需要动态传递构造参数时,使用 new 配合展开语法、工厂函数或明确的构造器设计,而不是 Reflect.apply(User, ...)

七、函数、Proxy 和可调用对象

call/apply 的接收者必须是可调用对象。普通对象没有 [[Call]] 内部方法,不能被直接调用:

const callable = function (value) {
  return value * 2
}

const proxy = new Proxy(callable, {
  apply(target, thisArg, args) {
    console.log('拦截调用', args)
    return Reflect.apply(target, thisArg, args)
  }
})

console.log(proxy.call(null, 3)) // 6

代理的 apply 陷阱会参与调用过程。把函数临时写入对象属性的教学实现可能额外触发 set/defineProperty/get 等陷阱,因而更不可能作为通用 polyfill。

八、常见的原生方法借用

call 常用于借用其他对象的方法,但现代 API 往往提供了更直接的替代:

const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty)
const object = { name: 'Ada' }

console.log(hasOwn(object, 'name')) // true
console.log(Object.hasOwn(object, 'name')) // true:现代首选

转换 array-like 数据时,原文时代常见 Array.prototype.slice.call(arguments);现代代码可以使用 Array.from() 或展开语法:

function toArray() {
  return Array.from(arguments)
}

console.log(toArray('a', 'b')) // ['a', 'b']

call/apply 仍有价值,但应优先选择语义更直接的标准 API。

九、测试矩阵

const assert = require('node:assert/strict')

function nonStrict() {
  return this
}

function strict() {
  'use strict'
  return this
}

const object = { value: 1 }
assert.strictEqual(nonStrict.call(object), object)
assert.strictEqual(strict.call(object), object)
assert.strictEqual(strict.call(null), null)
assert.equal(nonStrict.call(7).valueOf(), 7)
assert.equal(strict.apply(object, []).value, 1)
assert.equal(Reflect.apply(strict, object, []), object)

const arrow = () => object
assert.strictEqual(arrow.call(null), object)

console.log('call/apply 边界测试通过')

这个矩阵展示了普通函数、严格函数、原始值、箭头函数和 Reflect.apply 的差异。实际项目还应根据浏览器经典脚本、ES module、Node.js 和 Worker 分别验证宿主环境。

十、与 31 号文章的关系

原始文件 32..._2.md 抓取了 31..._1.md 的正文两遍,第二遍并没有提供新的实现或概念。现在保留共同的历史主线,并把本篇定位为进阶边界:

  • 31 号文章:从调用点和临时属性模型理解 call/apply,再转向 Reflect.apply
  • 32 号文章:集中说明严格模式、箭头函数、array-like 参数、Proxy、new、异常和 API 借用;
  • 两篇都不覆盖原生方法,不把 eval 或临时属性方案当成生产实现。

十一、参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS