Найшвидший спосіб дублювання масиву в JavaScript - зріз проти циклу "for"


634

Для дублювання масиву в JavaScript: що з наведеного нижче використовувати швидше?

Метод зрізів

var dup_array = original_array.slice();

For петля

for(var i = 0, len = original_array.length; i < len; ++i)
   dup_array[i] = original_array[i];

Я знаю, що обидва способи роблять лише дрібну копію : якщо original_array містить посилання на об’єкти, об’єкти не будуть клоновані, а будуть скопійовані лише посилання, і тому обидва масиви матимуть посилання на ті самі об’єкти. Але це не сенс цього питання.

Я питаю лише про швидкість.


3
jsben.ch/#/wQ9RU <= орієнтир для найпоширеніших способів клонування масиву
EscapeNetscape

Відповіді:


776

Існує щонайменше 5 (!) Способів клонування масиву:

  • петля
  • скибочку
  • Array.from ()
  • конкат
  • оператор розповсюдження (ШВИДКО)

Була знайдена низька нитка BENCHMARKS , яка надає таку інформацію:

  • для браузерів- блимачівslice() - це найшвидший метод, concat()трохи повільніше і while loopна 2,4 рази повільніше.

  • для інших браузерів while loop- це найшвидший метод, оскільки ці браузери не мають внутрішньої оптимізації для sliceта concat.

Це залишається вірним у липні 2016 року.

Нижче наведено прості сценарії, які можна скопіювати та вставити у консоль браузера та запустити кілька разів, щоб побачити зображення. Вони виводять мілісекунди, нижче краще.

поки петля

n = 1000*1000;
start = + new Date();
a = Array(n); 
b = Array(n); 
i = a.length;
while(i--) b[i] = a[i];
console.log(new Date() - start);

скибочку

n = 1000*1000;
start = + new Date();
a = Array(n); 
b = a.slice();
console.log(new Date() - start);

Зверніть увагу, що ці методи будуть клонувати сам об’єкт масиву, проте вміст масиву копіюється за допомогою посилання та не є глибоким клонуванням.

origAr == clonedArr //returns false
origAr[0] == clonedArr[0] //returns true

48
@ Cept0 ніяких емоцій, лише контрольні показники jsperf.com/new-array-vs-splice-vs-slice/31
Dan

2
@Dan Так що? Результати ваших тестових випадків: Firefox 30 щоночі все ще на 230% швидше, ніж Chrome. Перевірте вихідний код V8 на, spliceі ви здивуєтесь (поки ...)
mate64

4
На жаль, для коротких масивів відповідь сильно відрізняється . Наприклад, клонування маси слухачів, перш ніж викликати кожного з них. Ці масиви часто невеликі, як правило, 1 елемент.
gman

6
Ви пропустили цей метод:A.map(function(e){return e;});
wcochran

13
Ви пишете про браузери- блимання . Чи не блимає лише двигун компонування, головним чином впливаючи на перегляд HTML, і, таким чином, неважливо? Я думав, що ми швидше поговоримо про V8, Spidermonkey та друзів. Просто річ, яка мене бентежила. Просвіти мене, якщо я помиляюся.
Неоніт

241

Технічно slice це найшвидший спосіб. Однак це ще швидше, якщо додати 0індекс початку.

myArray.slice(0);

швидше, ніж

myArray.slice();

http://jsperf.com/cloning-arrays/3


І myArray.slice(0,myArray.length-1);швидше, ніж myArray.slice(0);?
jave.web

1
@ jave.web you; Ви щойно скинули останній елемент масиву. Повна копія - array.slice (0) або array.slice (0, array.length)
Марек Марчак

137

що про es6 спосіб?

arr2 = [...arr1];

23
якщо конвертується з дівою:[].concat(_slice.call(arguments))
CHAN

1
Не впевнений, звідки argumentsберуться ... Я думаю, що у ваших бабельних випусків виникає кілька різних особливостей. Це скоріше arr2 = [].concat(arr1).
Стерлінг Арчер

