Страницы

Поиск по вопросам

вторник, 24 декабря 2019 г.

Принцип работы Array.prototype.concat()

#javascript #ecmascript


Для скрипта в Adobe Photoshop, есть диалоговое окно с элементами выбора и управления:

var dlg = new Window('dialog{text: "Выбор размеров", bounds: [' + fBounds(150,200,370,150)
+ '], \
            panel_s: Panel{text: "Стандартные:", bounds: [' + fBounds(5,5,203,85) + '], \
                checkbox_16:  Checkbox{bounds: [' + fBounds(5,13,55,14) + '],   text:
"16 x 16"},   \
                checkbox_24:  Checkbox{bounds: [' + fBounds(5,33,55,14) + '],   text:
"24 x 24"},   \
                checkbox_32:  Checkbox{bounds: [' + fBounds(5,53,55,14) + '],   text:
"32 x 32"},   \
                checkbox_48:  Checkbox{bounds: [' + fBounds(67,13,55,14) + '],  text:
"48 x 48"},   \
                checkbox_64:  Checkbox{bounds: [' + fBounds(67,33,55,14) + '],  text:
"64 x 64"},   \
                checkbox_96:  Checkbox{bounds: [' + fBounds(67,53,55,14) + '],  text:
"96 x 96"},   \
                checkbox_128: Checkbox{bounds: [' + fBounds(129,13,65,14) + '], text:
"128 x 128"}, \
                checkbox_256: Checkbox{bounds: [' + fBounds(129,33,65,14) + '], text:
"256 x 256"}, \
                checkbox_512: Checkbox{bounds: [' + fBounds(129,53,65,14) + '], text:
"512 x 512"}  \
            }, \
            panel_o: Panel{text: "Другие:", bounds: [' + fBounds(206,5,69,64) + '],\
                checkbox_19: Checkbox{bounds: [' + fBounds(5,13,55,14) + '], text:
"19 x 19"}, \
                checkbox_38: Checkbox{bounds: [' + fBounds(5,33,55,14) + '], text:
"38 x 38"}  \
            }, \
            panel_u: Panel{text: "Свои:", bounds: [' + fBounds(273,5,92,85) + '],\
                checkbox_u1: Checkbox{bounds:   [' + fBounds(5,13,14,14) + ']}, 
                                               \
                static_u1:   StaticText{bounds: [' + fBounds(21,13,31,14) + '], text:
". . . x", justify: "right"},             \
                edittext_u1: EditText{bounds:   [' + fBounds(53,11,30,19) + '], text:
". . .", properties: {multiline: false}}, \
                \
                checkbox_u2: Checkbox{bounds:   [' + fBounds(5,33,14,14) + ']}, 
                                               \
                static_u2:   StaticText{bounds: [' + fBounds(21,33,31,14) + '], text:
". . . x", justify: "right"},             \
                edittext_u2: EditText{bounds:   [' + fBounds(53,31,30,19) + '], text:
". . .", properties: {multiline: false}}, \
                \
                checkbox_u3: Checkbox{bounds:   [' + fBounds(5,53,14,14) + ']}, 
                                               \
                static_u3:   StaticText{bounds: [' + fBounds(21,53,31,14) + '], text:
". . . x", justify: "right"},             \
                edittext_u3: EditText{bounds:   [' + fBounds(53,51,30,19) + '], text:
". . .", properties: {multiline: false}}  \
            }, \
            button_all:    Button{bounds:       [' + fBounds(208,70,64,19) + '],
text: "Все"},          \
            static_save:   StaticText{bounds:   [' + fBounds(5,99,75,14) + '],  
text: "Сохранить в:"}, \
            drop_save:     DropDownList{bounds: [' + fBounds(78,95,287,23) + '],
properties: {items: ["папку рядом с исходным файлом", "-", "Выбрать папку..."]}, helpTip:"Ooops!!!"}, \
            progress_save: Progressbar{bounds:  [' + fBounds(0,0,370,4) + '],   
value:50},             \
            button_run:    Button{bounds:       [' + fBounds(4,122,70,22) + '], 
text: "Создать"}       \
        };'
    );

    // -- Выбор всех значений

    dlg.show();


