Вдосконалені можливості масивів

Масив — це фундаментальний об’єкт JavaScript. Але, в той час, коли інші аспекти JavaScript вдосконалювались час від часу, масиви залишалися незмінними до того часу, коли ECMAScript 5 презентував кілька методів для їх легшого використання. ECMAScript 6 продовжує вдосконалювати масиви, додаючи більше функціональності, як нові методи створення, деякі корисні та зручні методи, а також можливість створювати типізовані масиви.

Створення масивів

До появи ECMAScript 6 існувало два основних способи створення масивів: конструктор Array та синтаксис літералу масиву. Обидва способи вимагають переліку членів масиву та, між тим, є досить обмеженими. Варіанти конвертації подібних до масивів об’єктів (маючи на увазі, об’єкт з числовими індексами та властивістю length) в масиви також були досить обмеженими й потребували додаткового коду. Для того, щоб полегшити створення масивів в JavaScript, ECMAScript 6 додає методи Array.of() та Array.from().

Метод Array.of()

Однією з причин додавання ECMAScript 6 нових методів JavaScript є бажання допомогти розробникам уникнути несподіванок при створенні масивів з конструктором Array. Конструктор new Array поводиться по-різному, залежно від типу й кількості аргументів наданих йому. Наприклад:

let items = new Array(2);
console.log(items.length);          // 2
console.log(items[0]);              // undefined
console.log(items[1]);              // undefined

items = new Array("2");
console.log(items.length);          // 1
console.log(items[0]);              // "2"

items = new Array(1, 2);
console.log(items.length);          // 2
console.log(items[0]);              // 1
console.log(items[1]);              // 2

items = new Array(3, "2");
console.log(items.length);          // 2
console.log(items[0]);              // 3
console.log(items[1]);              // "2"

Якщо конструктору Array надається один числовий аргумент, то буде встановлено лише властивість масиву length, яка відповідатиме значенню наданого аргументу. Якщо буде надано лише один аргумент не числового значення, то його значення стане єдиним членом масиву. Якщо надані кілька значень (числові або ні), то ці значення стануть елементами масиву. Така поведінка може збентежити і є ризикованою, тому що ви не завжди можете бути певними у тому, який тип даних буде надано конструктору.

Щоб вирішити цю проблему ECMAScript 6 представляє метод Array.of(). Метод Array.of() працює подібно до конструктора Array, але не має виключень з єдиним числовим значенням аргументу. Метод Array.of() завжди створює масив, який містить надані аргументи, незважаючи на тип або кількість наданих аргументів. Ось деякі приклади використання методу Array.of():

let items = Array.of(1, 2);
console.log(items.length);          // 2
console.log(items[0]);              // 1
console.log(items[1]);              // 2

items = Array.of(2);
console.log(items.length);          // 1
console.log(items[0]);              // 2

items = Array.of("2");
console.log(items.length);          // 1
console.log(items[0]);              // "2"

Щоб створити масив за допомогою методу Array.of(), достатньо просто передати йому значення, які ви бажаєте мати у вашому масиві. Перший приклад тут створює масив, який містить два числа, другий — містить одне число, а останній масив містить рядок. Це схоже на використання літералу об’єкту і ви можете використовувати літерал об’єкту замість Array.of() у більшості випадків. Але, якщо вам потрібно передати конструктор Array у функцію, то, можливо, ви захочете передати Array.of(), щоб переконатися у його відповідній поведінці. Наприклад:

function createArray(arrayCreator, value) {
    return arrayCreator(value);
}

let items = createArray(Array.of, value);

У цьому коді функція createArray() приймає функцію яка створює масив і значення, щоб вставити в масив. Ви можете передати Array.of() у якості першого аргументу до createArray(), щоб створити новий масив. Буде небезпечно передавати безпосередньо Array, якщо ви не можете гарантувати, що другий аргумент не буде числом.

I> Метод Array.of() не використовує властивість Symbol.species (про яку йшлося у Главі 9), щоб визначити тип значення яке повертається. Замість цього він використовує поточний конструктор (this всередині методу of()), щоб визначити коректний тип даних, які буде повернено.

Метод Array.from()

Конвертація об’єктів, які не є масивами, до справжніх масивів завжди була досить складною в JavaScript. Наприклад, якщо ви маєте об’єкт arguments (який подібний до масиву) та хочете використовувати його як масив, то ви маєте спершу конвертувати його. Щоб конвертувати подібний до масиву об’єкт в масив, у ECMAScript 5 ви мали б написати функцію, подібну до тієї, що є в цьому прикладі:

function makeArray(arrayLike) {
    var result = [];

    for (var i = 0, len = arrayLike.length; i < len; i++) {
        result.push(arrayLike[i]);
    }

    return result;
}

function doSomething() {
    var args = makeArray(arguments);

    // використовуємо аргументи
}

Цей метод штучно створює масив result та копіює кожен член з arguments в новий масив. Це працює, але потребує зайвого коду, щоб виконати відносно просту операцію. Кінець-кінцем, розробники встановили, що вони можуть зменшити кількість коду, використовуючи природний для масивів та об’єктів схожих на масиви метод slice(), як ось:

function makeArray(arrayLike) {
    return Array.prototype.slice.call(arrayLike);
}

function doSomething() {
    var args = makeArray(arguments);

    // використовуємо аргументи
}

Цей код функціонально відповідний до попереднього прикладу та працює, тому що він встановлює значення this для slice() відповідне до об’єкту схожого на масив. Оскільки slice() потребує тільки числові індекси та властивість length, щоб коректно працювати, будь-який подібний до масиву об’єкт буде працювати.

Незважаючи на те, що ця техніка потребує меншого написання коду, виклик Array.prototype.slice.call(arrayLike) не є повною відповідністю до «Конвертації arrayLike до масиву». На щастя, ECMAScript 6 додав метод Array.from() як очевидний, досить чистий, спосіб конвертації об’єктів у масиви.

Сприймаючи об’єкт, який може бути ітерабельним або схожим до масиву, як перший аргумент метод Array.from() повертає масив. Ось простий приклад:

function doSomething() {
    var args = Array.from(arguments);

    // використовуємо аргументи
}

Array.from() створює новий масив, що базується на елементах arguments. Таким чином args є екземпляром Array, який містить ті самі значення з тими ж індексами, що й arguments.

I> Метод Array.from() також використовує this, щоб визначити тип масиву, що повертається.

Маповані перетворення

Якщо ви хочете піти ще далі в конвертуванні масивів, то можете використовувати Array.from() з функцією мапування у якості другого аргументу. Ця функція оперує кожним значенням подібного до масиву об'єкту, та конвертує їх до остаточної форми перед тим як зберегти результат з відповідним індексом у кінцевому масиві. Наприклад:

function translate() {
    return Array.from(arguments, (value) => value + 1);
}

let numbers = translate(1, 2, 3);

console.log(numbers);               // 2,3,4

Тут у якості функції мапування до Array.from() передається (value) => value + 1, таким чином до кожного члену масиву додається 1 перед його записом. Якщо функція мапування є методом об’єкта, ви також можете передати до Array.from() третій не обов’язковий аргумент, який буде визначати значення this для функції мапування:

let helper = {
    diff: 1,

    add(value) {
        return value + this.diff;
    }
};

function translate() {
    return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);

console.log(numbers);               // 2,3,4

У цьому прикладі для конвертації у якості функції мапування передається helper.add(). Функція helper.add() використовує властивість this.diff, тому ви маєте впровадити третій аргумент для Array.from(), щоб вказати на значення this. Завдяки третьому аргументу Array.from() може легко конвертувати данні без використання bind() або визначення this якимось іншим чином.

Використання в ітерабельних об’єктах

Метод Array.from() працює як зі схожими до масивів, так і з ітеральними об’єктами. Це означає, що метод може конвертувати в масив будь–який обʼєкт з властивістю Symbol.iterator. Наприклад:

let numbers = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
};

let numbers2 = Array.from(numbers, (value) => value + 1);

console.log(numbers2);              // 2,3,4

Оскільки обʼєкт numbers ітерабельний ви можете передавати numbers безпосередньо до Array.from(), щоб конвертувати його значення у масив. Функція мапування додає 1 до кожного елементу, тому кінцевий масив містить 2, 3, та 4 замість 1, 2, та 3.

I> Якщо обʼєкт і схожих до масиву, і ітерабельний, тоді ітератор використовується Array.from() для визначення значень, які треба конвертувати.

Нові методи для всіх масивів

Продовжуючи тренд запроваджений ECMAScript 5, ECMAScript 6 додає кілька нових методів до масивів. Методи find() та findIndex() призначені, щоб допомогти розробникам використовувати масиви з будь-якими значеннями, в той час як fill() та copyWithin() натхненні використанням типізованих масивів, формою масиву введеною в ECMAScript 6, у якій використовуються тільки цифри.

Методи find() та findIndex()

До ECMAScript 5 пошук по масивах був досить незграбним, тому що для цього не було вбудованих методів. ECMAScript 5 додав методи indexOf() та lastIndexOf(), нарешті дозволивши розробникам шукати певні значення всередині масиву. Ці два методи були значним покращенням, але вони все ще були досить обмежені, бо ви могли шукати тільки одне значення за раз. Наприклад, якщо ви хотіли відшукати перше парне число в наборі чисел, ви мали б написати власний код для цього. ECMAScript 6 вирішив цю проблему, запропонувавши методи find() та findIndex().

Обидва, find() та findIndex(), приймають два аргументи: функцію зворотного виклику та необов’язкове значення для this в середині функції зворотного виклику. До функції зворотного виклику передається поточний елемент масиву, індекс поточного елементу в масиві та сам масив — такі ж аргументи, які передаються до методів map() та forEach(). Функція зворотного виклику повертає true, якщо поточне значення відповідає критеріям, які ви визначили. Обидва, find() та findIndex(), припиняють пошук по масиву, коли функція зворотного виклику вперше повертає true.

Єдина відмінність між цими методами в тому, що find() повертає значення в той час, коли findIndex() повертає індекс в якому було знайдене значення. Ось приклад для демонстрації:

let numbers = [25, 30, 35, 40, 45];

console.log(numbers.find(n => n > 33));         // 35
console.log(numbers.findIndex(n => n > 33));    // 2

У цьому коді find() та findIndex() викликаються щоб визначити перше значення в масиві numbers, яке є більшим за 33. Виклик find() повертає 35, а findIndex() повертає 2, індекс 35 в масиві numbers.

Обидва, find() та findIndex(), корисні, щоб відшукати елемент масиву, який відповідає певній умові, а не його значенню. Якщо вам потрібно відшукати значення, то краще використати indexOf() та lastIndexOf().

Метод fill()

Метод fill() заповнює один чи більше елементів масиву певним значенням. Коли передається лише значення, метод fill() переписує всі елементи масиву наданим значенням. Наприклад:

let numbers = [1, 2, 3, 4];

numbers.fill(1);

console.log(numbers.toString());    // 1,1,1,1