3
@SterlingArcher arr2 = [].conact(arr1)відрізняється від arr2 = [...arr1]. [...arr1]синтаксис перетворить дірку в undefined. Наприклад, arr1 = Array(1); arr2 = [...arr1]; arr3 = [].concat(arr1); 0 in arr2 !== 0 in arr3.
tsh

1
Я перевірив це у своєму браузері (Chrome 59.0.3071.115) на відповідь Дана вище. Це було в 10 разів повільніше, ніж .slice (). n = 1000*1000; start = + new Date(); a = Array(n); b = [...a]; console.log(new Date() - start); // 168
Гаррі Стівенс

1
Тим НЕ менше , НЕ буде клоном що - щось на зразок цього: [{a: 'a', b: {c: 'c'}}]. Якщо cзначення 'зміниться у "дублюваному" масиві, воно зміниться у вихідному масиві, оскільки це просто референсна копія, а не клон.
Нейромедіатор

44

Найпростіший спосіб глибокого клонування масиву чи об’єкта:

var dup_array = JSON.parse(JSON.stringify(original_array))

56
Важлива примітка для початківців: оскільки це залежить від JSON, це також успадковує його обмеження. Крім усього іншого, це означає, що ваш масив не може містити undefinedабо будь-який functions. Обидва ці програми будуть перетворені на nullвас під час JSON.stringifyпроцесу. Інші стратегії, такі як (['cool','array']).slice()не змінять їх, але також не заглиблюють об'єкти в масив. Тож відбувається компроміс.
Сет Холладай

27
Дуже погано Perf і не працюють зі спеціальними об'єктами, такими як DOM, дата, regexp, функція ... або з прототипами. Не підтримуйте циклічні посилання. Ніколи не слід використовувати JSON для глибокого клонування.
Yukulélé

17
найгірший можливий спосіб! Використовуйте лише, якщо з якоїсь проблеми всі інші не працюють. Це повільно, це ресурсомісткість, і він має всі обмеження JSON, вже згадані в коментарях. Не уявляю, як він отримав 25 голосів.
Лукас Ліесіс

2
Він глибоко копіює масиви з примітивами, а де властивості - це масиви з подальшими примітивами / масивами. Для цього це нормально.
Дренай

4
Я перевірив це у своєму браузері (Chrome 59.0.3071.115) на відповідь Дана вище. Це було майже в 20 разів повільніше, ніж .slice (). n = 1000*1000; start = + new Date(); a = Array(n); var b = JSON.parse(JSON.stringify(a)) console.log(new Date() - start); // 221
Гаррі Стівенс

29
var cloned_array = [].concat(target_array);

3
Поясніть, будь ласка, що це робить.
Джед Фокс

8
Хоча цей фрагмент коду може відповісти на запитання, він не містить жодного контексту, щоб пояснити, як і чому. Подумайте про додавання речення чи двох, щоб пояснити свою відповідь.
брендоскрипт

32
Я ненавиджу подібні коментарі. Очевидно, що це робить!
EscapeNetscape

6
Проста відповідь на прості запитання, без великої історії для читання. Мені подобаються такі відповіді +1
Ахім

15
"Я запитую лише про швидкість" - Ця відповідь не вказує на швидкість. Це головне запитання. Брендоскрипт має хороший момент. Для розгляду цієї відповіді потрібно більше інформації. Але якби це було простіше питання, це була б чудова відповідь.
TamusJRoyce

26

Я зібрав швидку демонстрацію: http://jsbin.com/agugo3/edit

Мої результати в Internet Explorer 8 - це 156, 782 та 750, що вказує на те slice, що в цьому випадку набагато швидше.


