JS `Array.prototype.copyWithin()`:在数组内部复制元素

各位观众,各位朋友,大家好!今天咱们来聊聊 JavaScript 数组里一个挺有意思,但可能平时不太常用的家伙:Array.prototype.copyWithin()

这玩意儿啊,就像数组里的 “乾坤大挪移”,能在数组内部把一部分元素复制到另一个位置,听起来有点绕,但用起来其实挺灵活的。

一、copyWithin() 的基本概念

简单来说,copyWithin() 方法允许你在同一个数组内,将数组的一部分浅拷贝到数组的另一个位置,而不会改变数组的长度。 记住,是浅拷贝!这意味着如果数组里存的是对象,复制的只是对象的引用,而不是对象本身。

它的语法结构是这样的:

array.copyWithin(target, start, end)

参数说明:

  • target (必须): 指定复制序列的目标位置的索引。从这个索引开始,元素将被覆盖。如果 target 是负数,则从数组的末尾开始计算(例如,-1 表示数组的最后一个元素)。
  • start (可选): 指定复制序列的起始位置的索引。从这个索引开始,元素将被复制。如果 start 是负数,则从数组的末尾开始计算。如果省略 start,则默认值为 0。
  • end (可选): 指定复制序列的结束位置的索引(不包括该索引对应的元素)。到这个索引之前的元素都将被复制。如果 end 是负数,则从数组的末尾开始计算。如果省略 end,则默认值为数组的长度。

返回值:修改后的数组本身。

二、举个栗子:最简单的用法

咱们先来个最简单的例子,让你有个直观的感受:

const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3, 4); // 从索引3开始复制一个元素(4),放到索引0的位置
console.log(arr); // 输出: [4, 2, 3, 4, 5]

在这个例子中,我们把索引 3 到 4 之间的元素(也就是 4),复制到索引 0 的位置。原来的 1 被覆盖了。

三、更复杂的例子:玩转参数

现在咱们来玩点更复杂的,看看不同参数组合的效果:

const arr1 = [1, 2, 3, 4, 5];
arr1.copyWithin(1, 3); // 从索引3开始复制到数组末尾,放到索引1的位置
console.log(arr1); // 输出: [1, 4, 5, 4, 5]

const arr2 = [1, 2, 3, 4, 5];
arr2.copyWithin(2, 0, 3); // 从索引0到3(不包括3)复制,放到索引2的位置
console.log(arr2); // 输出: [1, 2, 1, 2, 3]

const arr3 = [1, 2, 3, 4, 5];
arr3.copyWithin(0, -2); // 从倒数第二个元素开始复制到数组末尾,放到索引0的位置
console.log(arr3); // 输出: [4, 5, 3, 4, 5]

const arr4 = [1, 2, 3, 4, 5];
arr4.copyWithin(0, -2, -1); // 从倒数第二个元素到倒数第一个元素(不包括倒数第一个)复制,放到索引0的位置
console.log(arr4); // 输出: [4, 2, 3, 4, 5]

这些例子展示了 startend 参数的各种用法,包括省略和使用负数索引。 请仔细观察输出结果,体会每个参数的作用。

四、注意事项和坑点

在使用 copyWithin() 的时候,有一些需要注意的地方:

  1. 覆盖问题: copyWithin() 会覆盖目标位置上的原有元素。所以,在使用之前,一定要想清楚,你是否真的想覆盖这些元素。
  2. 复制范围: 复制的范围由 startend 决定,不包括 end 索引对应的元素。
  3. 负数索引: 负数索引表示从数组末尾开始计算,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。
  4. 浅拷贝: 前面已经强调过了,copyWithin() 执行的是浅拷贝。如果数组元素是对象,那么复制的只是对象的引用。修改复制后的对象,会影响原始对象。
  5. target 超出范围: 如果 target 大于等于数组的长度,则不会发生任何复制。
  6. start 和 end 超出范围: 如果 start 大于等于数组的长度,则不会发生任何复制。如果 end 大于数组的长度,则 end 会被视为数组的长度。

五、copyWithin() 的应用场景

虽然 copyWithin() 可能不像 map()filter() 那么常用,但在某些特定场景下,它可以发挥很大的作用。 常见的应用场景包括:

  1. 数据填充: 可以使用 copyWithin() 快速填充数组的某一部分。
  2. 数据移动: 可以将数组的一部分元素移动到数组的另一个位置。
  3. 循环移位: 可以实现数组元素的循环移位。

咱们来看几个例子:

  • 数据填充:
const arr = new Array(10).fill(0); // 创建一个长度为10,所有元素都为0的数组
arr.copyWithin(5, 0, 5); // 将前5个元素复制到后5个元素的位置
console.log(arr); // 输出: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 并没有如预期填充 因为都是0
const arr2 = [1,2,3,4,5,6,7,8,9,10];
arr2.copyWithin(5, 0, 5);
console.log(arr2); // 输出: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] 成功填充
  • 数据移动:
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(2, 0, 2); // 将前两个元素移动到索引2的位置
console.log(arr); // 输出: [1, 2, 1, 2, 5]
  • 循环移位:
