普通对象与异质对象:[[Get]][[Set]][[Call]] 内部方法
JavaScript 规范用一套统一的内部方法(Internal Methods)描述所有对象的行为。普通对象按默认规则实现这套方法;数组、函数、Proxy 这些"异质对象"则重写了其中某些方法,赋予了它们特殊能力。这不是语法魔法——是经过规范明确定义的行为差异。理解这些差异,才能解释数组的 length 为何自动更新、箭头函数为何不能 new、以及 arguments 对象那个著名的同步行为。
🔹 Level 1 · 你需要知道的
对象的内部方法
每个 JavaScript 对象都实现了以下内部方法(规范 §6.1.7.2):
| 内部方法 | 签名 | 触发时机 |
|---|---|---|
[[GetPrototypeOf]] |
() → Object | Null | Object.getPrototypeOf(obj) |
[[SetPrototypeOf]] |
(Object | Null) → Boolean | Object.setPrototypeOf(obj, proto) |
[[IsExtensible]] |
() → Boolean | Object.isExtensible(obj) |
[[PreventExtensions]] |
() → Boolean | Object.preventExtensions(obj) |
[[GetOwnProperty]] |
(propertyKey) → Descriptor | undefined | Object.getOwnPropertyDescriptor |
[[DefineOwnProperty]] |
(propertyKey, Descriptor) → Boolean | Object.defineProperty |
[[HasProperty]] |
(propertyKey) → Boolean | in 运算符 |
[[Get]] |
(propertyKey, Receiver) → any | 属性读取 |
[[Set]] |
(propertyKey, value, Receiver) → Boolean | 属性赋值 |
[[Delete]] |
(propertyKey) → Boolean | delete 运算符 |
[[OwnPropertyKeys]] |
() → List | Object.keys、for...in |
可调用对象还额外实现:
| 内部方法 | 签名 | 触发时机 |
|---|---|---|
[[Call]] |
(any, List) → any | func() |
[[Construct]] |
(List, Object) → Object | new func() |
普通对象 vs 异质对象
普通对象(Ordinary Object):按规范默认算法实现所有内部方法。用 {} 创建的对象都是普通对象。
异质对象(Exotic Object):重写了至少一个内部方法。主要类型:
异质对象类型及重写的内部方法:
Array 异质对象:
└── [[DefineOwnProperty]] — 自动维护 length
Function 对象:
└── [[Call]]、[[Construct]] — 使函数可调用/可 new
箭头函数对象:
└── [[Call]] 有值,但没有 [[Construct]] — 不能 new
Bound Function(bind 创建的):
└── [[Call]]、[[Construct]] 委托给目标函数
arguments 对象(非严格模式):
└── [[Get]]、[[Set]]、[[DefineOwnProperty]] — 与形参同步
String 异质对象(new String()):
└── [[GetOwnProperty]]、[[OwnPropertyKeys]] — 暴露字符索引
Proxy 对象:
└── 所有内部方法都可被陷阱拦截(见 ch15)
Module Namespace 对象:
└── [[Get]]、[[Set]] — 只读、特殊 get 行为
数组的自动 length 更新
这是最常见的"魔法"之一,其实是 [[DefineOwnProperty]] 的重写:
const arr = [1, 2, 3]; // length = 3
arr[5] = 99;
// 发生了什么?
// 1. [[DefineOwnProperty]]('5', {value: 99, ...}) 被调用
// 2. 数组异质对象的 [[DefineOwnProperty]] 检查:
// '5' 是有效的数组索引(整数字符串 >= 0)
// 5 >= arr.length(3),所以自动把 length 更新为 6
console.log(arr.length); // 6
console.log(arr); // [1, 2, 3, empty × 2, 99](稀疏数组)
// 反向:减小 length 会删除元素
arr.length = 2;
// [[DefineOwnProperty]]('length', {value: 2, ...})
// 删除所有 index >= 2 的元素
console.log(arr); // [1, 2]
函数的 [[Call]] vs [[Construct]]
function normalFn() { return 'called'; }
const arrowFn = () => 'called';
class MyClass {}
// [[Call]]:普通调用
normalFn(); // 'called'
arrowFn(); // 'called'
// MyClass() // TypeError: Class constructor cannot be invoked without 'new'
// [[Construct]]:new 调用
new normalFn(); // 创建新对象
// new arrowFn(); // TypeError: arrowFn is not a constructor
new MyClass(); // 创建 MyClass 实例
// 检查:
// 没有简单的方式在 JS 代码中检查 [[Construct]],
// 但可以通过 try-catch 探测:
function isConstructor(fn) {
try {
new fn();
return true;
} catch (e) {
return e instanceof TypeError && e.message.includes('is not a constructor');
}
}
arguments 对象的同步行为
这个行为是 JavaScript 最令人惊讶的特性之一:
// 非严格模式下,arguments 和形参共享同一个"槽"
function test(a, b) {
console.log(arguments[0]); // 1
a = 99; // 修改形参
console.log(arguments[0]); // 99 — arguments 也变了!
arguments[1] = 88; // 修改 arguments
console.log(b); // 88 — 形参也变了!
}
test(1, 2);
// 严格模式:完全断开连接
function strictTest(a, b) {
'use strict';
a = 99;
console.log(arguments[0]); // 1 — 不受影响
arguments[1] = 88;
console.log(b); // 2 — 不受影响
}
strictTest(1, 2);
5个常见错误
错误1:用 typeof 检查是否能 new
// typeof 无法区分"能 new"和"不能 new"
typeof function(){}; // 'function'
typeof (() => {}); // 'function' — 但不能 new!
typeof class C {}; // 'function' — 能 new,但不能普通调用
// 正确方式:只能用 try-catch 探测
错误2:以为 arr.length 是 O(1) 操作
// length 读取是 O(1)(直接读属性值)
arr.length; // 快
// 但是给 length 赋值可能是 O(n)!
arr.length = 0; // 删除所有元素,相当于清空数组
// 等价于删除所有数字索引属性,代价是 O(n)
// 另一个 O(n):稀疏数组赋值到超大索引
arr[1000000] = 1; // length 变为 1000001,但不会分配 1MB 内存
// V8 用稀疏存储(HashTable 或 Dictionary mode),不是连续数组
错误3:在 arguments 里用 rest 参数替换不彻底
// 注意:rest 参数不在 arguments 里
function test(...args) {
console.log(arguments[0]); // 1
console.log(args[0]); // 1
// 它们是分开的!args 是真正的数组,arguments 是类数组
Array.isArray(args); // true
Array.isArray(arguments); // false
}
test(1, 2, 3);
// arguments 不包含 rest 之前的参数的完整列表,只包含所有传入的参数
function mix(first, ...rest) {
console.log(arguments.length); // 传入的总参数数量
console.log(rest.length); // 除 first 外的参数数量
}
mix(1, 2, 3); // arguments.length = 3, rest.length = 2
错误4:new Array(n) 创建的是稀疏数组
const arr = new Array(3);
console.log(arr.length); // 3
console.log(arr[0]); // undefined
console.log(0 in arr); // false!— 是稀疏数组,没有索引0
// 对比:
const arr2 = [undefined, undefined, undefined];
console.log(0 in arr2); // true — 有索引,值是 undefined
// 危险:map、filter 在稀疏数组上跳过"洞"
new Array(3).map((_, i) => i); // [empty × 3],不是 [0, 1, 2]!
// 正确做法:
Array.from({length: 3}, (_, i) => i); // [0, 1, 2]
[...new Array(3)].map((_, i) => i); // [0, 1, 2]
错误5:delete 数组元素不更新 length
const arr = [1, 2, 3];
delete arr[1]; // 删除元素,创建稀疏数组
console.log(arr); // [1, empty, 3]
console.log(arr.length); // 3 — length 没变
console.log(1 in arr); // false — 索引1不存在
// 若要真正删除并移位:
arr.splice(1, 1);
// 或者 filter:
arr.filter((_, i) => i !== 1);
🔸 Level 2 · 它是怎么运行的
内部方法对比:普通对象 vs 异质对象
普通对象 vs 各类异质对象的内部方法实现:
内部方法 │ 普通对象 │ Array │ Function │ Arrow │ arguments(非严格) │
─────────────────┼────────┼───────┼──────────┼───────┼──────────────────┤
[[GetPrototypeOf]]│ 默认 │ 默认 │ 默认 │ 默认 │ 默认 │
[[SetPrototypeOf]]│ 默认 │ 默认 │ 默认 │ 默认 │ 默认 │
[[IsExtensible]] │ 默认 │ 默认 │ 默认 │ 默认 │ 默认 │
[[GetOwnProperty]]│ 默认 │ 默认 │ 默认 │ 默认 │ 特殊(索引映射) │
[[DefineOwnProperty]│ 默认 │ 特殊 │ 默认 │ 默认 │ 特殊(索引映射) │
[[HasProperty]] │ 默认 │ 默认 │ 默认 │ 默认 │ 默认 │
[[Get]] │ 默认 │ 默认 │ 默认 │ 默认 │ 特殊(索引映射) │
[[Set]] │ 默认 │ 默认 │ 默认 │ 默认 │ 特殊(索引映射) │
[[Delete]] │ 默认 │ 默认 │ 默认 │ 默认 │ 特殊(断开映射) │
[[OwnPropertyKeys]]│ 默认 │ 默认 │ 默认 │ 默认 │ 默认 │
[[Call]] │ 无 │ 无 │ 有 │ 有 │ 无 │
[[Construct]] │ 无 │ 无 │ 有 │ 无 │ 无 │
Array 异质对象:[[DefineOwnProperty]] 的详细流程
数组的 [[DefineOwnProperty]] 在普通对象的基础上增加了两个特殊逻辑:
ArrayDefineOwnProperty(A, P, Desc) 流程:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ P 是 'length' 吗? │
│ ├── 是 → ArraySetLength(A, Desc) │
│ │ └── 如果新 length < 旧 length,删除超出范围的索引属性 │
│ │ │
│ └── 否 → P 是有效的数组索引(非负整数字符串)吗? │
│ ├── 是 → 执行索引赋值逻辑: │
│ │ index = ToUint32(P) │
│ │ 如果 index >= A.length │
│ │ → 将 length 更新为 index + 1 │
│ │ → 调用普通 [[DefineOwnProperty]] 设置属性 │
│ │ │
│ └── 否 → 调用普通 [[DefineOwnProperty]](正常处理) │
└─────────────────────────────────────────────────────────────────┘
什么是"有效的数组索引"?规范定义:P 是一个字符串,当且仅当 ToString(ToUint32(P)) === P 且 ToUint32(P) !== 2^32 - 1 时,P 是有效的数组索引。
// 有效数组索引:
'0', '1', '1000000' // 都是
// 不是有效数组索引:
'-1' // 负数
'1.5' // 浮点
'4294967295' // 等于 2^32 - 1,不合法
'hello' // 非数字
函数对象的 [[Call]] 与 [[Construct]]
[[Call]] 的执行过程:
OrdinaryCallEvaluateBody(F, argumentsList):
1. 创建函数执行上下文(Function Execution Context)
2. 将 this 绑定到 Receiver(或 globalThis,取决于严格模式)
3. 将 arguments 绑定到参数列表
4. 执行函数体
5. 返回 Return 值(或 undefined)
[[Construct]] 与 [[Call]] 的关键区别:
new F(...args) 的执行(OrdinaryConstruct):
1. 创建一个新对象:thisValue = OrdinaryCreateFromConstructor(F, "%Object.prototype%")
└── 新对象的 [[Prototype]] 被设置为 F.prototype
2. 调用 [[Call]](thisValue, args)
└── 函数体中 this 指向这个新对象
3. 如果函数返回一个对象 → 使用该对象作为 new 表达式的结果
4. 否则 → 使用步骤1创建的新对象
关键点:步骤3意味着构造函数可以通过显式 return 一个对象来"劫持"new 的返回值
function Tricky() {
this.x = 1;
return { y: 2 }; // 显式返回对象
}
const t = new Tricky();
console.log(t.x); // undefined — 返回的是 {y:2},不是 this
console.log(t.y); // 2
function NotTricky() {
this.x = 1;
return 42; // 返回原始值——被忽略
}
const nt = new NotTricky();
console.log(nt.x); // 1 — this 被正常返回
arguments 对象的内部映射机制
非严格模式的 arguments 对象通过一个叫做 Arguments Exotic Object 的特殊机制实现参数同步:
Arguments 对象的内部结构(非严格模式):
arguments 对象
├── 数组索引属性(0, 1, 2, ...)
├── [[ParameterMap]]:存储索引 → 形参变量名的映射
└── 内部方法重写:
[[Get]](P):
如果 P 在 [[ParameterMap]] 中 → 读取对应形参的当前值
否则 → 普通对象的 [[Get]]
[[Set]](P, V):
如果 P 在 [[ParameterMap]] 中 → 设置对应形参的值
普通对象的 [[Set]](同时更新自有属性)
function example(x, y, z) {
// 内部:[[ParameterMap]] = { '0': x变量, '1': y变量, '2': z变量 }
x = 10;
// [[Set]] 发现 '0' 在 ParameterMap 中 → 修改 x 变量
// arguments[0] 读取时,[[Get]] 发现 '0' 在 ParameterMap → 返回 x 的当前值 10
arguments[1] = 20;
// [[Set]] 发现 '1' 在 ParameterMap 中 → 修改 y 变量
// y 现在是 20
// 断开映射:
delete arguments[2];
// [[Delete]] 从 ParameterMap 中移除 '2'
z = 30;
// z 变量变了,但 arguments[2] 不再同步
console.log(arguments[2]); // undefined — 映射已断开
}
String 异质对象
new String('hello') 创建的对象有特殊行为:
const s = new String('hello');
// 字符可通过数字索引访问
s[0]; // 'h'
s[1]; // 'e'
// 这些索引属性是只读、不可配置的
Object.getOwnPropertyDescriptor(s, '0');
// { value: 'h', writable: false, enumerable: true, configurable: false }
// 尝试修改:
s[0] = 'X'; // 静默失败(非严格模式)
// OwnPropertyKeys 包含数字索引
Object.getOwnPropertyNames(s);
// ['0', '1', '2', '3', '4', 'length'] — 包含所有字符索引
// 与原始字符串的对比:
typeof 'hello'; // 'string'(原始值)
typeof new String('hello'); // 'object'(包装对象)
'hello' instanceof String; // false(原始值不是对象)
new String('hello') instanceof String; // true
🔺 Level 3 · 规范怎么定义的
规范第10节:Ordinary and Exotic Object Behaviours
规范 §10 是对象行为的核心章节,结构如下:
§10 Ordinary and Exotic Object Behaviours
├── §10.1 Ordinary Object Internal Methods(默认实现,共11个方法)
├── §10.2 Function Objects(普通函数)
├── §10.3 Built-in Function Objects(内置函数)
├── §10.4 Built-in Exotic Object Internal Methods and Slots
│ ├── §10.4.1 Bound Function Exotic Objects
│ ├── §10.4.2 Array Exotic Objects ← 本节重点
│ ├── §10.4.3 String Exotic Objects
│ ├── §10.4.4 Arguments Exotic Objects ← 本节重点
│ └── §10.4.5 Integer-Indexed Exotic Objects(TypedArray)
└── §10.5 Proxy Object Internal Methods(见 ch15)
Array Exotic Objects 的 [[DefineOwnProperty]] 算法(§10.4.2.1)
规范原文关键部分(§10.4.2.1 ArrayDefineOwnProperty):
ArrayDefineOwnProperty ( A, P, Desc )
1. 如果 P 是 "length":
a. 如果 Desc 没有 [[Value]] 字段 → 按普通对象处理
b. newLen ← ToUint32(Desc.[[Value]])
c. numberLen ← ToNumber(Desc.[[Value]])
d. 如果 newLen ≠ numberLen → 抛出 RangeError("Invalid array length")
e. 将 Desc 的 [[Value]] 替换为 newLen
f. 如果 newLen >= oldLen → 按普通对象处理(仅增大,无需删除)
g. 否则(newLen < oldLen):
如果 oldLenDesc.[[Writable]] 是 false → 返回 false
...循环删除 index 从 oldLen-1 到 newLen 的属性...
如果任何属性不可删除(configurable:false)→ 返回 false
2. 否则 P 是数组索引:
index ← ToUint32(P)
如果 index >= oldLen 且 oldLenDesc.[[Writable]] 是 false → 返回 false
调用普通 [[DefineOwnProperty]] 设置索引属性
如果 index >= oldLen → 将 length 更新为 index + 1
返回 true
// 验证:length 不可写时无法扩展数组
const arr = [1, 2, 3];
Object.defineProperty(arr, 'length', { writable: false });
arr[3] = 4; // TypeError(严格模式)或静默失败(非严格模式)
// 验证:设置非法 length 抛出 RangeError
arr.length = -1; // RangeError: Invalid array length
arr.length = 2 ** 32; // RangeError: Invalid array length
arr.length = 'hello'; // 不报 RangeError(字符串会被转换为 NaN,再被转为 0)
// 实际上:ToUint32('hello') = 0,但 ToNumber('hello') = NaN,0 ≠ NaN → RangeError
Function Objects 的 [[Call]] 和 [[Construct]] 规范定义
[[Call]] (§10.2.1):
F.[[Call]](thisArgument, argumentsList):
1. callerContext ← 当前运行的执行上下文
2. 创建新的 ECMAScript 代码执行上下文 calleeContext
3. 调用 PrepareForOrdinaryCall(F, undefined)
→ 设置函数的 [[Environment]](作用域链)
4. 如果 F 是 ClassConstructor → 抛出 TypeError
(class 构造函数必须用 new 调用)
5. 调用 OrdinaryCallBindThis(F, calleeContext, thisArgument)
→ 将 this 绑定到执行上下文
6. 调用 OrdinaryCallEvaluateBody(F, argumentsList)
→ 执行函数体
7. 弹出 calleeContext,恢复 callerContext
8. 返回执行结果
[[Construct]] (§10.2.2):
F.[[Construct]](argumentsList, newTarget):
1. 如果 F 的 [[ConstructorKind]] 是 "base"(非 derived class):
thisArgument ← OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%")
→ 创建新对象,[[Prototype]] = newTarget.prototype
2. calleeContext ← PrepareForOrdinaryCall(F, newTarget)
3. 如果 [[ConstructorKind]] 是 "base" → OrdinaryCallBindThis 绑定 this
4. result ← OrdinaryCallEvaluateBody(F, argumentsList)
5. 如果 result 的 [[Type]] 是 return:
如果 result.[[Value]] 是对象 → 返回该对象(override 行为)
6. 如果 result.[[Type]] 不是 normal → 抛出异常
7. 如果 [[ConstructorKind]] 是 "base" → 返回 thisArgument
8. 如果 thisArgument 是 undefined → 抛出 ReferenceError
(super() 未被调用的 derived constructor)
9. 返回 thisArgument
Arguments Exotic Objects 的规范定义(§10.4.4)
规范在 CreateMappedArgumentsObject 中创建带映射的 arguments 对象:
CreateMappedArgumentsObject(func, formals, argumentsList, env):
1. 创建普通对象 obj
2. 设置 [[ParameterMap]] = 一个普通对象
3. 对每个形参 name(从后向前,索引 index):
如果 name 没有被重复使用(非重名形参):
mappedNames.add(name)
在 [[ParameterMap]] 上定义访问器属性 ToString(index):
get: 返回 env.GetBindingValue(name) ← 直接读形参变量
set: env.SetMutableBinding(name, v) ← 直接写形参变量
4. obj.[[Get]] = MakeArgGetter(...) ← 覆盖为使用 ParameterMap 的版本
5. obj.[[Set]] = MakeArgSetter(...)
6. ...设置 length、callee 等属性...
7. 返回 obj
💎 Level 4 · 边界与陷阱
陷阱1:new Array(3) 是稀疏数组,不是 [3]
new Array(3) // [empty × 3],不是 [3]
new Array(1, 2) // [1, 2]
// 规范行为:Array 构造函数有两种重载:
// new Array(len) — 单个数值参数 → 创建长度为 len 的空数组
// new Array(v1, v2, ...) — 多个参数 → 创建包含这些值的数组
// 陷阱:
new Array(3).map(x => x * 2); // [empty × 3],不是 [0, 0, 0]!
// 因为 map 跳过稀疏数组的"洞"(holes)
// 验证是否是洞:
0 in new Array(3); // false — 没有索引0
0 in [undefined, undefined, undefined]; // true — 有索引,值是 undefined
// 创建填充数组的正确方式:
Array.from({length: 3}, (_, i) => i * 2); // [0, 2, 4]
new Array(3).fill(0).map((_, i) => i * 2); // [0, 2, 4]
[...new Array(3)].map((_, i) => i * 2); // [0, 2, 4]
// 注意:展开运算符会将 empty 转换为 undefined,此时 map 会遍历它们
// 也要注意:
new Array(3).fill([]); // [[], [], []] — 所有元素引用同一个数组!
new Array(3).fill(null).map(() => []); // [[], [], []] — 各自独立
陷阱2:函数 length 属性的计算规则
函数的 length 属性是形参数量,但有几个不直觉的规则:
// 基本规则:
function f(a, b, c) {}
f.length; // 3
// 剩余参数不计入:
function f2(a, b, ...rest) {}
f2.length; // 2,不是 3
// 默认值参数不计入(从第一个有默认值的参数起都不计):
function f3(a, b = 1, c) {}
f3.length; // 1(只有 a,b 有默认值后面的 c 也不算)
function f4(a = 1, b, c) {}
f4.length; // 0(a 有默认值,从 a 开始都不算)
// 解构参数:
function f5({ x, y }) {}
f5.length; // 1(解构算一个参数)
function f6([a, b, c]) {}
f6.length; // 1
// bind 的影响:
function f7(a, b, c) {}
const bound = f7.bind(null, 1); // 绑定了一个参数
bound.length; // 2(3 - 1 = 2)
// 但不会是负数:
const bound2 = f7.bind(null, 1, 2, 3, 4);
bound2.length; // 0(最小为0)
// class 和箭头函数:
class C {
constructor(a, b) {}
}
C.length; // 2(构造函数的形参数量)
const arrow = (a, b, c) => {};
arrow.length; // 3
陷阱3:箭头函数没有 [[Construct]],new 报 TypeError
const Arrow = () => {};
new Arrow(); // TypeError: Arrow is not a constructor
// 为什么?
// 箭头函数在规范中被定义为不分配 [[Construct]] 内部方法
// 同时也没有 prototype 属性(new 需要用 F.prototype 设置原型)
Arrow.prototype; // undefined
// 对比:普通函数
function Normal() {}
Normal.prototype; // {constructor: Normal}
// 也不能当做构造函数的类型:
typeof Arrow; // 'function' — typeof 无法区分
// 只能通过检查 prototype 来粗略判断:
function isArrowOrBound(fn) {
return !fn.prototype;
}
// 注意:bound function 也没有 prototype:
const bound = (function(){}).bind(null);
bound.prototype; // undefined
// 但 bound function 有 [[Construct]](委托给原函数)
new bound(); // 可以 new(如果原函数可以 new)
为什么箭头函数不能 new?
设计原因:箭头函数没有自己的 this,它从外层词法作用域捕获 this。new 操作需要创建一个新的 this 对象,然后绑定到函数。由于箭头函数的 this 固定,这在概念上是矛盾的——如果允许 new,this 会指向外层的对象,而不是新创建的实例,这会造成无法预测的行为。
陷阱4:非严格模式 arguments 的同步行为完整案例
function analyze(x, y) {
console.log('初始状态:');
console.log('x =', x, ' arguments[0] =', arguments[0]); // 1, 1
// 场景1:修改形参,arguments 同步
x = 100;
console.log('修改 x 后:');
console.log('x =', x, ' arguments[0] =', arguments[0]); // 100, 100
// 场景2:修改 arguments,形参同步
arguments[0] = 999;
console.log('修改 arguments[0] 后:');
console.log('x =', x, ' arguments[0] =', arguments[0]); // 999, 999
// 场景3:用 let 解构断开绑定
let [a] = arguments; // 展开为新变量,切断与 arguments 的联系
arguments[0] = 777;
console.log('a =', a, ' x =', x, ' arguments[0] =', arguments[0]);
// a = 999(快照),x = 777(还在同步),arguments[0] = 777
// 场景4:多余的 arguments(没有对应形参的)
console.log('arguments[2] =', arguments[2]); // 3(传入了第三个参数)
// 注意:arguments[2] 没有对应的形参,是普通属性,不参与同步
}
analyze(1, 2, 3);
性能提示:现代 V8 对使用 arguments 的函数会阻止某些优化。如果在性能关键路径上需要访问参数,用 rest 参数 ...args 替代 arguments。
陷阱5:delete arr[0] vs arr.splice(0,1)
const arr = [1, 2, 3, 4, 5];
// delete:移除元素,留下空洞,length 不变
delete arr[1];
console.log(arr); // [1, empty, 3, 4, 5]
console.log(arr.length); // 5 — 未变
console.log(1 in arr); // false — 索引1不存在
// splice:移除元素,移位后续元素,length 减少
const arr2 = [1, 2, 3, 4, 5];
arr2.splice(1, 1);
console.log(arr2); // [1, 3, 4, 5]
console.log(arr2.length); // 4
// 实际影响:稀疏数组对各方法的影响
const sparse = [1, , 3]; // 稀疏数组(index 1 不存在)
sparse.forEach(x => console.log(x)); // 1, 3(跳过 empty)
sparse.map(x => x * 2); // [2, empty, 6](保留 empty 位置)
sparse.filter(x => x > 0); // [1, 3](跳过 empty)
[...sparse]; // [1, undefined, 3](展开后 empty 变 undefined)
Array.from(sparse); // [1, undefined, 3]
为什么 delete 不改 length?
规范的 [[Delete]] 内部方法:对于数组,删除数字索引属性时只是移除那个属性,不触发 ArraySetLength。这是因为 delete 的语义是"移除属性",而不是"从序列中删除元素"。这两个概念对数组来说是不同的,但日常使用中经常被混淆。
本章小结
-
所有 JS 对象都有同一套内部方法接口:普通对象用默认实现,异质对象(Array、函数、arguments)重写了其中部分方法,这是"魔法行为"的根本来源。
-
Array 的 length 自动更新来自 [[DefineOwnProperty]] 的重写:索引赋值时自动更新 length,减小 length 时自动删除超出的元素。
delete arr[i]不改 length 是因为它绕过了这个逻辑。 -
函数的 [[Construct]] 不是所有函数都有:箭头函数、方法简写(
obj.method(){})、class 没有 [[Construct]],不能 new。函数的length属性只计算到第一个有默认值的参数之前。 -
非严格模式下 arguments 和形参共享绑定:通过 [[ParameterMap]] 实现,
delete arguments[i]会断开这个映射。严格模式下不存在这个同步行为。 -
new Array(n) 创建稀疏数组:空洞(empty)和
undefined是不同的——0 in new Array(3)是false,但0 in [undefined, undefined, undefined]是true。用Array.from、fill或展开运算符创建填充数组。