Javascript 数组和字符串的 slice 方法 – wiki基地


深入理解 JavaScript 的 slice 方法:数组与字符串的非破坏性提取利器

在 JavaScript 的世界里,数组(Array)和字符串(String)是两种极其常用的数据类型。它们各自拥有丰富的内置方法,用于处理和操作数据。在这些方法中,slice() 无疑是一个非常强大且实用的工具。尽管它在数组和字符串上都存在,但它们共享着相似的语义和非破坏性(non-mutating)特性,使得 slice() 成为提取数据片段的首选方法。

本文将对 JavaScript 数组的 slice() 方法和字符串的 slice() 方法进行深入探讨,详细解析它们的语法、参数、行为以及实际应用场景,帮助你全面掌握这一重要方法。

1. slice() 方法概述:非破坏性的力量

在深入细节之前,我们首先理解 slice() 方法的核心特性:非破坏性。这意味着当你对一个数组或字符串调用 slice() 方法时,它不会改变原始的数组或字符串。相反,它会创建一个新的数组或字符串,其中包含从原始数据中提取出来的片段,并将这个新的数据片段作为方法的返回值。

这种非破坏性特性在现代 JavaScript 开发中尤其重要,特别是在函数式编程风格或需要维护数据不可变性的场景下。它避免了意外修改原始数据导致的副作用,使得代码更易于理解、测试和维护。

slice() 方法的基本作用是根据指定的起始和结束位置,从一个序列(数组或字符串)中提取一部分,并返回一个新的序列。接下来,我们将分别详细考察它在数组和字符串上的具体表现。

2. Array.prototype.slice():提取数组片段

Array.prototype.slice() 方法返回一个从原数组中提取出来的新数组,其中包含从 startend(不包括 end 自身)的元素。

2.1 语法

javascript
arr.slice(start, end)

  • start (可选):提取开始位置的索引。slice 会从这个索引处开始提取元素。如果省略该参数,则从索引 0 开始。如果是负数,表示从数组末尾开始的偏移量。例如,-1 表示最后一个元素,-2 表示倒数第二个元素,依此类推。如果 start 超出了数组的长度,则结果为一个空数组。
  • end (可选):提取结束位置的索引。slice 会在达到这个索引前停止提取。也就是说,end 位置的元素不会被包含在返回的新数组中。如果省略该参数,则提取到数组的末尾(包括最后一个元素)。如果是负数,表示从数组末尾开始的偏移量。例如,-1 表示最后一个元素,-2 表示倒数第二个元素。如果 end 的计算值大于或等于数组的长度,则提取到数组的末尾。

2.2 详细行为与示例

让我们通过一系列示例来深入理解 Array.prototype.slice() 的各种用法。

示例 1:省略 startend

当调用 slice() 时不带任何参数,它会从索引 0 开始提取直到数组末尾。这实际上是创建了原数组的一个浅拷贝

“`javascript
const originalArray = [10, 20, 30, 40, 50];
const newArray = originalArray.slice();

console.log(newArray); // 输出: [10, 20, 30, 40, 50]
console.log(originalArray === newArray); // 输出: false (是不同的数组对象)

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
“`

这个用法是复制数组的一种常见方式,比手动循环复制要简洁得多。

示例 2:只提供 start 参数

提供 start 参数时,slice()start 索引开始提取,直到数组末尾。

“`javascript
const originalArray = [10, 20, 30, 40, 50];
const newArray = originalArray.slice(2); // 从索引 2 (值为 30) 开始提取

console.log(newArray); // 输出: [30, 40, 50]

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
“`

示例 3:提供 startend 参数

提供 startend 参数时,slice()start 索引开始提取,直到 end 索引之前

“`javascript
const originalArray = [10, 20, 30, 40, 50];
const newArray = originalArray.slice(1, 4); // 从索引 1 (值为 20) 开始,到索引 4 (值为 50) 之前提取

console.log(newArray); // 输出: [20, 30, 40] (索引 1, 2, 3 的元素)

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
“`

记住,end 索引对应的元素是不包含在结果中的,这是 slice 方法的一个重要特性。

示例 4:使用负数索引

