Как стать автором
Обновить

Комментарии 76

Это гораздо лучше, чем перебирать массив с помощью цикла for.

Серьезно? Клубника гораздо лучше малины, красный цвет гораздо лучше зеленого и т.п.
Очевидно же, что перебрать массив двумя циклами гораздо лучше, чем одним.
Автор, как мне кажется, сравнивает тёплое с мягким(массивы со списками).
Arrays are list-like objects whose prototype has methods to perform traversal and mutation operations. Neither the length of a JavaScript array nor the types of its elements are fixed.


Приэтом, даже в С, циклом for возможность итерации не ограничена.
Как я понял, Вы тоже с нетерпением ждёте продолжение про Map и Set в JavaScript?
Ооочень много неточностей. Прям безобразно много.
Не всякий примитивный тип может быть ключом в PHP. Например bool и float преобразуются в int. Некоторые цифросодержащие строки будут преобразованы к int.
Порядок элементов при переборе в ассоциативном массиве в PHP будет соответ соответствовать порядку добавления, в JS порядок ключей объекта независим.
Поиск по значению через array_flip не учитывает сложности самого вызова array_flip.
в JS порядок ключей объекта независим

Нет. Вернее, уже нет.
Я, проводя собеседования, обнаружил, что достаточно работать в HR отделе, чтобы создалось впечатление, будто я знаю, что такое массивы.
let sum = 0;
for (i = 0; i < fibonacci.length; i++) {
  sum += fibonacci[i];
}


Однако у нас нет необходимости в использовании такого подхода к перебору JS-массивов. Например, тут имеются ненужные промежуточные переменные


Если использовать метод .reduce вместо цикла for то точно также создадутся промежуточные переменные только неявно внутри движка javascript. И точно также можно наступить на те же грабли. стоит использовать встроенные методы вместо циклов но по другой причине. Так код становится чище и понятнее для разработчика.
Вы не можете (и не должны) знать, во что ваш код превращается внутри движка JavaScript. Движков, как минимум, несколько, они между собой конкурируют производительностью и микрооптимизациями, и каждая новая версия оптимизирует один и тот же код по-разному. В настоящее время методы .reduce, .filter, .map могут работать быстрее ванильного цикла for, хотя, казалось бы, вызывая три этих метода, мы запускаем три цикла. Но движок преобразовывает его в один.
Вы должны просто писать чистый и понятный код, а оптимизацию предоставить интерпретатору.
В настоящее время методы .reduce, .filter, .map могут работать быстрее ванильного цикла for, хотя, казалось бы, вызывая три этих метода, мы запускаем три цикла. Но движок преобразовывает его в один.

Хм… А это где-то задокументировано? Об этом писали в блоге V8 или ещё где-нибудь?

Зачем вам это в документации? Это проверяется самостоятельно и должно всегда проверяться самостоятельно, без слепого доверия документации и/или статей, комментаторов и т.п.
Вот:
codesandbox.io/s/fervent-colden-cdqwu?file=/src/index.js

Смотришь пример и казалось бы, да разницы нет, у всех одна скорость практически, но в codesandbox ограничение на циклы, теперь берем этот код где уже изначальный массив больше, вставляем в консоль браузера и результаты становятся совсем другие. И для большей наглядности в разницы в движках вставьте этот же код в Firefox, результаты там противоположные.
const arr = [];

for (let i = 0; i < 1000000; i++) {
  arr.push({
    index: i
  });
}

function test1() {
  const result = [];
  for (const item of arr) {
    if (item.index > 999990) result.push(item);
  }

  return result;
}

function test2() {
  return arr.filter(item => item.index > 999990);
}

function test3() {
  const len = arr.length;
  const result = [];
  for (let i = 0; i < len; i++) {
    const item = arr[i];
    if (item.index > 999990) result.push(item);
  }

  return result;
}

console.time("test1 #1");
const res1 = test1();
console.timeEnd("test1 #1");

console.time("test1 #2");
const res2 = test1();
console.timeEnd("test1 #2");

console.time("test2 #1");
const res3 = test2();
console.timeEnd("test2 #1");

console.time("test2 #2");
const res4 = test2();
console.timeEnd("test2 #2");

