拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript 数组原地区间过滤:实现 filterRangeInPlace 与 splice 的实战演练

JavaScript 数组原地区间过滤:实现 filterRangeInPlace 与 splice 的实战演练
  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

导读

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 functionfilterRangeInPlace(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]

需要特别注意的是三个硬性约束:

  1. 保留条件:a ≤ arr[i] ≤ b,即闭区间,边界值本身也要保留;
  2. 原地修改:必须直接操作传入的数组arr,不得新建数组再重新赋值;
  3. 无返回值:函数体内不能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]

逐行拆解

  1. for (let i = 0; i < arr.length; i++):正向遍历数组。注意循环条件每次都会重新读取arr.length,这在下文删除场景中是关键;
  2. let val = arr[i];:缓存当前元素,避免重复取值;
  3. if (val < a || val > b):判定是否在区间之外——val < a或val > b任一成立即删除。这里巧妙地用“排除法”实现区间过滤,与任务要求的保留条件a ≤ arr[i] ≤ b逻辑等价;
  4. arr.splice(i, 1);:从索引i处删除 1 个元素,后续元素全部前移一位;
  5. i--;:索引回退。删除后原位于i+1的元素移动到了i的位置,如果不把i减回去,下一次循环就会跳过这个被“挤”过来的元素。

为什么必须i--:模拟运行

以arr = [5, 3, 8, 1]、区间[1, 4]为例逐步推演:

步骤i数组状态当前值判定操作后数组
10[5, 3, 8, 1]55 > 4,删除[3, 8, 1]
20(i--后回到 0,循环再i++)[3, 8, 1]31 ≤ 3 ≤ 4,保留[3, 8, 1]
31[3, 8, 1]88 > 4,删除[3, 1]
41(回退后)[3, 1]11 ≤ 1 ≤ 4,保留[3, 1]
52[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 ); // 3

delete 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)); }); });

两个用例的用意值得逐条分析:

  1. 第一个用例assert.deepEqual(arr, [5, 3]):传入[5, 3, 8, 1]与区间[2, 5],期望原数组被原地修改为[5, 3]。注意这里区间是[2, 5],而任务示例用的是[1, 4]——测试刻意更换了区间,验证函数对任意a、b都成立。assert.deepEqual用于深度比较数组内容,若函数返回了新数组而没改原数组,此断言必然失败;
  2. 第二个用例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.filterfor循环 +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 数组操作的三组关键知识:

  1. splice的原地删除语义:它不同于delete的“留空洞”,会真正缩短数组并前移元素;
  2. 索引回退技巧:正向遍历中删除元素后必须i--,否则会跳过紧邻元素——这是数组原地删除类算法通用的反直觉陷阱;
  3. “修改 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

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载
上一篇:LeetCode-Go 题解:226. Invert Binary Tree 翻转二叉树的 Go 递归实现与测试详解
下一篇:Composio DigitalOcean 工具包认证完全指南:托管 OAuth2、自定义 OAuth2 与 Personal Access Token 三种接入方式

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表