负数索引使得从数组末尾开始计算位置变得非常方便。-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。

“`javascript
const originalArray = [10, 20, 30, 40, 50];

// 从倒数第三个元素 (-3) 开始,到倒数第一个元素 (-1) 之前提取
const newArray1 = originalArray.slice(-3, -1);
console.log(newArray1); // 输出: [30, 40] (索引 2 和 3 的元素)

// 从倒数第二个元素 (-2) 开始,直到末尾 (省略 end 参数)
const newArray2 = originalArray.slice(-2);
console.log(newArray2); // 输出: [40, 50]

// 从开始 (省略 start 参数),到倒数第二个元素 (-2) 之前提取
const newArray3 = originalArray.slice(0, -2); // 或者简写为 originalArray.slice(0, originalArray.length – 2)
console.log(newArray3); // 输出: [10, 20, 30] (索引 0, 1, 2 的元素)

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
“`

当使用负数索引时,JavaScript 内部会将其转换为对应的正数索引。对于一个长度为 length 的数组,负数索引 index 会被计算为 length + index。例如,在长度为 5 的数组中,-3 对应 5 + (-3) = 2,而 -1 对应 5 + (-1) = 4。所以 slice(-3, -1) 等价于 slice(2, 4)

示例 5:start 大于或等于 end

如果 start 索引经过计算后大于或等于 end 索引经过计算后的值,slice() 方法会返回一个空数组。

“`javascript
const originalArray = [10, 20, 30, 40, 50];

const newArray1 = originalArray.slice(3, 2); // start (3) > end (2)
console.log(newArray1); // 输出: []

const newArray2 = originalArray.slice(3, 3); // start (3) == end (3)
console.log(newArray2); // 输出: []

const newArray3 = originalArray.slice(-2, -3); // -2 (3) > -3 (2) after calculation
console.log(newArray3); // 输出: []

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
“`

示例 6:startend 超出数组边界

如果 start 索引超出了数组的范围(大于等于数组长度),或者经过计算后的 start 大于等于数组长度,结果会是一个空数组。如果 end 索引超出了数组的范围(大于等于数组长度),或者经过计算后的 end 大于等于数组长度,它会被视为等于数组长度,即提取到数组末尾。

“`javascript
const originalArray = [10, 20, 30, 40, 50]; // 长度为 5

// start 超出范围
const newArray1 = originalArray.slice(10); // start = 10 > 4 (最大索引)
console.log(newArray1); // 输出: []

// start 超出范围 (负数)
const newArray2 = originalArray.slice(-10); // -10 + 5 = -5 < 0 (最小索引) -> Treated as 0 internally, but start(0) > end(undefined -> length). Let’s clarify: if start is effectively < 0, it’s treated as 0. So slice(-10) is slice(0).
console.log(newArray2); // 输出: [10, 20, 30, 40, 50] (slice(-10) is treated as slice(0))
// Let’s re-test negative start far out:
const newArray2b = originalArray.slice(-100); // -100 + 5 = -95 < 0 -> treated as 0
console.log(newArray2b); // Output: [10, 20, 30, 40, 50] (slice(0))
// Let’s re-test positive start far out:
const newArray2c = originalArray.slice(100); // start = 100 >= 5 -> empty array
console.log(newArray2c); // Output: []

// end 超出范围
const newArray3 = originalArray.slice(1, 10); // end = 10 > 4 (最大索引) -> treated as 5
console.log(newArray3); // 输出: [20, 30, 40, 50] (slice(1, 5))

// end 超出范围 (负数)
const newArray4 = originalArray.slice(0, -10); // -10 + 5 = -5 < 0 -> treated as 0. slice(0, 0)
console.log(newArray4); // 输出: [] (slice(0, 0))
// Let’s re-test negative end just below 0:
const newArray4b = originalArray.slice(0, -6); // -6 + 5 = -1 < 0 -> treated as 0. slice(0, 0)
console.log(newArray4b); // Output: []

// start 位于范围内,end 超出
const newArray5 = originalArray.slice(3, 10); // start = 3, end = 10 -> treated as 5
console.log(newArray5); // Output: [40, 50] (slice(3, 5))

// 验证原数组未改变
console.log(originalArray); // 输出: [10, 20, 30, 40, 50]
``
**边界值处理总结 (Array slice):**
* 实际
start索引被计算为Math.max(0, start < 0 ? start + length : start).
* 实际
end索引被计算为Math.max(0, Math.min(length, end < 0 ? end + length : end)).
* 如果实际
start>= 实际end,返回空数组。
* 否则,返回从实际
start到实际end`(不包含)的元素。