Не забувайте про додаткову вартість сміттєзбірника, якщо вам доведеться це зробити дуже швидко. Я копіював кожен сусідній масив для кожної комірки в своїх стільникових автоматах за допомогою фрагмента, і це було набагато повільніше, ніж повторне використання попереднього масиву та копіювання значень. Chrome зазначив, що приблизно 40% всього часу було витрачено на збирання сміття.
drake7707

21

a.map(e => e)є ще однією альтернативою для цієї роботи. На сьогоднішній день .map()це дуже швидко (майже так само швидко .slice(0)), як у Firefox, але не в Chrome.

З іншого боку, якщо масив є багатовимірним, оскільки масиви - це об'єкти, а об'єкти - еталонні типи, жоден із методів зрізування або конматів не буде лікуванням ... Отже, один правильний спосіб клонування масиву - це винахід Array.prototype.clone()як випливає.

Array.prototype.clone = function(){
  return this.map(e => Array.isArray(e) ? e.clone() : e);
};

var arr = [ 1, 2, 3, 4, [ 1, 2, [ 1, 2, 3 ], 4 , 5], 6 ],
    brr = arr.clone();
brr[4][2][1] = "two";
console.log(JSON.stringify(arr));
console.log(JSON.stringify(brr));


Непогано, але, на жаль, це не працює, якщо у вашому масиві Object: \ JSON.parse (JSON.stringify (myArray)) працює в цьому випадку краще.
GBMan

17

Найшвидший спосіб клонування масиву

Я зробив цю дуже просту функцію утиліти, щоб перевірити час, який потрібен для клонування масиву. Він не на 100% надійний, однак може дати вам групове уявлення про те, скільки часу потрібно для клонування існуючого масиву:

function clone(fn) {
    const arr = [...Array(1000000)];
    console.time('timer');
    fn(arr);
    console.timeEnd('timer');
}

І перевірено інший підхід:

1)   5.79ms -> clone(arr => Object.values(arr));
2)   7.23ms -> clone(arr => [].concat(arr));
3)   9.13ms -> clone(arr => arr.slice());
4)  24.04ms -> clone(arr => { const a = []; for (let val of arr) { a.push(val); } return a; });
5)  30.02ms -> clone(arr => [...arr]);
6)  39.72ms -> clone(arr => JSON.parse(JSON.stringify(arr)));
7)  99.80ms -> clone(arr => arr.map(i => i));
8) 259.29ms -> clone(arr => Object.assign([], arr));
9) Maximum call stack size exceeded -> clone(arr => Array.of(...arr));

ОНОВЛЕННЯ :
Примітка. З усіх них єдиний спосіб глибокого клонування масиву - це використання JSON.parse(JSON.stringify(arr)).

Однак, не використовуйте вище, якщо ваш масив може включати функції, оскільки він повернеться null.
Дякую @GilEpshtain за це оновлення .


2
Я спробував зіставити
mesqueeb

@mesqueeb, тести можуть змінюватися, залежно від курсу ур-машини. Однак не соромтеся оновити відповідь результатом свого тесту. Хороша робота!
Ліор

Мені дуже подобається ваша відповідь, проте я спробую ваш тест і отримаю arr => arr.slice()це найшвидше.
Гіл Епштейн

1
@LiorElrom, ваше оновлення не є правильним через те, що методи не піддаються серіалізації. Наприклад: JSON.parse(JSON.stringify([function(){}]))вийде[null]
Гіл Епштейн

1
Хороший орієнтир. Я перевірив це на моєму Mac в 2 браузерах: Chrome версії 81.0.4044.113 і Safari версії 13.1 (15609.1.20.111.8) і швидким є операція поширення: [...arr]з 4.653076171875msв Chrome і 8.565msв Safari. Другий швидкий в Chrome є функція скибочки arr.slice()з 6.162109375msі в Safari другий знаходиться [].concat(arr)з 13.018ms.
edufinn

7

Подивіться: посилання . Йдеться не про швидкість, а про комфорт. Крім того, як ви бачите, ви можете використовувати лише фрагмент (0) лише для примітивних типів .