Тут виклик numbers.fill(1) змінює всі значення в numbers на 1. Якщо ви хочете замінити лише певні елементи, радше ніж усі, то ви можете визначити початковий та кінцевий індекси, наприклад так:

let numbers = [1, 2, 3, 4];

numbers.fill(1, 2);

console.log(numbers.toString());    // 1,2,1,1

numbers.fill(0, 1, 3);

console.log(numbers.toString());    // 1,0,0,1

У виклику numbers.fill(1,2), 2 визнає місцем початку заповнення елементів індекс 2. Кінцевий індекс не визначено у якості третього аргументу, тому для кінцевого індексу використано numbers.length, це означає, що останні два елементи у numbers будуть заповненні значенням 1. Операція numbers.fill(0, 1, 3) заповнює елементи масиву з індексами 1 та 2 значенням 0. Виклик fill() з другим та третім аргументами дозволяє вам заповнити декілька елементів за один раз без переписування всього масиву.

I> Якщо початковий або кінцевий індекси є від’ємними, то ці значення будуть додані до розміру масиву, щоб визначити кінцеве розміщення. Наприклад, початкове знаходження -1 дає array.length — 1 у якості індексу, де array є масивом, до якого було викликано fill().

Метод copyWithin()

Метод copyWithin() схожий до fill() в тому, що він змінює декілька членів масиву за раз. Але, замість того, щоб визначати одне значення яке буде надане елементам масиву, copyWithin() дозволяє вам скопіювати значення елементів масиву з самого себе. Щоб досягти цього вам потрібно передати два аргументи до методу copyWithin(): індекс, з якого метод має почати заповнювати значення та індекс, з якого почнеться копіювання значень.

Наприклад, щоб скопіювати значення перших двох елементів масиву до останніх двох, ви можете зробити наступне:

let numbers = [1, 2, 3, 4];

// вставити значення у масив, починаючи з індексу 2
// скопіювати значення масиву, починаючи з індексу 0
numbers.copyWithin(2, 0);

console.log(numbers.toString());    // 1,2,1,2

Цей код вставляє значення в numbers, починаючи з індексу 2, тому обидва індекси (2 і 3) будуть переписані. Передаючи 0 як другий аргумент до copyWithin(), ми позначаємо початок, отже значення будуть копіюватися з індексу 0 і доти, поки більше не залишиться елементів для копіювання.

За замовчуванням, copyWithin() завжди копіює значення аж до кінця масиву, але ви можете надати не обов'язковий третій аргумент, щоб обмежити кількість елементів, які будуть переписані. Цей третій аргумент є кінцевим індексом на якому копіювання значень буде зупинено. Ось приклад:

let numbers = [1, 2, 3, 4];

// вставити значення у масив, починаючи з індексу 2
// скопіювати значення масиву, починаючи з індексу 0
// перестати копіювати, коли ви досягнете індексу 1
numbers.copyWithin(2, 0, 1);

console.log(numbers.toString());    // 1,2,1,4

У цьому прикладі тільки значення з індексом 0 скопійоване, бо необовʼязковий кінцевий аргумент встановлено до 1. Останній елемент в масиві залишається незмінним.

I> Так само як і з методом fill(), якщо ви передасте відʼємне значення до будь-якого аргументу методу copyWithin(), то довжина масиву буде автоматично додана до цього значення, щоб визначити індекс, який слід використовувати.

В даний момент вам може бути незрозумілою доцільність використання fill() та copyWithin(). Це тому, що методи, в першу чергу, розроблялись для типізованих масивів та були додані до звичайних масивів для більшої одноманітності. Однак, як тільки ви дізнаєтесь, з наступного розділу, про використання типізованих масиви для маніпуляцій бітовими числами, ці методи стануть вам у нагоді.

Типізовані масиви

Типізовані масиви є масиви спеціального призначення, розроблені для роботи з числовими типами (маються на у вазі не всі названі типи). Типізовані масиви беруть початок з WebGL, порту Open GL ES 2.0, призначеного для використання веб-сторінок з елементом <canvas>. Типізовані масиви були створені як частина порту, щоб забезпечити швидкої побітової арифметики в JavaScript.

Обчислення звичайних чисел в JavaScript було занадто повільним для WebGL, тому що числа були збережені у 64-бітному форматі з рухомою комою та перетворювалися у 32-розрядні цілі числа за потребою. Типізовані масиви були введені, щоб обійти це обмеження і забезпечити вищу швидкодію для арифметичних операцій. Концепція в тому, що будь-яке одиничне число може розглядатися як масив бітів і, таким чином, можна використовувати звичні методи, доступні для масивах JavaScript.

У ECMAScript 6 було прийнято типізовані масиви в якості офіційної частини мови для забезпечення кращої сумісності між рушіями JavaScript та взаємодією з масивами JavaScript. Не дивлячись на те, що типізовані масиви у ECMAScript 6 не зовсім такі ж, як у WebGL, вони мають достатньо багато спільного, щоб зробити версію ECMAScript 6 еволюційною на шляху до версії WebGL, замість того, щоб використовувати інший підхід.

Числові типи даних

Числа в JavaScript зберігаються в форматі IEEE 754, який використовує 64 біта для зберігання представлення числа з рухомою комою. Цей формат представляє як цілі числа так і числа з рухомою комою в JavaScript, з перетворенням між двома форматами з частотою зміни чисел. Типізовані масиви дозволяють зберігати і маніпулювати вісьмома різними числовими типами:

  1. ціле 8-бітове число зі знаком (int8);
  2. ціле 8-бітове число без знаку (uint8);
  3. ціле 16-бітове число зі знаком (int16);
  4. ціле 16-бітове число без знаку (uint16);
  5. ціле 32-бітове число зі знаком (int32);
  6. ціле 32-бітове число без знаку (uint32);
  7. 32-бітове число з рухомою комою (float32);
  8. 64-бітове число з рухомою комою (float64).