示例 7:处理包含对象的数组 (浅拷贝)

当数组中包含对象时,slice() 进行的是浅拷贝。这意味着新数组中的对象引用与原数组中的对象引用是相同的。修改新数组中对象属性会影响原数组中的对象。

“`javascript
const obj1 = { name: ‘A’ };
const obj2 = { name: ‘B’ };
const originalArray = [1, obj1, 3, obj2];

const newArray = originalArray.slice(); // 浅拷贝

console.log(newArray); // 输出: [1, { name: ‘A’ }, 3, { name: ‘B’ }]
console.log(originalArray[1] === newArray[1]); // 输出: true (引用相同)

// 修改新数组中的对象属性
newArray[1].name = ‘Alice’;

console.log(newArray); // 输出: [1, { name: ‘Alice’ }, 3, { name: ‘B’ }]
console.log(originalArray); // 输出: [1, { name: ‘Alice’ }, 3, { name: ‘B’ }] (原数组也受到影响)
“`

如果需要对包含对象的数组进行深拷贝,slice() 是不够的,通常需要结合其他技术(如递归拷贝,或者对于简单可序列化对象使用 JSON.parse(JSON.stringify(arr)),但这有局限性)。

2.3 slice() 与数组类对象

slice() 方法也可以用于“数组类对象”(array-like objects)。这些对象拥有 length 属性,并且属性名是索引(如 arguments 对象或 DOM 集合)。

在 ES6 之前,将 arguments 对象转换为真正的数组是 slice 的一个常见用途:

“`javascript
function sum() {
// arguments 是一个数组类对象,不是真正的数组
console.log(arguments instanceof Array); // 输出: false

// 使用 slice 将 arguments 转换为数组
const argsArray = Array.prototype.slice.call(arguments);
console.log(argsArray instanceof Array); // 输出: true

const total = argsArray.reduce((acc, current) => acc + current, 0);
console.log(total);
}

sum(1, 2, 3, 4); // 输出: 10
“`

这里使用了 call() 方法来借用 Array.prototype 上的 slice 方法,并将其应用于 arguments 对象。在 ES6 之后,使用 Array.from() 或扩展运算符(...)通常是更现代且推荐的方式:

“`javascript
function sumModern(…args) {
console.log(args instanceof Array); // 输出: true (…rest parameters creates a real array)
const total = args.reduce((acc, current) => acc + current, 0);
console.log(total);
}
sumModern(1, 2, 3, 4); // 输出: 10

function sumArrayFrom() {
const argsArray = Array.from(arguments); // 使用 Array.from
console.log(argsArray instanceof Array); // 输出: true
const total = argsArray.reduce((acc, current) => acc + current, 0);
console.log(total);
}
sumArrayFrom(1, 2, 3, 4); // 输出: 10
``
尽管有了更现代的方法,了解
slice.call(arguments)` 这种经典用法仍然是有益的。

3. String.prototype.slice():提取字符串片段

与数组的 slice() 方法非常相似,String.prototype.slice() 方法从原字符串中提取出一个片段,并返回一个新的字符串。其参数和行为规则与数组的 slice 几乎完全一致。

3.1 语法

javascript
str.slice(start, end)

  • start (可选):提取开始位置的索引。slice 会从这个索引处开始提取字符。如果省略该参数,则从索引 0(字符串的开头)开始。如果是负数,表示从字符串末尾开始的偏移量。例如,-1 表示最后一个字符,-2 表示倒数第二个字符,依此类推。如果 start 超出了字符串的长度,则结果为一个空字符串。
  • end (可选):提取结束位置的索引。slice 会在达到这个索引前停止提取。也就是说,end 位置的字符不会被包含在返回的新字符串中。如果省略该参数,则提取到字符串的末尾(包括最后一个字符)。如果是负数,表示从字符串末尾开始的偏移量。例如,-1 表示最后一个字符,-2 表示倒数第二个字符。如果 end 的计算值大于或等于字符串的长度,则提取到字符串的末尾。

