Чистий JavaScript: повернення до джерел
Привіт, я Дмитро Попов, сертифікований Software Engineer. Десять років працюю з JavaScript. Поговорімо про первозданний JavaScript, JS до 2015. JS, який знають піонери й про який, на жаль, не чуло молоде покоління.
JavaScript (JS) — інтерпретована (або JIT) скриптова мова зі слабкою системою типів й динамічною типізацією. Розробив JS Брендан Ейх в 1995 році для браузера Netscape Navigator, нібито за 10 днів. Спершу JS називався LiveScript, а вже потім JavaScript.
Спочатку JavaScript не був таким потужним, як сьогодні, і в основному використовувався для анімацій і чуда, відомого на той час, як Dynamic HTML — попередника DOM. Зараз JavaScript вже вийшов за межі скриптової мови і є мовою загального призначення. Ви можете створювати десктопні та серверні застосунки за допомогою Node.js.
Мова JavaScript стандартизована як ECMAScript, стандартом ECMA-262. Оскільки основне джерело з JavaScript, а саме MDN (Mozilla), посилається на ECMA-262, можна сказати, що JavaScript (JS) — це поточне втілення ECMAScript (ES).
Головні особливості JS:
- First-class functions.
- Closure.
- Scope.
- Object literals.
- Implicit boxing.
Вже маючи своє уявлення про філософію JS, я перечитав книгу Дугласа Крокфорда JavaScript: The Good parts, яка вийшла в далекому 2008 році. Колись мене ця книга зацікавила з двох основних причин:
1. Інтригуюча назва. Які ж хороші риси JS? Враховуючи, що навіть у 2012, коли я вивчав JS, ця мова була надзвичайно контроверсійною, навіть для пітоністів.
2. В книзі використані так званні railroad-діаграми для опису граматики. Це дуже цікавий візуальний спосіб опису синтаксису мови програмування. Railroad-діаграми візуальні, на відміну від форми Бекуса-Наура для опису контекстно-вільних граматик. Дуглас Крокфорд є автором JSON, граматику якого він описав у формі МакКімена (спрощена форма Бекуса-Наура).
Дуглас пише в книзі про ECMAScript 3, який вийшов в 1999. ECMAScript 4 відмінили, ECMAScript 5 (2009) додав незначні зміни, зокрема, strict mode (use strict). ES6 (ECMAScript 2015) ввів класи, модулі та інше.
Що я можу відмітити з його слів. JavaScript набрав свою популярність завдяки браузерам. Всі інші спроби типу Java аплети, ActionScript, чи Dart чомусь не прижилися. Хто знає, чому?
Зараз можна програмувати браузер за допомогою JavaScript та WebAssembly. JS, як пише Дуглас Крокфорд, був написаний в поспіху і має багато поганих частин. Думаю, що про поспіх — це міф. JS написаний за стільки, за скільки написаний. Якщо копнути, то виявиться, що C++ та Java також були «на колінці» створені :)
На думку Дугласа DOM API у 2008 було настільки погане, що навіть важко було написати книжку DOM: The Good parts, бо їх там як кіт наплакав. У якому стані зараз DOM API, то інша тема. В книзі «Елегантний JavaScript» також згадано про проблеми з DOM.
Думаю, таку негативну думку сформувала історія навколо Dynamic HTML, XHTML, а також сумісність між Microsoft Internet Explorer та іншими браузерами. Крім того, розгляньмо властивість ChildNodes, яку мають елементні вузли в DOM. Ця властивість містить об’єкт, подібний до масиву, з властивістю length та властивостями, позначеними числами для доступу до дочірніх вузлів. Але це екземпляр типу NodeList, а не реальний JS масив, тому він не має таких методів, як slice і map.
Крім того, немає способу створити новий вузол і негайно додати до нього дітей або атрибутів. Замість цього ви повинні спочатку створити його, а потім додати дочірні елементи та атрибути один за одним, викликаючи побічні ефекти.
Дуглас пише:
JavaScript побудований на деяких дуже хороших і дуже поганих ідеях. Дуже хороші ідеї містять функції, слабку типізацію, динамічні об’єкти та літеральні об’єкти. Погані ідеї містять модель програмування, засновану на глобальних змінних. JavaScript — це перша лямбда-мова, яка стала мейнстрімом. У глибині душі JavaScript має більше спільного з Lisp і Scheme, ніж з Java. Це Lisp в одязі С. Що робить JavaScript надзвичайно потужною мовою.
Тут Дуглас Крокфорд дуже влучно підкреслює, що в JS функції є first class objects, тобто вони можуть бути записані в змінні, передані як аргументи, та повернені як значення, включно зі змиканням (closure), яке було введено в мові Lisp, та каруванням (curring) як в лямбда-численні. Тут C, C++, Java «пасуть задніх», бо саме JavaScript зробив популярними анонімні функції, IIFE, функції першого класу та функції вищих порядків. Інші мови пізніше додали деякі можливості щодо лямбда-функцій.
Далі, для всіх любителів TypeScript, які вважають що це нині неписаний стандарт, Дуглас пише, що слабка типізація в JS є перевагою. Плюсом, а не мінусом. Питання з типами стане ще цікавішим, коли Дуглас дійде до розмови про літеральні об’єкти, які він вважає хорошою рисою.
Якщо використовувати лінтер (JSLint розробив Дуглас Крокфорд), оператор typeof, й тестувати гарно код, тоді навіщо нам строга типізація? Тим паче якщо вона структурна й без автоматичного виведення типів? Для захисту від дурня? А ви впевнені, що дурень пропише типи нормально?
Варто зауважити, що лінтер може виділити subset (підмножину мови), але TypeScript це superset (надмножина), тобто TS додає нові синтаксичні конструкції з певною семантикою.
JS — неохайна мова, яка містить в собі елегантну кращу мову
Так писав Дуглас Крокфорд. TypeScript навпаки робить обгортку над JS. Далі в книзі йдеться про те, як реалізувати метод Object.create та Array.isArray, які зараз вже додані в мову. Автор також пише, що потрібно ввести block scope в JS, що було виконано.
Дуглас також обговорює прототипне наслідування.
Object.getPrototypeOf повертає прототип об’єкта.
Коли створюється об’єкт функції, конструктор функції, який створює об’єкт функції, запускає такий код:
this.prototype = {constructor: this};
Тобто:
function User(name){ this.name = name; } const john = new User("John"); console.log(john.constructor===User); //true console.log(john.__proto__ === User.prototype); //true
Дуглас пропонує відмовитися від використання безпосередньо функцій конструкторів в JS, та говорить, що класичне класове наслідування в основному зумовлене строгою типізацією й не потрібне в JS.
Є так звана проблема номінальної та структурної перевірки типів. Це вірно, бо відсутність множинного наслідування та ведення «інтерфейсів» якраз розв’язує набридливе питання типів, а не перевикористання методів.
Відсутність множинного наслідування також зумовлене Diamond problem.
Дуглас пропонує патерн, який дає змогу створювати об’єкти на основі інших об’єктів та при цьому інкапсулювати значення. Цей патерн відомий, а саме обгортання створення об’єкта в функцію потрібно для того, щоб реалізувати інкапсуляцію. Складна система типів створює те, що називається accidental complexity, а нас цікавить essential complexity.
Дуглас пише:
Багато функцій JavaScript були запозичені з інших мов. Синтаксис походить від Java, функції — зі Scheme, а прототипне успадкування — від Self. Регулярні вирази JavaScript були запозичені з Perl.
Коротко кажучи, хороші сторони ECMAScript3 (JS) за Дугласом Крокфордом — це:
- Функції як об’єкти першого класу.
- Динамічні об’єкти з прототипним успадкуванням.
- Літерали об’єктів і літерали масивів.
Як я розумію, наявність літеральних об’єктів унеможливлює номінальну перевірку типів.
Вперше класи були реалізовані в мові Симула, в якій ви не могли створити об’єкт, не створюючи спершу клас. В JS, навпаки, об’єкт є первинним, а не клас. Крім того, майже все за замовчуванням обгортається в об’єкт.
Автор, звичайно, жаліється на те, що повертає typeof оператор. Він також критикує застосування стандарту IEEE 754, який веде до 0.2 + 0.1 ≠ 0.3. Він критикує такі штуки як with, eval, == (loose comparison).
У 2018 році, через десять років після публікації JavaScript:The Good parts, Дуглас Крокфорд написав іншу книгу — How JavaScript Works (2018). Де він виступив різко проти використання класів в JS, точніше проти їхньої імітації.
Він також пропонував здихатися ключового слова this та типу null, бо вони створюють більше проблем, ніж користі. Коротше, конфузять. Якщо наявність одразу null та undefined конфузять, уявіть, що в TS є undefined, null, void, never, unknown, any.
Дуглас також проти прямого використання ключового слова new, яке веде до правила, що всі функції-конструктори мають писатися з великої літери, щоб їх випадково не викликали без new, просто як звичайну функцію. А ще він проти використання генераторів, які ввели в ES6, бо в JS це можна зробити краще через змикання (closure).
function counter_constructor() { let counter = 0; function up() { counter += 1; return counter; } function down() { counter -= 1; return counter; } return Object.freeze({ up, down }); }
Об’єкт, який повертається, заморожений. Об’єкт не може бути пошкоджений. Об’єкт має стан. Змінна лічильника є приватною властивістю об’єкта. До неї можна отримати доступ лише за допомогою методів. І нам не потрібно користуватися ключовим словом this.
Насправді мінуси в JS, які описує Дуглас Крокфорд, або легко усуваються, або є незначними в порівнянні з плюсами мови JavaScript. JavaScript такий гнучкий, що навіть відома книга Structure and Interpretation of Computer Programs в 2022 році вийшла у JavaScript-версії, хоча в оригіналі вона була чи то на Lisp, чи Scheme. До речі, Lisp не мав статичної типізації, але мав garbage collector. На відміну від JS, який має інфіксну нотацію, Lisp мав префіксну нотацію, схожу на польську нотацію.
Щодо TypeScript, то він зайняв свою нішу. Але величезні набори типів додають випадкову складність (accidental complexity), тобто складність, яка безпосередньо не розв’язує нашу задачу, хоча може давати певні переваги. Тут варто знайти баланс. Крім того, TS не є класичною ООП мовою, бо перейняв прототипне наслідування з JS.
JavaScript (ECMAScript) визначає колекцію вбудованих об’єктів. Ці вбудовані об’єкти містять глобальний об’єкт (global, window); об’єкти, які є фундаментальними для семантики виконання мови, включно з Object, Function, Boolean, Symbol. Та різні об’єкти Error; об’єкти, які представляють і маніпулюють числовими значеннями, включно з Math, Number і Date. Об’єкти обробки тексту String і RegExp. Об’єкти, які є індексованими колекціями значень, включно з Array і дев’ятьма різними типами типізованих масивів, усі елементи яких мають конкретне представлення числових даних. Колекції з ключем, включно з об’єктами Map і Set. Об’єкти, що підтримують структуровані дані, включно з об’єктами JSON, ArrayBuffer, SharedArrayBuffer і DataView. Об’єкти для generator functions та Promise та об’єкти відображення, включно з Proxy та Reflect.
Структура JavaScript:
- statement and expression in JS
- const, var, let (hoisting, temporary dead zone, immutability)
- data type (Primitive types: number, bigint, string, boolean, null, undefined, symbol; Reference types: object, (array, function)), typeof operator
- arithmetic operations (+,-,*,/,%). Math object, Math.random();
- logic operations (&&, ||, !)
- Conditional statement (if, switch), ternary operator
- Loops (for, while, do-while, forEach, map, for-of, for-in, ...)
- function declaration and function expression, arrow function, IIFE, anonymous function
- constructor function, this, .prototype, .__proto__, isPrototypeOf
- class, super(), extends, this, getter, setter, instanceof
- Promises, async, await, timers, event loop, micro and macro tasks
- Generators, new Proxy
- new Date, RegExp, JSON.stringify, parseInt, parseFloat, template literals
Методи масиву: reverse, sort, splice, forEach, map, flatMap, filter, find, reducer (sum array), some, includes, concat, length, Array.isArray.
Методи string: padStart, indexOf, substring, length, trim, toUpperCase, replace, replaceAll, match, search.
Методи number: toFixed, toPrecision, toString. Number.isInteger.
Методи об’єкта: Object.seal, .create, .is, .defineProperty, .keys, .values, .entries, .freeze, .hasOwnProperty, .assign.
Методи функцій: bind, call, apply.
Методи new Map, Set: has, add, remove and many others methods from set theory.
Декілька GoF дизайнів ООП-патернів в JS:
const singleton = {text:"hello"}.
Уявляєте, в JS singleton можна реалізувати простим літеральним об’єктом. В багатьох мовах просто немає літеральних об’єктів.
const factory = Object.
Так, Object — це фабрика (factory method), він віддає вам різні типи об’єктів залежно від параметрів. new Object(1), new Object("string«).
Патерн Ітератор:
function* generator(){ yield 1; yield 2; yield 3; } const gen = generator(); //Генеруємо ітератор console.log(gen.next().value) //1
Шаблон Прототип в JS, реалізований «з коробки»:
const obj2 = Object.create(obj1); Патерн Проксі в JS "з коробки" const target = { message:"text" }; const handler = { get(target){ return "hello"; } }; const proxy = new Proxy(target, handler); proxy.message // hello
Патерн (шаблон) Chain of responsibility можна реалізувати з допомогою new Promise. Просто будуєте ланцюжок промісів.
new Promise((resolve, reject) => { }).then(result => { return new Promise(); }).catch(error => { });
Шаблон Observer реалізований в стейт-менеджерах типу Redux, також в RxJS. Він реалізований і в document.addEventListener та EventEmitter.
const evt = new Event("look", { bubbles: true, cancelable: false }); document.dispatchEvent(evt);
Джерела про JS, які згадуються в статті:
- «JavaScript: The Good parts» (2008), Douglas Crockford.
- «Pro JavaScript Design Patterns» (2008), Ross Harmes.
- «JavaScript Patterns» (2010), Stoyan Stefanov.
- «JavaScript. The Definitive Guide. 7th Edition», David Flanagan.
- «How JavaScript Works» (2018), Douglas Crockford.
- «Eloquent JavaScript 3rd edition» (2018), Marijn Haverbeke.
- «JavaScript for impatient programmers» (2022), Dr. Axel Rauschmayer.
- «You don’t know JS», Kyle Simpson.
- ECMAScript® 2021 Language Specification.
- MDN, developer.mozilla.org.
60 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів