跳转至

数组 — 一个伪装成数组的对象,和一套函数式工具箱

这一章讲两件事: JavaScript 数组的「对象本性」——它为什么越界读不报错、 越界写会留洞、长度能用手截断;以及数组自带的那套以回调为核心的方法—— map/filter/reduce 们如何把「写循环」升级成「描述想要的结果」。 读完你会明白:delete 数组元素为什么是灾难,以及为什么 sort 不传比较器时 10 排在 2 前面。

1. 先看现象:这不是你以为的那种数组

从 C/Java 过来的人,脑子里的数组是:内存里一排等大的格子,每个格子存同类型的值, 按下标 O(1) 直达。JavaScript 数组不是那个东西,书里的定性一句话:

数组里每项可以是不同类型;长度随时可变;最稀奇的是,每个数组本身也是一个对象。1

「数组是对象」不是修辞,它是本章所有怪脾气的统一解释:

你做的事 数组的反应 如果它是真·数组 作为对象的解释
读越界下标 ninjas[4] 返回 undefined,不报错 抛越界异常 读对象不存在的属性,本来就是 undefined
写下标 4(数组长 3) 数组扩展到 5,中间留个洞 抛越界异常 给对象加属性,length 跟着最高下标走
delete ninjas[1] 长度不变,1 号位变成洞 — delete 只删属性,不挪动别的属性
ninjas.length = 2 数组被截断,后段丢弃 length 是只读概念 length 是可写属性,写小值=砍数组

主线例子:一队忍者的进出

全章用一队忍者走到底。建队、点名、插队、离队、比武排序、统计——每个数组方法都是旅程一站:

  建队:["Kuma","Hattori","Yagyu"]                —— 字面量与 length(§2)
  越界点名与扩编                                    —— 对象本性三连(§2)
  队尾进出:push / pop                             —— 栈(§3)
  队首进出:unshift / shift(以及为什么贵)          —— 队列(§3)
  中途离队:delete 的灾难与 splice 的正确姿势       —— 洞与缝合(§3)
  变换、检验、查找:map / every / find / filter     —— 函数式工具箱(§4)
  按 locale 比武排序:sort + Intl.Collator          —— 排序与字符串陷阱(§4)
  全队列总兵力:reduce                              —— 累加器(§4)

2. 建队与点名:对象本性现场演示

2.1 两种造法

const ninjas = ["Kuma", "Hattori", "Yagyu"];        // 数组字面量:99% 时候用它
const samurai = new Array("Oda", "Tomoe");           // Array 构造器

构造器有个著名的怪癖(书里单独立框):给它一个数字参数,那数字被当成长度—— new Array(5) 是「长度 5 的空数组」,不是「含元素 5 的数组」;而 new Array(5, 6) 才是 [5, 6]。 构造器的一个正当用途:配合 fill 预填大数组——new Array(100).fill("z") 一次造出 100 个 "z", 比循环 push 快,因为引擎不必反复扩容。2

2.2 点名:length、越界读、越界写、截断

书里的 Listing 10.1 是一组断言,逐条走(数字全是原书的):3

const ninjas = ["Kuma", "Hattori", "Yagyu"];
ninjas.length === 3            // 长度
ninjas[0] === "Kuma"           // 下标从 0 起
ninjas.at(-1) === "Yagyu"      // ES2022 的 at:负下标从尾数,-1 是最后一个
ninjas[4] === undefined        // 越界读:不报错,undefined

ninjas[4] = "Ishi";            // 越界写:数组扩展
ninjas.length === 5            // 长度 = 最高下标 + 1
// 但下标 3 上**什么都没有**——不是 undefined,是「洞」(hole)

ninjas.length = 2;             // 手动写小 length:截断!
ninjas[2] === undefined       // "Yagyu" 被砍掉了

三宗脾气的机制注脚:

  • 越界读给 undefined:和 obj.nonexistent 一个道理——属性不存在,读出来就是 undefined;
  • 越界写扩数组并留洞:在下标 4 写值,3 号位自动成洞。洞 ≠ 值为 undefined: 读洞给 undefined,但 forEach 之类的迭代器跳过洞;把 undefined 显式写进 3 号位,迭代器反而不会跳。 这个差别很细,但调试「数组明明 length 5,forEach 只跑 3 次」时全靠它;4
  • length 可写:写大值扩出空位,写小值从尾巴砍——砍掉的元素不可恢复。

3. 进出两端与中途缝合