Щоб зробити незалежну копію масиву, а не копію рефлексії до нього, можна використовувати метод фрагмента масиву.

Приклад:

Щоб зробити незалежну копію масиву, а не копію рефлексії до нього, можна використовувати метод фрагмента масиву.

var oldArray = ["mip", "map", "mop"];
var newArray = oldArray.slice();

Щоб скопіювати або клонувати об’єкт:

function cloneObject(source) {
    for (i in source) {
        if (typeof source[i] == 'source') {
            this[i] = new cloneObject(source[i]);
        }
        else{
            this[i] = source[i];
  }
    }
}

var obj1= {bla:'blabla',foo:'foofoo',etc:'etc'};
var obj2= new cloneObject(obj1);

Джерело: посилання


1
Коментар примітивних типів також стосується forциклу у питанні.
користувач113716

4
якби я копіював масив об’єктів, я б очікував, що новий масив посилатиметься на ті самі об’єкти, а не клонує об’єкти.
lincolnk

7

Як @Dan сказав: "Ця відповідь швидко застаріла. Використовуйте орієнтири для перевірки фактичної ситуації", є одна конкретна відповідь від jsperf, яка не мала для себе відповіді: while :

var i = a.length;
while(i--) { b[i] = a[i]; }

мав 960,589 ops / sec при запуску a.concat()при 578,129 ops / sec, що становить 60%.

Це останній 64-бітний Firefox (40).


@aleclarson створив новий, більш надійний орієнтир.


1
Вам дійсно слід зв’язати jsperf. Той, про кого ви думаєте, зламаний, тому що новий масив створюється у кожному тестовому випадку, за винятком тесту "while loop".
aleclarson

1
Я створив новий jsperf, який є більш точним: jsperf.com/clone-array-3
aleclarson

60% що? На 60% швидше?
Пітер Мортенсен

1
@PeterMortensen: 587192 становить ~ 60% (61,1 ...) з 960589.
серв-ін

7

ECMAScript 2015 спосіб з Spreadоператором:

Основні приклади:

var copyOfOldArray = [...oldArray]
var twoArraysBecomeOne = [...firstArray, ..seccondArray]

Спробуйте в консолі браузера:

var oldArray = [1, 2, 3]
var copyOfOldArray = [...oldArray]
console.log(oldArray)
console.log(copyOfOldArray)

var firstArray = [5, 6, 7]
var seccondArray = ["a", "b", "c"]
var twoArraysBecomOne = [...firstArray, ...seccondArray]
console.log(twoArraysBecomOne);

Список літератури


Напевно, єдине, що швидко розповсюджується - це набрати його. Це waaay менш ефективно, ніж інші способи цього зробити.
XT_Nova

3
Надайте, будь ласка, кілька посилань щодо вашої аргументації.
Marian07

6

Це залежить від браузера. Якщо ви подивитесь на повідомлення в блозі Array.prototype.slice vs створення вручну масиву , є орієнтовний посібник щодо виконання кожного з них:

Введіть тут опис зображення

Результати:

Введіть тут опис зображення


1
argumentsне є належним масивом, і він використовує, callщоб змусити sliceпрацювати на колекції. результати можуть вводити в оману.
lincolnk

Так, я хотів сказати, що у своєму дописі ця статистика, ймовірно, зміниться зараз, коли броузери покращаться, але це дає загальне уявлення.
kyndigs

2
@diugalde Я думаю, що єдиною ситуацією, коли розміщення коду як зображення є прийнятним, є те, коли код є потенційно небезпечним і не повинен бути вставлений під копію. Але в цьому випадку це досить смішно.
Флоріан Вендельборн

6

Є набагато більш чисте рішення:

var srcArray = [1, 2, 3];
var clonedArray = srcArray.length === 1 ? [srcArray[0]] : Array.apply(this, srcArray);

Перевірка довжини потрібна, тому що Arrayконструктор поводиться інакше, коли він викликається точно одним аргументом.