Есть обработчик кнопки, для выбора всех чекбоксов:

// -- Выбор всех значений
dlg.button_all.addEventListener('click', function() {

    alert(dlg.panel_s.children + '\r\n' + dlg.panel_s.children.length);
    // Коллекция (9 элементов) и количество (9) отображаются верно

    alert(dlg.panel_o.children + '\r\n' + dlg.panel_o.children.length);
    // Коллекция (2 элемента) и количество (2) отображаются верно

    // Объединяем обе коллекции и три отдельных элемента
    var aPanel = [].concat(
        dlg.panel_s.children, 
        dlg.panel_o.children, 
        dlg.panel_u.checkbox_u1, 
        dlg.panel_u.checkbox_u2, 
        dlg.panel_u.checkbox_u3
    );

    alert(aPanel + '\r\n' + aPanel.length);
    // Массив отображается верно (14 элементов), но количество не совпадает (5) !
});


Из комментариев в коде обработчика видно, где возникает проблема, и вопрос следующий...Метод
Array.prototype.concat(), согласно документации, должен объединять массивы, раскрывая
их до первого уровня вложенности. Собственно, так и происходит, если верить последнему
всплывающему окну (см. код обработчика) - там отображаются все элементы в порядке добавления.Но,
при обращении к aPanel[0], возвращается не первый элемент (checkbox_16), а вся коллекция
dlg.panel_s.children . Соответственно при этом, длина массива aPanel составляет 5,
а добраться до нужного элемента можно только указав вложенность в коллекции, т.е. aPanel[0][0]
.Это баг или хитрая логика? И как правильно объединить массивы с помощью concat() ?
    


Ответы

Ответ 1