3.1 四件两端工具

四个方法背成两对(书里给了图 10.2 和完整演示):5

方法 干什么 返回
push(x) 尾部加 加完后的新长度
pop() 尾部删 被删的那个
unshift(x) 头部加 加完后的新长度
shift() 头部删 被删的那个

push/pop 组合 = 栈(后进先出,像一摞盘子);push/shift 组合 = 队列(先进先出, 像排队)。带数走书里的序列:[] → push "Kuma" → push "Hattori" → unshift "Yagyu" (队列成 ["Yagyu","Kuma","Hattori"])→ pop 拿走 "Hattori" → shift 拿走 "Yagyu"。

性能注脚(书里加粗了):头部操作要把后面所有元素挨个挪一格,大数组上 unshift/shift 代价很高;尾部操作只是改改指针,数组多大都一样快。这条在第 1 章「性能提示」的精神之内。6

3.2 delete 的灾难与 splice 的缝合

中途想删一个元素?先看书里的反例:7

const ninjas = ["Yagyu", "Kuma", "Hattori"];
delete ninjas[1];
ninjas.length === 3      // 长度没变!
ninjas[1] === undefined  // 2 号位还在原地——数组里留下一个洞

delete 是对象的删除(第 9 章),它删属性不挪位。要的是「删掉并让后面补位」, 正解是 splice(书中演示,带数走):8

const ninjas = ["Yagyu", "Kuma", "Hattori", "Fuma"];

ninjas.splice(1, 1);          // 从下标 1 起,删 1 个
// 返回 ["Kuma"](被删的项);数组成 ["Yagyu","Hattori","Fuma"]

ninjas.splice(1, 2, "Mochizuki", "Yoshi", "Momochi");
// 从下标 1 起,删 2 个("Hattori","Fuma"),插入 3 个新名字
// 数组成 ["Yagyu","Mochizuki","Yoshi","Momochi"]

splice 的参数表:(起删下标, 删几个, 要插入的项……)——三个实参干三件事, 删插一体,所以叫缝合。参数省略规则:只给一个,从该下标删到尾;给两个,只删不插。

一个字母之差的老冤家:slice——不动原数组,拷贝出一段(起止下标,止不含):

const ninjas = ["Yagyu", "Kuma", "Hattori", "Fuma"];
const middle = ninjas.slice(1, 3);   // ["Kuma","Hattori"] —— ninjas 自己还是 4 个

记法:splice 会perturb(扰动)原数组,slice 只是lift(取出)一份拷贝。 拷贝整个数组的老姿势 array.slice(0) 如今归展开运算符:[...array]。9

4. 函数式工具箱:把循环换成声明

以下每个方法的内核都是同一个模式(第 5 章的 callback 分工):数组提供遍历, 你提供一个回调函数做判断或变换。「循环+判断」的样板消失,留下的是「我要什么」。

4.1 遍历:for、for...of、entries 与 forEach 的局限

三种正经遍历:for(需要下标时)、for...of(只要值时)、for...of + entries() (下标值都要时,每次迭代出一个 [下标, 值] 二元组)。10

forEach 是方法形态的遍历,但有两个要命的局限(书里明说,这决定了它的小众地位):11

  1. 不能中途退出。for/for...of 里 break 一响循环即停;forEach 的回调里 return 只是「结束本次回调」,循环照样跑完——想硬停只能 throw;
  2. 不能异步。在 for/for...of 的循环体里可以 await(第 8 章);forEach 的回调里写 async 函数,各次迭代互不等待——下一圈立刻开始,异步全部并发(多半不是你要的)。 typescript-eslint 的 no-misused-promises 规则会抓这种误用。

顺带一个高频笔误:for...in(枚举键)和 for...of(枚举值)。对数组用 for...in 尤其怪—— 它会枚举出下标字符串和任何挂在数组上的属性。有专门的 lint 规则 no-for-in-array。12

4.2 map:一对一变换出新数组

需求:从忍者档案数组提取兵器清单。for...of 版要写循环、建空数组、逐个 push; map 版一句话(书里的例子):13

const ninjas = [
  { name: "Yagyu", weapon: "shuriken" },
  { name: "Yoshi", weapon: "katana" },
  { name: "Kuma",  weapon: "wakizashi" },
];
const weapons = ninjas.map(({ weapon }) => weapon);
// ["shuriken", "katana", "wakizashi"]