Якщо ви представляєте число, яке відповідає int8 як нормальне число в JavaScript, ви будете витрачати 56 біт. Ці біти могли б краще використовувати для зберігання додаткових значень int8 або будь-якого іншого числа, яке вимагає менше, ніж 56 біт. Ефективніше використання бітів - це один з варіантів використання типізованих масивів.

Всі операції та об'єкти, пов'язані з типізованими масивами? зосереджені навколо цих восьми типів даних. Але, для того, щоб використовувати їх, вам необхідно створити буферний масив для зберігання даних.

I> У цій книзі я буду посилатися на ці типи відповідно до скорочень, які я вказав в дужках. Ці скорочення фактично не з'являються в коді JavaScript; це просто скорочення для більш довгих описів.

Буферні масиви

Основою для всіх типізованих масивів є буферний масив, який являє собою ділянку пам'яті, що може містити певну кількість байтів. Створення буферного масиву є чимось на кшталт виклику malloc() в C, щоб виділити пам'ять без вказівки того, що блок пам'яті містить. Ви можете створити буферний масив за допомогою конструктору ArrayBuffer наступним чином:

let buffer = new ArrayBuffer(10);   // виділити 10 байтів

Просто передайте число байтів, які повинен містити буферний масив під час виклику конструктора. Цей оператор let створює буферний масив довжиною 10 байт. Після того, як буферний масив створений, ви можете отримати кількість байтів в ньому, перевіряючи властивість byteLength:

let buffer = new ArrayBuffer(10);   // виділити 10 байтів
console.log(buffer.byteLength);     // 10

Ви також можете використовувати метод slice(), щоб створити новий буферний масив, який містить частину існуючого буферного масиву. Метод slice() працює як і метод slice() у масивах: ви передаєте йому початковий і кінцевий індекси в якості аргументів, і він повертає новий екземпляр ArrayBuffer, що складається з тих самих елементів оригіналу. Наприклад:

let buffer = new ArrayBuffer(10);   // виділити 10 байтів


let buffer2 = buffer.slice(4, 6);
console.log(buffer2.byteLength);    // 2

У цьому коді buffer2 створюється шляхом вилучення байтів за індексами 4 та 5. Так само, як при виклику версії цього методу для масиву, другий аргумент slice() є виключаючим.

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

I> Буферний масив завжди представляє точне число байтів, зазначених при його створенні. Ви можете змінити дані, що містяться в буферному масиві, але не розмір самого буферного масиву.

Маніпулювання буферними масивами за допомогою представлень

Буферні масиви представляють ділянки пам'яті, а представлення — це інтерфейс, який ви будете використовувати для виконання маніпуляцій з пам'яттю. Представлення оперує буферним масивом або підмножиною байтів буферного масиву, зчитуючи і записуючи інформацію в одному з числових типів даних. Тип DataView являє собою загальний вигляд буферного масиву, який дозволяє працювати усіма вісьмома типами числових даних.

Для того, щоб використовувати DataView, спочатку потрібно створити екземпляр ArrayBuffer та використовувати його, щоб створити новий DataView. Ось приклад:

let buffer = new ArrayBuffer(10),
    view = new DataView(buffer);

В цьому прикладі об'єкт view має доступ до всіх 10 байт в buffer. Ви також можете створити представлення тільки для частини буфера. Просто вкажіть значення байту для зміщення і, при необхідності, число байтів, які треба взяти починаючи з цього зміщення. Коли число байтів не включене, DataView за замовчуванням буде проходити від зміщення до кінця буфера. Наприклад:

let buffer = new ArrayBuffer(10),
    view = new DataView(buffer, 5, 2);      // покриває байти 5 та 6

Тут, view працює тільки з байтами за індексами 5 і 6. Цей підхід дозволяє створювати кілька представлень з тим же самим буферним масивом, що може бути корисним, коли ви хочете використовувати одну комірку пам'яті для всієї програми, а не динамічно виділяти простір при необхідності.

Отримуємо інформацію з представлення

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

  • buffer - буферний масив, з яким пов'язане представлення
  • byteOffset - другий аргумент конструктору DataView, якщо наданий (0 за замовчуванням)
  • byteLength - третій аргумент конструктору DataView, якщо наданий (властивість byteLength від buffer за замовчуванням)

Використовуючи ці властивості, ви можете перевірити, де саме працює представлення, ось як:

let buffer = new ArrayBuffer(10),
    view1 = new DataView(buffer),           // покриває всі байти
    view2 = new DataView(buffer, 5, 2);     // покриває байти 5 та 6

console.log(view1.buffer === buffer);       // true
console.log(view2.buffer === buffer);       // true
console.log(view1.byteOffset);              // 0
console.log(view2.byteOffset);              // 5
console.log(view1.byteLength);              // 10
console.log(view2.byteLength);              // 2

Цей код створює view1, представлення для всього буферного масиву і view2, який діє на невеликій ділянці буферного масиву. Ці представлення мають еквівалентні властивості buffer, тому що обидва працюють у тому ж самому буферному масиві. Однак, byteOffset та byteLength різні для кожного представлення. Вони відображають частину буферного масиву, якими оперує кожне представлення.

Звичайно, читання інформації про пам'ять само собою не дуже корисне. Щоб отримати хоч якусь користь з цього, вам потрібно записати дані і зчитувати їх з пам'яті.