2
Але це найшвидше?
Кріс Вессілінг

14
Більш смисловий, ніж splice(), можливо. Але дійсно, застосуйте, і це все, але не інтуїтивно.
Майкл Піефель

показує найповільнішу виставу на chrome- jsperf.com/new-array-vs-splice-vs-slice/113
chrismarx

3
Ви можете використовувати Array.ofта ігнорувати довжину:Array.of.apply(Array, array)
Oriol

6

Пам'ятайте .slice () не працюватиме для двовимірних масивів. Вам знадобиться така функція:

function copy(array) {
  return array.map(function(arr) {
    return arr.slice();
  });
}

3
У Javascript немає двовимірних масивів. Є просто масиви, що містять масиви. Те, що ви намагаєтеся зробити, - це глибока копія яка не потрібна у питанні.
Aloso

5

Це залежить від довжини масиву. Якщо довжина масиву <= 1 000 000, то sliceі concatметоди займають приблизно однаковий час. Але коли ви даєте ширший діапазон, concatметод виграє.

Наприклад, спробуйте цей код:

var original_array = [];
for(var i = 0; i < 10000000; i ++) {
    original_array.push( Math.floor(Math.random() * 1000000 + 1));
}

function a1() {
    var dup = [];
    var start = Date.now();
    dup = original_array.slice();
    var end = Date.now();
    console.log('slice method takes ' + (end - start) + ' ms');
}

function a2() {
    var dup = [];
    var start = Date.now();
    dup = original_array.concat([]);
    var end = Date.now();
    console.log('concat method takes ' + (end - start) + ' ms');
}

function a3() {
    var dup = [];
    var start = Date.now();
    for(var i = 0; i < original_array.length; i ++) {
        dup.push(original_array[i]);
    }
    var end = Date.now();
    console.log('for loop with push method takes ' + (end - start) + ' ms');
}

function a4() {
    var dup = [];
    var start = Date.now();
    for(var i = 0; i < original_array.length; i ++) {
        dup[i] = original_array[i];
    }
    var end = Date.now();
    console.log('for loop with = method takes ' + (end - start) + ' ms');
}

function a5() {
    var dup = new Array(original_array.length)
    var start = Date.now();
    for(var i = 0; i < original_array.length; i ++) {
        dup.push(original_array[i]);
    }
    var end = Date.now();
    console.log('for loop with = method and array constructor takes ' + (end - start) + ' ms');
}

a1();
a2();
a3();
a4();
a5();

Якщо встановити довжину original_array в 1 000 000, sliceметод іconcat спосіб займають приблизно однаковий час (3-4 мс, залежно від випадкових чисел).

Якщо встановити довжину original_array в 10 000 000, то slice метод займає понад 60 мс, а concatметод - понад 20 мс.


dup.pushнеправильно a5, замість цього dup[i] = слід використовувати
4esn0k


2
        const arr = ['1', '2', '3'];

         // Old way
        const cloneArr = arr.slice();

        // ES6 way
        const cloneArrES6 = [...arr];

// But problem with 3rd approach is that if you are using muti-dimensional 
 // array, then only first level is copied

        const nums = [
              [1, 2], 
              [10],
         ];

        const cloneNums = [...nums];

// Let's change the first item in the first nested item in our cloned array.

        cloneNums[0][0] = '8';

        console.log(cloneNums);
           // [ [ '8', 2 ], [ 10 ], [ 300 ] ]

        // NOOooo, the original is also affected
        console.log(nums);
          // [ [ '8', 2 ], [ 10 ], [ 300 ] ]

Тож, щоб уникнути подібних сценаріїв, використовуйте

        const arr = ['1', '2', '3'];

        const cloneArr = Array.from(arr);

