JavaScript е странен език. Въпреки че е вдъхновен от Smalltalk, той използва синтаксис, подобен на C. Той съчетава аспекти на процедурни, функционални и обектно-ориентирани програми (OOP) парадигми. То има многобройни , често излишни, подходи за решаване на почти всички възможни проблеми на програмирането и не е силно убеден в това кои са предпочитани. Той е слабо и динамично въведен, с лабиринт подход към принуда за тип, който задейства дори опитни разработчици.
JavaScript също има своите брадавици, капани и съмнителни функции. Новите програмисти се борят с някои от по-трудните му концепции - мислят за асинхронност, затваряне и повдигане. Програмистите с опит в други езици разумно предполагат, че неща с подобни имена и външен вид ще работят по същия начин в JavaScript и често грешат. Масивите всъщност не са масиви; каква е сделката с this
, какво е прототип и какво означава new
всъщност?
Най-лошият нарушител досега е нов в последната версия на JavaScript, ECMAScript 6 (ES6): класове . Някои от разговорите около класовете са откровено тревожни и разкриват дълбоко вкоренено неразбиране за това как всъщност работи езикът:
„JavaScript най-накрая е истински обектно-ориентиран език сега, когато има класове! '
Или:
„Класовете ни освобождават от мисълта за счупения модел на наследяване на JavaScript.“
Или дори:
„Класовете са по-безопасен и лесен подход за създаване на типове в JavaScript.“
Тези твърдения не ме притесняват, защото предполагат, че има нещо нередно прототипно наследяване ; нека оставим настрана тези аргументи. Тези твърдения ме притесняват, тъй като нито едно от тях не е вярно и те демонстрират последиците от подхода „всичко за всички“ на JavaScript към езиковия дизайн: Той осакатява разбирането на езика на програмиста по-често, отколкото позволява. Преди да продължа по-нататък, нека илюстрираме.
function PrototypicalGreeting(greeting = 'Hello', name = 'World') { this.greeting = greeting this.name = name } PrototypicalGreeting.prototype.greet = function() { return `${this.greeting}, ${this.name}!` } const greetProto = new PrototypicalGreeting('Hey', 'folks') console.log(greetProto.greet())
class ClassicalGreeting { constructor(greeting = 'Hello', name = 'World') { this.greeting = greeting this.name = name } greet() { return `${this.greeting}, ${this.name}!` } } const classyGreeting = new ClassicalGreeting('Hey', 'folks') console.log(classyGreeting.greet())
Отговорът тук е няма такъв . Те правят ефективно едно и също нещо, въпрос е само дали е използван синтаксис на клас ES6.
Вярно е, че вторият пример е по-изразителен. Само поради тази причина може да твърдите, че class
е приятно допълнение към езика. За съжаление проблемът е малко по-фин.
function Proto() { this.name = 'Proto' return this; } Proto.prototype.getName = function() { return this.name } class MyClass extends Proto { constructor() { super() this.name = 'MyClass' } } const instance = new MyClass() console.log(instance.getName()) Proto.prototype.getName = function() { return 'Overridden in Proto' } console.log(instance.getName()) MyClass.prototype.getName = function() { return 'Overridden in MyClass' } console.log(instance.getName()) instance.getName = function() { return 'Overridden in instance' } console.log(instance.getName())
Правилният отговор е, че той отпечатва на конзола:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
Ако сте отговорили неправилно, не разбирате какво class
всъщност е. Това не е твоята вина. Подобно на Array
, class
не е езикова функция, това е синтактичен мракобесие . Той се опитва да скрие прототипния модел за наследяване и несръчните идиоми, които идват с него, и предполага, че JavaScript прави нещо, което не е.
Може да ви е било казано, че class
беше въведен в JavaScript, за да направят разработчиците на класически OOP, идващи от езици като Java, по-комфортни с модела за наследяване на клас ES6. Ако ти са един от тези разработчици, този пример вероятно ви ужаси. Би трябвало. Това показва, че JavaScript е class
ключовата дума не идва с нито една от гаранциите, които класът трябва да предостави. Той също така демонстрира една от ключовите разлики в модела за наследяване на прототипа: Прототипите са екземпляри на обекти , не видове .
Най-важната разлика между наследяването на базата на клас и прототип е, че клас дефинира a Тип който може да бъде създаден по време на изпълнение, докато прототипът сам по себе си е обект на екземпляр.
Дете от клас ES6 е друго Тип дефиниция, която разширява родителя с нови свойства и методи, които от своя страна могат да бъдат създадени по време на изпълнение. Дете на прототип е друг обект инстанция която делегира на родителя всички свойства, които не са внедрени в детето.
Странична бележка: Може би се чудите защо споменах методите на класа, но не и прототипите. Това е така, защото JavaScript няма концепция за методи. Функциите са първи клас в JavaScript и те могат да имат свойства или да бъдат свойства на други обекти.
Конструктор на клас създава екземпляр на класа. Конструкторът в JavaScript е просто обикновена стара функция, която връща обект. Единственото нещо специално за конструктора на JavaScript е, че при извикване с new
ключова дума, тя присвоява своя прототип като прототип на върнатия обект. Ако това ви звучи малко объркващо, вие не сте сами - това е и е голяма част от причините, поради които прототипите са слабо разбрани.
За да поставим наистина фина точка върху това, дете от прототип не е копие на своя прототип, нито е обект със същото форма като негов прототип. Детето има жива справка да се прототипът и всяко свойство на прототип, което не съществува в дъщерното устройство, е еднопосочна препратка към свойство със същото име в прототипа.
Помислете за следното:
let parent = { foo: 'foo' } let child = { } Object.setPrototypeOf(child, parent) console.log(child.foo) // 'foo' child.foo = 'bar' console.log(child.foo) // 'bar' console.log(parent.foo) // 'foo' delete child.foo console.log(child.foo) // 'foo' parent.foo = 'baz' console.log(child.foo) // 'baz'
Забележка: Почти никога не бихте писали подобен код в реалния живот - това е ужасна практика - но демонстрира лаконично принципа.В предишния пример, докато child.foo
беше undefined
, той се позовава parent.foo
. Веднага щом определихме foo
на child
, child.foo
имаше стойността 'bar'
, но parent.foo
запази първоначалната си стойност. След като | | + _ | той отново се отнася до delete child.foo
, което означава, че когато променяме стойността на родителя, parent.foo
се отнася до новата стойност.
Нека разгледаме какво се е случило току-що (за по-ясна илюстрация ще се преструваме, че това са child.foo
, а не низови литерали, разликата тук няма значение):
Начинът, по който това работи под предния капак, и особено особеностите на Strings
и new
, са тема за друг ден, но Mozilla има задълбочена статия за веригата за наследяване на прототипи на JavaScript ако искате да прочетете повече.
Ключовият извод е, че прототипите не дефинират this
; те самите са type
и те са променливи по време на изпълнение, с всичко, което предполага и предполага.
Още ли си с мен? Нека се върнем към дисектирането на JavaScript класове.
Нашите свойства на прототипа и класа по-горе не са толкова „капсулирани“, колкото „висящи несигурно през прозореца“. Трябва да поправим това, но как?
Тук няма примери за кодове. Отговорът е, че не можете.
JavaScript няма никаква концепция за поверителност, но има затваряне:
instances
Разбирате ли какво се случи току-що? Ако не, не разбирате затварянията. Това е добре, наистина - те не са толкова плашещи, колкото са си представени, те са супер полезни и вие трябва отделете малко време, за да научите за тях .
function SecretiveProto() { const secret = 'The Class is a lie!' this.spillTheBeans = function() { console.log(secret) } } const blabbermouth = new SecretiveProto() try { console.log(blabbermouth.secret) } catch(e) { // TypeError: SecretiveClass.secret is not defined } blabbermouth.spillTheBeans() // 'The Class is a lie!'
Ключова дума?Извинете, това е поредният трик въпрос. Можете да направите основно едно и също нещо, но изглежда така:
class
Кажете ми дали това изглежда по-лесно или по-ясно, отколкото в class SecretiveClass { constructor() { const secret = 'I am a lie!' this.spillTheBeans = function() { console.log(secret) } } looseLips() { console.log(secret) } } const liar = new SecretiveClass() try { console.log(liar.secret) } catch(e) { console.log(e) // TypeError: SecretiveClass.secret is not defined } liar.spillTheBeans() // 'I am a lie!'
. Според мен е малко по-лошо - нарушава идиоматичната употреба на SecretiveProto
декларации в JavaScript и не работи много, както бихте очаквали да идва от, да речем, Java. Това ще стане ясно от следното:
class
ДаНека разберем:
SecretiveClass::looseLips()
Е ... това беше неудобно.
Познахте, това е друг трик - опитни разработчици на JavaScript са склонни да избягват и двете, когато могат. Ето един добър начин да направите горното с идиоматичен JavaScript:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }
Тук не става въпрос само за избягване на присъщата грозота на наследството или за налагане на капсулиране. Помислете какво друго бихте могли да направите с function secretFactory() { const secret = 'Favor composition over inheritance, `new` is considered harmful, and the end is near!' const spillTheBeans = () => console.log(secret) return { spillTheBeans } } const leaker = secretFactory() leaker.spillTheBeans()
и secretFactory
което не бихте могли лесно да направите с прототип или клас.
Първо, можете да го деструктурирате, защото не е нужно да се притеснявате за контекста на leaker
:
this
Това е доста хубаво. Освен избягване на const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)
и new
tomfoolery, тя ни позволява да използваме обектите си взаимозаменяемо с модулите CommonJS и ES6. Също така прави композицията малко по-лесна:
this
Клиенти на function spyFactory(infiltrationTarget) { return { exfiltrate: infiltrationTarget.spillTheBeans } } const blackHat = spyFactory(leaker) blackHat.exfiltrate() // Favor composition over inheritance, (...) console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)
не трябва да се притеснявате къде blackHat
дойде от и exfiltrate
не трябва да се забърква с spyFactory
жонглиране на контекст или дълбоко вложени свойства. Имайте предвид, че не трябва да се притесняваме много за Function::bind
в прост синхронен процедурен код, но причинява всякакви проблеми в асинхронния код, които е по-добре да се избягват.
С малко размисъл, this
може да се превърне в изключително сложен инструмент за шпионаж, който може да се справи с всякакви цели на проникване - или с други думи, фасада .
Разбира се, бихте могли да направите това и с клас, или по-скоро асортимент от класове, които всички наследяват от spyFactory
или abstract class
... с изключение на това, че JavaScript няма концепция за резюмета или интерфейси.
Нека се върнем към приветливия пример, за да видим как бихме го внедрили с фабрика:
interface
Може би сте забелязали, че тези фабрики стават все по-кратки, докато вървим напред, но не се притеснявайте - те правят същото. Тренировъчните колела се свалят, хора!
Това вече е по-малко шаблон от прототипа или версията на класа на същия код. На второ място, постига по-ефективно капсулиране на своите свойства. Освен това той има по-нисък отпечатък в паметта и производителността в някои случаи (на пръв поглед може да не изглежда така, но JIT компилаторът работи тихо зад кулисите, за да намали дублирането и да направи изводи).
Така че е по-безопасно, често е по-бързо и е по-лесно да пишете код като този. Защо отново се нуждаем от класове? О, разбира се, многократна употреба. Какво се случва, ако искаме нещастни и ентусиазирани по-приветливи варианти? Е, ако използваме function greeterFactory(greeting = 'Hello', name = 'World') { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory('Hey', 'folks').greet()) // Hey, folks!
клас, вероятно се впускаме директно в сънуването на йерархия на класа. Знаем, че ще трябва да параметризираме пунктуацията, така че ще направим малко рефакторинг и ще добавим някои деца:
ClassicalGreeting
Това е добър подход, докато някой не се появи и не поиска функция, която не се вписва чисто в йерархията и цялото нещо спира да има смисъл. Поставете щифт в тази мисъл, докато се опитваме да напишем същата функционалност с фабриките:
// Greeting class class ClassicalGreeting { constructor(greeting = 'Hello', name = 'World', punctuation = '!') { this.greeting = greeting this.name = name this.punctuation = punctuation } greet() { return `${this.greeting}, ${this.name}${this.punctuation}` } } // An unhappy greeting class UnhappyGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, ' :(') } } const classyUnhappyGreeting = new UnhappyGreeting('Hello', 'everyone') console.log(classyUnhappyGreeting.greet()) // Hello, everyone :( // An enthusiastic greeting class EnthusiasticGreeting extends ClassicalGreeting { constructor(greeting, name) { super(greeting, name, '!!') } greet() { return super.greet().toUpperCase() } } const greetingWithEnthusiasm = new EnthusiasticGreeting() console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!
Не е очевидно, че този код е по-добър, въпреки че е малко по-кратък. Всъщност бихте могли да спорите, че е по-трудно за четене и може би това е тъп подход. Не бихме могли просто да имаме const greeterFactory = (greeting = 'Hello', name = 'World', punctuation = '!') => ({ greet: () => `${greeting}, ${name}${punctuation}` }) // Makes a greeter unhappy const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ':(') console.log(unhappy(greeterFactory)('Hello', 'everyone').greet()) // Hello, everyone :( // Makes a greeter enthusiastic const enthusiastic = (greeter) => (greeting, name) => ({ greet: () => greeter(greeting, name, '!!').greet().toUpperCase() }) console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!
и unhappyGreeterFactory
?
Тогава клиентът ви идва и казва: „Имам нужда от нов приветстващ, който е нещастен и иска цялата стая да знае за него!“
enthusiasticGreeterFactory
Ако трябваше да използваме този ентусиазирано нещастен приветливец повече от веднъж, бихме могли да го улесним:
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
Има подходи към този стил на композиция, които работят с прототипи или класове. Например, можете да преосмислите const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory('You're late', 'Jim').greet())
и UnhappyGreeting
като декоратори . Все пак ще са необходими повече образци от използвания по-горе подход във функционален стил, но това е цената, която плащате за безопасността и капсулирането на истински класове.
Работата е там, че в JavaScript не получавате тази автоматична безопасност. JavaScript рамки, които подчертават EnthusiasticGreeting
използването прави много „магия“, за да отпечата тези проблеми и принуждава класовете да се държат добре. Погледнете Polymer’s class
програмен код известно време, смея ви. Това са нива на арка-съветник на JavaScript arcana и имам предвид, че без ирония или сарказъм.
Разбира се, можем да поправим някои от обсъдените по-горе проблеми с ElementMixin
или Object.freeze
за по-голям или по-малък ефект. Но защо да имитираме формата без функцията, като същевременно игнорираме инструментите JavaScript прави първоначално ни предоставят, че може да не намерим на езици като Java? Бихте ли използвали чук с етикет „отвертка“, за да забиете винт, когато вашата кутия с инструменти имаше истинска отвертка, седнала точно до него?
Разработчиците на JavaScript често наблягат на добрите части на езика, както в разговорно, така и по отношение на едноименната книга . Опитваме се да избягваме капаните, поставени от по-съмнителния избор на езиков дизайн и се придържаме към частите, които ни позволяват да пишем чист, четим, минимизиращ грешки код за многократна употреба.
Има разумни аргументи за това кои части на JavaScript отговарят на условията, но се надявам да съм ви убедил, че Object.defineProperties
не е един от тях. В противен случай се надяваме, че разбирате, че наследяването в JavaScript може да бъде объркваща бъркотия и че class
нито го поправя, нито спестява, че трябва да разбирате прототипи. Допълнителен кредит, ако сте разбрали намеците, че обектно-ориентираните дизайнерски модели работят добре без класове или ES6 наследяване.
Не ви казвам да избягвате class
изцяло. Понякога се нуждаете от наследство и class
осигурява по-чист синтаксис за това. По-специално, class
е много по-хубав от стария прототип. Освен това много популярни фреймворк рамки насърчават използването му и вероятно трябва да избягвате да пишете странни нестандартни кодове само по принцип. Просто не ми харесва къде отива това.
В моите кошмари се пише цяло поколение JavaScript библиотеки, използвайки class X extends Y
, с очакването, че ще се държи подобно на други популярни езици. Откриват се цели нови класове грешки (предназначени за игра на думи). Възкресени са стари, които лесно биха могли да бъдат оставени в гробището на неправилно сформиран JavaScript, ако не бяхме небрежно попаднали в class
капан. Опитните разработчици на JavaScript са измъчвани от тези чудовища, защото популярното не винаги е доброто.
В крайна сметка всички се отказваме разочаровани и започваме да преоткриваме колелата в Rust, Go, Haskell или кой знае какво още, а след това да компилираме в Wasm за мрежата, а новите уеб рамки и библиотеки се разпространяват в многоезична безкрайност.
Наистина ме държи буден през нощта.
ES6 е най-новата стабилна реализация на ECMAScript, отвореният стандарт, на който се основава JavaScript. Той добавя редица нови функции към езика, включително официална модулна система, променливи и константи с обхват на блокове, функции със стрелки и много други нови ключови думи, синтаксиси и вградени обекти.
ES6 (ES2015) е най-новият стандарт, който е стабилен и се прилага изцяло (с изключение на правилните опашки и някои нюанси) в най-новите версии на основните браузъри и други JS среди. ES7 (ES2016) и ES8 (ES2017) също са стабилни спецификации, но изпълнението е доста смесено.
JavaScript има силна поддръжка на обектно-ориентирано програмиране, но използва различен модел на наследяване (прототип) в сравнение с повечето популярни езици OO (които използват класическо наследяване). Той също така поддържа процедурни и функционални стилове на програмиране.
В ES6 ключовата дума 'class' и свързаните с нея функции са нов подход за създаване на прототипни конструктори. Те не са истински класове по начин, който би бил познат на потребителите на повечето други обектно-ориентирани езици.
Човек може да внедри наследяване в JavaScript ES6 чрез ключовите думи 'class' и 'extends'. Друг подход е чрез идиома на функцията 'конструктор' плюс присвояване на функции и статични свойства на прототипа на конструктора.
В прототипното наследяване прототипите са екземпляри на обекти, на които дъщерни екземпляри делегират недефинирани свойства. За разлика от това класовете в класическото наследяване са дефиниции на типове, от които дъщерни класове наследяват методи и свойства по време на създаването на инстанция.