Зчитування і запис даних

Для кожного з восьми числових типів даних в JavaScript, прототип DataView має метод для запису даних і спосіб зчитування даних з буферного масиву. Назви методів починаються з "set" або "get" і відповідають абревіатурам типів даних. Наприклад, ось список методів читання і запису, які можуть працювати зі значеннями int8 та uint8:

  • getInt8(byteOffset, littleEndian) - зчитує дані int8, починаючи з byteOffset
  • setInt8(byteOffset, value, littleEndian) - записує дані int8, починаючи з byteOffset
  • getUint8(byteOffset, littleEndian) - зчитує дані uint8, починаючи з byteOffset
  • setUint8(byteOffset, value, littleEndian) - записує дані uint8, починаючи з byteOffset

Методи "get" приймають два аргументи: значення байту зміщення для читання і необов'язкове логічне значення, яке вказує, чи слід зчитувати значення як прямий порядок байтів (прямий порядок байтів означає, що молодший байт є в позиції нульового байту, замість останнього.) Методи "set" приймають три аргументи: значення байту зміщення для запису, значення, яке слід записати, і необов'язкове логічне значення, яке вказує, чи повинне значення бути збережене дотримуючись прямого порядку байтів.

Хоча я тільки показав методи, які ви можете використовувати з 8-бітовими значеннями, одні і ті ж методи існують для роботи з 16-ти і 32-бітними значеннями. Просто замініть 8 в кожному імені на 16 або 32. Поряд з усіма цими методами для цілих чисел, DataView також має наступні методи читання і запису для чисел з рухомою комою:

  • getFloat32(byteOffset, littleEndian) - зчитує дані float32, починаючи з byteOffset
  • setFloat32(byteOffset, value, littleEndian) - записує дані float32, починаючи з byteOffset
  • getFloat64(byteOffset, littleEndian) - зчитую дані float64, починаючи з byteOffset
  • setFloat64(byteOffset, value, littleEndian) - записує дані float64, починаючи з byteOffset

Щоб побачити методи "set" та "get" в дії, розглянемо наступний приклад:

let buffer = new ArrayBuffer(2),
    view = new DataView(buffer);

view.setInt8(0, 5);
view.setInt8(1, -1);

console.log(view.getInt8(0));       // 5
console.log(view.getInt8(1));       // -1

Цей код використовує буферний масив з двох байтів для зберігання двох значень у форматі int8. Перше значення встановлюється зі зміщенням 0, а друге зі зміщенням 1, вказуючи, що кожне значення охоплює повний байт (8 біт). Ці значення пізніше вилучені зі своїх позицій за допомогою методу getInt8(). В той час, коли в цьому прикладі використовується значення int8, ви можете використовувати будь-який з восьми числових типів з відповідними методами.

Представлення цікаві тим, що вони дозволяють читати і записувати в будь-якому форматі, в будь-який момент часу, незалежно від того, як дані були попередньо збережені. Наприклад, запис двох значень int8 і зчитування буферу за допомогою методу int16 працює просто відмінно, як в цьому прикладі:

let buffer = new ArrayBuffer(2),
    view = new DataView(buffer);

view.setInt8(0, 5);
view.setInt8(1, -1);

console.log(view.getInt16(0));      // 1535
console.log(view.getInt8(0));       // 5
console.log(view.getInt8(1));       // -1

Виклик view.getInt16(0) зчитує всі байти з представлення та інтерпретує ці байти як число 1535. Для того, щоб зрозуміти, чому це відбувається, подивіться на Малюнок 10-1, який показує, що кожен рядок setInt8() робить в буферному масиві.

new ArrayBuffer(2)      0000000000000000
view.setInt8(0, 5);     0000010100000000
view.setInt8(1, -1);    0000010111111111

Буферний масив починається 16 бітами, кожен з яких рівний нулю. Запис 5 у перший байт з setInt8() вводить кілька одиниць (у 8-бітовій репрезентації, 5 — це 00000101). Запис -1 до другого байту встановлює всі біти в цьому байті у 1, що відповідає репрезентації -1 в байт-коді. Після другого виклику setInt8() буферний масив містить 16 біт, і getInt16() зчитує ці біти у вигляді одного 16-розрядного цілого числа, яке має значення 1535 в десятковій системі числення.

Об'єкт DataView ідеально підходить для випадків використання, що змішують різні типи даних. Однак, якщо ви використовуєте тільки один конкретний тип даних, то представлення конкретного типу є кращим вибором.

Типізовані масиви — це представлення

Типізовані масиви ECMAScript 6 фактично є специфічними до типу даних представленнями буферного масиву. Замість того, щоб використовувати універсальний об'єкт DataView для роботи з буферним масивом, ви можете використовувати об'єкти, які будуть застосовувати особливі типи даних. Є вісім специфічних до конкретного типу даних представлення, відповідно до восьми числових типів даних, плюс додаткова опція для значень uint8.

Таблиця 10-1 показує скорочений варіант повного списку представлень специфічних до типу даних з розділу 22.2 специфікації ECMAScript 6.

Таблиця 10-1: Деякі специфічні до типу даних представлення в ECMAScript 6

Ім’я конструктора Розмір елементу (в байтах) Опис Еквівалентний тип в C
Int8Array 1 8-бітний код цілого числа зі знаком signed char
Uint8Array 1 8-бітний код цілого числа без знаку unsigned char
Uint8ClampedArray 1 8-бітний код цілого числа без знаку (стисле перетворення) unsigned char
Int16Array 2 16-бітний код цілого числа зі знаком short
Uint16Array 2 16-бітний код цілого числа без знаку unsigned short
Int32Array 4 32-бітний доповняльний код числа зі знаком int
Uint32Array 4 32-бітний доповняльний код числа без знаку int
Float32Array 4 32-бітне IEEE число з рухомою комою float
Float64Array 8 64-бітне IEEE число з рухомою комою double

