数组 — 一个伪装成数组的对象,和一套函数式工具箱
数组 — 一个伪装成数组的对象,和一套函数式工具箱
这一章讲两件事: 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
- 不能中途退出。for/for...of 里
break一响循环即停;forEach 的回调里 return 只是「结束本次回调」,循环照样跑完——想硬停只能 throw; - 不能异步。在 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
- 它是原地的——直接改被调用的数组,同时返回同一个数组的引用。
const sorted = arr.sort()里sorted === arr!要保原数组,先[...arr].sort(...); (ES2023 加了toSorted:不碰原数组,返回排好的新数组——原书提及了这个更优雅的新方案。) - 不传回调 = 字典序——元素转字符串再比。数字数组
[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. 可带走的(合上这一章,你应该有这十条)
- 数组是对象:越界读 undefined、越界写扩长、length 可写可截断——三宗脾气一个根源。
- 洞 ≠ undefined:读都给 undefined,但迭代器跳过洞、不跳过显式的 undefined。
- 字面量造数组;
new Array(一个数字)得到的是「定长的空」,不是含该元素。 - push/pop 栈、push/shift 队列;头部操作在大数组上昂贵,尾部操作恒定便宜。
- delete 只留洞不补位;中途增删用 splice(起删下标, 删几个, 插入项…);slice 是拷贝不碰原数组。
- map 一对一出新数组、flatMap 一对多(跳过/展开一箭双雕)、forEach 无返回且不能 break 不能 await。
- every/some 带短路;find 给第一个满足的(或 undefined),filter 给新数组(或空数组)。
- sort 原地改、返回同一引用;不传回调按字符串排(10 在 2 前);要 locale 正确用
Intl.Collator复用实例。 - reduce = 回调(累加器, 当前项) + 初始值,把数组折叠成任意类型;可读性输循环时,别硬用。
- 想要下标+值一起遍历:
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」) |
Footnotes
-
出处:「10.1 Arrays」第 22–33 段(
text/13-ch10-01-10-1-arrays.txt:27,搜「each array is also an object」)。JavaScript 数组与 C 式数组的对照及三重动态性。 ↩ ↩2 -
出处:「10.1 Arrays」第 48–56 段(
text/13-ch10-01-10-1-arrays.txt:49,搜「There are two basic ways to create a」);与第 123–134 段边栏(搜「a single numeric argument」)。两种造法、单数字参数怪癖与 new Array(100).fill("z") 的正当用途。 ↩ ↩2 -
出处:「10.1 Arrays」第 57–167 段(
text/13-ch10-01-10-1-arrays.txt:57,搜「Listing 10.1」)。建队与点名的完整断言序列。 ↩ ↩2 -
出处:「10.1 Arrays」第 153–162 段(
text/13-ch10-01-10-1-arrays.txt:154,搜「no item at index 3」)。「technically speaking, there's no value at all at that point」;forEach 跳过见同段。 ↩ ↩2 -
出处:「10.1 Arrays」第 170–254 段(
text/13-ch10-01-10-1-arrays.txt:175,搜「push adds an item」)。四件工具的定义;栈与队列的组合;Listing 10.2 的完整带数序列。 ↩ ↩2 -
出处:「10.1 Arrays」第 252–254 段(
text/13-ch10-01-10-1-arrays.txt:253,搜「very expensive operations」)。unshift/shift 在大数组上的代价警告。 ↩ ↩2 -
出处:「10.1 Arrays」第 256–290 段(
text/13-ch10-01-10-1-arrays.txt:261,搜「Listing 10.3」)。delete 反例:length 不变、2 号位原地、洞留在那里。 ↩ ↩2 -
出处:「10.1 Arrays」第 293–349 段(
text/13-ch10-01-10-1-arrays.txt:296,搜「Listing 10.4」)。splice 两步演示与三参数语义说明(第 350–353 段)。 ↩ ↩2 -
出处:「10.1 Arrays」第 354–371 段(
text/13-ch10-01-10-1-arrays.txt:358,搜「slice」)。slice 拷贝语义与 [...array] 的新惯例。 ↩ ↩2 -
出处:「10.1 Arrays」第 389–420 段(
text/13-ch10-01-10-1-arrays.txt:391,搜「classic for loop」);与第 410–415 段(搜「entries iterator」)。三种遍历与 entries 二元组。 ↩ ↩2 -
出处:「10.1 Arrays」第 422–449 段(
text/13-ch10-01-10-1-arrays.txt:431,搜「noteworthy limitations」)。forEach 的两条局限(不退出、不等待)原文。 ↩ ↩2 -
出处:「10.1 Arrays」第 417–420 段(
text/13-ch10-01-10-1-arrays.txt:417,搜「easy mistake」)。for...in vs for...of 与 no-for-in-array 规则。 ↩ ↩2 -
出处:「10.1 Arrays」第 451–503 段(
text/13-ch10-01-10-1-arrays.txt:472,搜「Listing 10.5」)。map 提取兵器清单的完整例子。 ↩ ↩2 -
出处:「10.1 Arrays」第 489–496 段(
text/13-ch10-01-10-1-arrays.txt:489,搜「the map method passes each item」)。map 与 forEach 的返回值分界线;解构参数说明见第 480 段附近。 ↩ ↩2 -
出处:「10.1 Arrays」第 505–546 段(
text/13-ch10-01-10-1-arrays.txt:511,搜「flat method flattens」)。flat 的摊平语义与 map+flat 到 flatMap 的演化。 ↩ ↩2 -
出处:「10.1 Arrays」第 543–547 段(
text/13-ch10-01-10-1-arrays.txt:543,搜「As a JavaScript developer, there will be many」)。flatMap 对 reduce 的替代主张。 ↩ ↩2 -
出处:「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」的断言组。 ↩ -
出处:「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 的细则;隐式布尔转换的提醒。 ↩ -
出处:「10.1 Arrays」第 621–625 段(
text/13-ch10-01-10-1-arrays.txt:621,搜「significant overhead」)。多次迭代不如一次循环的性能注脚。 ↩ -
出处:「10.1 Arrays」第 629–683 段(
text/13-ch10-01-10-1-arrays.txt:633,搜「Listing 10.8」)。find(第一个/元素/undefined)与 filter(全部/新数组)的对照示例。 ↩ -
出处:「10.1 Arrays」第 685–726 段(
text/13-ch10-01-10-1-arrays.txt:703,搜「Listing 10.9」)。比较器契约与 Intl.Collator 排序演示。 ↩ -
出处:「10.1 Arrays」第 727–740 段(
text/13-ch10-01-10-1-arrays.txt:729,搜「goes off the rails」)。字符编码比较在 Unicode 下的崩溃;localeCompare 每次调用重复初始化的代价。 ↩ -
出处:「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 段。 ↩ -
出处:「10.1 Arrays」第 749–791 段(
text/13-ch10-01-10-1-arrays.txt:764,搜「Listing 10.10」)。reduce 求和示例与图 10.7 的机制描述。 ↩ -
出处:「10.1 Arrays」第 776–783 段(
text/13-ch10-01-10-1-arrays.txt:776,搜「The reduce method takes a callback and a」)。reduce 的机制逐步说明。 ↩ -
出处:「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 规则的彩蛋。 ↩ -
出处:「10.1 Arrays」第 741–743 段(
text/13-ch10-01-10-1-arrays.txt:741,搜「modifies the array」);与第 57–115 段(搜「Listing 10.1」)。可复现断言的代表。 ↩