- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
导读
filterRangeInPlace(arr, a, b)是 Modern JavaScript Tutorial 中 JavaScript 数组方法章节(1-js/05-data-types/05-array-methods/)的一道经典编程任务:它要求在不创建新数组的前提下,从原数组中删除所有不在闭区间[a, b]内的元素。它与姊妹任务filterRange形成鲜明对比——一个修改原数组、一个返回新数组,两者共同帮你吃透“原地修改(in-place mutation)”与“纯函数式过滤”两种编程范式的边界。阅读本文后,你将掌握用Array.prototype.splice安全删除数组元素的完整套路(含索引回退技巧),并能对照仓库中的官方测试用例验证你的实现是否正确。
任务原文与核心要求
任务定义位于 3-filter-range-in-place/task.md,原文要点如下:
Write a function
filterRangeInPlace(arr, a, b)that gets an arrayarrand removes from it all values except those that are betweenaandb. The test is:a ≤ arr[i] ≤ b. The function should only modify the array. It should not return anything.
任务示例(保留区间[1, 4]内的值):
let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 alert( arr ); // [3, 1]需要特别注意的是三个硬性约束:
- 保留条件:
a ≤ arr[i] ≤ b,即闭区间,边界值本身也要保留; - 原地修改:必须直接操作传入的数组
arr,不得新建数组再重新赋值; - 无返回值:函数体内不能
return任何值(返回undefined),调用方只能通过原数组的引用观察结果。
这三条约束与姊妹任务filterRange(见 2-filter-range/task.md)完全相反——filterRange明确要求"should not modify the array"并返回新数组。两个任务放在一起,正是一组刻意设计的对照实验:同一个过滤语义,分别用"返回新数组"和"原地删除"两种方式实现,帮助读者建立"哪些数组操作会产生副本、哪些会原地生效"的深刻认知。
官方参考解法:splice + 索引回退
仓库在 solution.md 给出了官方参考实现,可运行的独立版本位于 _js.view/solution.js:
function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { let val = arr[i]; // remove if outside of the interval if (val < a || val > b) { arr.splice(i, 1); i--; } } } let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 alert( arr ); // [3, 1]逐行拆解
for (let i = 0; i < arr.length; i++):正向遍历数组。注意循环条件每次都会重新读取arr.length,这在下文删除场景中是关键;let val = arr[i];:缓存当前元素,避免重复取值;if (val < a || val > b):判定是否在区间之外——val < a或val > b任一成立即删除。这里巧妙地用“排除法”实现区间过滤,与任务要求的保留条件a ≤ arr[i] ≤ b逻辑等价;arr.splice(i, 1);:从索引i处删除 1 个元素,后续元素全部前移一位;i--;:索引回退。删除后原位于i+1的元素移动到了i的位置,如果不把i减回去,下一次循环就会跳过这个被“挤”过来的元素。
为什么必须i--:模拟运行
以arr = [5, 3, 8, 1]、区间[1, 4]为例逐步推演:
| 步骤 | i | 数组状态 | 当前值 | 判定 | 操作后数组 |
|---|---|---|---|---|---|
| 1 | 0 | [5, 3, 8, 1] | 5 | 5 > 4,删除 | [3, 8, 1] |
| 2 | 0(i--后回到 0,循环再i++) | [3, 8, 1] | 3 | 1 ≤ 3 ≤ 4,保留 | [3, 8, 1] |
| 3 | 1 | [3, 8, 1] | 8 | 8 > 4,删除 | [3, 1] |
| 4 | 1(回退后) | [3, 1] | 1 | 1 ≤ 1 ≤ 4,保留 | [3, 1] |
| 5 | 2 | [3, 1] | — | i < arr.length为假,循环结束 | [3, 1] |
如果去掉i--:删除 5 后索引跳到 1,直接检查原索引 2 处的元素 8——数组此时是[3, 8, 1],索引 1 处是 8,8 会被删除;但紧随其后的逻辑会跳过紧邻的下一个元素。更致命的是,若连续多个元素需要删除(例如[6, 7, 8]配区间[1, 4]),删除 6 后索引跳到 1,此时索引 1 处已是 7,随后 8 却因索引跳跃被漏掉。这就是经典的“删除元素后索引前移”陷阱。
为什么不用delete arr[i]
教程正文 article.md 专门对比了delete与splice:
let arr = ["I", "go", "home"]; delete arr[1]; // remove "go" alert( arr[1] ); // undefined // now arr = ["I", , "home"]; alert( arr.length ); // 3delete arr[i]只会把该位置的值清为undefined,数组长度不变,后面的元素也不会前移——它留下了空洞。任务要求“remove from the array”,期望的是数组变短、元素紧凑,这正是 arr.splice 的职责:它以arr.splice(start, deleteCount, elem1, ..., elemN)的形式原地删除、插入或替换元素,并返回被删除元素的数组。
官方测试用例:验证三种约束
仓库在 _js.view/test.js 中提供了基于 Mocha + Chai 的完整测试套件,恰好逐条验证了任务的三个硬性约束:
describe("filterRangeInPlace", function() { it("returns the filtered values", function() { let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 2, 5); assert.deepEqual(arr, [5, 3]); }); it("doesn't return anything", function() { assert.isUndefined(filterRangeInPlace([1,2,3], 1, 4)); }); });两个用例的用意值得逐条分析:
- 第一个用例
assert.deepEqual(arr, [5, 3]):传入[5, 3, 8, 1]与区间[2, 5],期望原数组被原地修改为[5, 3]。注意这里区间是[2, 5],而任务示例用的是[1, 4]——测试刻意更换了区间,验证函数对任意a、b都成立。assert.deepEqual用于深度比较数组内容,若函数返回了新数组而没改原数组,此断言必然失败; - 第二个用例
assert.isUndefined(...):直接断言函数返回值是undefined,即“不返回任何东西”。这条用例对没有return的函数天然通过;若实现中不小心写了return arr或return filteredArray,测试就会亮红灯。
这套测试可配合仓库中其他任务的测试规范(如 3-pow-test-wrong 对 Mocha 用法的讲解)理解:describe/it组织用例、assert断言结果,正是 Modern JavaScript Tutorial 中“测试驱动开发”章节贯穿始终的实践方式。
与 filterRange 的对照:副本 vs 原地
理解本题最佳路径是对照阅读姊妹任务 filterRange。该任务要求:
let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1 (matching values) alert( arr ); // 5,3,8,1 (not modified)两个任务对同一份数据、同一个区间语义a ≤ x ≤ b,给出截然相反的 API 设计:
| 维度 | filterRange(返回新数组) | filterRangeInPlace(原地修改) |
|---|---|---|
| 是否修改原数组 | 否,原数组保持不变 | 是,原数组被直接改写 |
| 返回值 | 新数组(过滤结果) | undefined(不返回) |
| 典型实现手段 | arr.filter | for循环 +arr.splice |
| 适用场景 | 保留原始数据、链式调用 | 省内存、不需要保留原数组 |
filterRange的经典一行实现基于教程正文讲解的 arr.filter(fn):
function filterRange(arr, a, b) { return arr.filter(item => (a <= item && item <= b)); }filter会为每个元素调用回调,返回所有让回调为true的元素组成的新数组,且绝不触碰原数组——这与splice的原地语义形成教科书级对比。教程正文的总结也印证了这一点(见 article.md 附近):sort、reverse和splice会修改数组本身,而filter、map、slice等返回新副本。
边界情况与扩展思考
边界情形
- 区间外元素集中在开头:如
[9, 8, 7]配[1, 4],每次删除后i--回退,循环会反复在索引 0 处删除,直到遇到区间内元素——i--保证不遗漏任何一个被挤过来的元素; - 空区间 / 无匹配:如
filterRangeInPlace([1, 2], 5, 9),所有元素都在区间外,最终数组被清空为[];若数组本就为空,for循环体一次都不执行; - 边界值本身:
a、b是闭区间端点,arr[i] === a或arr[i] === b必须保留,条件里用的是<、>而非<=、>=,这一点测试用例(保留 5 和 3,端点 2、5 均在列)可以印证。
为什么不在循环里用arr.length快照
如果写成let len = arr.length提前缓存长度,删除元素后len不会变,循环会多遍历几个已经不存在的索引,虽然访问到undefined不会崩溃(undefined < a为假,不会被误删),但会做无谓的迭代。教程解法直接在条件里读取arr.length,删除后长度自然缩短,循环自动提前结束——简洁且正确。
时间复杂度视角
每次splice(i, 1)都是 O(n) 操作(后续元素整体前移),最坏情况下(全部元素需要删除)总复杂度为 O(n²)。对于追求性能的场景,更优的原地做法是用“双指针”先压缩保留元素再裁剪长度,例如:
function filterRangeInPlaceFast(arr, a, b) { let write = 0; for (let read = 0; read < arr.length; read++) { if (arr[read] >= a && arr[read] <= b) { arr[write++] = arr[read]; } } arr.length = write; }这种写法把时间复杂度降到 O(n)。但需要明确:这是教程之外的延伸优化,官方参考解法采用splice是为了最直观地展示“删除元素”的语义与索引处理技巧。初学阶段建议先吃透官方解法,再考虑优化。
总结
filterRangeInPlace虽然只是 数组方法章节 中的一个练习,但它浓缩了 JavaScript 数组操作的三组关键知识:
splice的原地删除语义:它不同于delete的“留空洞”,会真正缩短数组并前移元素;- 索引回退技巧:正向遍历中删除元素后必须
i--,否则会跳过紧邻元素——这是数组原地删除类算法通用的反直觉陷阱; - “修改 vs 副本”的 API 设计意识:与
filterRange对照,理解何时该返回新数组、何时该原地修改,以及相应的返回值约定(undefined)。
动手验证方式:将 solution.js 的代码粘贴到浏览器控制台或 Node.js 中运行,配合 _js.view/test.js 的断言(例如把任务示例的区间从[1, 4]换成[2, 5]),即可完整复现官方测试的通过流程。建议进一步阅读 数组方法教程正文 中splice、filter、slice的完整讲解,把这三个方法的行为边界彻底理清。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
json-render 中 Remotion Composition 的定义与动态元数据:从 Root.tsx 到 Timeline Spec
json render 中 Remotion Composition 的定义与动态元数据:从 Root.tsx 到 Timeline Spec 本文围绕 ski
文档/教程前端freeCodeCamp Slice and Splice 挑战解析:用 slice 与 splice 实现不可变数组合并
freeCodeCamp Slice and Splice 挑战解析:用 slice 与 splice 实现不可变数组合并 本文基于 freeCodeCamp
前端后端教育Ghidra BSim 过滤机制详解:服务器端与客户端过滤器的工作原理与实战演练
Ghidra BSim 过滤机制详解:服务器端与客户端过滤器的工作原理与实战演练 本篇技术指南聚焦 Ghidra BSim(二进制相似性匹配)功能的过滤器体系:
逆向工程网络安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考