У лівій колонці перераховані конструктори типізованих масивів, а решта стовпців описують дані які може містити кожний типізований масив. Uint8ClampedArray те ж саме, що й Uint8Array, якщо значення в буферному масиві менше 0 або більше ніж 255. Uint8ClampedArray перетворює значення менше від 0 до 0 (наприклад, -1 стає рівним 0) і перетворює значення вище ніж 255 до 255 (так 300 стає рівним 255).

Операції з типізованими масивами працюють тільки з певним типом даних. Наприклад, всі операції з Int8Array використовують значення int8. Розмір елемента в типізованому масиві також залежить від типу масиву. В той час, коли елемент в Int8Array має довжину в один байт, Float64Array використовує вісім байт для кожного елемента. На щастя, доступ до елементів здійснюється за допомогою числових індексів, як і в звичайних масивах, що дозволяє уникнути кількох незручних викликів методів "set" і "get" в DataView.

A> Розмір елемента

A> Кожен типізований масив складається з ряду елементів, а розміром елемента є кількість байтів, яку представляє кожен елемент. Це значення зберігається у властивості BYTES_PER_ELEMENT кожного конструктора і кожного примірника, так що ви можете легко запросити розмір елемента:

console.log(UInt8Array.BYTES_PER_ELEMENT);      // 1
console.log(UInt16Array.BYTES_PER_ELEMENT);     // 2

let ints = new Int8Array(5);
console.log(ints.BYTES_PER_ELEMENT);            // 1

Створення представлень специфічних до типу даних