function rotateArray(arr, k) {
  k = k % arr.length; // 处理 k 大于数组长度的情况
  if (k < 0) {
    k += arr.length; // 处理 k 为负数的情况
  }
  arr.copyWithin(0, arr.length - k); // 将后 k 个元素复制到数组开头
  arr.copyWithin(k, 0, arr.length - k); // 将前 arr.length - k 个元素复制到 k 之后的位置
  return arr;
}

const arr = [1, 2, 3, 4, 5];
rotateArray(arr, 2); // 向右循环移位 2 位
console.log(arr); // 输出: [4, 5, 1, 2, 3]

这个 rotateArray 函数实现了数组的循环移位功能。它首先处理了 k 大于数组长度以及 k 为负数的情况,然后使用 copyWithin() 方法将数组的后 k 个元素复制到数组的开头,再将数组的前 arr.length - k 个元素复制到 k 之后的位置。

六、copyWithin() 与其他方法的比较

你可能会问,copyWithin() 和其他数组方法有什么区别? 什么时候应该用 copyWithin(),什么时候应该用其他方法?

咱们来比较一下 copyWithin() 和几个常用的数组方法:

方法 功能 是否改变原数组 浅拷贝/深拷贝 适用场景
copyWithin() 在数组内部复制元素 浅拷贝 在数组内部进行数据填充、移动或循环移位
splice() 删除、替换或添加元素 浅拷贝 删除、替换或添加数组元素,可以改变数组的长度
slice() 提取数组的一部分,返回一个新数组 浅拷贝 获取数组的子集,不改变原数组
concat() 将多个数组或值合并成一个新数组 浅拷贝 合并数组,不改变原数组
map() 对数组中的每个元素执行一个函数,返回一个新数组 取决于函数 对数组中的每个元素进行转换
filter() 过滤数组中的元素,返回一个新数组 浅拷贝 根据条件筛选数组元素

从表格中可以看出,copyWithin() 的一个主要特点是在数组内部进行操作,并且会改变原数组。 而 slice()concat()map()filter() 等方法都不会改变原数组。

所以,如果你需要在数组内部进行数据操作,并且允许改变原数组,那么 copyWithin() 可能是一个不错的选择。 如果需要创建新的数组,或者不希望改变原数组,那么应该选择其他方法。

七、性能考量

虽然 copyWithin() 在某些场景下很方便,但也要注意它的性能。 由于 copyWithin() 需要在数组内部进行复制操作,所以当数组很大时,性能可能会受到影响。

在对性能要求很高的场景下,可以考虑使用其他方法来优化性能。 例如,可以使用循环来实现相同的功能,或者使用 WebAssembly 来加速计算。

八、兼容性

copyWithin() 是 ES6 (ECMAScript 2015) 中新增的方法。 如果需要在旧版本的浏览器中使用 copyWithin(),可以使用 Polyfill 来提供兼容性支持。

一个简单的 copyWithin() Polyfill 实现如下:

if (!Array.prototype.copyWithin) {
  Array.prototype.copyWithin = function(target, start, end) {
    // Steps 1-2.
    if (this == null) {
      throw new TypeError('this is null or not defined');
    }

    var O = Object(this);

    // Steps 3-5.
    var len = O.length >>> 0;

    // Steps 6-8.
    var relativeTarget = target >> 0;

    var to = relativeTarget < 0 ?
      Math.max(len + relativeTarget, 0) :
      Math.min(relativeTarget, len);

    // Steps 9-11.
    var relativeStart = start >> 0;

    var from = relativeStart < 0 ?
      Math.max(len + relativeStart, 0) :
      Math.min(relativeStart, len);

    // Steps 12-14.
    var relativeEnd = end === undefined ?
      len : end >> 0;

    var final = relativeEnd < 0 ?
      Math.max(len + relativeEnd, 0) :
      Math.min(relativeEnd, len);

    // Step 15.
    var count = Math.min(final - from, len - to);

    // Steps 16-17.
    var direction = 1;

    if (from < to && to < (from + count)) {
      direction = -1;
      from = from + count - 1;
      to = to + count - 1;
    }

    // Step 18.
    while (count > 0) {
      if (from in O) {
        O[to] = O[from];
      } else {
        delete O[to];
      }
      from = from + direction;
      to = to + direction;
      count--;
    }

    // Step 19.
    return O;
  };
}

这个 Polyfill 实现了 copyWithin() 的基本功能,可以在不支持 copyWithin() 的浏览器中使用。

九、总结

好了,关于 Array.prototype.copyWithin() 的讲解就到这里。 总结一下:

  • copyWithin() 可以在数组内部复制元素。
  • 它会改变原数组。
  • 它执行的是浅拷贝。
  • 要注意参数的用法和边界情况。
  • 在特定场景下,它可以简化代码,提高效率。

希望通过今天的讲解,你对 copyWithin() 有了更深入的了解。 在实际开发中,可以根据具体的需求,灵活运用 copyWithin(),让你的代码更加简洁高效。

感谢各位的观看,下次再见!

发表回复

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