第三章练习答案:核对数据与边界
对象属性
第一题 Object.hasOwn(obj, 'count') 返回 true,而 obj.count !== undefined 返回 false。前者判断属性存在,后者判断读到的值,不是同一问题。
第二题:
const { title: name, minutes = 10 } = { title: 'JS', minutes: 0 };
console.log(name, minutes); // JS 0重命名只改变局部变量名称,缺省值只处理 undefined。
第三题:点号使用固定名称,方括号先计算表达式得到键。缺属性和undefined都会启用默认表达式,null与0保留,因此正文四次解构的默认函数只调用两次。默认值是在读取之后判断,不能用来区分属性不存在与存在但为undefined。
第四题:
function makeRecord(row) {
const [title, , minutes = 10, ...topics] = row;
return { title, minutes, topics };
}
console.log(makeRecord(['JS', 'internal', 0, '函数', '对象']));
// { title: 'JS', minutes: 0, topics: ['函数', '对象'] }
console.log(makeRecord(['JS']));
// { title: 'JS', minutes: 10, topics: [] }
console.log(makeRecord(['JS', 'internal', null]));
// { title: 'JS', minutes: null, topics: [] }第二位置被消费但不绑定。第三位置缺失时读到undefined,默认值才执行;0和null按输入保留。rest始终得到一个数组,即使没有余项。本题只组织已约定的数组形状,并没有校验标题类型或分钟是否有效;默认值不能替代这些规则。
数组转换
第一题:
const source = [5, 12, 7];
const result = source.filter(n => n >= 7).map(n => n * 2);
console.log(result); // [24, 14]
console.log(source); // [5, 12, 7]第二题 map(n => n * 2) 得到 [2, <空槽>, 6];filter(() => true) 得到 [1, 3];展开得到 [1, undefined, 3]。方法是否检查属性存在、是否读取缺失项决定其差异,不能仅凭“创建新数组”推导元素行为。
第三题未提供初始值且没有任何可用元素,reduce 无法建立累计起点,抛 TypeError。定义空和为 0:
const total = [].reduce((sum, n) => sum + n, 0);
console.log(total); // 0第四题:for...of会读取空槽为undefined,filter通常先检查索引存在性并跳过缺失位置。reduce把回调返回值交给下一轮,缺少return会传入undefined。多数数组回调方法保存起始length,但到访问时才读取具体元素,因此新尾项可能不访问,而范围内尚未访问的修改会被看到。
Map 与 Set
第一题:
const counts = new Map();
for (const name of ['a', 'b', 'a']) {
counts.set(name, (counts.get(name) ?? 0) + 1);
}
console.log([...counts]); // [['a', 2], ['b', 1]]这里计数值只会是非负整数,所以用 ?? 0 表示缺失是明确的。更新已有键保留首次插入位置。
第二题 Set 按对象身份判相等,不读取 id;先把 id 提取为集合键,或用 id 到对象的 Map 明确选择首次或末次数据。WeakMap 不提供 size,因为条目是否回收与时机有关;暴露数量会让程序观察垃圾回收的不确定状态。
第三题:覆盖保留最后写入的值,但键仍按首次插入位置排列;最后出现顺序需要另行定义。WeakMap不阻止键回收,却不会削弱外部数组的强引用;数组仍可达且保存该对象时,不能因为使用了WeakMap就认为对象可回收。
字符串单位
第一题表情占两个 UTF-16 码元,slice 截一位会拆开代理对。按码点可以 [...text].slice(0, 1).join('');按字素需要 Intl.Segmenter:
const text = 'e\u0301😀';
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' });
console.log([...segmenter.segment(text)][0].segment); // é第二题结果为 ['a', 'b']。limit 限制输出项数,后面的片段被丢弃,不会自动拼到最后一项里。
第三题:码点截一个只得到e,字素截一个保留e和尖音符组成的整体。规范化不能普遍把字素簇变成单一码点。过滤空字符串会把原本代表空字段的占位移除,使后续字段左移,因此应先确定空片段语义。
第四题先按所需单位分段,再决定是否真的截断:
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' });
function shorten(text, limit) {
const parts = [...segmenter.segment(text)].map(part => part.segment);
const kept = parts.slice(0, limit).join('');
return kept + (parts.length > limit ? '…' : '');
}
const text = 'e\u0301😀!';
console.log(shorten(text, 0)); // …
console.log(shorten(text, 1)); // é…
console.log(shorten(text, 2)); // é😀…
console.log(shorten(text, 3)); // é😀!
console.log(shorten('', 0)); // 空字符串判定依据是分段后的数量,不能用原文本length与limit比较,否则e加组合音符会被误算为两份。limit为0只表示不保留原内容,非空原稿仍按题目约定显示省略号;空字符串没有内容被删除,所以不添加。这个实现会先收集全文,适合普通昵称或摘要;处理巨大文本时应另行考虑逐段消费,不能从短文本例子推断资源成本恒定。
正则语法与状态
第一题:
const pattern = /^NOTE-\d{2,4}$/;
console.log(pattern.test('NOTE-12'), pattern.test('NOTE-1234')); // true true
console.log(pattern.test('NOTE-1'), pattern.test('xNOTE-12')); // false false第二题对同一个正则实例,g 使 test 使用并更新 lastIndex,成功后位置在末尾,下次找不到便失败并重置。若每次重新写正则字面量,就会创建新的实例,不能用它观察同一游标的变化。仅校验写 /a/;若业务确实需要逐次扫描,应明确维护游标,而不是每次随意重置。
第三题:
function noteNumbers(text) {
return [...text.matchAll(/NOTE-(\d+)/g)].map(match => match[1]);
}
console.log(noteNumbers('NOTE-12; NOTE-345; bad')); // ['12', '345']
console.log(noteNumbers('bad')); // []matchAll提供每次匹配及捕获组,展开先收集成数组,再用已学map读取索引1的数字文本。无匹配时展开为空数组,map仍返回空数组。这里允许其他文本,也会在更长文本里提取片段;若需求改为严格检查整条记录,应另写完整格式验证,不能以“找到一个NOTE”为通过依据。
第四题:先试ab分支后剩余b无法匹配,回溯改试a分支,最后b消费成功;/a|ab/的第一分支a已经让整个模式成功,便停止,不追求最长结果。matchAll返回一次性迭代器,第一次消费已到末尾;重新扫描应新建迭代器,复用结果则保存数组。
第五题:
const code = /^([A-Z]+):\1$/;
console.log(['JS:JS', 'JS:TS', ':', 'JS:js'].map(text => code.test(text)));
// [true, false, false, false]
const insensitive = /^([A-Z]+):\1$/i;
console.log(insensitive.test('JS:js')); // true
const nonzero = new RegExp('^[[0-9]--0]+$', 'v');
console.log(['123', '10', '12', ''].map(text => nonzero.test(text)));
// [true, false, false, false]第一组用加号要求至少一个字母,反向引用比较此前捕获的内容;i让两边都采用忽略大小写的规则,不能只给第二个字符类另加小写范围来表示“相同”。集合先取ASCII数字再减去0,加号要求非空。这里也可用更简单的 [1-9];写v版本是练习集合差集,不说明复杂写法在这个需求中更优。
日期与时区
第一题标准的仅日期形式按 UTC 午夜解释,日期加时间却未带偏移时按本地时间解释。非 UTC 地区的本地午夜对应另一个 UTC 时间点,所以时间戳不同。
第二题:
const source = new Date('2026-09-14T00:00:00Z');
const later = new Date(source.getTime() + 24 * 60 * 60 * 1000);
console.log(source.toISOString(), later.toISOString());
// 2026-09-14T00:00:00.000Z 2026-09-15T00:00:00.000Z它添加固定 86400000 毫秒;处于夏令时切换地区时,本地下一日相同墙上时间可能并不相隔这个长度。
第三题:setUTCMonth保留原日数31,二月容纳不下会自动进位。先复制Date并记住原日,设置为1日,再换月份,计算目标月最后一天,最后取原日与月末的较小值。这样实现的是明确的月末截断政策,不是Date原本默认的溢出政策。
JSON 与数据图
第一题对象序列化为 {"b":null},数组序列化为 [null,null]。对象属性中的 undefined 被省略,数组要保留位置所以用 null,NaN 两处都变 null。
第二题:
const obj = { name: '环' };
obj.self = obj;
const copy = structuredClone(obj);
console.log(copy !== obj, copy.self === copy, copy.self !== obj); // true true true副本是另一张数据图,图内的回边重新指向副本根。JSON 默认无法表示这样的循环引用,因此同一输入直接 stringify 会抛错。
第三题:默认返回undefined会删除对应属性,根返回undefined会让整个JSON.parse结果成为undefined。JSON往返把两个对象位置分别重建,通常失去共享身份;structuredClone记录原对象到副本的映射,副本内部仍共同指向同一副本对象,同时与原对象隔离。