Перший повинен бути останнім із масивами JavaScript

Тож останнє буде [0], а перше [довжина - 1]. - Адаптовано від Матвія 20:16

Я пропущу Мальтузіанську катастрофу і дійду до неї: масиви - одна з найпростіших і найважливіших структур даних. Хоча до термінальних елементів (першого та останнього) часто звертаються, Javascript не надає зручних властивостей або методів для цього, а використання індексів може бути надлишковим та схильним до побічних ефектів та окремих помилок.

Менш відома нещодавня пропозиція JavaScript TC39 пропонує заспокоєння у вигляді двох "нових" властивостей: Array.lastItem& Array.lastIndex.

Масиви Javascript

У багатьох мовах програмування, включаючи Javascript, масиви нульово індексуються. Кінцеві елементи перших і last- доступні через [0]та [length — 1]індекси, відповідно. Цим задоволенням ми зобов'язані прецеденту, встановленому C, де індекс представляє зміщення від голови масиву. ЯКИЙ РОБИТЬ нуль першого індексу , тому що це головний масив. Також Дейкстра проголосив "нуль як найбільш природне число". Тож нехай буде написано. Тож нехай це робиться.

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

На відміну від інших мов сценаріїв (скажімо, PHP або Elixir), Javascript не забезпечує зручного доступу до елементів масиву терміналів. Розглянемо тривіальний приклад заміни останніх елементів у двох масивах:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals[animals.length - 1];animals[animals.length - 1] = faces[faces.length - 1];faces[faces.length - 1] = lastAnimal;

Для логіки обміну потрібні 2 масиви, на які посилаються 8 разів у 3 рядки! У реальному коді це може швидко стати дуже повторюваним і складно для людини проаналізувати (хоча це цілком читається для машини).

Більше того, виключно використовуючи індекси, ви не можете визначити масив і отримати останній елемент у тому самому виразі. Це може здатися не важливим, але розглянемо інший приклад, коли функція getLogins(), робить асинхронний виклик API і повертає відсортований масив. Припускаючи, що ми хочемо останню подію входу в кінці масиву:

let lastLogin = async () => { let logins = await getLogins(); return logins[logins.length - 1];};

Якщо довжина не визначена і не відома заздалегідь, нам потрібно призначити масив локальній змінній для доступу до останнього елемента. Одним із найпоширеніших способів вирішити це в таких мовах, як Python та Ruby, є використання негативних індексів. Потім [length - 1]можна скоротити до [-1], усунувши потребу в місцевій довідці.

Я вважаю -1лише незначно читабельнішим, ніж length — 1, і хоча можливо наблизити негативні індекси масивів у Javascript за допомогою ES6 Proxy, або Array.slice(-1)[0]обидва вони мають значні наслідки для продуктивності для того, що в іншому випадку повинно становити простий довільний доступ.

Підкреслення & Lodash

Одним з найбільш відомих принципів розробки програмного забезпечення є «Не повторюйся» (СУХИЙ). Оскільки доступ до елементів терміналів настільки поширений, чому б не написати допоміжну функцію для цього? На щастя, багато бібліотек, таких як Underscore і Lodash, вже надають утиліти для _.first& _.last.

Це пропонує значне покращення у lastLogin()наведеному вище прикладі:

let lastLogin = async () => _.last(await getLogins());

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

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = _.last(animals);animals[animals.length - 1] = _.last(faces);faces[faces.length - 1] = lastAnimal;

Ці функції утиліти видалили 2 з 8 посилань, лише зараз ми ввели зовнішню залежність, яка, як не дивно, не включає функції для встановлення елементів терміналу.

Швидше за все, така функція навмисно виключається, оскільки її API буде заплутаним і важко читабельним. Ранні версії Лодаша передбачали метод, _.last(array, n)коли n - кількість предметів з кінця, але в підсумку було вилучено на користь _.take(array, n).

Якщо припустити nums, що це масив чисел, якою буде очікувана поведінка _.last(nums, n)? Він може повернути два останні елементи типу _.take, або може встановити значення останнього елемента рівним n .

Якби ми писали функцію для встановлення останнього елемента в масиві, існує лише кілька підходів, які слід розглянути, використовуючи чисті функції, ланцюжок методів або використання прототипу:

let nums = ['d', 'e', 'v', 'e', 'l']; // set first = last
_.first(faces, _.last(faces)); // Lodash style
$(faces).first($(faces).last()); // jQuery style
faces.first(faces.last()); // prototype

