Първият трябва да бъде последен с JavaScript масиви

Така че последният ще бъде [0]и първият [дължина - 1]. - Адаптиран от Матей 20:16

Ще пропусна Малтусианската катастрофа и ще стигна до нея: масивите са една от най-простите и важни структури от данни. Докато терминалните елементи (първи и последен) са често достъпни, Javascript не предоставя удобно свойство или метод за това и използването на индекси може да бъде излишно и склонно към странични ефекти и единични грешки.

По-малко известно, скорошно предложение за TC 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

Един от най-добре познатите принципи при разработването на софтуер е „Не се повтаряйте“ (DRY) Тъй като достъпът до терминални елементи е толкова често срещан, защо не напишете помощна функция, за да го направите? За щастие, много библиотеки като 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 би бил объркващ и труден за четене. Ранните версии на Lodash предоставят метод, _.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 Technical Candidate (TC39) се опитва да реши този проблем, като дефинира две нови свойства на Arrayобекта: lastItem& lastIndex. Това предложение вече се поддържа в core-js 3 и може да се използва днес в Babel 7 и TypeScript. Дори и да не използвате преводач, това предложение включва polyfill.

Лично аз не намирам голяма стойност в lastIndexи предпочитам по-краткото именуване на Ruby за 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