Поправочка Действительно, согласно справке MDN Метод concat создаёт новый массив, состоящий из элементов в объекте, на котором он был вызван, за которыми по порядку следуют, для каждого аргумента, все его элементы (если аргумент является массивом), либо сам аргумент (если он массивом не является) Только вот про первый уровень вложенности, как видите, тут ни слова.Вот Вам наглядный пример: let x = [].concat(1, 2, [3, 4], [5, 6, [7, 8]]); console.log(x); // Выведем полученный массив console.log(x.length); // Длина равняется не 8, как если бы все раскрывалось // до первого уровня вложенности, а 7! console.log(x[6]); // 6-ой элемент является массивом Однако это не отменяет Вашей проблемы, так как в Вашем случае, если бы элементы dlg.{foo}.children были массивами, то их разворачивание все равно бы произошло Так в чем же дело? Не буду Вас томить и наконец скажу, в чем же проблема. Еще раз обратите внимание на строчку: если аргумент является массивом К чему я клоню? Указанные Вами элементы не являются массивами! Да, они являются коллекциями и подобны массивам, но все же спецификация строга. Давайте посмотрим, как метод concat ведет себя с объектом arguments, который, как и Ваши элементы, является по сути коллекцией, но не массивом: function test() { console.log(Array.isArray(arguments)); // false return [].concat(arguments, arguments); } console.log(test(1, 2, 3).toString()); // [object Arguments], [object Arguments] Как видите, concat не стал разбираться, коллекция ли это или обычный объект. Для него важно лишь то, что данная конструкция не является массивом. P.S. - чтобы иметь возможность проверять, является ли объект массивом или нет, советую посмотреть в сторону функции Array.isArray() Что делать? Лучшим вариантом будет не полениться и все добавить руками: var aPanel = []; for (let i = 0; i < dlg.panel_s.children.length; i++) aPanel.push(dlg.panel_s.children[i]); for (let i = 0; i < dlg.panel_o.children.length; i++) aPanel.push(dlg.panel_o.children[i]); aPanel.push(dlg.panel_u.checkbox_u1, dlg.panel_u.checkbox_u2, dlg.panel_u.checkbox_u3); Другой вариант Можете определить свой метод на уровне прототипа, который как раз и будет разворачивать и массивы, и коллекции до первого уровня: Array.prototype.fullConcat = function() { const exclude = ["string"]; // Пусть строка и является коллекцией, но мы хотим считать ее цельным объектом function getElements(item) { // Рекурсивно получим все элементы внутри заданного (если таковые есть) let result = []; if (item.length === undefined || exclude.includes(typeof item)) result.push(item); else { for (let i = 0; i < item.length; i++) { let add = getElements(item[i]); for (let j = 0; j < add.length; j++) result.push(add[j]); } } return result; } let concated = []; for (let i = 0; i < arguments.length; i++) { // Начнем объединение let item = arguments[i]; if (item.length === undefined || exclude.includes(typeof item)) concated.push(item); else { for (let i = 0; i < item.length; i++) { let add = getElements(item[i]); for (let j = 0; j < add.length; j++) concated.push(add[j]); } } } // Не забудем и про контекст this, который может оказаться совсем не пустым return this.length == 0 ? concated : [].fullConcat(this, concated); }; function testArguments() { return [].fullConcat(arguments, arguments); } // Проверим, как метод расправится со вложенностью console.log([1, 2, 3].fullConcat([4], [5, 6, [7, 8, [9, 10]]], "hello", "world")); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "hello", "world" // Проверим, как метод расправится с коллекцией в лице arguments console.log(testArguments(1, 2, 3, [4, 5])); // 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 UPD: Большое спасибо @Grundy за то, что обратил мое внимание на следующий момент: Давайте же глянем не справку MDN, а на спецификацию ECMAScript. Обратим внимание на строчки: Repeat, while items is not empty a. Remove the first element from items and let E be the value of the element. b. Let spreadable be ? IsConcatSpreadable(E). c. If spreadable is true, then // copy items from within the object d. Else: // copy the object Или на русском: Повторять, пока items не пуст a. Удалить первый элемент из items, присвоить E значение элемента. b. Присвоить spreadable значение IsConcatSpreadable (E). c. Если spreadable является true, тогда // копировать элементы изнутри объекта d. Иначе: // копировать сам объект То есть за то, будет ли элемент добавлен к результирующему массиву в явном виде, или же он будет предварительно развернут как коллекция, отвечает результат выполнения некой функции IsConcatSpreadable(O). Давайте же взглянем, что это и с чем ее едят: If Type(O) is not Object, return false. Let spreadable be ? Get(O, @@isConcatSpreadable). If spreadable is not undefined, return ToBoolean(spreadable). Return ? IsArray(O). Или на русском: Если тип O не Object, вернуть false. Присвоить spreadable значение свойства @@isConcatSpreadable объекта O. Если spreadable не undefined, вернуть ToBoolean (spreadable). Вернуть IsArray(O). То есть проверка на то, является ли объект массивом, идет в самую последнюю очередь. До этого проверяется, обладает ли объект свойством Symbol.isConcatSpreadable, и если он таковым обладает, то его булевое значение и будет показателем того, будет ли он раскрываться при concat. Давайте теперь применим это на практике: // Создадим собственную коллекцию var userCollection = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; // Убедимся, что она не является массивом console.log(Array.isArray(userCollection)); // false // Посмотрим, что вернет нам concat console.log([].concat(userCollection, userCollection)); // Array [ Object(3), Object(3) ] // Теперь маленькая хитрость: // Установим внутреннее свойство, отвечающее // за результат IsConcatSpreadable(O), равным true userCollection[Symbol.isConcatSpreadable] = true; // Посмотрим, что теперь вернет нам concat console.log([].concat(userCollection, userCollection)); // Array [ "a", "b", "c", "a", "b", "c" ] // Или же сразу создадим коллекцию с указанным свойством: // Создадим собственную коллекцию var userCollection_1 = { 0: 'a', 1: 'b', 2: 'c', length: 3, [Symbol.isConcatSpreadable]: true }; // Посмотрим, что вернет нам concat console.log([].concat(userCollection_1, userCollection_1)); // Array [ "a", "b", "c", "a", "b", "c" ] Работает как часы. Давайте для чистоты эксперимента проверим это на существующем перечисляемом типе. Вспомним опять же про наш arguments: // arguments не указан, как spreadable function nonSpreadable() { return [].concat(arguments, arguments); } // arguments указан, как spreadable function spreadable() { arguments[Symbol.isConcatSpreadable] = true; return [].concat(arguments, arguments); } // Какой же массив вернется при объединении 2-х arguments? console.log(nonSpreadable(1, 2)); // Array [ Arguments, Arguments ] // А теперь?) console.log(spreadable(1, 2)); // Array [ 1, 2, 1, 2 ] console.log(spreadable(1, 2, 3)); // Array [ 1, 2, 3, 1, 2, 3 ] Как видите, нам удалось с помощью concat объединить внутренние значения arguments в массив Для очистки совести напомню: лезть таким вот образом во внутренние свойства объекта, изменяя логику его работы, - неправильно и кощунственно! Почему? Давайте представим ситуацию: Вы видите массив. Как он должен вести себя с методом concat? Правильно, согласно уже не раз указанной справке он обязан раскрыться при объединении! А что если кто-то с полученным Вами массивом поигрался следующим образом: // Создадим обычные массивы var arr_0 = [1, 2]; var arr_1 = [3, 4]; // Убедимся, что это именно массивы, а не что-либо еще console.log(Array.isArray(arr_0)); // true console.log(Array.isArray(arr_1)); // true // Объединим их var arr_2 = [].concat(arr_0, arr_1); console.log(arr_2); // Array [ 1, 2, 3, 4 ] console.log(arr_2.length); // 4, как и полагается // А теперь сделаем парочку богопротивных действий arr_0[Symbol.isConcatSpreadable] = arr_1[Symbol.isConcatSpreadable] = false; // Объединим их снова var arr_3 = [].concat(arr_0, arr_1); console.log(arr_3); // Array [ Array(2), Array(2) ] console.log(arr_3.length); // 2, что-то пошло не так) Только что мы изменили поведение стандартного типа, что очень и очень неправильно! Так что старайтесь избегать таких вот подходов! Все, совесть очищена, так что можем злоупотреблять дальше :) Еще один вариант решения Вашей проблемы: На основе манипулирования результатом исполнения IsConcatSpreadable(O) Вашу задачу можно решить таким вот образом: // Колдуем над isConcatSpreadable: dlg.panel_s.children[Symbol.isConcatSpreadable] = dlg.panel_0.children[Symbol.isConcatSpreadable] = true; // Объединяем обе коллекции и три отдельных элемента var aPanel = [].concat( dlg.panel_s.children, dlg.panel_o.children, dlg.panel_u.checkbox_u1, dlg.panel_u.checkbox_u2, dlg.panel_u.checkbox_u3 ); И живой пример для наглядности: // Сформируем свои коллекции var userCollection_0 = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; var userCollection_1 = { 0: 'd', 1: 'e', 2: 'f', 3: 'g', length: 4 }; // Колдуем над isConcatSpreadable userCollection_0[Symbol.isConcatSpreadable] = userCollection_1[Symbol.isConcatSpreadable] = true; // Объединяем обе коллекции и три отдельных элемента var aPanel = [].concat( userCollection_0, userCollection_1, 'h', 'i', 'j' ); // Посмотрим, что внутри console.log(aPanel); // Array [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ] // Убедимся, что длина также совпадает console.log(aPanel.length); // 10 из 10) Надеюсь, мой ответ помог Вам в разрешении проблемы! Удачи в Ваших начинаниях!

Комментариев нет:

Отправить комментарий