Конструктори типізованого масиву приймають кілька типів аргументів, тому існує кілька способів створення типізованих масивів. По-перше, ви можете створити новий типізований масив, передаючи ті ж аргументи, що приймає DataView (буферний масив, не обовʼязковий байт зміщення, та необов'язкове значення довжини байтів). Наприклад:

let buffer = new ArrayBuffer(10),
    view1 = new Int8Array(buffer),
    view2 = new Int8Array(buffer, 5, 2);

console.log(view1.buffer === buffer);       // true
console.log(view2.buffer === buffer);       // true
console.log(view1.byteOffset);              // 0
console.log(view2.byteOffset);              // 5
console.log(view1.byteLength);              // 10
console.log(view2.byteLength);              // 2

У цьому коді обидва представлення є екземплярами Int8Array, що використовує buffer. Обидва view1 та view2 мають однакові властивості buffer, byteOffset та byteLength, які існують в екземплярах DataView. Досить легко перейти до використання типізованого масиву всюди, де ви використовуєте DataView до тих пір, поки ви працюєте тільки з даними одного числового типу.

По–друге, способом створення типізованого масиву є надання єдиного числового значення до конструктору. Це число є число елементів (не байт), які слід зарезервувати в масиві. Конструктор створить новий буфер з правильною кількістю байтів, щоб представити відповідну кількість елементів масиву, і ви можете отримати доступ до кількості елементів у масиві за допомогою властивості length. Наприклад:

let ints = new Int16Array(2),
    floats = new Float32Array(5);

console.log(ints.byteLength);       // 4
console.log(ints.length);           // 2

console.log(floats.byteLength);     // 20
console.log(floats.length);         // 5

Масив ints створюється з місцем для двох елементів. Кожному 16-бітному цілому числу потрібно два байти на значення, тому масиву виділяється чотири байти. Масив floats створений, щоб вміщувати п'ять елементів, таким чином, кількість необхідних байтів 20 (чотири байти на елемент). В обох випадках створюється новий буфер, який може бути доступний за допомогою властивості buffer у разі необхідності.

W> Якщо до конструктору типізованого масиву не передається жодного аргументу, конструктор діє, ніби було отримано 0. Таким чином створюється типізований масив, який не може містити дані, оскільки у буфері було розмічено нуль байтів.

По–третє, способом створення типізованого масиву є передача обʼєкту, як єдиного аргументу конструктора. Обʼєкт може бути одним з перехованих нижче:

  • Типізований масив - Кожен елемент копіюється до нового елементу в типізованому масиві. Наприклад, якщо ви передасте int8 до конструктору Int16Array, значення int8 буде скопійоване до масиву int16. Новий типізований масив матимете інший буфер, ніж той, що був переданий.
  • Ітерабельне - Викликається ітератор об’єкта, щоб отримати члени, які будуть вставлені у типізований масив. Конструктор буде викликати помилку, якщо будь-який елемент не буде відповідати типу поточного представлення.
  • Масив - Елементи масиву будуть скопійовані до нового типізованого масиву. Конструктор буде викликати помилку, якщо будь-який елемент не буде відповідати поточному типу.
  • Обʼєкт, подібний до масиву - Поводиться так само як і масив.

У кожному з цих випадків, новий типізований масив створюється з даними з вихідного об'єкту. Це може бути особливо корисно, коли ви хочете ініціювати типізований масив з деякими значеннями, наприклад:

let ints1 = new Int16Array([25, 50]),
    ints2 = new Int32Array(ints1);

console.log(ints1.buffer === ints2.buffer);     // false

console.log(ints1.byteLength);      // 4
console.log(ints1.length);          // 2
console.log(ints1[0]);              // 25
console.log(ints1[1]);              // 50

console.log(ints2.byteLength);      // 8
console.log(ints2.length);          // 2
console.log(ints2[0]);              // 25
console.log(ints2[1]);              // 50

У цьому прикладі створюємо Int16Array та ініціалізуємо його з масивом з двох значень. Потім створюється Int32Array, якому передається Int16Array. Значення 25 і 50 копіюються з ints1 до ints2, тому два типізованих масиви мають абсолютно окремі буфери. Одні і ті ж числа представлені в обох типізованих масивах, але ints2 має вісім байт для представлення даних в той час, коли ints1 має тільки чотири.

Спільні риси у типізованих та звичайних масивів

Типізовані та звичайні масиви мають декілька спільних рис, і, як ви вже бачили в цій главі, типізовані масиви можуть бути використані як звичайні масиви у багатьох ситуаціях. Наприклад, ви можете перевірити, скільки елементів містить типізований масив за допомогою властивості length, і ви можете дістатися елементу типізованого масиву безпосередньо за допомогою цифрових індексів. Наприклад:

let ints = new Int16Array([25, 50]);

console.log(ints.length);          // 2
console.log(ints[0]);              // 25
console.log(ints[1]);              // 50

ints[0] = 1;
ints[1] = 2;

console.log(ints[0]);              // 1
console.log(ints[1]);              // 2

У цьому коді створюється новий Int16Array з двома елементами. Елементи зчитуються і записуються з використанням їх числових індексів, і ці символи будуть автоматично зберігатися і перетворюватися в int16 значення, як частина операції. Однак, на цьому схожість не закінчуються.

I> На відміну від звичайних масивів, ви не можете змінити розмір типізованого масиву за допомогою властивості length. Властивість length не доступна для запису, так що будь-яка спроба змінити її ігнорується в нестрогому режимі і видає помилку в строгому.

Базові методи

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

  • copyWithin()
  • entries()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • slice()
  • some()
  • sort()
  • values()

Майте на увазі, що хоча ці методи й діють як і їх аналоги на Array.prototype, вони не повністю однакові. Методи типізованого масиву мають додаткові перевірки для безпеки числового типу і, коли масив повертається, буде повернено типізований масив замість звичайного масиву (відповідно до Symbol.species). Ось простий приклад, щоб продемонструвати різницю:

let ints = new Int16Array([25, 50]),
    mapped = ints.map(v => v * 2);

console.log(mapped.length);        // 2
console.log(mapped[0]);            // 50
console.log(mapped[1]);            // 100

console.log(mapped instanceof Int16Array);  // true

Цей код використовує метод map(), щоб створити масив зі значень в ints. Функція мапування подвоює кожне значення в масиві і повертає новий Int16Array.

Однакові ітератори

Типізовані масиви мають ті ж три ітератори, що й звичайні масиви. Це метод entries(), метод keys() та метод values(). Це означає, що ви можете використовувати оператор розширення та for-of цикли з типізованими масивами так само, як і при роботі зі звичайними масивами. Наприклад:

let ints = new Int16Array([25, 50]),
    intsArray = [...ints];

console.log(intsArray instanceof Array);    // true
console.log(intsArray[0]);                  // 25
console.log(intsArray[1]);                  // 50

Цей код створює новий масив з ім'ям intsArray, що містить ті самі дані, що і типізований масив ints. Як і з іншими ітерабельними об’єктами, оператор розширення робить легкою конвертацію типізованих масивів у звичайні.

Методи of() та from()

І нарешті, всі типізовані масиви мають статичні методи of() та from(), які працюють як методи Array.of() та Array.from(). Різниця полягає в тому, що методи типізованих масивів повертають типізований масив замість звичайного масиву. Ось деякі приклади, які використовують ці методи для створення типізованих масивів:

let ints = Int16Array.of(25, 50),
    floats = Float32Array.from([1.5, 2.5]);

console.log(ints instanceof Int16Array);        // true
console.log(floats instanceof Float32Array);    // true

console.log(ints.length);       // 2
console.log(ints[0]);           // 25
console.log(ints[1]);           // 50

console.log(floats.length);     // 2
console.log(floats[0]);         // 1.5
console.log(floats[1]);         // 2.5

В цьому прикладі методи of() та from() використовуються, щоб створити Int16Array та Float32Array, відповідно. Ці методи переконують, що типізовані масиви можуть бути створені так само легко, як і звичайні масиви.

Відмінності між типізованими та звичайними масивами

Найбільша різниця між типізованими та звичайними масивами полягає у тому, що типізовані масиви не є звичайними масивами. Типізовані масиви не успадкуються від Array, тому Array.isArray() повертає false при передачі типізованого масиву. Наприклад:

let ints = new Int16Array([25, 50]);

console.log(ints instanceof Array);     // false
console.log(Array.isArray(ints));       // false

Оскільки змінна ints є типізованим масивом, вона не є екземпляром Array і так само не може бути ідентифікована як масив. Ця відмінність важлива, тому що в той час як типізовані і звичайні масиви є схожими, існує багато способів, в яких типізовані масиви поводяться по-різному.

Відмінності у поведінці

Тоді, як звичайні масиви можуть збільшуватися і зменшуватися, коли ви взаємодієте з ними, типізовані масиви завжди залишаються того ж розміру. Ви не можете привласнити значення неіснуючому числовому індексу у типізованому масиві, як ви можете зробити у звичайному, тому що типізовані масиви ігнорують таку операцію. Ось приклад:

let ints = new Int16Array([25, 50]);

console.log(ints.length);          // 2
console.log(ints[0]);              // 25
console.log(ints[1]);              // 50

ints[2] = 5;

console.log(ints.length);          // 2
console.log(ints[2]);              // undefined

Не зважаючи на присвоєння 5 числовому індексу 2 в цьому прикладі, масив ints не збільшується. Властивість length залишається незмінною, а значення відкидається.

Типізовані масиви також мають перевірку, щоб переконатися, що використовуються тільки відповідні типи даних. Нуль використовується замість будь-яких невідповідних типів даних. Наприклад:

let ints = new Int16Array(["hi"]);

console.log(ints.length);       // 1
console.log(ints[0]);           // 0

Цей код намагається використовувати рядкове значення "hi" в Int16Array. Звичайно, рядки є неприпустимими типами даних в типізованих масивах, тому замість цього вставляється значення 0. Властивість length масиву рівна одиниці, і навіть не дивлячись на те, що слот ints[0] існує, він містить тільки 0.

Всі методи, які змінюють значення у типізованому масиві застосовують одні й ті ж обмеження. Наприклад, якщо функція надана до map() повертає неправильне для типу масиву значення, то натомість використовується 0:

let ints = new Int16Array([25, 50]),
    mapped = ints.map(v => "hi");

console.log(mapped.length);        // 2
console.log(mapped[0]);            // 0
console.log(mapped[1]);            // 0

console.log(mapped instanceof Int16Array);  // true
console.log(mapped instanceof Array);       // false

Оскільки рядкове значення "hi" не 16-бітове ціле число, воно замінюється на 0 в отриманому масиві. Завдяки цій поведінці з виправленням помилок, методам типізованого масиву не доведеться турбуватися про кидання помилок при введенні неправильних даних, тому що в масиві ніколи не буде невідповідних даних.

Відсутні методи

У той час як типізовані масиви дійсно мають багато тих же методів, що і звичайні масиви, все ж таки їм не вистачає декількох методів звичайних масивів. Наступні методи не доступні для типізованих масивів:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

За винятком методу concat(), методи в цьому списку можуть змінювати розмір масиву. Типізовані масиви не можуть змінювати розмір, тому вони не доступні для типізованих масивів. Метод concat() не доступний, оскільки результат конкатенації двох типізованих масивів (особливо, якщо вони мають справу з різними типами даних) буде невизначеним, і це, в першу чергу, буде суперечити сенсу використання типізованих масивів.

Додаткові методи

Нарешті, перелік методів типізованих масивів містить два методи, яких немає у звичайних масивах: set() та subarray(). Ці два методи є протилежними в тому, що set() копіює інший масив в існуючий типізований масив, в той час як subarray() витягує частину існуючого типізованого масиву в новий типізований масив.

Метод set() приймає масив (типізований чи звичайний) і необовʼязковий аргумент — зміщення з якого почати вставляти дані (якщо ви не передаєте нічого, зміщення за замовчуванням дорівнює 0). Дані з масиву аргументу копіюються до цільового типізованого масиву, забезпечуючи при цьому використання тільки дійсних типів даних. Ось приклад:

let ints = new Int16Array(4);

ints.set([25, 50]);
ints.set([75, 100], 2);

console.log(ints.toString());   // 25,50,75,100

Цей код створює Int16Array з чотирма елементами. Перший виклик set() копіює два значення в першому і другому елементах масиву. Другий виклик set() використовує зсув 2, щоб вказати, що значення повинні бути поміщені в масив, починаючи з третього елемента.

Метод subarray() приймає необов'язковий початковий і кінцевий індекс (кінець індексу є винятковим, як в методі slice()) і повертає новий типізований масив. Ви також можете опустити обидва аргументи, щоб створити клон типізованого масиву. Наприклад:

let ints = new Int16Array([25, 50, 75, 100]),
    subints1 = ints.subarray(),
    subints2 = ints.subarray(2),
    subints3 = ints.subarray(1, 3);

console.log(subints1.toString());   // 25,50,75,100
console.log(subints2.toString());   // 75,100
console.log(subints3.toString());   // 50,75

У цьому прикладі з оригінального масиву ints створюються три типізовані масиви. Масив subints1 являє собою клон ints, який містить ту ж інформацію. Оскільки масив subints2 копіює дані, починаючи з індексу 2, він містить тільки останні два елементи масиву ints (75 та 100). Масив subints3 містить тільки два середні елементи масиву ints, оскільки subarray() був викликаний як з початковим так і кінцевим індексами.

Підсумок

ECMAScript 6 продовжує роботу ECMAScript 5, роблячи масиви більш корисним. Є ще два способи створення масивів: методи Array.of() та Array.from(). Метод Array.from() також може конвертувати ітерабельні і подібні до масивів об'єкти в масиви. Обидва методи успадковуються похідними класами масиву і використовують властивість Symbol.species, щоб визначити, який тип значення має бути повернено (інші успадковані методи також використовують Symbol.species при поверненні масиву).

Є також кілька нових методів для масивів. Методи fill() та copyWithin() дозволяють змінювати елементи масиву на місці. Методи find() та findIndex() корисні для знаходження першого елемента в масиві, який відповідає певним критеріям. Один повертає перший елемент, який відповідає критеріям, а інший повертає індекс елемента.

Типізовані масиви технічно не є масивами, оскільки вони не успадковуються від Array, але за виглядом і поведінкою вони дуже схожі до масивів. Типізовані масиви містять один з восьми різних типів числових даних і побудовані на обʼєктах ArrayBuffer, які представляють бітові дані числа або послідовності чисел. Типізовані масиви є більш ефективним способом виконувати побітову арифметику, оскільки значення не перетворюються між форматами, як у випадку числовим типом у JavaScript.

results matching ""

    No results matching ""