3.2 详细行为与示例

让我们看看 String.prototype.slice() 的各种用法。

示例 1:省略 startend

不带参数调用 slice() 会返回原字符串的一个副本。

“`javascript
const originalString = “HelloWorld”;
const newString = originalString.slice();

console.log(newString); // 输出: “HelloWorld”
console.log(originalString === newString); // 输出: true (原始值比较,字符串是原始类型,值相同则相等)

// 验证原字符串未改变
console.log(originalString); // 输出: “HelloWorld”
``
注意:虽然是不同的字符串对象(在某些引擎内部实现可能是),但因为字符串是原始类型,
===比较的是值,所以会返回true。与数组的浅拷贝行为不同,字符串是不可变的,slice()` 返回的始终是一个新的字符串值。

示例 2:只提供 start 参数

提供 start 参数时,从 start 索引开始提取到字符串末尾。

“`javascript
const originalString = “JavaScript”;
const newString = originalString.slice(4); // 从索引 4 (字符 ‘S’) 开始提取

console.log(newString); // 输出: “Script”

// 验证原字符串未改变
console.log(originalString); // 输出: “JavaScript”
“`

示例 3:提供 startend 参数

提供 startend 参数时,从 start 索引开始提取到 end 索引之前

“`javascript
const originalString = “Programming”;
const newString = originalString.slice(3, 7); // 从索引 3 (字符 ‘g’) 开始,到索引 7 (字符 ‘m’) 之前提取

console.log(newString); // 输出: “gram” (索引 3, 4, 5, 6 的字符)

// 验证原字符串未改变
console.log(originalString); // 输出: “Programming”
“`

示例 4:使用负数索引

负数索引在字符串 slice 中的行为与数组完全相同。

“`javascript
const originalString = “Frontend”;

// 从倒数第三个字符 (-3) 开始,到倒数第一个字符 (-1) 之前提取
const newString1 = originalString.slice(-3, -1);
console.log(newString1); // 输出: “en” (索引 5 和 6 的字符)

// 从倒数第二个字符 (-2) 开始,直到末尾 (省略 end 参数)
const newString2 = originalString.slice(-2);
console.log(newString2); // 输出: “nd”

// 从开始 (省略 start 参数),到倒数第二个字符 (-2) 之前提取
const newString3 = originalString.slice(0, -2); // 或者简写为 originalString.slice(0, originalString.length – 2)
console.log(newString3); // 输出: “Front”

// 验证原字符串未改变
console.log(originalString); // 输出: “Frontend”
“`

示例 5:start 大于或等于 end

如果 start 索引经过计算后大于或等于 end 索引经过计算后的值,slice() 方法会返回一个空字符串 ""

“`javascript
const originalString = “Developer”;

const newString1 = originalString.slice(5, 2); // start (5) > end (2)
console.log(newString1); // 输出: “”

const newString2 = originalString.slice(5, 5); // start (5) == end (5)
console.log(newString2); // 输出: “”

const newString3 = originalString.slice(-2, -3); // -2 (7) > -3 (6) after calculation (length = 9)
console.log(newString3); // 输出: “”

// 验证原字符串未改变
console.log(originalString); // 输出: “Developer”
“`

示例 6:startend 超出字符串边界

字符串 slice 在处理超出边界的索引时,行为也与数组 slice 类似。超出范围的索引会被截断到最近的有效边界(0 或字符串长度)。