console.time("test3 #1");
const res5 = test3();
console.timeEnd("test3 #1");

console.time("test3 #2");
const res6 = test3();
console.timeEnd("test3 #2");

Зачем вам это в документации? Это проверяется самостоятельно и должно всегда проверяться самостоятельно, без слепого доверия документации и/или статей, комментаторов и т.п.

Затем что benchmark-и это минное поле. Там слишком много нюансов, чтобы доверять их результатам. Про это был уже не 1 материал. Самый простой способ обмануть самого себя это сделать бенчмарк.

Про это был уже не 1 материал.

А что если завтра вы увидите материал в котором все в точности да наоборот? Измените свое мнение?) Или выберете для себя чей «авторитет» материала будет перевешивать?)

Самый простой способ обмануть самого себя это сделать бенчмарк.

Ну смотрите, допустим у вас есть некие вводные данные и нужно их преобразовать, это можно сделать разными способами разумеется и это узкое место в вашем приложении по производительности, как вы думаете как с этим борются?

1) Вариант:
Делают все возможные варианты реализации преобразования и выбирают тот, который будет работать быстрее всего.

2) Вариант:
Посмотрят документации или статье где говорят что условный .map .reduce или что угодно допустим быстрее чем for или там do while, не суть, и опираясь на это пишем код и пусть оно работает так, ведь я же это прочитал в «авторитетном источнике», а значит это правда и конечно же применимо для моего конкретного случая, и будет быстро и эффективно работать.

Лично я всегда иду по варианту номер 1, и надеюсь подавляющее большинство тоже. Вы я так понимаю приверженец варианта 2?
А что если завтра вы увидите материал в котором все в точности да наоборот?

Ну если сегодня в v8-блоге напишут одно, а завтра другое, то вероятно вторая статья будет начинаться с: ранее мы сделали Х, потом выяснилось что это плохо работает в условиях Y, поэтому теперь мы сделали Z. Мне было бы такое интересно почитать. Например у нас была оптимизация хвостовой рекурсии. Была.


Или выберете для себя чей «авторитет» материала будет перевешивать?)

Ну определённо ребята работающие над V8 имеют для меня более высокий приоритет, чем рядовой разработчик. Особенно учитывая страшно низкое качество статей в нашей области.


Вы я так понимаю приверженец варианта 2?

Нет я сторонник варианта 3: просто пишу как так, чтобы потом с ним было комфортно работать. Крайне редко в нашей области нужно гнаться за производительностью на этом уровне. Я знаю много оптимизационных фишек на уровне языка. Но не пользуюсь ими почти никогда.


Возвращаясь к моему вопросу. Я нисколько не спорю с вот этим:


Вы должны просто писать чистый и понятный код, а оптимизацию предоставить интерпретатору

Мне просто интересно, откуда вот эта информация:


Но движок преобразовывает его в один.

Не из каких бенчмарков такой вывод сделать нельзя, т.к. даже если какой-то код окажется быстрее, вы никогда наверняка не знаете, за счёт чего конкретно он быстрее. А домыслы такие домыслы.


В общем мною тут движет именно любопытство.

Нет я сторонник варианта 3: просто пишу как так, чтобы потом с ним было комфортно работать

Так это и так понятно и все так делают, и я так делаю, потому что в реальной жизни словить проблемы с производительности крайне редкий случай, но все же я описал специально кейс где есть реальные проблемы с производительностью и их надо решать. Исходя из ваших суждений, я делаю вывод, что вы либо вообще это проигнорируете и забьете, либо выберете вариант 2 и на этом ваша совесть будет чиста) Т.к. первый вариант для вас не подходит однозначно, потому что вы читали какие-то статьи про бенчмарки, и оптимизировать свой код методом проб и ошибок, а именно делать разные реализации одного и того же и замерять скорость(бенчмарк) это минное поле.
В моей практике было много случаев оптимизации SQL запросов, где в документациях и рекомендациях говорится одно, а на деле все обстоит совсем иначе и мои запросы начинали выполнятся в 5-10 раз быстрее, нежели запросы по рекомендациям. Я бы с удовольствием привел конкретные примеры, которые можно воспроизвести самому и убедится, но уже года 4 на бэке не пишу код, конкретика улетучилась из памяти.
Мораль такова, есть такая штука называется «критическое мышление», оно присуще не всем, поэтому часть людей доверяет статьям и т.п., а вторая часть ставит под сомнение, проверяет и доверяет лишь своим подтвержденным результатам.

