JavaScript je čudna jezik. Iako nadahnut Smalltalkom, koristi sintaksu nalik na C. Kombinira aspekte paradigmi proceduralnog, funkcionalnog i objektno orijentiranog programiranja (OOP). Ima brojne , često suvišan, pristupa rješavanju gotovo svih zamislivih problema programiranja i nema snažno mišljenje o tome koji se preferiraju. Slabo je i dinamično tipkan, s labirintnim pristupom prisili tipa, koji pokreće čak i iskusne programere.
JavaScript također ima svoje bradavice, zamke i upitne značajke. Novi programeri bore se s nekim težim konceptima - misle na asinkronost, zatvaranje i podizanje. Programeri s iskustvom na drugim jezicima razumno pretpostavljaju da će stvari sa sličnim imenima i izgledima raditi na isti način u JavaScript-u i često griješe. Nizovi zapravo nisu nizovi; o čemu se radi this
, što je prototip, a što new
zapravo učiniti?
Do sada je najgori prijestupnik novost u najnovijoj izdanju JavaScript-a, ECMAScript 6 (ES6): razreda . Neki od razgovora oko predavanja iskreno su alarmantni i otkrivaju duboko ukorijenjeno nerazumijevanje kako jezik zapravo funkcionira:
“JavaScript je napokon a stvaran objektno orijentirani jezik sada kad ima nastavu! '
Ili:
'Predavanja nas oslobađaju razmišljanja o slomljenom JavaScript modelu nasljeđivanja.'
Ili čak:
'Predavanja su sigurniji, lakši pristup stvaranju vrsta u JavaScript-u.'
Ove me izjave ne smetaju jer impliciraju da nešto nije u redu prototipsko nasljeđivanje ; ostavimo te argumente po strani. Ove me izjave smetaju jer niti jedna od njih nije istinita i pokazuju posljedice JavaScript-ovog pristupa 'sve za svakoga' dizajniranju jezika: programeru umanjuje razumijevanje jezika češće nego što omogućuje. Prije nego što nastavim dalje, ilustrirajmo.
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())
Ovdje je odgovor nema jednog . Oni učinkovito rade isto, samo je pitanje je li korištena sintaksa klase ES6.
c korporacija vs s korporacija vs llc
Istina, drugi je primjer izražajniji. Samo iz tog razloga možete tvrditi da class
je lijep dodatak jeziku. Nažalost, problem je malo suptilniji.
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())
Točan odgovor je da se ispisuje na konzolu:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
Ako ste netočno odgovorili, ne razumijete što class
zapravo jest. Nisi ti kriv. Slično kao Array
, class
nije jezična značajka, to je sintaktičko mračnjaštvo . Pokušava sakriti prototipski model nasljeđivanja i nespretne idiome koji dolaze s njim, a podrazumijeva da JavaScript radi nešto što nije.
Možda su vam rekli da class
je uveden u JavaScript kako bi klasični OOP programeri koji dolaze iz jezika poput Jave bili ugodniji za model nasljeđivanja klase ES6. Ako ti jesu jedan od onih programera, taj vas je primjer vjerojatno užasnuo. Trebalo bi. To pokazuje da JavaScript ima class
ključna riječ ne sadrži niti jedno jamstvo koje klasa treba pružiti. Također pokazuje jednu od ključnih razlika u modelu nasljeđivanja prototipa: Prototipi su instance objekta , nije vrste .
Najvažnija razlika između nasljeđivanja na temelju klase i prototipa je u tome što klasa definira a tip koji se mogu instancirati tijekom izvođenja, dok je prototip sam po sebi objektna instanca.
Dijete razreda ES6 je još jedno tip definicija koja proširuje roditelja novim svojstvima i metodama, koje se zauzvrat mogu pokrenuti u vrijeme izvođenja. Dijete prototipa je još jedan objekt primjer koja roditelju delegira sva svojstva koja nisu implementirana na djetetu.
Napomena: Možda se pitate zašto sam spomenuo metode klase, ali ne i prototipove. To je zato što JavaScript nema koncept metoda. Funkcije su prvi razred u JavaScript-u, a mogu imati svojstva ili biti svojstva drugih objekata.
Konstruktor klase stvara instancu klase. Konstruktor u JavaScript-u samo je obična stara funkcija koja vraća objekt. Jedino što je posebno kod JavaScript konstruktora je to što se, kada se pozove s new
ključna riječ, svoj prototip dodjeljuje kao prototip vraćenog objekta. Ako vam to zvuči pomalo zbunjujuće, niste sami - jest i velik je dio zašto se prototipi slabo razumiju.
Da bismo na to stavili doista finu stvar, dijete prototipa nije kopirati njegovog prototipa, niti je objekt s istim oblik kao njegov prototip. Dijete ima živu referencu do prototip, a svako svojstvo prototipa koje ne postoji na djetetu jednosmjerna je referenca na istoimeno svojstvo na prototipu.
Uzmite u obzir sljedeće:
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'
Napomena: Gotovo nikada ne biste napisali ovakav kôd u stvarnom životu - to je užasna praksa - ali sažeto pokazuje princip.U prethodnom primjeru, dok child.foo
je undefined
, referencirano je parent.foo
. Čim smo definirali foo
na child
, child.foo
imao vrijednost 'bar'
, ali parent.foo
zadržao svoju izvornu vrijednost. Jednom kad smo delete child.foo
opet se odnosi na parent.foo
, što znači da kada promijenimo vrijednost roditelja, child.foo
odnosi se na novu vrijednost.
Pogledajmo što se upravo dogodilo (u svrhu jasnije ilustracije pretvarat ćemo se da su to Strings
a ne nizalni literali, razlika ovdje nije bitna):
Način na koji ovo funkcionira ispod haube, a posebno posebnosti new
i this
, tema su za neki drugi dan, ali Mozilla jest temeljit članak o JavaScriptovom lancu nasljeđivanja prototipa ako želite pročitati više.
Ključno je što prototipi ne definiraju type
; oni su sami instances
i oni su promjenjivi tijekom izvođenja, sa svime što podrazumijeva i podrazumijeva.
Još uvijek ste sa mnom? Vratimo se seciranju JavaScript klasa.
Naša gornja svojstva prototipa i klase nisu toliko „inkapsulirana“ koliko „nesigurno vise na prozoru“. To bismo trebali popraviti, ali kako?
Ovdje nema primjera koda. Odgovor je da ne možete.
JavaScript nema nikakav koncept privatnosti, ali ima zatvaranja:
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!'
Razumijete li što se upravo dogodilo? Ako ne, ne razumijete zatvaranja. To je u redu, stvarno - oni nisu toliko zastrašujući kao što se pretvaralo, oni su super korisni, a vi biste trebali odvojite malo vremena da naučite o njima .
class
Ključna riječ?Oprostite, ovo je još jedno trik pitanje. U osnovi možete učiniti isto, ali to izgleda ovako:
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!'
Javite mi izgleda li to lakše ili jasnije nego u SecretiveProto
. Po mom osobnom gledištu, to je nešto gore - razbija idiomatsku uporabu class
deklaracije u JavaScript-u i ne radi baš onako kako biste očekivali da dolazi od, recimo, Jave. To će biti jasno razvidno iz sljedećeg:
SecretiveClass::looseLips()
Čini?Hajde da vidimo:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }
Pa ... to je bilo neugodno.
glavna datoteka c ++
Pogađate, to je još jedno trik pitanje - iskusni programeri JavaScript obično izbjegavaju oboje kad mogu. Evo lijepog načina da gore navedeno učinimo s idiomatskim JavaScriptom:
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()
Ovdje se ne radi samo o izbjegavanju urođene ružnoće nasljeđa ili o provođenju inkapsulacije. Razmislite što biste još mogli učiniti s secretFactory
i leaker
što ne biste mogli lako učiniti s prototipom ili klasom.
Kao prvo, možete ga destrukturirati jer ne morate brinuti o kontekstu this
:
const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)
To je prilično lijepo. Osim izbjegavanja new
i this
tomfoolery, omogućuje nam upotrebu naših objekata naizmjenično s modulima CommonJS i ES6. Također olakšava sastav:
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)
Klijenti blackHat
ne brinite gdje exfiltrate
potječe iz, i spyFactory
ne mora se petljati sa Function::bind
žongliranje kontekstom ili duboko ugniježđena svojstva. Pazite, ne moramo se puno brinuti o this
u jednostavnom sinkronom proceduralnom kodu, ali uzrokuje sve vrste problema u asinkronom kodu koje je bolje izbjeći.
Uz malo razmišljanja, spyFactory
može se razviti u visoko sofisticirani alat za špijunažu koji može obrađivati sve vrste infiltracijskih ciljeva - ili drugim riječima, fasada .
Naravno da biste to mogli učiniti i s klasom, ili bolje rečeno, asortimanom klasa, koje sve nasljeđuju iz abstract class
ili interface
... osim što JavaScript nema nikakav koncept sažetaka ili sučelja.
Vratimo se na zeleniji primjer da vidimo kako bismo to primijenili u tvornici:
function greeterFactory(greeting = 'Hello', name = 'World') { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory('Hey', 'folks').greet()) // Hey, folks!
Možda ste primijetili da su ove tvornice sve jače dok idemo dalje, ali ne brinite - rade isto. Skidaju se kotačići za trening, ljudi!
To je već manje uobičajeno od prototipa ili klasične verzije istog koda. Drugo, učinkovitije postiže inkapsulaciju svojih svojstava. Također, u nekim slučajevima ima nižu memoriju i učinak (možda se na prvi pogled ne čini tako, ali JIT-ov kompajler tiho radi iza kulisa kako bi umanjio dupliciranje i zaključio vrste).
Tako je sigurnije, često je brže i lakše je pisati ovakav kod. Zašto nam opet trebaju satovi? O, naravno, ponovna upotrebljivost. Što se događa ako želimo nesretne i entuzijastične dobrodošlice? Pa, ako koristimo ClassicalGreeting
razreda, vjerojatno uskačemo izravno u sanjarenje hijerarhije razreda. Znamo da ćemo morati parameterizirati interpunkciju, pa ćemo malo refaktorizirati i dodati nekoliko djece:
// 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!!
To je fin pristup, sve dok se netko ne pojavi i zatraži značajku koja se ne uklapa čisto u hijerarhiju i cijela stvar prestane imati smisla. Stavite pribadaču u tu misao dok pokušavamo napisati istu funkcionalnost s tvornicama:
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!!
Nije očito da je ovaj kôd bolji, iako je nešto kraći. Zapravo, mogli biste tvrditi da je teže čitati, a možda je ovo tup pristup. Ne bismo li mogli imati samo unhappyGreeterFactory
i an enthusiasticGreeterFactory
?
Tada dolazi vaš klijent i kaže: 'Treba mi novi pozdrav koji je nesretan i želi da cijela soba zna za to!'
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
Ako bismo ovaj entuzijastično nesretni pozdrav trebali koristiti više puta, mogli bismo si olakšati:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory('You're late', 'Jim').greet())
Postoje pristupi ovom stilu kompozicije koji rade s prototipovima ili klasama. Na primjer, možete preispitati UnhappyGreeting
i EnthusiasticGreeting
kao dekorateri . I dalje bi trebalo više uzorka od prethodno korištenog pristupa u funkcionalnom stilu, ali to je cijena koju plaćate za sigurnost i inkapsulaciju stvaran razreda.
Stvar je u tome što u JavaScript-u ne dobivate onu automatsku sigurnost. JavaScript okviri koji ističu class
upotreba čini puno 'magije' da se papiriraju ove vrste problema i prisile klase da se ponašaju. Pogledajte Polymer's ElementMixin
izvorni kod neko vrijeme, usudit ću se. To su razine čarobnjaka za arh JavaScript, i mislim na to bez ironije i sarkazma.
Naravno, neke od gore raspravljenih problema možemo popraviti pomoću Object.freeze
ili Object.defineProperties
s većim ili manjim učinkom. Ali zašto imitirati obrazac bez funkcije, a zanemarujući alate JavaScript čini izvorno nam pružaju da ih možda ne bismo pronašli na jezicima poput Jave? Da li biste koristili čekić s oznakom 'odvijač' za zabijanje vijka, kad je vaš kutija s alatima kao stvarni odvijač sjedio tik do njega?
Programeri JavaScript često ističu dobre dijelove jezika, kako u kolokvijalnom, tako i u odnosu na istoimena knjiga . Nastojimo izbjeći zamke postavljene upitnijim odabirom jezika i držimo se dijelova koji nam omogućavaju da napišemo čist, čitljiv kôd za ponovnu upotrebu koji smanjuje pogreške.
Postoje opravdani argumenti oko toga koji dijelovi JavaScript ispunjavaju uvjete, ali nadam se da sam vas uvjerio da class
nije jedan od njih. Ako to ne uspije, nadamo se da razumijete da nasljeđivanje u JavaScriptu može biti zbunjujući nered i da class
niti je popravlja niti štedi zbog razumijevanja prototipova. Dodatni kredit ako ste shvatili nagovještaje da objektno orijentirani uzorci dizajna dobro funkcioniraju bez klasa ili nasljeđivanja ES6.
Ne kažem vam da izbjegavate class
u cijelosti. Ponekad vam treba nasljedstvo i class
pruža čistiju sintaksu za to. Konkretno, class X extends Y
je mnogo ljepši od starog prototipa. Uz to, mnogi popularni front-end okviri potiču njegovu upotrebu i vjerojatno biste trebali izbjegavati pisanje čudnih nestandardnih kodova samo u principu. Jednostavno ne volim kuda ovo ide.
U mojim noćnim morama cijela generacija JavaScript knjižnica napisana je pomoću class
, s očekivanjem da će se ponašati slično kao i drugi popularni jezici. Otkrivaju se cijele nove klase bugova (namijenjene igri riječi). Uskrsnuće stari koji bi lako mogli ostati na groblju neispravnog JavaScript-a da nismo neoprezno upali u class
zamka. Iskusna JavaScript programera muče ova čudovišta, jer ono što je popularno nije uvijek ono što je dobro.
Na kraju svi frustrirano odustanemo i počnemo izmišljati kotače u Rustu, Gou, Haskellu ili tko zna čemu još, a zatim se kompiliramo za Wasm za web, a novi mrežni okviri i knjižnice rastu u višejezičnu beskonačnost.
Stvarno me drži budnim noću.
ES6 je najnovija stabilna implementacija ECMAScripta, otvorenog standarda na kojem se temelji JavaScript. Jeziku dodaje brojne nove značajke, uključujući službeni sustav modula, varijable i konstante s opsegom bloka, funkcije strelica i brojne druge nove ključne riječi, sintakse i ugrađene objekte.
ES6 (ES2015) najnoviji je standard koji je stabilan i u potpunosti se implementira (osim za odgovarajuće rep pozive i neke nijanse) u najnovijim verzijama glavnih preglednika i drugih JS okruženja. ES7 (ES2016) i ES8 (ES2017) također su stabilne specifikacije, ali provedba je prilično mješovita.
kako funkcioniraju konvertibilne note
JavaScript ima snažnu podršku objektno orijentiranom programiranju, ali koristi drugačiji model nasljeđivanja (prototipski) u usporedbi s većinom popularnih OO jezika (koji koriste klasično nasljeđivanje). Također podržava proceduralne i funkcionalne stilove programiranja.
U ES6, ključna riječ 'class' i pridružene značajke novi su pristup stvaranju prototipskih konstruktora. Oni nisu istinske klase na način koji bi bio poznat korisnicima većine drugih objektno orijentiranih jezika.
Nasljeđivanje se može implementirati u JavaScript ES6 putem ključnih riječi 'class' i 'extends'. Sljedeći je pristup putem idioma funkcije 'konstruktor', plus dodjela funkcija i statičkih svojstava prototipu konstruktora.
U prototipskom nasljeđivanju, prototipovi su objektne instance kojima podređene instance delegiraju nedefinirana svojstva. Suprotno tome, klase u klasičnom nasljeđivanju definicije su tipa iz kojih podređene klase nasljeđuju metode i svojstva tijekom instanciranja.