Дійсно вказати на те, як зміна cloneNums[0][0]у вашому прикладі поширювала зміни на nums[0][0]- але це тому, що nums[0][0]фактично є об'єктом, на який посилається скопійований cloneNumsоператором розповсюдження. Все, що означає, така поведінка не вплине на код, куди ми копіюємо за значенням (int, string та літерали).
Депутат Адітія

1

Орієнтовний час!

function log(data) {
  document.getElementById("log").textContent += data + "\n";
}

benchmark = (() => {
  time_function = function(ms, f, num) {
    var z = 0;
    var t = new Date().getTime();
    for (z = 0;
      ((new Date().getTime() - t) < ms); z++)
      f(num);
    return (z)
  }

  function clone1(arr) {
    return arr.slice(0);
  }

  function clone2(arr) {
    return [...arr]
  }

  function clone3(arr) {
    return [].concat(arr);
  }

  Array.prototype.clone = function() {
    return this.map(e => Array.isArray(e) ? e.clone() : e);
  };

  function clone4(arr) {
    return arr.clone();
  }


  function benchmark() {
    function compare(a, b) {
      if (a[1] > b[1]) {
        return -1;
      }
      if (a[1] < b[1]) {
        return 1;
      }
      return 0;
    }

    funcs = [clone1, clone2, clone3, clone4];
    results = [];
    funcs.forEach((ff) => {
      console.log("Benchmarking: " + ff.name);
      var s = time_function(2500, ff, Array(1024));
      results.push([ff, s]);
      console.log("Score: " + s);

    })
    return results.sort(compare);
  }
  return benchmark;
})()
log("Starting benchmark...\n");
res = benchmark();

console.log("Winner: " + res[0][0].name + " !!!");
count = 1;
res.forEach((r) => {
  log((count++) + ". " + r[0].name + " score: " + Math.floor(10000 * r[1] / res[0][1]) / 100 + ((count == 2) ? "% *winner*" : "% speed of winner.") + " (" + Math.round(r[1] * 100) / 100 + ")");
});
log("\nWinner code:\n");
log(res[0][0].toString());
<textarea rows="50" cols="80" style="font-size: 16; resize:none; border: none;" id="log"></textarea>

Еталон буде працювати протягом 10 секунд після натискання на кнопку.

Мої результати:

Chrome (двигун V8):

1. clone1 score: 100% *winner* (4110764)
2. clone3 score: 74.32% speed of winner. (3055225)
3. clone2 score: 30.75% speed of winner. (1264182)
4. clone4 score: 21.96% speed of winner. (902929)

Firefox (SpiderMonkey Engine):

1. clone1 score: 100% *winner* (8448353)
2. clone3 score: 16.44% speed of winner. (1389241)
3. clone4 score: 5.69% speed of winner. (481162)
4. clone2 score: 2.27% speed of winner. (192433)

Код переможця:

function clone1(arr) {
    return arr.slice(0);
}

Двигун-переможець:

SpiderMonkey (Mozilla / Firefox)


1

Швидкі способи дублювання масиву в JavaScript в порядку:

#1: array1copy = [...array1];

#2: array1copy = array1.slice(0);

#3: array1copy = array1.slice();

Якщо об’єкти масиву містять деякий JSON-не серіалізаційний вміст (функції, номер.POSITIVE_INFINITY тощо), краще використовувати

array1copy = JSON.parse(JSON.stringify(array1))


0

Ви можете слідувати цьому коду. Клон безперервного способу. Це ідеальний спосіб клонування масиву


const array = [1, 2, 3, 4]

const newArray = [...array]
newArray.push(6)
console.log(array)
console.log(newArray)

0

В ES6 можна просто використовувати синтаксис Spread .

Приклад:

let arr = ['a', 'b', 'c'];
let arr2 = [...arr];

Зауважте, що оператор розповсюдження генерує абсолютно новий масив, тому зміна одного не вплине на іншого.

Приклад:

arr2.push('d') // becomes ['a', 'b', 'c', 'd']
console.log(arr) // while arr retains its values ['a', 'b', 'c']
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.