Вы напридумывали себе каких-то глупостей про меня. Устроили тут какой-то непонятный спор. Пытаетесь мне что-то доказать. Я всё никак не пойму зачем оно вам надо. Я ведь уже чёрным по белому написал, мне любопытно правда ли JS-движки (или хотя бы 1 движок) преобразуют связку .filter .map .reduce в 1 цикл или нет. Судя по комментарию от pharrell, он где-то об этом читал.


Да мне интересно как v8 устроен изнутри. Да и другие движки тоже. Я надеюсь я поставил на этом странном "споре" точку.

Затем что benchmark-и это минное поле. Там слишком много нюансов, чтобы доверять их результатам. Про это был уже не 1 материал. Самый простой способ обмануть самого себя это сделать бенчмарк.

Вот же ваше утверждение, или вы от него теперь открещиваетесь?

Я просто привел конкретный пример, что вот у нас есть функция, которая работает медленно(скажем 12 секунд) и есть две стратегии как сделать так, чтобы она работало максимально быстро на сколько это возможно и спросил какой стратегии придерживаетесь вы. Вы же просто увиливаете от ответа.
Вот же ваше утверждение, или вы от него теперь открещиваетесь?

Оно в чём-то неправдиво чтобы мне от него открещиваться? Бенчмарки это очень скользкая тема.


Вы же просто увиливаете от ответа.

Потому что сам спор мне кажется в высшей степени нелепым. И самое главное он вообще не к месту. Зачем вы вообще его начали? Нет у вас данных по вопросу — ну пройдите мимо. Ещё так много народу не уверовало в MobX, ещё столько работы впереди. Причём тут я?


Если мне в какой-то момент необходимо написать предельно производительный код на JS, то я применю сразу оба подхода:


  • я учту то что читал про это ранее
  • и проведу ряд экспериментов с замерами
  • буду готов в последствии поменять решение на другое, если выяснится, что мои замеры были некорректными
  • возможно посоветуюсь с более опытными товарищами на случай, что я мог упустить

Я надеюсь теперь я ответил на ваш вопрос?

и проведу ряд экспериментов с замерами

Не стыкуется с
benchmark-и это минное поле

Ну да ладно, можете больше не отвечать) Я сделал свои выводы) Мне было интересно как вы ответите на это)
Не стыкуется с

Очень даже стыкуется. Это когда ты пишешь замеры, но с грустью на лице, понимая, что сейчас у тебя одни результаты. А в бою могут быть другими. Или скажем в бою через полгода при обновлении nodejs на более новую версию, они станут другими. Или вообще как было с оптимизацией хвостовой рекурсии — просто начнёт всё падать :-D

Как вы думаете, каких стандартных возможностей больше всего не хватает JavaScript-массивам?

Что-нибудь вместо list[list.length - 1].

Да, last точно не хватает, есть конечно .slice(-1)[0], но это тоже как-себе решение.

Немножко покороче, но тоже не очень читаемо. Плюс промежуточно создается ненужный массив...


Можно как-нибудь так:


const last = Symbol('last')
Array.prototype[last] = function() {
    return this[this.length - 1];
}

Но хотелось бы все-таки прям в языке.

А зачем такая хитрая конструкция с
Symbol('last')
?
Ее ведь невозможно использовать, если нет доступа к переменной last.

Ну Symbol — чтобы не поломать прототип.
А переменную last либо экспортировать, либо использовать Symbol.for.

И тут я наконец понял практический смысл Symbol

Даже лучше вот так, наверное:


export const last = Symbol('last');

Object.defineProperty(Array.prototype, last, {
  enumerable: false,
  configurable: false,
  get() {
      return this[this.length - 1];
  }
});

// console.assert([1,2,3][last] === 3);
// console.assert([][last] === undefined);

Прошло всего-то пару лет, и у нас есть .at(-1).

Массивы и их родной язык — C

