Bir JS Geliştiricisi Olarak, Beni Geceleri Uyandıran Bu
Yayınlanan: 2022-03-11JavaScript, bir dilin tuhaf bir topudur. Smalltalk'tan ilham almasına rağmen, C benzeri bir sözdizimi kullanır. Prosedürel, işlevsel ve nesne yönelimli programlama (OOP) paradigmalarının özelliklerini birleştirir. Neredeyse akla gelebilecek tüm programlama problemlerini çözmek için çok sayıda, genellikle gereksiz yaklaşımlara sahiptir ve hangilerinin tercih edildiği konusunda güçlü bir fikir sahibi değildir. Deneyimli geliştiricileri bile tetikleyen tür zorlaması için labirent benzeri bir yaklaşımla, zayıf ve dinamik bir şekilde yazılmıştır.
JavaScript'in ayrıca siğilleri, tuzakları ve şüpheli özellikleri vardır. Yeni programcılar, daha zor olan bazı kavramlarla mücadele ediyor; eşzamansızlık, kapanışlar ve kaldırmayı düşünün. Diğer dillerde deneyimi olan programcılar, benzer adlara ve görünüme sahip şeylerin JavaScript'te aynı şekilde çalışacağını makul bir şekilde varsayıyorlar ve genellikle yanılıyorlar. Diziler aslında diziler değildir; this
ne alakası var , prototip nedir ve new
aslında ne yapar?
ES6 Sınıflarıyla İlgili Sorun
Şimdiye kadarki en kötü suçlu, JavaScript'in en son yayın sürümü olan ECMAScript 6 (ES6): sınıflarında yenidir. Sınıflarla ilgili bazı konuşmalar açıkçası endişe verici ve dilin gerçekte nasıl çalıştığına dair köklü bir yanlış anlaşılmayı ortaya koyuyor:
"JavaScript, sınıfları olduğu için sonunda gerçek bir nesne yönelimli dil oldu!"
Veya:
"Sınıflar bizi JavaScript'in bozuk kalıtım modelini düşünmekten kurtarıyor."
Ya da:
"Sınıflar, JavaScript'te tür oluşturmaya yönelik daha güvenli ve daha kolay bir yaklaşımdır."
Bu ifadeler beni rahatsız etmiyor çünkü prototipik kalıtımla ilgili yanlış bir şeyler olduğunu ima ediyorlar; bu argümanları bir kenara bırakalım. Bu ifadeler beni rahatsız ediyor çünkü hiçbiri doğru değil ve JavaScript'in dil tasarımına yönelik “her şey herkes için” yaklaşımının sonuçlarını gösteriyorlar: Bir programcının dili anlamasını mümkün olduğundan daha sık sakatlıyor. Daha ileri gitmeden önce, örnekleyelim.
JavaScript Pop Quiz #1: Bu Kod Blokları Arasındaki Temel Fark Nedir?
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())
Buradaki cevap, bir tane olmadığıdır . Bunlar aynı şeyi etkili bir şekilde yapar, sadece ES6 sınıfı sözdiziminin kullanılıp kullanılmadığı sorusudur.
Doğru, ikinci örnek daha anlamlı. Sırf bu nedenle bile, class
dile güzel bir katkı olduğunu iddia edebilirsiniz. Ne yazık ki, sorun biraz daha ince.
JavaScript Pop Quiz #2: Aşağıdaki Kod Ne Yapar?
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())
Doğru cevap, konsola yazdırmasıdır:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
Yanlış cevap verdiyseniz, class
gerçekte ne olduğunu anlamıyorsunuz. Bu senin hatan değil. Array
gibi, class
bir dil özelliği değildir, sözdizimsel belirsizliğidir . Prototipik kalıtım modelini ve onunla birlikte gelen beceriksiz deyimleri gizlemeye çalışır ve JavaScript'in yapmadığı bir şeyi yaptığını ima eder.
Java gibi dillerden gelen klasik OOP geliştiricilerini ES6 sınıfı miras modeliyle daha rahat hale getirmek için class
JavaScript ile tanıtıldığı size söylenmiş olabilir. Bu geliştiricilerden biriyseniz , bu örnek muhtemelen sizi dehşete düşürdü. Olması gerekiyor. JavaScript'in class
anahtar sözcüğünün, bir sınıfın sağlaması gereken hiçbir garantiyle birlikte gelmediğini gösterir. Ayrıca prototip kalıtım modelindeki önemli farklılıklardan birini de gösterir: Prototipler, türler değil, nesne örnekleridir .
Prototipler ve Sınıflar
Sınıf ve prototip tabanlı kalıtım arasındaki en önemli fark, bir sınıfın çalışma zamanında somutlaştırılabilen bir türü tanımlaması, oysa prototipin kendisinin bir nesne örneği olmasıdır.
Bir ES6 sınıfının çocuğu, üst öğeyi yeni özellikler ve yöntemlerle genişleten ve çalışma zamanında başlatılabilen başka bir tür tanımıdır. Bir prototipin çocuğu, alt öğede uygulanmayan tüm özellikleri üst öğeye devreden başka bir nesne örneğidir .
Yan not: Neden sınıf yöntemlerinden bahsettiğimi ama prototip yöntemlerinden bahsetmediğimi merak ediyor olabilirsiniz. Bunun nedeni, JavaScript'in bir yöntem kavramına sahip olmamasıdır. İşlevler JavaScript'te birinci sınıftır ve özelliklere sahip olabilir veya diğer nesnelerin özellikleri olabilir.
Bir sınıf yapıcısı, sınıfın bir örneğini oluşturur. JavaScript'teki bir yapıcı, yalnızca bir nesne döndüren eski bir işlevdir. Bir JavaScript yapıcısıyla ilgili özel olan tek şey, new
anahtar sözcüğüyle çağrıldığında, prototipini döndürülen nesnenin prototipi olarak atamasıdır. Bu size biraz kafa karıştırıcı geliyorsa, yalnız değilsiniz - öyle ve prototiplerin neden yeterince anlaşılmadığının büyük bir kısmı bu.
Buna gerçekten iyi bir nokta koymak için, bir prototipin çocuğu, prototipinin bir kopyası olmadığı gibi, prototipiyle aynı şekle sahip bir nesne de değildir. Alt öğenin prototipe canlı bir referansı vardır ve alt öğede bulunmayan herhangi bir prototip özelliği, prototip üzerinde aynı adı taşıyan bir özelliğe tek yönlü bir başvurudur.
Aşağıdakileri göz önünde bulundur:
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'
Önceki örnekte, child.foo
undefined
iken, parent.foo
. foo
on child
öğesini tanımladığımız anda, child.foo
'bar'
değerine sahipti, ancak parent.foo
orijinal değerini korudu. delete child.foo
sonra tekrar parent.foo
atıfta bulunur, bu da ebeveynin değerini değiştirdiğimizde child.foo
yeni değeri ifade ettiği anlamına gelir.
Şimdi ne olduğuna bir bakalım (daha açık bir örnekleme amacıyla, bunların dize değişmezleri değil de Strings
olduğunu farz edeceğiz, fark burada önemli değil):
Bunun gizli çalışma şekli ve özellikle new
ve this
öğelerinin özellikleri başka bir günün konusu, ancak daha fazlasını okumak isterseniz Mozilla'nın JavaScript'in prototip miras zinciri hakkında kapsamlı bir makalesi var.
Buradaki temel çıkarım, prototiplerin bir type
tanımlamamasıdır; kendileri instances
ve ima ve gerekli olan her şeyle birlikte çalışma zamanında değişebilirler.
Hala benimle? JavaScript sınıflarını incelemeye geri dönelim.
JavaScript Pop Quiz #3: Sınıflarda Gizliliği Nasıl Uygularsınız?
Yukarıdaki prototip ve sınıf özelliklerimiz, "pencereden dışarı sarkıyor" kadar "kapsüllenmiş" değildir. Bunu düzeltmeliyiz ama nasıl?
Burada kod örneği yok. Cevap, yapamayacağınızdır.
JavaScript'in herhangi bir gizlilik kavramı yoktur, ancak kapanışları vardır:
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!"
Az önce ne olduğunu anladın mı? Değilse, kapanışları anlamıyorsunuz. Sorun değil, gerçekten - göründükleri kadar korkutucu değiller, çok kullanışlılar ve onları öğrenmek için biraz zaman ayırmalısınız.
JavaScript Pop Quiz #4: class
Anahtar Kelimesini Kullanarak Yukarıdakilerin Eşdeğeri Nedir?
Üzgünüm, bu başka bir hileli soru. Temelde aynı şeyi yapabilirsiniz, ancak şuna benziyor:
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
daha kolay veya net görünüyorsa bana bildirin. Kişisel görüşüme göre, durum biraz daha kötü—JavaScript'te class
bildirimlerinin deyimsel kullanımını bozuyor ve örneğin Java'dan beklediğiniz gibi çalışmıyor. Bu, aşağıdakilerle açıklığa kavuşturulacaktır:

