Bir JS Geliştiricisi Olarak, Beni Geceleri Uyandıran Bu

Yayınlanan: 2022-03-11

JavaScript, 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'
Not: Gerçek hayatta neredeyse asla böyle bir kod yazmazsınız - bu korkunç bir uygulamadır - ancak prensibi kısa ve öz bir şekilde gösterir.

Ö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):

JavaScript'te eksik referansların nasıl ele alındığını göstermek için prototip zincirinde gezinme.

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.