Я не вважаю жоден із цих підходів значним покращенням. Насправді тут втрачено щось важливе. Кожен виконує присвоєння, але жоден не використовує оператор присвоєння ( =). Це можна зробити більш очевидним за допомогою правил іменування, як getLastі setFirst, але це швидко стає надто багатослівним. Не кажучи вже про п’яте коло пекла, повне програмістів, які змушені орієнтуватися за “самодокументованим” застарілим кодом, де єдиний спосіб отримати доступ або змінити дані - через геттери та сеттери.

Так чи інакше, схоже , ми застрягли з [0]& [length — 1]...

Чи ми? ?

Пропозиція

Як уже згадувалося, пропозиція технічного кандидата ECMAScript (TC39) намагається вирішити цю проблему шляхом визначення двох нових властивостей Arrayоб’єкта: lastItem& lastIndex. Ця пропозиція вже підтримується в core-js 3 і сьогодні може використовуватися в Babel 7 & TypeScript. Навіть якщо ви не використовуєте компілятор, ця пропозиція включає поліфіл.

Особисто я не знаходжу багато значення в lastIndexі віддаю перевагу більш короткий іменування Рубі для firstі last, хоча це було виключено з - за можливі проблеми сумісності веб. Я також здивований, що ця пропозиція не пропонує firstItemвластивості для послідовності та симетрії.

Тим часом я можу запропонувати підхід без залежності від Ruby-esque в ES6:

Перший Останній

Тепер у нас є дві нові властивості Array– first& last–і рішення, яке:

✓ Використовує оператор присвоєння

✓ Не клонує масив

✓ Може визначити масив і отримати термінальний елемент в одному виразі

✓ Читається людиною

✓ Забезпечує один інтерфейс для отримання та налаштування

Ми можемо переписати lastLogin()ще раз в один рядок:

let lastLogin = async () => (await getLogins()).last;

Але справжній виграш приходить, коли ми поміняємо останні елементи двома масивами на половину кількості посилань:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals.last;animals.last = faces.last;faces.last = lastAnimal;

Все ідеально, і ми вирішили одну з найскладніших проблем CS. У цьому підході не ховаються злі завіти ...

Прообраз параної

Звичайно, немає нікого [програміста] настільки праведного, щоб творити добро, не згрішивши ніколи. - Адаптовано з Екклезіяста 7:20

Many consider extending a native Object’s prototype an anti-pattern and a crime punishable by 100 years of programming in Java. Prior to the introduction of the enumerable property, extending Object.prototype could change the behavior of for in loops. It could also lead to conflict between various libraries, frameworks, and third-party dependencies.

Perhaps the most insidious issue is that, without compile-time tools, a simple spelling mistake could inadvertently create an associative array.

let faces = ["?", "?", "?", "?", "?"];let ln = faces.length 
faces.lst = "?"; // (5) ["?", "?", "?", "?", "?", lst: "?"] 
faces.lst("?"); // Uncaught TypeError: faces.lst is not a function 
faces[ln] = "?"; // (6) ["?", "?", "?", "?", "?", "?"] 

Це занепокоєння не є унікальним для нашого підходу, воно стосується всіх власних прототипів об’єктів (включаючи масиви). Однак це пропонує безпеку в іншій формі. Масиви в Javascript не мають фіксованої довжини і, отже, їх немає IndexOutOfBoundsExceptions. Використовуючи Array.lastгарантії, ми випадково не намагаємось отримати доступ [length]і ненавмисно зайти на undefinedтериторію.

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

Продовжуючи сторонні біблійні посилання, припускаючи, що ми не віримо, що продовження Array.prototype- це вічний гріх, або ми готові вкусити заборонений плід, ми можемо використовувати цей стислий і читабельний синтаксис сьогодні!

Останні слова

Програми повинні бути написані для читання людьми і лише випадково для запуску машин. - Гарольд Абельсон

In scripting languages like Javascript, I prefer code that is functional, concise, and readable. When it comes to accessing terminal array elements, I find the Array.last property to be the most elegant. In a production front-end application, I might favor Lodash to minimize conflict and cross-browser concerns. But in Node back-end services where I control the environment, I prefer these custom properties.

I am certainly not the first, nor will I be the last, to appreciate the value (or caution about the implications) of properties like Array.lastItem, which is hopefully coming soon to a version of ECMAScript near you.

Follow me on LinkedIn · GitHub · Medium