JavaScript Pop Quiz #5: SecretiveClass::looseLips()
Ne Yapar?
Hadi bulalım:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }
Bu garipti.
JavaScript Pop Quiz #6: Deneyimli JavaScript Geliştiricileri Hangisini Tercih Ediyor—Prototipler mi, Sınıflar mı?
Tahmin ettiniz, bu başka bir hileli soru - deneyimli JavaScript geliştiricileri, ellerinden geldiğince her ikisinden de kaçınma eğilimindedir. İşte yukarıdakileri deyimsel JavaScript ile yapmanın güzel bir yolu:
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()
Bu sadece kalıtımın doğal çirkinliğinden kaçınmak veya kapsüllemeyi zorlamakla ilgili değildir. Bir prototip veya sınıfla kolayca yapamayacağınız secretFactory
ve leaker
ile başka neler yapabileceğinizi düşünün.
Birincisi, onu yok edebilirsiniz çünkü this
bağlamı hakkında endişelenmenize gerek yok:
const { spillTheBeans } = secretFactory() spillTheBeans() // Favor composition over inheritance, (...)
Bu oldukça güzel. new
ve this
saçma sapan şeylerden kaçınmanın yanı sıra, nesnelerimizi CommonJS ve ES6 modülleriyle dönüşümlü olarak kullanmamıza izin veriyor. Ayrıca kompozisyonu biraz daha kolaylaştırır:
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
istemcileri, sızıntının nereden geldiği konusunda endişelenmek zorunda değildir ve exfiltrate
, Function::bind
bağlam hokkabazlığı veya derinlemesine iç içe geçmiş özelliklerle spyFactory
zorunda değildir. Dikkat edin, basit senkron prosedür kodunda this
konuda çok fazla endişelenmemize gerek yok, ancak asenkron kodda kaçınılması daha iyi olan her türlü soruna neden oluyor.
Biraz düşünülerek, spyFactory
her türlü sızma hedefini veya başka bir deyişle bir cepheyi idare edebilecek son derece karmaşık bir casusluk aracına dönüştürülebilir.
Elbette bunu bir sınıfla da yapabilirsiniz, ya da daha doğrusu, tümü abstract class
veya interface
miras alınan çeşitli sınıflarla…
Bunu bir fabrikada nasıl uygulayacağımızı görmek için karşılama örneğine dönelim:
function greeterFactory(greeting = "Hello", name = "World") { return { greet: () => `${greeting}, ${name}!` } } console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!
Biz ilerledikçe bu fabrikaların daha da özlü hale geldiğini fark etmiş olabilirsiniz, ancak endişelenmeyin, onlar da aynı şeyi yapıyorlar. Eğitim çarkları çıkıyor millet!
Bu, aynı kodun prototipinden veya sınıf versiyonundan zaten daha az standart. İkincisi, özelliklerinin kapsüllenmesini daha etkin bir şekilde gerçekleştirir. Ayrıca, bazı durumlarda daha düşük bellek ve performans ayak izine sahiptir (ilk bakışta öyle görünmeyebilir, ancak JIT derleyicisi, çoğaltmayı azaltmak ve türleri çıkarmak için sahne arkasında sessizce çalışıyor).
Bu yüzden daha güvenli, genellikle daha hızlı ve böyle kod yazmak daha kolay. Neden tekrar derslere ihtiyacımız var? Ah, elbette, yeniden kullanılabilirlik. Mutsuz ve coşkulu karşılama çeşitleri istersek ne olur? ClassicalGreeting
sınıfını kullanıyorsak, muhtemelen doğrudan bir sınıf hiyerarşisi hayal etmeye başlarız. Noktalama işaretlerini parametreleştirmemiz gerektiğini biliyoruz, bu yüzden biraz yeniden düzenleme yapacağız ve bazı çocuklar ekleyeceğiz:
// 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!!
Biri gelip hiyerarşiye tam olarak uymayan bir özellik isteyinceye ve her şey mantıklı olmayı bırakana kadar bu iyi bir yaklaşım. Biz fabrikalarla aynı işlevi yazmaya çalışırken bu düşünceye bir iğne koyun:
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!!
Biraz daha kısa olmasına rağmen bu kodun daha iyi olduğu açık değil. Aslında, okumanın daha zor olduğunu iddia edebilirsiniz ve belki de bu geniş bir yaklaşımdır. Mutsuz bir unhappyGreeterFactory
ve enthusiasticGreeterFactory
birGreeterFactory'ye sahip olamaz mıyız?
Sonra müvekkiliniz gelir ve “Mutsuz ve tüm odanın bunu bilmesini isteyen yeni bir selamlayıcıya ihtiyacım var!” der.
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(
Bu coşkulu mutsuz selamlayıcıyı bir kereden fazla kullanmamız gerekirse, bunu kendimiz için kolaylaştırabiliriz:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory)) console.log(aggressiveGreeterFactory("You're late", "Jim").greet())
Prototipler veya sınıflarla çalışan bu kompozisyon tarzına yaklaşımlar vardır. Örneğin, UnhappyGreeting
ve EnthusiasticGreeting
dekoratörler olarak yeniden düşünebilirsiniz. Yine de, yukarıda kullanılan işlevsel tarzdaki yaklaşımdan daha fazla standart gerektirir, ancak gerçek sınıfların güvenliği ve kapsüllenmesi için ödediğiniz bedel budur.
Mesele şu ki, JavaScript'te bu otomatik güvenliği alamıyorsunuz. class
kullanımını vurgulayan JavaScript çerçeveleri, bu tür problemlerin üstesinden gelmek için çok fazla "sihir" yapar ve sınıfları kendi kendilerine davranmaya zorlar. Bir ElementMixin
kaynak koduna bir göz atın, size meydan okuyorum. JavaScript arcana'nın baş büyücü seviyeleri ve bunu ironi veya alay olmadan kastediyorum.
Elbette, yukarıda tartışılan sorunlardan bazılarını Object.freeze
veya Object.defineProperties
ile daha fazla veya daha az etkili olacak şekilde düzeltebiliriz. Ancak JavaScript'in bize Java gibi dillerde bulamayacağımızı doğal olarak sağladığı araçları görmezden gelirken, işlevi işlevsiz olarak neden taklit edelim? Alet kutunuzun yanında gerçek bir tornavida varken, bir vidayı sürmek için "tornavida" etiketli bir çekiç kullanır mıydınız?
İyi Parçaları Bulma
JavaScript geliştiricileri, hem konuşma dilinde hem de aynı adlı kitaba atıfta bulunarak genellikle dilin iyi yanlarını vurgular. Daha şüpheli dil tasarım seçeneklerinin oluşturduğu tuzaklardan kaçınmaya çalışıyoruz ve temiz, okunabilir, hatayı en aza indiren, yeniden kullanılabilir kod yazmamıza izin veren kısımlara bağlı kalıyoruz.
JavaScript'in hangi bölümlerinin uygun olduğuna dair makul argümanlar var, ancak umarım sizi class
onlardan biri olmadığına ikna edebilmişimdir. Bunu başaramazsanız, JavaScript'teki kalıtımın kafa karıştırıcı bir karışıklık olabileceğini ve bu class
ne onu düzeltmediğini ne de prototipleri anlama zorunluluğunu ortadan kaldırmadığını anladığınızı umarız. Nesne yönelimli tasarım modellerinin sınıflar veya ES6 mirası olmadan iyi çalıştığına dair ipuçlarını aldıysanız ekstra kredi.
Sana class
tamamen kaçınmanı söylemiyorum. Bazen mirasa ihtiyaç duyarsınız ve class
bunu yapmak için daha temiz sözdizimi sağlar. Özellikle, class X extends Y
, eski prototip yaklaşımından çok daha iyidir. Bunun yanı sıra, birçok popüler ön uç çerçeve, kullanımını teşvik eder ve muhtemelen tek başına standart olmayan garip kod yazmaktan kaçınmalısınız. Sadece bunun gittiği yeri sevmiyorum.
Kabuslarımda, diğer popüler dillere benzer şekilde davranacağı beklentisiyle, tüm nesil JavaScript kitaplıkları class
kullanılarak yazılır. Yepyeni böcek sınıfları (punto amaçlı) keşfedildi. class
tuzağına dikkatsizce düşmemiş olsaydık, Hatalı Biçimlendirilmiş JavaScript Mezarlığında kolayca bırakılabilecek eskiler yeniden dirildi. Deneyimli JavaScript geliştiricileri bu canavarlar tarafından rahatsız edilir, çünkü popüler olan her zaman iyi değildir.
Sonunda hepimiz hayal kırıklığı içinde pes edip Rust, Go, Haskell veya kim bilir başka nelerde tekerlekleri yeniden icat etmeye ve ardından web için Wasm'ı derlemeye başlıyoruz ve yeni web çerçeveleri ve kütüphaneler çok dilli sonsuzluğa dönüşüyor.
Gerçekten geceleri beni ayakta tutuyor.