第 14 章

普通对象与异质对象:[[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.keysfor...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)) === PToUint32(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,它从外层词法作用域捕获 thisnew 操作需要创建一个新的 this 对象,然后绑定到函数。由于箭头函数的 this 固定,这在概念上是矛盾的——如果允许 newthis 会指向外层的对象,而不是新创建的实例,这会造成无法预测的行为。

陷阱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 的语义是"移除属性",而不是"从序列中删除元素"。这两个概念对数组来说是不同的,但日常使用中经常被混淆。


本章小结

  1. 所有 JS 对象都有同一套内部方法接口:普通对象用默认实现,异质对象(Array、函数、arguments)重写了其中部分方法,这是"魔法行为"的根本来源。

  2. Array 的 length 自动更新来自 [[DefineOwnProperty]] 的重写:索引赋值时自动更新 length,减小 length 时自动删除超出的元素。delete arr[i] 不改 length 是因为它绕过了这个逻辑。

  3. 函数的 [[Construct]] 不是所有函数都有:箭头函数、方法简写(obj.method(){})、class 没有 [[Construct]],不能 new。函数的 length 属性只计算到第一个有默认值的参数之前。

  4. 非严格模式下 arguments 和形参共享绑定:通过 [[ParameterMap]] 实现,delete arguments[i] 会断开这个映射。严格模式下不存在这个同步行为。

  5. new Array(n) 创建稀疏数组:空洞(empty)和 undefined 是不同的——0 in new Array(3)false,但 0 in [undefined, undefined, undefined]true。用 Array.fromfill 或展开运算符创建填充数组。

本章评分
4.6  / 5  (21 评分)

💬 留言讨论