“`javascript
const originalString = “Web”; // 长度为 3

// start 超出范围
const newString1 = originalString.slice(10); // start = 10 > 2 (最大索引)
console.log(newString1); // 输出: “”

// start 超出范围 (负数) -> treated as 0
const newString2 = originalString.slice(-10); // -10 + 3 = -7 < 0
console.log(newString2); // 输出: “Web” (slice(0))

// end 超出范围 -> treated as length
const newString3 = originalString.slice(1, 10); // end = 10 > 2 (最大索引)
console.log(newString3); // 输出: “eb” (slice(1, 3))

// end 超出范围 (负数) -> treated as 0
const newString4 = originalString.slice(0, -10); // -10 + 3 = -7 < 0
console.log(newString4); // 输出: “” (slice(0, 0))

// start 位于范围内,end 超出
const newString5 = originalString.slice(2, 5); // start = 2, end = 5 -> treated as 3
console.log(newString5); // 输出: “b” (slice(2, 3))

// 验证原字符串未改变
console.log(originalString); // 输出: “Web”
“`

边界值处理总结 (String slice):
* 实际 start 索引被计算为 Math.max(0, start < 0 ? start + length : start).
* 实际 end 索引被计算为 Math.max(0, Math.min(length, end < 0 ? end + length : end)).
* 如果实际 start >= 实际 end,返回空字符串 ""
* 否则,返回从实际 start 到实际 end(不包含)的字符组成的子字符串。

4. slice() 在数组和字符串上的共同点与区别

通过上面的详细介绍,我们可以总结出 slice() 方法在数组和字符串上的共同点和区别:

共同点:

  1. 非破坏性: 这是最重要的共同点。无论是数组还是字符串,调用 slice() 都不会修改原始数据,而是返回一个新的数据副本或片段。
  2. 参数一致: 都接受可选的 startend 两个参数,且它们的含义相同:start 是起始索引(包含),end 是结束索引(不包含)。
  3. 负数索引支持: 都支持使用负数索引来从末尾计算位置,计算规则相同(length + index)。
  4. 省略参数的行为: 都支持省略 start(默认为 0)和 end(默认为 length),用于复制整个序列或提取到末尾。
  5. 处理 start >= end 当计算后的 start 大于或等于 end 时,都返回一个空的序列(空数组 [] 或空字符串 "")。
  6. 处理越界索引: 都以类似的方式处理超出范围的索引,将它们截断到最近的有效边界。

区别:

  1. 操作对象类型: Array.prototype.slice() 操作的是数组,String.prototype.slice() 操作的是字符串。
  2. 返回值类型: Array.prototype.slice() 返回一个新的数组,String.prototype.slice() 返回一个新的字符串。
  3. 浅拷贝 vs. 原始值复制: 对于数组,如果元素是对象,slice 是浅拷贝,新数组中的对象引用与原数组相同。对于字符串(原始类型),返回的是一个新的字符串值,修改新字符串不会影响原字符串(因为字符串本身是不可变的)。
  4. 可用于数组类对象: Array.prototype.slice 可以通过 callapply 方法用于数组类对象,而 String.prototype.slice 通常只用于字符串原始值或 String 对象。

5. slice() 的实际应用场景

slice() 方法因其非破坏性、简洁性和灵活性,在各种 JavaScript 编程场景中都有广泛的应用。

  1. 复制数组或字符串:

    • 最简单的用法就是不带参数调用 slice() 来创建一个原数组或字符串的副本。这是在不修改原数据的情况下进行后续操作的基础。
      javascript
      const arrCopy = myArray.slice();
      const strCopy = myString.slice();
  2. 提取子数组或子字符串:

    • 根据需要获取数组或字符串的某个片段。
      javascript
      const firstThree = myArray.slice(0, 3); // 获取前三个元素/字符
      const lastTwo = myArray.slice(-2); // 获取后两个元素/字符
      const middlePart = myString.slice(5, 10); // 获取中间一段子字符串
  3. 处理函数参数 (arguments 对象):

    • 虽然现在更推荐使用扩展运算符或 Array.from,但 Array.prototype.slice.call(arguments) 仍然是理解老代码和处理数组类对象的一种方式。
  4. 截断数组或字符串:

    • 需要获取数组或字符串的前 N 个元素/字符时,可以使用 slice(0, N)
      javascript
      function truncateString(str, maxLength) {
      if (str.length <= maxLength) {
      return str;
      }
      return str.slice(0, maxLength) + '...'; // 截断并添加省略号
      }
  5. 分页处理 (概念上):

    • 虽然实际分页通常涉及后端逻辑和数据获取,但在前端处理已有的完整数据集时,可以使用 slice 来模拟分页展示不同页的数据。
      javascript
      function getPageData(dataArray, pageNumber, itemsPerPage) {
      const startIndex = (pageNumber - 1) * itemsPerPage;
      const endIndex = startIndex + itemsPerPage;
      return dataArray.slice(startIndex, endIndex);
      }
  6. 提取文件扩展名:

    • 使用负数索引可以方便地从文件名中提取扩展名。
      javascript
      function getFileExtension(filename) {
      const lastDotIndex = filename.lastIndexOf('.');
      if (lastDotIndex === -1) {
      return ''; // 没有点,可能没有扩展名
      }
      return filename.slice(lastDotIndex + 1);
      }
      console.log(getFileExtension('document.txt')); // 输出: "txt"
      console.log(getFileExtension('archive.tar.gz')); // 输出: "gz"
      console.log(getFileExtension('noextensionfile')); // 输出: ""
  7. 处理路径或 URL 片段:

    • 从 URL 字符串中提取特定部分。
      javascript
      const url = "https://www.example.com/path/to/resource?query=string";
      const path = url.slice(url.indexOf('/path/'), url.indexOf('?'));
      console.log(path); // 输出: "/path/to/resource" (假设路径结构已知)