Если мне не изменяет память, Pascal появился немного раньше Си. И массивы там были. Там строки даже являются массивами типа Char.
И, кстати, именно Pascal в большей степени повлиял на JavaScript. У них структура кода более схожа, чем у JS и Си.
Массивы (правда, только одномерные) были ещё в Алгол 60.
Одно дело — умение писать код. И совершенно другое — понимание внутренних механизмов используемых языков.


После такой серьезной заявки я ожидал чего-то похардкорней: внутреннее представление, как аллоцируется память, какие-нибудь хитрые оптимизации :)

В любом случае было интересно задуматься над этим вопросом, обратить внимание. Возможно следующие строчки кода будут более обдуманно писаться :)

В этом и соль! В сях я примерно представляю как это все хранится в памяти и как примерно обрабатывается, а вот в PHP или JS не имею такого понимания. Жаль, что статья не об этом. Точнее об этом, но не так глубоко, как я ожидал

А автор точно знает как перебирать массивы? После такого


for (i = 0; i < 9; i++) {


для массива из 10 элементов в этом очень большие сомнения.

foreach расслабляет )

Компьютер вообще расслабляет. То ли дело раньше, когда его не было.

У автора конец рабочего дня, а он еще дневной план по буквам не выполнил.
Намешал массивы со списками, статическую типизацию с динамической, накосячил в примерах…
А куда деваться? Х… як Раз-раз — и в продакшн.

А в PHP легко сделать так, чтобы временная сложность такой же операции составила бы O(1):


Это вы шутите так? А то, что array_flip сам по себе как минимум линеен (это я даже в доки не смотрел, а чисто по абстрактной сложности: вставка в хэш-таблицу в среднем O(1), повторить N раз => в среднем O(N), а в худшем квадратично будет).
Не говоря уж про то, что в C++ массив в хэш-таблицу «превратить» ровно также легко, и это вообще никаким боком к «разнице языков» не относится.

В общем да, ожидал интересную статью с деталями, а оказалось ни о чем да еще с кучей фактических ошибок.
…а уж если разобраться, что у подобных фич под капотом…
Не очень понял, что имелось в виду. Если хэш-таблица как структура под капотом, то ассимптотические сложности известны вне зависимости от реализации (разве что реализация алгоритмически хуже, но мы же не про студенческие лабы говорим, а про продакшн код).

Вот, к слову, наглядное сравнение gist.github.com/ksimka/21a6ff74b41451c430e8 — использование array_flip для поиска *на порядок* хуже чем обычного поиска.
Дак вот о том и речь.
Если один раз флипнуть, а потом тысячу раз искать — это одно. А если флипнуть-искать, флипнуть-искать — совсем другое.
КМК, использование in_array все же более жизнеспособно.
В данном случае проще когда ключ равен значению (Естественно если можно флипнуть без последствий)
Есть интереснее вопрос: В чём особенности null в JS, PHP и SQL?
В PHP доступ по индексу null к сущностям, не предусматривающим такой доступ (например null, boolean, объект класса, не реализующего ArrayAccess), выдает null, правда с PHP 7.4 будет Notice. JS в таком случает выдаст TypeError.
А как насчёт null or true?
Статья откровенно плохая с кучей фактических ошибок и религиозных советов на тему: как автору больше нравится работать с массивами.

1. В C как и в подавляющем большинстве языков высокого уровня есть массивы переменной длины. Поэтому противопоставление массивов PHP/JS статическим массивам C выглядит мягко говоря странно.

2. Автор сам говорит, что массивы в PHP — это фактически такой специфический Hash Map, а потом зачем-то говорит, что они похожи на списки на основании того, что есть функции pop, push и подобные. И массивы и ассоциативные массивы, — это очевидно не списки и работают совершенно не как списки (Можно обратится к элементу по произвольному индексу/ключу без перебора элементов, как в списке).

3. Forech — это не улучшенная версия for для динамических языков, а более высокоуровневая конструкция для большенства практических применений for в этих языках. То есть это синтаксический сахар для частного случая for.