机制:map 把回调应用到每个元素,把每次的返回值收进新数组。 和 forEach 的分界线就在这:map 返回新数组,forEach 什么都不返回。 「新数组」三个字也是关键——map 不改原数组(不可变风格,第 12 章的主旋律)。 回调里 ({ weapon }) => weapon 是解构参数:传进来的是整个忍者对象, 花括号直接拆出 weapon 字段。14

4.3 flatMap:一对多的变换

map 的局限:输出和输入等长(1 对 1)。想跳过某些项、或一项变多项, 老办法是 map 出嵌套数组再 flat 摊平一层——ES2019 把两步合一,叫 flatMap:15

const ninjas = [
  { name: "Yagyu", weapons: ["shuriken", "kunai"] },
  { name: "Yoshi", weapons: [] },                    // 没武器
  { name: "Kuma",  weapons: ["katana", "tanto"] },
];
const weapons = ninjas.flatMap(({ weapons }) => weapons);
// ["shuriken", "kunai", "katana", "tanto"] —— Yoshi 贡献零项,Kuma 贡献两项

带数走:回调对三个忍者分别返回 ["shuriken","kunai"]、[]、["katana","tanto"], flatMap 摊平拼接,4 件兵器,顺序与忍者顺序一致。空数组自然消失——过滤和展开一次完成。 书里的判词:以前这类需求会把人推向 reduce(以及关于可读性的永恒争吵),有了 flatMap, 「一入习惯,你会发现它更优雅(而且可能更快)」。16

4.4 every 与 some:带提前退出的检验

需求变了:不是变换,是判定——「是不是所有忍者都有名字?」「是不是至少有人带武器?」 笨办法是设标志位 + 循环 + break;两个方法把循环内化(书里的例子):17

const allNinjasAreNamed = ninjas.every((n) => "name" in n);   // true:人人有名字
const allNinjasAreArmed = ninjas.every((n) => "weapon" in n); // false:Yoshi 空手
const someNinjasAreArmed  = ninjas.some((n) => "weapon" in n); // true:有人带武器
  • every:回调对每个元素返回真值,才返回 true;任一返回假,立刻短路返回 false;
  • some:回调对任一元素返回真值,立刻短路返回 true;全假才返回 false。

短路是它们相对 forEach 的隐藏优势。回调返回值走隐式布尔转换(第 2 章的 truthy/falsy: null、undefined、0、"" 转 false)——所以回调要返回「真值」,不是「true」本身。18

一个诚实的性能注脚(书里给的):对大数组多次 every 遍历,不如自己写一次循环把几个检查一起做—— 内存翻页的开销是真实的。简洁与性能的老天平。19

4.5 find 与 filter:找一个,或找一批

const ninjaWithWakizashi = ninjas.find((n) => n.weapon === "wakizashi");
// 整个对象 { name: "Kuma", weapon: "wakizashi" } —— 第一个满足者
ninjas.find((n) => n.weapon === "katana");   // undefined —— 找不到就是 undefined

const armedNinjas = ninjas.filter((n) => "weapon" in n);
// 新数组:所有带武器的 —— 一个子集拷贝

区分清楚:find 找第一个满足的、返回元素本身,找不到给 undefined(所以访问结果字段时 TypeScript 会催你用 ?.);filter 找所有满足的、返回新数组,一个都不剩就是 []。20

4.6 sort:原地排序,以及字符串陷阱

排序回调的契约(第 5 章比较器的正式版):两两比较,回调返回

  • 负数 → 第一个排前面;
  • 正数 → 第二个排前面;
  • 0 → 平手,顺序随意。

具体算法归引擎,你只提供裁判。书里的忍者按名字排序:21

const ninjas = [{ name: "Yoshi" }, { name: "Yagyu" }, { name: "Kuma" }];
const collator = new Intl.Collator();        // 按用户 locale 的排序器
ninjas.sort((n1, n2) => collator.compare(n1.name, n2.name));
// 排好:Kuma, Yagyu, Yoshi

为什么要 Intl.Collator,不能直接 a < b? 能,但 < 比的是字符编码(code unit), 纯英文字母凑合,一碰上 Unicode(重音、中文、德文 ß)就崩。 Intl.Collator 按用户所在地区的习惯排序,还能配置大小写、数字、标点规则。 字符串自带的 localeCompare 功能相同但每次调用都重复初始化,建一个 collator 复用快得多。22

