- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
数组的**头部(head)**指其第一个元素,尾部(tail)指除第一个元素外的其余部分——这是函数式编程与算法题中极为常见的操作。本文以 30-seconds-of-code 仓库中 array-head-tail.md 为主体,系统讲解如何在不修改原数组的前提下,用解构赋值、下标访问与Array.prototype.slice()三套方案安全、简洁地获取数组的首元素与剩余部分,并覆盖空数组、单元素数组等边界情况。
什么是数组的 head 与 tail
在任意编程语言中,head与tail都是对序列最直观的切分:head 是序列的第一个元素,tail 是序列去掉第一个元素后剩下的部分。例如对于数组[1, 2, 3],其 head 为1,tail 为[2, 3]。
这一对概念的价值在于它构成了许多递归算法与函数式工具(如fold、map)的基础——通过不断取 head 处理、对 tail 递归,可以遍历整个序列。因此,如何在 JavaScript 中干净地实现这两个操作,是每个开发者都应掌握的基操。
需要特别强调的是,本文介绍的两种主方案(解构赋值与slice())都是非破坏性的:它们只产生新值,不会修改原数组。这与shift()、pop()等原地修改数组的破坏性方法形成鲜明对比,后者在使用时应格外谨慎。
方式一:利用解构赋值一次取到 head 与 tail
获取 head 与 tail 最简单的方式是使用 ES6 引入的解构赋值(destructuring assignment)语法。借助数组解构,可以同时把第一个元素解构到变量head,并用剩余参数(rest pattern)...tail将余下元素收集为新的数组:
const arr = [1, 2, 3]; const [head, ...tail] = arr; // head = 1, tail = [2, 3]这段代码最值得注意的一点是它对空数组同样成立。当数组为空时,head会得到undefined,而tail则是一个空数组[],不会抛错:
const arr = []; const [head, ...tail] = arr; // head = undefined, tail = []从实现原理上看,解构赋值本质上是基于迭代协议(iterable protocol)对数组元素进行"按位抽取",...tail收集所有剩余元素并组成一个新数组,因此原数组arr保持不变。关于数组解构的更多细节(如跳过索引、默认值、嵌套解构),可以参考仓库中同主题的 destructuring-assignment.md,其中对 Array destructuring 一节有更完整的讲解。
适用场景:当你需要同时使用 head 和 tail 时(例如递归遍历数组、实现 reduce 式逻辑),解构赋值是最简洁的写法。
方式二:单独获取数组的 head
解构赋值固然方便,但如果只需要 head 或 tail 其中之一,你既可以使用解构赋值然后忽略不需要的变量,也可以用更"单点"的方式。
用下标访问获取第一个元素
获取 head 本质就是读取数组索引0处的元素,使用**括号表示法(bracket notation)**即可:
const head = arr => (arr && arr.length ? arr[0] : undefined); head([1, 2, 3]); // 1 head([]); // undefined注意这里的函数对输入做了防御性检查:先确认arr存在(arr &&),再确认它非空(arr.length)。这一层判断的价值在于:
- 当传入
null或undefined时,直接访问arr[0]会抛出TypeError,加上守卫后可安全返回undefined; - 当传入空数组时,
arr[0]本身就会返回undefined,守卫让行为保持一致且意图明确; - 从源码结构看,
arr.length的检查使该实现天然兼容拥有length属性的类数组对象(array-like objects)。
关于"类数组"的一点说明
原文档特别提到,加入length检查后,该函数实际上可处理"类数组"值——即像字符串、arguments对象这样具有length属性但并非真正数组的结构。不过需要留意的是,对字符串直接使用括号表示法虽能取到字符,但更规范的替代是String.prototype.charAt()或Array.from()转换后再操作。这个检查并非必须,但作为防御性编程习惯,值得保留。
方式三:单独获取数组的 tail
获取 tail 需要返回除第一个元素以外的所有元素。Array.prototype.slice()是完成这一任务的理想工具——只要指定起始索引为1,就能拿到去掉首元素后的全部剩余元素:
const tail = arr => (arr && arr.length > 1 ? arr.slice(1) : []); tail([1, 2, 3]); // [2, 3] tail([1]); // [] tail([]); // []与 head 类似,这里也加入了守卫判断:要求值存在(arr &&)且元素个数大于 1(arr.length > 1)。当数组只有 1 个元素或为空时,直接返回空数组[],保证返回值类型始终一致——这比返回undefined更利于后续链式调用。
Array.prototype.slice()的语义需要准确理解:
slice(start, end)返回从start到end(不含)之间的元素组成的新数组,原数组不被修改;- 省略
end时取到数组末尾;slice(1)即"从索引 1 到最后"; - 支持负索引(如
slice(-2)取最后两个元素)与越界索引(越界时自动收敛到有效边界,返回浅拷贝),这些特性在扩展场景中非常有用。
边界情况一览:四种典型输入的行为对照
为了便于记忆与测试,把上面两套独立函数在典型输入下的行为汇总如下:
| 输入 | head(arr) | tail(arr) |
|---|---|---|
[1, 2, 3] | 1 | [2, 3] |
[1] | 1 | [] |
[] | undefined | [] |
null/undefined | undefined | [] |
可见:head 在无元素可取时返回undefined,tail 在不足两个元素时返回空数组,两条函数在防御性与返回类型一致性上都做了妥善处理。
扩展阅读:从数组首尾获取或移除 n 个元素
理解了 head/tail 的单元素版本后,不妨将其推广到n 个元素的场景。仓库中同属js语言array标签的 first-last-n-elements.md 展示了如何用一行代码取前 n 个或后 n 个元素:
const firstN = (arr, n = 1) => arr.slice(0, n); const lastN = (arr, n = 1) => arr.slice(-n); const arr = ['a', 'b', 'c', 'd']; firstN(arr); // ['a'] firstN(arr, 2); // ['a', 'b'] firstN(arr, 5); // ['a', 'b', 'c', 'd'](越界时返回浅拷贝) lastN(arr); // ['d'] lastN(arr, 2); // ['c', 'd'] lastN(arr, 5); // ['a', 'b', 'c', 'd']与之对称的是 remove-first-last-n-array-elements.md,其中明确对比了破坏性方法(shift()/pop()/splice())与非破坏性方法(slice())的差异,并给出drop与dropLast的slice实现:
const drop = (arr, n = 1) => arr.slice(n); // 去掉前 n 个 const dropLast = (arr, n = 1) => arr.slice(0, -n); // 去掉后 n 个 drop([1, 2, 3]); // [2, 3] dropLast([1, 2, 3]); // [1, 2]这些扩展实现与本文的 head/tail 一脉相承:tail(arr)等价于drop(arr, 1),head(arr)等价于firstN(arr, 1)[0],可见slice()一族工具可以统一覆盖"取首尾元素"的几乎所有变体。
为什么优先选择非破坏性实现
在 JavaScript 中,获取首尾元素存在多条路径,但破坏性方法有明确代价:
Array.prototype.shift()会原地删除第一个元素并返回它,导致原数组被修改,后续代码若仍引用原数组容易产生隐蔽 Bug;Array.prototype.pop()同理,原地删除最后一个元素;Array.prototype.splice()同样会修改原数组。
而解构赋值与slice()始终返回新值、保持原数组不变,更符合函数式编程与不可变数据(immutability)的实践原则。若你的代码在 React 等强调不可变更新的场景中运行,这一区别尤为重要——修改原数组可能绕过状态更新机制,造成 UI 与数据不一致。
源码位置与文档索引
本文主体对应仓库中的 content/snippets/js/s/array-head-tail.md,相关材料一并列出,便于深入研读:
- content/snippets/js/s/destructuring-assignment.md:解构赋值语法详解(数组解构、默认值、嵌套解构);
- content/snippets/js/s/first-last-n-elements.md:用
slice()获取前/后 n 个元素; - content/snippets/js/s/remove-first-last-n-array-elements.md:破坏性 vs 非破坏性移除首尾元素;
- content/collections/js/array.yaml:
js语言下array标签集合的聚合入口,本文与其他数组类 snippet 均收录于此; - content/redirects.yaml:其中记录了
/js/s/head、/js/s/tail、/js/s/array-head、/js/s/array-tail四个旧路径均以 301 重定向到/js/s/array-head-tail,说明该文档在站点中承担了 head/tail 主题的唯一权威入口。
小结
获取数组的 head 与 tail 是 JavaScript 日常开发中的高频操作。核心要点可归纳为三条:
- 同时取 head 与 tail:优先使用解构赋值
const [head, ...tail] = arr;,空数组时得到head === undefined、tail === []; - 只取 head:用
arr[0]配合存在性/非空守卫,兼顾类数组与空值安全; - 只取 tail:用
arr.slice(1)配合长度守卫,保证返回空数组而非undefined。
三套方案均不修改原数组,配合slice()一族工具(firstN/lastN/drop/dropLast),足以应对从"取首尾元素"到"取/删 n 个元素"的绝大多数实际需求。
- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
相关推荐
终极指南:如何用Lodash的head、tail与last函数优雅处理数组首尾元素
终极指南:如何用Lodash的head、tail与last函数优雅处理数组首尾元素 在JavaScript开发中,数组操作是日常任务的重要组成部分。无论是获取数
前端后端旅行视频SEO化:如何用Timeline Visualizer的标题模板让作品更容易被搜索到
旅行视频SEO化:如何用Timeline Visualizer的标题模板让作品更容易被搜索到 想让旅行视频被更多人搜到?关键往往不在画质,而在标题。Timeli
教程文档解锁Wand游戏修改器全部高级功能:5分钟免费激活终极指南
解锁Wand游戏修改器全部高级功能:5分钟免费激活终极指南 还在为Wand(原WeMod)游戏修改器的付费墙而烦恼吗?Wand Enhancer为你提供了一个完
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考