6. slice() 与其他类似方法的比较

在 JavaScript 中,还有一些方法也能用于提取或操作数组/字符串的一部分,但它们与 slice() 有关键区别。了解这些区别有助于在不同场景下选择最合适的方法。

  • Array.prototype.splice() (数组独有):

    • 主要区别: splice() 是一个破坏性方法,它会改变原始数组。
    • 用途: 用于在数组中添加、删除或替换元素。它的参数含义也与 slice 不同(start, deleteCount, item1, item2, ...)。
    • 结论: slice 用于提取不改变原数组,splice 用于修改原数组。不要混淆它们的用途和效果。
  • String.prototype.substring() (字符串独有):

    • 区别: substring() 也用于提取字符串片段,也是非破坏性的。但它的参数处理与 slice 略有不同:
      • 如果 start 大于 endsubstring() 会自动交换这两个参数,使得始终从较小的索引开始提取。而 slice 在这种情况下返回空字符串。
      • substring() 不支持负数索引。负数索引会被视为 0。
    • 结论: slice 的参数行为(特别是负数索引和 start >= end)与数组 slice 更一致,通常更推荐使用 slice 进行字符串提取,除非需要 substring 特有的参数交换行为或明确不使用负数索引。
  • String.prototype.substr() (字符串独有):

    • 区别: substr() 的第二个参数不是结束索引,而是要提取的长度str.substr(start, length)
    • 状态: substr() 方法已经被标记为废弃 (deprecated),不推荐在新的代码中使用。
    • 结论: 避免使用 substr,改用 slicesubstring

7. 总结与展望

通过对 Array.prototype.slice()String.prototype.slice() 的全面剖析,我们可以看到它们是 JavaScript 中用于非破坏性地提取数据片段的基石方法。它们拥有高度一致的参数结构和行为,特别是在处理可选参数、负数索引和越界索引方面。

掌握 slice() 方法对于编写健壮、易读且没有意外副作用的 JavaScript 代码至关重要。无论你是需要复制一个数组进行进一步操作,还是从长字符串中提取特定信息,slice() 都能提供简洁高效的解决方案。

在日常开发中,始终优先考虑 slice() 的非破坏性特性,这有助于你更好地管理数据状态,尤其是在处理复杂的数据结构或采用函数式编程范式时。虽然其他方法如 splicesubstring 在特定场景下也有其用武之地,但理解 slice 的核心优势和统一行为,将极大地提升你的 JavaScript 编程效率和代码质量。

随着 JavaScript 语言的不断发展,新的方法如扩展运算符和 Array.from 提供了更多处理数组和数组类对象的方式,但 slice 作为一种经典且高效的提取方法,仍然在许多场景下保持着其不可替代的价值。熟练运用 slice,你将能够更加自如地驾驭 JavaScript 的数组和字符串操作。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部