函数对象内部结构:[[Call]] vs [[Construct]]、arguments、bind、尾调用
JavaScript 中每一个函数都是一个对象,但并非每一个函数对象都拥有相同的内部能力——箭头函数缺少 [[Construct]],绑定函数拥有额外的内部槽,这些差异不是语法层面的约定,而是规范在对象结构上的硬性区别。
🔹 Level 1 · 你需要知道的
函数的三个基本属性
函数对象暴露三个可观察的属性:length、name、prototype。
| 属性 | 含义 | 特殊情况 |
|---|---|---|
length |
形参数量(不计默认参数和剩余参数) | function f(a, b=1, ...c){} → f.length === 1 |
name |
函数名称字符串 | 匿名函数赋值时推断:const fn = () => {} → fn.name === 'fn' |
prototype |
作为构造函数时,新对象的 [[Prototype]] |
箭头函数没有此属性 |
function greet(name, greeting = 'Hello', ...extras) {
return `${greeting}, ${name}!`
}
console.log(greet.length) // 1,只计 name 这一个必填参数
console.log(greet.name) // 'greet'
const arrow = (x, y) => x + y
console.log(arrow.length) // 2
console.log(arrow.prototype) // undefined,箭头函数没有 prototype
箭头函数的三个限制
箭头函数不是普通函数的简写,它在设计上有三处硬性限制:
- 没有
prototype属性 → 不能作为构造函数(new arrow()报TypeError) - 没有
arguments对象 → 只能用剩余参数...args this是词法绑定的 → 来自定义时的外层作用域,不受call/apply/bind影响
const Counter = (start) => {
this.count = start // 这里的 this 是词法 this,不是新创建的对象
}
// new Counter(0) // TypeError: Counter is not a constructor
// 正确做法:
function Counter(start) {
this.count = start
}
const c = new Counter(0)
console.log(c.count) // 0
bind 的作用
Function.prototype.bind(thisArg, ...partialArgs) 返回一个新函数,永久固定 this 和部分前置参数,不可再次被 call/apply 覆盖。
function multiply(a, b) {
return a * b
}
const double = multiply.bind(null, 2) // 固定第一个参数为 2
console.log(double(5)) // 10
console.log(double(10)) // 20
// bind 后的 length 减少
console.log(multiply.length) // 2
console.log(double.length) // 1(减去已绑定的参数数量)
arguments 对象(非严格模式)
非箭头函数在非严格模式下拥有 arguments 对象,它包含所有实际传入的参数。
function sum() {
let total = 0
for (let i = 0; i < arguments.length; i++) {
total += arguments[i]
}
return total
}
console.log(sum(1, 2, 3, 4)) // 10,接受任意数量参数
现代写法应优先使用剩余参数,arguments 对象是历史遗留产物。
🔸 Level 2 · 它是怎么运行的
函数对象的内部槽完整列表
ECMAScript 规范将函数对象的内部能力编码为内部槽。下表列出 ECMAScript 函数对象(区别于内置函数、绑定函数等)的关键内部槽:
┌─────────────────────────────────────────────────────────────────┐
│ ECMAScript Function Object 内部槽 │
├──────────────────────┬──────────────────────────────────────────┤
│ 内部槽 │ 说明 │
├──────────────────────┼──────────────────────────────────────────┤
│ [[Environment]] │ 创建时捕获的词法环境(闭包的来源) │
│ [[PrivateEnvironment]]│ 私有名称环境(class 私有字段用) │
│ [[FormalParameters]] │ 形参列表(AST 节点) │
│ [[ECMAScriptCode]] │ 函数体代码(AST 节点) │
│ [[Realm]] │ 函数所属的 Realm │
│ [[ScriptOrModule]] │ 所属脚本或模块记录 │
│ [[ThisMode]] │ lexical / strict / global │
│ [[Strict]] │ 是否严格模式 │
│ [[HomeObject]] │ super 指向的对象(方法简写才有) │
│ [[Fields]] │ 类字段定义列表 │
│ [[PrivateMethods]] │ 私有方法列表 │
│ [[ClassFieldInitializer]]│ 字段初始化器标记 │
│ [[ConstructorKind]] │ base / derived(子类构造函数是 derived) │
└──────────────────────┴──────────────────────────────────────────┘
所有可调用对象都必须实现 [[Call]] 内部方法;只有构造函数同时实现 [[Construct]]:
函数类型与内部方法的对应关系:
普通函数 function f() {}
├── [[Call]] ✓
└── [[Construct]] ✓
箭头函数 () => {}
├── [[Call]] ✓
└── [[Construct]] ✗ (没有)
方法简写 { m() {} }
├── [[Call]] ✓
└── [[Construct]] ✗ (没有)
class 构造函数
├── [[Call]] ✗ (不能直接调用,报 TypeError)
└── [[Construct]] ✓
绑定函数 f.bind(...)
├── [[Call]] ✓
└── [[Construct]] ✓(若原函数有 [[Construct]])
[[Call]] 的执行步骤
F.[[Call]](thisArgument, argumentsList) 是普通函数调用(f())触发的内部方法。规范 10.2.1.1 给出完整算法,简化后为:
1. 令 callerContext = 运行中的执行上下文
2. 创建新的执行上下文 calleeContext
3. 令 localEnv = NewFunctionEnvironment(F, thisArgument)
- 如果 F.[[ThisMode]] === 'lexical'(箭头函数):
不绑定 this,this 来自外层
- 如果 F.[[ThisMode]] === 'strict':
this = thisArgument(可以是任意值)
- 否则(global 模式):
this = ToObject(thisArgument)(undefined/null → 全局对象)
4. 将 argumentsList 绑定到形参
5. 将 calleeContext 推入执行上下文栈
6. 执行 F.[[ECMAScriptCode]]
7. 弹出 calleeContext
8. 返回执行结果
[[Construct]] 的执行步骤
F.[[Construct]](argumentsList, newTarget) 是 new F() 触发的内部方法。newTarget 是被 new 直接调用的那个函数(在继承中和 F 可能不同)。
[[Construct]] 算法(base 构造函数):
1. 令 thisArgument = OrdinaryCreateFromConstructor(newTarget, '%Object.prototype%')
即:新建对象,其 [[Prototype]] = newTarget.prototype
(注意是 newTarget.prototype,不一定是 F.prototype)
2. 执行 F.[[Call]](thisArgument, argumentsList)
3. 若函数体 return 了一个对象:使用该返回值
否则:使用 thisArgument
返回值规则图示:
new F()
│
├── 函数体 return { ... }(对象)
│ └── 使用返回的对象
│
├── 函数体 return 42(原始值)
│ └── 忽略,使用 thisArgument(新创建的对象)
│
└── 函数体没有 return / return undefined
└── 使用 thisArgument
function Factory() {
this.type = 'default'
return { type: 'override' } // 返回对象,new 的结果是这个对象
}
console.log(new Factory()) // { type: 'override' }
function Factory2() {
this.type = 'default'
return 42 // 返回原始值,忽略,new 的结果是 this
}
console.log(new Factory2()) // Factory2 { type: 'default' }
绑定函数(BoundFunction)的内部结构
f.bind(thisArg, ...args) 创建的绑定函数不是普通 ECMAScript 函数对象,它是 BoundFunction Exotic Object,有自己的内部槽:
BoundFunction 对象结构:
┌──────────────────────────────────────┐
│ BoundFunctionObject │
├──────────────────────────────────────┤
│ [[BoundTargetFunction]] → 原函数 F │
│ [[BoundThis]] → thisArg │
│ [[BoundArguments]] → [arg1...] │
├──────────────────────────────────────┤
│ 没有自己的 [[Environment]] │
│ 没有自己的 [[ECMAScriptCode]] │
│ 调用时委托给 [[BoundTargetFunction]] │
└──────────────────────────────────────┘
绑定函数的 [[Call]] 实现:
BoundFunction.[[Call]](thisArgument, argumentsList):
1. 取出 F = [[BoundTargetFunction]]
2. 取出 boundThis = [[BoundThis]]
3. 取出 boundArgs = [[BoundArguments]]
4. 组合:args = boundArgs + argumentsList
5. 调用 F.[[Call]](boundThis, args)
注意:thisArgument 被丢弃,始终使用 boundThis
[[ThisMode]] 的三种取值
函数对象创建时,[[ThisMode]] 根据语法形式确定,之后不可更改:
[[ThisMode]] |
对应语法 | this 绑定行为 |
|---|---|---|
lexical |
箭头函数 () => {} |
来自词法外层,不可被覆盖 |
strict |
严格模式函数 'use strict'; function f(){} |
this 就是传入的 thisArgument,可以是 undefined |
global |
非严格普通函数 function f(){} |
this 为 undefined/null 时,替换为全局对象 |
'use strict'
function strictFn() {
return this
}
console.log(strictFn.call(undefined)) // undefined(strict 模式不替换)
console.log(strictFn.call(42)) // 42(strict 模式保留原始值)
function sloppyFn() {
return this
}
console.log(sloppyFn.call(undefined)) // globalThis(global 模式替换为全局对象)
console.log(sloppyFn.call(42)) // Number {42}(原始值被 ToObject 包装)
arguments 对象的参数同步机制
非严格模式下,arguments 对象和命名形参之间存在一个"活性映射"(parameter map)。规范称之为 Arguments Exotic Object。
非严格模式 arguments 与形参的关系:
function f(a, b) {
// 内部存在一个参数映射:
// arguments[0] ↔ a(互相镜像)
// arguments[1] ↔ b(互相镜像)
}
修改任意一方,另一方也随之改变:
f(1, 2):
a = 1, b = 2, arguments[0] = 1, arguments[1] = 2
执行 arguments[0] = 99:
→ a 变成 99(参数映射同步)
执行 a = 777:
→ arguments[0] 变成 777(参数映射同步)
function f(a) {
arguments[0] = 99
return a // 返回 99,不是 1
}
console.log(f(1)) // 99
// 严格模式下不存在这个同步
function g(a) {
'use strict'
arguments[0] = 99
return a // 仍然返回 1
}
console.log(g(1)) // 1
// 有默认参数时,即使非严格模式也不同步
function h(a = 0) {
arguments[0] = 99
return a // 仍然返回 1(有默认参数时 arguments 是简单副本)
}
console.log(h(1)) // 1
尾调用优化(TCO)
严格模式下,若某次函数调用是尾位置(Tail Position)调用,规范允许引擎复用当前栈帧而不是新建。这让纯函数式的递归写法不再有栈溢出风险。
尾位置的判断规则(规范 14.6):
- 函数体最后一个语句的
return表达式 return f()中,f()处于尾位置return a ? f() : g()中,f()和g()都处于尾位置return f() + 1中,f()不在尾位置(还要做加法)
普通递归(无 TCO)的栈增长:
fact(5)
└── fact(4)
└── fact(3)
└── fact(2)
└── fact(1)
└── fact(0) → 1
每一层都需要保留栈帧,等待子调用返回
尾递归(有 TCO 时)的栈:
fact(5, 1)
↓ 复用当前栈帧
fact(4, 5)
↓ 复用
fact(3, 20)
↓ 复用
fact(2, 60)
↓ 复用
fact(1, 120)
↓ 复用
fact(0, 120) → 120
始终只有一个栈帧
// 非尾递归版本(n 大时 stack overflow)
function factorial(n) {
if (n <= 1) return 1
return n * factorial(n - 1) // 乘法在尾位置,factorial 调用不在尾位置
}
// 尾递归版本(规范允许 TCO,但 V8 没实现)
'use strict'
function factorial(n, acc = 1) {
if (n <= 1) return acc
return factorial(n - 1, n * acc) // 这才是尾调用
}
TCO 的实现现状:Safari(JavaScriptCore)在严格模式下实现了 TCO;V8(Chrome/Node.js)明确拒绝实现,理由是会破坏调用栈信息,使调试困难。因此 TCO 目前只是规范要求,不是可以依赖的跨引擎特性。
🔺 Level 3 · 规范怎么定义的
10.2 ECMAScript Function Objects
规范第 10.2 节定义了 ECMAScript 函数对象的创建过程。OrdinaryFunctionCreate 是核心操作:
OrdinaryFunctionCreate(functionPrototype, sourceText, ParameterList, Body, thisMode, env, privateEnv):
1. 令 F = OrdinaryObjectCreate(functionPrototype)
// 以 functionPrototype 为原型创建普通对象
2. 设置内部槽:
F.[[Call]] = %ECMAScriptFunction%.[[Call]] 的实现
F.[[FormalParameters]] = ParameterList
F.[[ECMAScriptCode]] = Body
F.[[Realm]] = 当前 Realm
F.[[Environment]] = env
F.[[PrivateEnvironment]] = privateEnv
F.[[ScriptOrModule]] = 当前脚本或模块
F.[[Strict]] = strict(由 Body 是否包含 'use strict' 决定)
3. 若 thisMode === 'lexical-this':
F.[[ThisMode]] = 'lexical'
否则若 F.[[Strict]]:
F.[[ThisMode]] = 'strict'
否则:
F.[[ThisMode]] = 'global'
4. F.[[IsClassConstructor]] = false
5. F.[[Fields]] = []
6. F.[[PrivateMethods]] = []
7. F.[[ClassFieldInitializer]] = false
8. F.[[ConstructorKind]] = 'base'
9. 返回 F
此时函数对象没有 [[Construct]]。只有当函数声明/表达式被处理为非箭头函数时,规范才会调用 MakeConstructor(F) 为其添加 [[Construct]] 能力:
MakeConstructor(F):
1. 令 prototype = OrdinaryObjectCreate(%Object.prototype%)
2. 定义 prototype.constructor = F(不可枚举,可写,可配置)
3. 定义 F.prototype = prototype(不可枚举,可写,不可配置)
4. 设置 F.[[Construct]] 为 OrdinaryConstruct 的实现
10.2.1.1 [[Call]] 算法(完整版)
F.[[Call]](thisArgument, argumentsList):
1. 令 callerContext = 运行中的执行上下文
2. 令 calleeContext = PrepareForOrdinaryCall(F, undefined)
// PrepareForOrdinaryCall 做了:
// a. 创建新的执行上下文
// b. 创建新的 FunctionEnvironment
// c. 将其推入执行上下文栈
3. 断言:calleeContext 现在是运行中的执行上下文
4. OrdinaryCallBindThis(F, calleeContext, thisArgument)
// OrdinaryCallBindThis:
// 若 F.[[ThisMode]] === 'lexical':返回(不绑定)
// 若 F.[[ThisMode]] === 'strict':envRec.BindThisValue(thisArgument)
// 否则:
// 若 thisArgument 是 undefined 或 null:
// thisValue = F.[[Realm]].[[GlobalEnv]].[[GlobalThisValue]]
// 否则:
// thisValue = ToObject(thisArgument)
// envRec.BindThisValue(thisValue)
5. 令 result = OrdinaryCallEvaluateBody(F, argumentsList)
// 执行函数体,处理形参绑定
6. 从执行上下文栈移除 calleeContext
7. 若 callerContext 当前不在栈顶(TCO 情形),则恢复它
8. 若 result.[[Type]] 是 return:返回 result.[[Value]]
否则:返回 undefined
10.2.1.2 [[Construct]] 算法(完整版)
F.[[Construct]](argumentsList, newTarget):
1. 令 kind = F.[[ConstructorKind]]
2. 若 kind === 'base':
a. 令 thisArgument = OrdinaryCreateFromConstructor(newTarget, '%Object.prototype%')
// thisArgument.[[Prototype]] = newTarget.prototype
//(若 newTarget.prototype 不是对象,则用 %Object.prototype%)
3. 令 calleeContext = PrepareForOrdinaryCall(F, newTarget)
4. 若 kind === 'base':
OrdinaryCallBindThis(F, calleeContext, thisArgument)
5. 令 result = OrdinaryCallEvaluateBody(F, argumentsList)
6. 从执行上下文栈移除 calleeContext
7. 若 result.[[Type]] 是 return:
若 Type(result.[[Value]]) 是 Object:返回 result.[[Value]]
若 kind === 'base':返回 thisArgument
若 result.[[Value]] 不是 undefined:抛出 TypeError
// derived 构造函数(子类)如果显式 return 非 undefined 原始值会报错
8. 若 result.[[Type]] 不是 normal:抛出 result 的错误
9. 若 kind === 'base':返回 thisArgument
// 未显式返回对象 → 返回 this
10. 令 thisBinding = calleeContext 的 envRec.GetThisBinding()
若 thisBinding 是 undefined:抛出 ReferenceError
// derived 构造函数没调用 super() → this 未初始化
返回 thisBinding
14.6 尾位置调用规范定义
规范 14.6.1 定义了哪些语法位置是尾位置(Tail Position)。以下是完整的位置列表(节选关键部分):
IsInTailPosition(call) 为 true 当且仅当:
对于 FunctionBody:
- 严格代码
- call 在 FunctionBody 的 return 语句的 Expression 中
对于 ConciseBody(箭头函数简写体):
- 严格代码
- call 就是 ConciseBody 的 AssignmentExpression
对于 StatementListItem 列表:
- call 在列表最后一项的尾位置
对于 ReturnStatement:`return Expression`
- call 在 Expression 的尾位置
对于条件表达式 `a ? b : c`:
- call 在 b 的尾位置,或在 c 的尾位置
对于逻辑表达式 `a || b`、`a && b`、`a ?? b`:
- call 在 b 的尾位置(右操作数)
以下情况不是尾位置:
- `return f() + 1`(f() 之后还有加法)
- `return f().toString()`(f() 之后还有方法调用)
- `return [f()]`(f() 在数组字面量里)
- `return f(), g()`(逗号表达式只有最后一项在尾位置)
规范 10.2.1.1 在 [[Call]] 的第 8 步中通过 PrepareForTailCall() 实现 TCO:丢弃当前栈帧并在同一位置重用,使递归调用不增长调用栈。
💎 Level 4 · 边界与陷阱
陷阱 1:new (()=>{})() 报 TypeError
箭头函数没有 [[Construct]] 内部方法。new 运算符在执行前会检查目标是否有 [[Construct]],没有则立即抛出 TypeError,不会尝试执行函数体。
const Arrow = () => {}
new Arrow()
// TypeError: Arrow is not a constructor
// 同样报错的还有:
// new (function.bind(null)()) 当 bind 的原函数是箭头函数时
// new ({method() {}}.method) 方法简写也没有 [[Construct]]
class Cls {}
const BoundCls = Cls.bind(null)
new BoundCls() // 正常,Cls 有 [[Construct]],绑定函数会委托给它
根本原因:规范定义 new expr(args) 调用 GetValue(expr).[[Construct]](args, expr),若对象没有 [[Construct]] 方法则抛出 TypeError(规范 13.3.5.1 步骤 7)。
陷阱 2:length 的四种计算规则
Function.prototype.length 的值不是"所有参数的数量",而是满足特定条件的参数数量:
// 规则:length = 第一个默认参数或剩余参数之前的参数数量
function f1(a, b, c) {} // 3
function f2(a, b = 1, c) {} // 1(b 是默认参数,b 及之后不计)
function f3(a, ...rest) {} // 1(rest 不计)
function f4(a, b = 1, ...rest) {} // 1
function f5({ a, b }, c) {} // 2(解构参数算一个)
console.log(f1.length, f2.length, f3.length, f4.length, f5.length)
// 3 1 1 1 2
// bind 之后:length = max(0, 原 length - 绑定参数数量)
const f = function(a, b, c) {} // length = 3
const b1 = f.bind(null) // length = 3(绑定 this 不算参数)
const b2 = f.bind(null, 1) // length = 2
const b3 = f.bind(null, 1, 2, 3, 4) // length = 0(不会是负数)
console.log(b1.length, b2.length, b3.length)
// 3 2 0
这个计算规则来自规范 10.2.10 SetFunctionLength:统计形参列表中第一个默认参数或 rest 参数之前的参数个数。
陷阱 3:arguments 同步的边界条件
非严格模式下 arguments 与形参同步,但有三种情况会破坏这个同步:
// 情况 1:有默认参数时,即使非严格模式,也不同步
function f1(a = 0) {
arguments[0] = 99
return a
}
console.log(f1(1)) // 1,不是 99
// 情况 2:有剩余参数时,也不同步
function f2(...args) {
args[0] = 99 // 注意:这不是 arguments,而是剩余参数数组
return args[0]
}
console.log(f2(1)) // 99(但这是数组,不是 arguments 对象)
// 情况 3:未传入的参数,不建立同步
function f3(a, b) {
arguments[1] = 99
return b // b 是 undefined,不是 99
}
console.log(f3(1)) // undefined,未传入的参数不建立映射
// 情况 4:严格模式,完全不同步
function f4(a) {
'use strict'
arguments[0] = 99
return a // 仍然是 1
}
console.log(f4(1)) // 1
规范对这个"神奇同步"的实现是 Arguments Exotic Object(规范 10.4.4):对象内部有一个 [[ParameterMap]] 属性,存储 arguments 索引到形参 Environment Record 绑定的映射。只有被实际传入的命名参数才会建立映射;有默认参数或剩余参数时(规范 10.2.11 步骤 20),整个 arguments 对象退化为普通对象(不建立映射)。
陷阱 4:bind 之后无法再改变 this
bind 创建的绑定函数的 [[BoundThis]] 是硬编码的,后续任何 call/apply/bind 都无法改变:
function greet() {
return `Hello, ${this.name}`
}
const alice = { name: 'Alice' }
const bob = { name: 'Bob' }
const greetAlice = greet.bind(alice)
console.log(greetAlice()) // 'Hello, Alice'
console.log(greetAlice.call(bob)) // 'Hello, Alice'(call 无效)
console.log(greetAlice.apply(bob, [])) // 'Hello, Alice'(apply 无效)
const greetAliceToo = greetAlice.bind(bob) // 再次 bind 也无效
console.log(greetAliceToo()) // 'Hello, Alice'(仍然是 Alice)
但有一个例外:当绑定函数被 new 调用时,[[BoundThis]] 会被忽略(规范 10.3.2 BoundFunctionExoticObject.[[Construct]] 步骤 6:对于 new 调用,newTarget 被传递给原函数的 [[Construct]],this 由原函数的 [[Construct]] 创建):
function Point(x, y) {
this.x = x
this.y = y
}
const OriginPoint = Point.bind({ name: 'ignored' }, 0)
const p = new OriginPoint(5)
console.log(p) // Point { x: 0, y: 5 },不是 { name: 'ignored', x: 0, y: 5 }
console.log(p instanceof Point) // true
陷阱 5:尾调用的常见误判
以下写法看似尾调用,实际不是:
'use strict'
// 错误 1:调用后还有操作
function a(n) {
return n > 0 ? a(n - 1) + 1 : 0 // +1 不是尾位置
}
// 错误 2:包在数组/对象字面量里
function b(n) {
return [b(n - 1)] // 不是尾位置
}
// 错误 3:作为参数传给另一个函数
function c(n) {
return console.log(c(n - 1)) // c(n-1) 是 console.log 的参数,不是尾位置
}
// 正确的尾调用
function d(n, acc = 0) {
if (n === 0) return acc
return d(n - 1, acc + 1) // 这才是尾调用
}
本章小结
- 所有函数都有
[[Call]],但只有普通函数和类构造函数有[[Construct]];箭头函数、方法简写没有[[Construct]],不能用new调用。 [[Construct]]执行时首先创建新对象([[Prototype]]来自newTarget.prototype),执行函数体后若返回对象则用返回值,否则用this。bind生成的绑定函数拥有独立的[[BoundTargetFunction]]/[[BoundThis]]/[[BoundArguments]]内部槽,this无法被后续call/apply/bind覆盖(除非通过new调用)。Function.prototype.length计算规则:第一个默认参数或剩余参数之前的参数数量;bind后 length 减少绑定参数数量(最小为 0)。- 尾调用优化(TCO)在规范第 14.6 节有明确定义,严格模式下适用,但 V8 引擎拒绝实现,目前只有 Safari 支持;生产代码中不应依赖 TCO 进行递归优化。