Жаль, что это перевод, потому что у меня есть задачка для автора: пройтись по массиву с шагом 3 элемента, не перебирая весь массив при этом с помощью foreach. Она по-моему наглядно показывает, что for не только более низкоуровневая конструкция, но и более гибкая и все еще полезная в ряде случаев почти что в любом языке.

Ну и выше в комментариях еще написано про ошибки.
Автор сам говорит, что массивы в PHP — это фактически такой специфический Hash Map, а потом зачем-то говорит, что они похожи на списки на основании того, что есть функции pop, push и подобные. И массивы и ассоциативные массивы, — это очевидно не списки и работают совершенно не как списки (Можно обратится к элементу по произвольному индексу/ключу без перебора элементов, как в списке).

В PHP "массив" — это и хешмап и связанный список одновременно. Соответственно, с ними можно и работать и как с хешмапом, и как со связанным списокм.

Это верно. Двухсвязный список используется для итерации и сортировки, но так же есть и доступ к произвольному элементу по хэшу, что делает утверждение из статьи: «PHP и JavaScript массивы — это, по сути, слабо типизированные списки переменной длины.» мягко говоря неверным.

Можно подробнее про массивы переменной длины в С, VLA что ли?
В C++ есть std::vector, да.

Возможно, имеется в виду динамическая память и realloc. Только формально это не массивы, конечно.

Ну формально это и в PHP не массивы) Я про динамические массивы писал к тому, что сравнивать нужно сравнимые вещи, а не те, которые удобно сравнивать.

Какая поверхностная писулька. Спесь с js начинает спадать, когда понадобятся 3д- и более массивы.
И остальное тоже кислое с мягким

А в чем проблема с N-мерными массивами-то?
js может без труда объявлять 2d массивы, на манер var arrName = [[]];
для последующих измерений начинается подобие игры в указатели, где последующие измерения надо объявлять руками, что не очень хорошо коррелирует с легендарной крутизной языка.

Ниже примерчик 3D, который ощутимо отличается от 2D.

var P = new Array();
P[0] = new Array();
P[0][0] = new Array();
P[0][0][0] = val1 ;

P[0][1] = new Array();
P[0][1][0] = val4

P[0][2] = new Array();
P[0][2][0] = val6 ;

Нет никаких отличий от объявления 2-мерного и N (N > 2) мерного массива — разница только в том, сколько вам нужно будет сделать дополнительных объявлений (N — 1, то есть для двумерных это нужно будет сделать один раз).

Это уж не говоря о том, что ничего не мешает хранить всё в линейном массиве, всё, что тут нужно — вычисление линейного индекса из набора индексов по измерениям, ragged arrays (т.е. такие, в которых длина везде может быть разная) в N-мерных случаях обычно никому не нужны, и размеры по каждому из измерений постоянны.
function getLinearIndex(sizes, indices) {
  let shiftAccumulator = 1;
  const shifts = sizes.map(size => {
    const out = shiftAccumulator;
    shiftAccumulator *= size;
    return out;
  });
  return indices.reduce((acc, index, i) => acc + index * shifts[i], 0);
}
Нет отличий, потом есть разница. Т.е. — есть.

В векторах хранить данные лично я еще на ассемблере наигрался. Хочется чего-то высокоуровнего от языка высокого уровня.
Нет отличий, потом есть разница. Т.е. — есть.

Не юродствуйте. Код без отличий делает ровно то, что и другой такой же код. Если вам нужно сделать всё-таки что-то иначе — разница будет в любом случае. В данном случае — разница в глубине обхода.

Хочется чего-то высокоуровнего от языка высокого уровня.

Телепатии? Ну, удачи с этим.
для последующих измерений начинается подобие игры в указатели, где последующие измерения надо объявлять руками
чего-чего?
var P = [[[val1],[val4],[val6]]];
Возможно и так, но изначально задачка была чуть другой.
Нужен был пустой массив с возможностью заполнить когда удобно. (ниже немного описал)
var p = [
  [
    [val1],
  ],
  [
    [val4],
  ],
  [
    [val6]
  ]
]

Мимо пробегал и мог не уловить всего контекста. Но не это ли вы хотели получить?
Впрочем Keyten написал тоже самое, но в 1 строку.