sort 的两个坑(原书专门点出):23

  1. 它是原地的——直接改被调用的数组,同时返回同一个数组的引用。 const sorted = arr.sort() 里 sorted === arr!要保原数组,先 [...arr].sort(...); (ES2023 加了 toSorted:不碰原数组,返回排好的新数组——原书提及了这个更优雅的新方案。)
  2. 不传回调 = 字典序——元素转字符串再比。数字数组 [10, 2, 1].sort() 得 [1, 10, 2]!

4.7 reduce:一个累加器吃掉全数组

求和的最笨办法是循环;reduce 把「循环+累计变量」收成一个表达式(书里的例子,带数走):24

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, number) => acc + number, 0);
// 初始 acc = 0
// 第 1 次:acc = 0 + 1 = 1
// 第 2 次:acc = 1 + 2 = 3
// 第 3 次:acc = 3 + 3 = 6
// 第 4 次:acc = 6 + 4 = 10  → 返回 10

机制:reduce 收一个回调和一个初始值;回调带两个参数——累加器(accumulator,常写 acc) 和当前元素;回调的返回值成为下一圈的累加器;最后一圈的返回值就是整个 reduce 的结果。25

reduce 的可怕之处(和它的口碑来源)在于:累加器可以是任意东西——对象、Map、数组。 「把数组折叠成一个复杂结构」一表达式完成,是它的粉丝爱它的原因,也是它的黑粉 (Sindre Sorhus 专门做了 no-array-reduce 的 lint 规则)烦它的原因:嵌套的 reduce 可读性常常不如一个朴素的循环。书里的态度折中:「值得花时间理解它——野外代码里大量存在」。26

5. 作者的判断与证据

书内可验的机制(全部带 assert):

  • 越界读 undefined、越界写扩长留洞、length 截断——Listing 10.1 十个断言逐一可跑;
  • splice 的删插一体、slice 不碰原数组,均有前后数组状态对照;
  • sort 的原地性与 sortedArray === array 的引用事实。27

作者立场(当立场读):

  • 「forEach 已沦为小众」是作者观察到的行业现状,不是规则;
  • 「flatMap 比 reduce 更优雅」是风格立场,性能主张(「可能更快」)未经书中测量;
  • 「大数组上自己写循环胜过多次 every」是性能直觉,无量化依据。

6. 边界与局限

  • TypedArray(Uint8Array 等) 只在边栏出现——那是另一族为原始二进制数据优化的数组, 与本章的普通数组接口完全不同;书把它留给了后续章(workers 场景)。
  • toSorted/toReversed 等 ES2023 不可变方法只提了 toSorted 一句,没有系统介绍。
  • sort 的稳定性(现代引擎已保证,旧引擎不保证)书中未提。
  • forEach 的洞跳过行为书中一句带过,但它是「数组是对象」最隐蔽的推论,值得亲手实验。

7. 可带走的(合上这一章,你应该有这十条)

  1. 数组是对象:越界读 undefined、越界写扩长、length 可写可截断——三宗脾气一个根源。
  2. 洞 ≠ undefined:读都给 undefined,但迭代器跳过洞、不跳过显式的 undefined。
  3. 字面量造数组;new Array(一个数字) 得到的是「定长的空」,不是含该元素。
  4. push/pop 栈、push/shift 队列;头部操作在大数组上昂贵,尾部操作恒定便宜。
  5. delete 只留洞不补位;中途增删用 splice(起删下标, 删几个, 插入项…);slice 是拷贝不碰原数组。
  6. map 一对一出新数组、flatMap 一对多(跳过/展开一箭双雕)、forEach 无返回且不能 break 不能 await。
  7. every/some 带短路;find 给第一个满足的(或 undefined),filter 给新数组(或空数组)。
  8. sort 原地改、返回同一引用;不传回调按字符串排(10 在 2 前);要 locale 正确用 Intl.Collator 复用实例。
  9. reduce = 回调(累加器, 当前项) + 初始值,把数组折叠成任意类型;可读性输循环时,别硬用。
  10. 想要下标+值一起遍历:for...of + entries();别拿 for...in 遍历数组(它枚举键字符串)。

9. 深读一:数组行为的对象学解释,逐个现象对号

第 1 节的行为表值得再压一层:每一个「怪脾气」都能从「数组是对象」一条推出, 这个推导过程本身就是对这门语言最好的思维训练。

现象:越界读给 undefined。 对象行为的直接后果:ninjas[4] 本质是属性访问 ninjas["4"],属性不存在 → undefined。别的语言抛「越界异常」, 本质上是数组类型自己装了检查;JavaScript 的数组类型就是 Object, 没有这道检查。1

现象:越界写扩长并留洞。 给对象加属性从不报错;数组只是额外维护一个 length 属性,规则是「最高数字下标 + 1」。在下标 4 写值,length 变 5, 下标 3 上「什么都没有」——不是 undefined,是洞:读它给 undefined(属性不存在), 但迭代器跳过它(洞里没有可迭代的值)。把 undefined 显式写进 3 号位,迭代器又照跑—— 「读出来一样、迭代时两样」是数组最隐蔽的坑。2

现象:length 可写、可截断。 length 是普通属性,写大扩空位、写小砍尾巴, 砍掉的不可恢复。「length 是数组的长度」这句话要修正为: length 是数组自己上报的长度,它说了算,你不一定能信。

9.1 构造器的唯一正当用途

new Array(100).fill("z")——一次造出 100 个 "z" 的数组。 为什么这比 [] 加 100 次 push 高效?书里给了机制:push 过程中引擎要反复给数组 扩容(分配更大的内存块、搬移元素),而 new Array(100) 一次把容量定死, fill 原地填值,一次搬迁都没有。大数据量预分配时,这个差别可测。3

10. 深读二:四件两端工具的成本模型

push/pop/unshift/shift 的行为之外,把成本模型记牢(书里 NOTE 照录): 在数组中间或头部增删,后面的每个元素都要在内存里挪一格;push/pop 动的是尾部, 没有「后面」,所以数组多大都一样快。4

四个方法的完整剧本(书里的 Listing 10.2 带数走):

  起点:[]
  push "Kuma"      → ["Kuma"]                    返回 1
  push "Hattori"   → ["Kuma","Hattori"]          返回 2
  unshift "Yagyu"  → ["Yagyu","Kuma","Hattori"]  返回 3(后两个各挪一格)
  pop()            → 返回 "Hattori",数组剩两个
  shift()          → 返回 "Yagyu",数组剩 ["Kuma"](Kuma 挪回首位)

两类数据结构因此零成本可得:push+pop = 栈(后进先出),push+shift = 队列 (先进先出)。算法面试里的栈和队列,在 JavaScript 里就是这两个方法对。5

11. 深读三:splice 的三参数语义与全部省略形态

splice 的三参数表 (起删下标, 删几个, 插入项…) 有三个省略形态,书里的说明合起来:

调用 行为
splice(i) 从下标 i 删到末尾
splice(i, n) 从下标 i 删 n 个
splice(i, n, x, y, z) 先删 n 个,再在 i 处插入 x、y、z
splice(i, 0, x) 不删,纯插入

返回值永远是被删项组成的数组——哪怕你只删一个,拿到的也是 ["Kuma"] 而不是 "Kuma",这个包装常被忘记。6

splice 与 slice 的一对一区分(书里怕混,我们也怕):

  • splice 会改原数组,删插一体,返回被删项;
  • slice 不改原数组,拷贝一段,返回拷贝——参数是 (起, 止),止不含。 slice(1, 3) 拿的是下标 1、2 两项。「拷整个数组」的老姿势 slice() 或 slice(0),新姿势是 [...array]。

一句口诀(自创但合书意):splice 是手术刀,slice 是复印机。

12. 深读四:遍历的五种姿势与选型

  for (let i = 0; i < a.length; i++)   需要下标/要跳着走时
  for (const x of a)                    只要值,最常用
  for (const [i, x] of a.entries())     下标值都要
  a.forEach(cb)                         回调要复用给多个数组时
  for (const k in a)                    【别】对数组用!枚举键字符串+挂着的属性

for...in 的怪(书里的 NOTE 扩展):它枚举的是键,对数组就是下标字符串 "0","1","2",还会带上任何挂在数组对象上的自定义属性——这仍是「数组是对象」的 余毒。typescript-eslint 的 no-for-in-array 规则专治此误。7

forEach 的两个局限(书里明说,决定了它的小众地位): 不能中途退出(回调里 return 只是结束本次;想硬停只能 throw), 不能异步(回调若是 async 函数,各次迭代互不等待——并发地发起,而不是顺序地等待)。 for/for...of 里可以 await,forEach 里不行——第 8 章的 for await...of 是异步遍历的正解。8

13. 深读五:函数式方法的逐个账本

每个方法记四件事:输入回调的签名、返回什么、改不改原数组、提前退出吗。

13.1 map 与 forEach 的分界线

map 与 forEach 传回调的方式完全相同,差别只在返回值: map 把每次回调的返回值收进新数组;forEach 什么都不返回(undefined)。 「要不要新数组」就是选谁的标准,不要新数组还用 map,lint 会嫌你浪费。9

13.2 flatMap 的「一退一进」

map 1 对 1,flatMap 1 对任意:回调返回数组,结果被摊平一级拼进输出。 书里的用法要点:回调返回 [] 等于跳过该项;返回多元素数组等于一项变多项—— 过滤与展开一次完成。它等价于 map(...).flat(),但单次遍历、少建中间数组 (书里注了「potentially more performant」,未给测量)。10

13.3 every/some 的短路,与「空数组」的边角

两个方法都提前退出:every 遇第一个假值就停,some 遇第一个真值就停。 一个容易忘的边角(书里在 every 的说明里):空数组的 every 返回 true (「所有元素都满足」对空集平凡成立——数学里叫 vacuous truth)。 回调返回值走隐式布尔转换,null/undefined/0/"" 都是 falsy,写条件时要 conscious。11

13.4 find 与 filter 的三点区分

find 返回第一个匹配的元素本身,找不到给 undefined(所以访问结果字段前, TypeScript 催你用 ?.——书里的例子正是 ninjaWithWakizashi?.name); filter 返回所有匹配组成的新数组,一个都没有是 [](不是 undefined)。 find 找到即停,filter 必须扫完。12

13.5 sort 的全部注意事项,合成一页

array.sort((a, b) => a - b);    // 数字升序
array.sort((a, b) => b - a);    // 数字降序
  • 原地改:sort 改被调用的数组,同时返回同一个数组的引用—— const sorted = arr.sort() 里 sorted 和 arr 是同一个东西; 想保原数组:[...arr].sort(...),或 ES2023 的 toSorted(不碰原数组);
  • 默认比较器是字典序:元素转字符串再比——[10, 2, 1].sort() 得 [1, 10, 2];
  • 字符串排序用 Intl.Collator:按用户 locale 的正确顺序,实例建一次反复复用 (每次 localeCompare 都重复初始化,大数组排序时可见地慢);
  • 具体算法归引擎:你只提供裁判(比较器),引擎爱用归并还是快排是它的自由—— 这也意味着比较器必须稳定可靠,不要在比较器里搞副作用。13

13.6 reduce 的完整教学与「两极口碑」

reduce 的机制一句话:把数组折叠成一个值,折叠的规则由回调给出。 回调 (acc, item) => nextAcc 配上初始值,逐元素滚一遍。书里的求和例子 (带数:0→1→3→6→10)是最小演示;真正的威力在累加器可以是任意结构—— 对象、Map、另一个数组,「分组」「建索引」「流水线合并」都是一句话。14

它的两极口碑也照实说(书里两边都引了):爱的人爱它「一切折叠一表达式」; 恨的人恨它「嵌套起来比循环难读」——Sindre Sorhus 干脆给 eslint-plugin-unicorn 写了 no-array-reduce 规则。我们的立场沿用书里的折中:读得懂别人的 reduce 是必修课(野外代码大量存在),写的时候量力而行,可读性输循环就别硬凹。15

14. 深读六:「数组是对象」的遗产清单

把全章的对象学推论收拢成一张遗产清单,每一条都值得在 code review 时想起来:

现象 根源 应对
越界读 undefined 属性访问无检查 边界自觉;必要时 at()/length 判断
越界写留洞 length=最高下标+1 别跳着赋值;用 push
delete 留洞 delete 是对象操作 中途删除用 splice
for...in 怪行为 枚举键+自定义属性 数组用 for...of
sort 默认字典序 比较器默认转字符串 永远传比较器

这张表的本质:JavaScript 数组是「披着数组外衣的对象」—— 享受对象的灵活(随便扩、随便挂属性),就要承担对象的天真(没有类型与边界的守卫)。 这门语言的成年礼,就是认清这层关系。16

15. 原文地图

主题 原书章 原文位置
数组的对象本性 10.1 Arrays text/13-ch10-01-10-1-arrays.txt:27(搜「each array is also an object」)
typed arrays 边栏 同上 :34(搜「GETTING LOW-LEVEL WITH TYPED ARRAYS」)
两种造法与 new Array 怪癖 同上 :48(搜「Creating arrays」) · :123(搜「a single numeric argument」)
Listing 10.1 十断言 同上 :57(搜「Listing 10.1」)
越界写扩长与洞 同上 :149(搜「expands the array」) · :160(搜「no item at index 3」)
length 截断 同上 :163(搜「trim the array from the end」)
push/pop/shift/unshift 同上 :170(搜「Adding and removing items」) · :185(搜「Listing 10.2」)
头部操作性能 同上 :252(搜「very expensive operations」)
delete 灾难 同上 :256(搜「Listing 10.3」)
splice 删插与 slice 对照 同上 :293(搜「Listing 10.4」) · :350(搜「cousin slice」)
遍历三式与 for...in 坑 同上 :389(搜「ITERATING OVER ARRAYS」) · :417(搜「easy mistake」)
forEach 两局限 同上 :422(搜「noteworthy limitations」)
map 与解构 同上 :451(搜「MAPPING ARRAYS」) · :472(搜「Listing 10.5」)
flat 与 flatMap 同上 :505(搜「ONE-TO-MANY」) · :523(搜「Listing 10.6」)
every/some 与短路 同上 :550(搜「TESTING ARRAY ITEM CONDITIONS」) · :567(搜「Listing 10.7」)
truthy/falsy 边栏 同上 :607(搜「TRUTHY AND FALSY VALUES」)
多次迭代性能注脚 同上 :621(搜「significant overhead」)
find/filter 同上 :629(搜「SEARCHING AND FILTERING ARRAYS」) · :633(搜「Listing 10.8」)
sort 契约与 collator 同上 :685(搜「SORTING ARRAYS」) · :703(搜「Listing 10.9」)
< 的 Unicode 陷阱与 localeCompare 同上 :727(搜「goes off the rails」) · :736(搜「every call」)
sort 原地性与 toSorted 同上 :741(搜「modifies the array」) · :744(搜「toSorted」)
reduce 与带数图 同上 :749(搜「ACCUMULATING A VALUE WITH REDUCE」) · :764(搜「Listing 10.10」) · :786(搜「Figure 10.7」)
no-array-reduce 规则 同上 text/14-ch10-02-10-2-maps.txt:3(搜「no-array-reduce」)
数组章收尾 同上 text/14-ch10-02-10-2-maps.txt:5(搜「That wraps up our coverage of arrays」)

  1. 出处:「10.1 Arrays」第 136–148 段(text/13-ch10-01-10-1-arrays.txt:147,搜「a nonexistent object property」)。越界读 undefined 的对象学解释原文。 ↩↩

  2. 出处:「10.1 Arrays」第 149–162 段(text/13-ch10-01-10-1-arrays.txt:154,搜「no item at index 3」)。越界写扩长与洞的定义;迭代器跳过见同段。 ↩↩

  3. 出处:「10.1 Arrays」第 123–134 段(text/13-ch10-01-10-1-arrays.txt:134,搜「never has to resize the array」)。new Array + fill 的扩容机制解释。 ↩↩

  4. 出处:「10.1 Arrays」第 252–254 段(text/13-ch10-01-10-1-arrays.txt:253,搜「moved in memory」)。头部操作昂贵的成本模型原文。 ↩↩

  5. 出处:「10.1 Arrays」第 176–183 段(text/13-ch10-01-10-1-arrays.txt:181,搜「function as a stack」)。栈与队列的组合定义;完整剧本见 Listing 10.2(:185)。 ↩↩

  6. 出处:「10.1 Arrays」第 293–353 段(text/13-ch10-01-10-1-arrays.txt:350,搜「three arguments」)。splice 参数语义;返回被删项见 Listing 10.4 的断言(:303)。 ↩↩

  7. 出处:「10.1 Arrays」第 389–421 段(text/13-ch10-01-10-1-arrays.txt:417,搜「easy mistake」)。for...in 对数组的怪行为与 no-for-in-array 规则。 ↩↩

  8. 出处:「10.1 Arrays」第 422–449 段(text/13-ch10-01-10-1-arrays.txt:431,搜「noteworthy limitations」)。forEach 两局限原文。 ↩↩

  9. 出处:「10.1 Arrays」第 451–503 段(text/13-ch10-01-10-1-arrays.txt:494,搜「The big difference between forEach and map」)。map/forEach 的返回值分界线。 ↩↩

  10. 出处:「10.1 Arrays」第 505–548 段(text/13-ch10-01-10-1-arrays.txt:518,搜「single optimized operation」)。flatMap 的来历与「可能更快」的注。 ↩↩

  11. 出处:「10.1 Arrays」第 550–625 段(text/13-ch10-01-10-1-arrays.txt:602,搜「or if the array is empty」)。every 空数组返回 true 的细则;truthy/falsy 边栏见第 607–615 段。 ↩↩

  12. 出处:「10.1 Arrays」第 629–683 段(text/13-ch10-01-10-1-arrays.txt:666,搜「The find method returns」)。find/filter 的三点区分与可选链式访问的示例。 ↩↩

  13. 出处:「10.1 Arrays」第 685–747 段(text/13-ch10-01-10-1-arrays.txt:741,搜「modifies the array that it's called on」)。sort 原地性与引用事实;toSorted 见第 744–746 段;Intl.Collator 与 localeCompare 的效率对比见第 736–739 段。 ↩↩

  14. 出处:「10.1 Arrays」第 749–792 段(text/13-ch10-01-10-1-arrays.txt:764,搜「Listing 10.10」)。reduce 求和示例与「any kind of value as the accumulator」的灵活性说明(第 789–792 段)。 ↩↩

  15. 出处:「10.1 Arrays」第 781–783 段(text/13-ch10-01-10-1-arrays.txt:782,搜「overly "fancy"」);与 text/14-ch10-02-10-2-maps.txt:3(搜「no-array-reduce」)。reduce 的两极口碑与 unicorn 规则。 ↩↩

  16. 出处:「10.1 Arrays」第 27–33 段(text/13-ch10-01-10-1-arrays.txt:27,搜「each array is also an object」)。「数组是对象」的定性——遗产清单的总根源。 ↩↩

  17. 出处:「10.1 Arrays」第 550–620 段(text/13-ch10-01-10-1-arrays.txt:567,搜「Listing 10.7」)。every/some 完整示例与「all of them are named / not all of them are armed」的断言组。 ↩

  18. 出处:「10.1 Arrays」第 600–606 段(text/13-ch10-01-10-1-arrays.txt:600,搜「The every method takes a callback that, for」);与第 607–615 段边栏(搜「TRUTHY AND FALSY」)。every 的短路与空数组返回 true 的细则;隐式布尔转换的提醒。 ↩

  19. 出处:「10.1 Arrays」第 621–625 段(text/13-ch10-01-10-1-arrays.txt:621,搜「significant overhead」)。多次迭代不如一次循环的性能注脚。 ↩

  20. 出处:「10.1 Arrays」第 629–683 段(text/13-ch10-01-10-1-arrays.txt:633,搜「Listing 10.8」)。find(第一个/元素/undefined)与 filter(全部/新数组)的对照示例。 ↩

  21. 出处:「10.1 Arrays」第 685–726 段(text/13-ch10-01-10-1-arrays.txt:703,搜「Listing 10.9」)。比较器契约与 Intl.Collator 排序演示。 ↩

  22. 出处:「10.1 Arrays」第 727–740 段(text/13-ch10-01-10-1-arrays.txt:729,搜「goes off the rails」)。字符编码比较在 Unicode 下的崩溃;localeCompare 每次调用重复初始化的代价。 ↩

  23. 出处:「10.1 Arrays」第 741–747 段(text/13-ch10-01-10-1-arrays.txt:741,搜「modifies the array」)。sort 原地性与「sortedArray is just a reference to array」;toSorted 见第 744–746 段。 ↩

  24. 出处:「10.1 Arrays」第 749–791 段(text/13-ch10-01-10-1-arrays.txt:764,搜「Listing 10.10」)。reduce 求和示例与图 10.7 的机制描述。 ↩

  25. 出处:「10.1 Arrays」第 776–783 段(text/13-ch10-01-10-1-arrays.txt:776,搜「The reduce method takes a callback and a」)。reduce 的机制逐步说明。 ↩

  26. 出处:「10.1 Arrays」第 789–792 段(text/13-ch10-01-10-1-arrays.txt:782,搜「overly "fancy"」);与 text/14-ch10-02-10-2-maps.txt:3(搜「no-array-reduce」)。reduce 的两极口碑与 unicorn 规则的彩蛋。 ↩

  27. 出处:「10.1 Arrays」第 741–743 段(text/13-ch10-01-10-1-arrays.txt:741,搜「modifies the array」);与第 57–115 段(搜「Listing 10.1」)。可复现断言的代表。 ↩