Мне надо объявить массив, потом посчитать и заполнить.
var z=[[[]]];
z[2][2][2] =777;
Не работает. А синтаксис выше, что Вы привели, — надо какие-то данные заполнять, короче, — согласен.
Видимо, что-то я не догоняю и надо почитать что и как устроено получше.
В JSbin у меня работало, на голом хроме — нет.

В JavaScript еще можно так


for (i in someArray) {
    x = someArray[i];
}

Только это медленнее и имеет непредсказуемое поведение если на входе объект, имитирующий массив. Уместнее говорить о for of

А что «неудачного» в переборе массива через for? Если нужно, можно break поставить, в отличие от функционального перебора
Ничего, если это for..in, for..of, а с C-style for есть проблемы с «разреженными массивами». То есть когда вы создали массив следующим образом:
const x = [];
x[1000] = 1;

то обычный for произведет 1001 итерацию, а forEach, for..in, for..of только одну
В JavaScript массивы имеют переменную длину. Тип их содержимого не контролируется — точно так же, как и тип обычных переменных. Язык берёт на себя управление памятью, в результате длина массива способна увеличиваться или уменьшаться, а разработчик может об этом не задумываться. JavaScript-массивы, на самом деле, очень похожи на списки.

Пошли бы дальше. На самом деле, если не считать UInt8Array и всего этого семейства, массивы в js это обычные объекты у которых есть прототип Array и вшитое непереопрелеляемое свойство, по которому Array.isArray проверяет что это массив. Это означает, что массив не расположен линейно в памяти, а при записи в какую-либо ячейку за пределами происходит обновление параметра length. Обновив его ручками размер массива так же поменяется в любую сторону, причём внутри при этом ничего особого не произойдёт, он лишь потрёт оставшиеся за границей ссылки. При расширении он даже не пропишет в ячейки undefined, оставит их empty (фактически несуществующие, но находящиеся в пределах длины массива, ячейки). Вы можете любой объект превратить в лжемассив просто указав Array как прототип (Array.isArray работать не будет, все остальное вроде как будет).


const foo = {};
foo.__proto__ = Array.prototype;
foo.length = 4;
console.log(foo);
> (4) [empty x 4];

foo[10] = 1;
console.log(foo.length)
> 4 // хм, setter он не ставит, что логично. С прокси можно сэмулировать.

Array.isArray(foo)
> false

Ну нет, массив — это не обычный объект, а exotic object. Основное отличие от объекта — то самое "обновление параметра length", обычные объекты так не умеют:


let a = [];
console.log(a.length); // 0
a[0] = 0;
console.log(a.length); // 1

Да, вы правы, хотя это можно и сэмулировать через Proxy.


Вот что еще забавно,


const foo = new Int8Array(4);
Array.isArray(foo); // > false
foo.length = 8;
console.log(foo.length); // 4, не можем переписать, хотя ошибка и не вылетает

Ну, вот как раз read-only свойства и у обычных объектов бывают.

Это означает, что массив не расположен линейно в памяти

ЕМНИП v8 старается работать с массивами не так, как с объектами.


  • Array-indexed properties are stored in a separate elements store
  • Elements and properties can either be arrays or dictionaries
  • Adding array-indexed properties does not create new HiddenClasses
  • (for arrays) If we know that there are no holes, i.e. the elements store is packed, we can perform local operations without expensive lookups on the prototype chain.
    (source)

и


  • (elements = numbered keys) Most commonly, objects have fast elements, which means that elements are stored in a contiguous array
  • If you assign to an index that's way past the end of your the elements array, V8 may downgrade the elements to dictionary mode. In this representation, elements are stored in a hash table.

Дальше пока некогда копать. Вроде как числовые ключи хранятся в отдельной коллекции "elements", и она бывает 2-х видов: быстрая и медленная. Быстрая для массивов почти (или совсем) без дырок и медленная для массива с дырками. Полагаю, что быстрая это обыкновенный статический массив (возможно с некоторым резервом).

Указать вопиющие неточности в рекламном посте, порождающем комментарии, сделано чтобы KPI был высоким?
У вас ошибка в коде

$myArray[0.02] = 'the 2%';


в php так сделать нельзя. В результате получим

Array
(
    [0] => the 2%
)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий