JavaScript’i oluşturan şeyler nelerdir?

Gizem Korkmaz
9 min readJun 13, 2022

--

Bu yazı Dan Abramov’un What Is JavaScript Made Of? adlı makalesinin bir çevirisidir.

JavaScript’i kullanmaya başladığım ilk yıllarda kendimi bir sahtekar gibi hissediyordum. Frameworkler kullanarak web siteleri yapabiliyor olsam da bir şeyler eksikti. JavaScript mülakatlarından ödüm kopuyordu çünkü temellerini tam anlamıyla kavramış değildim.

Yıllar geçtikçe JavaScript konusunda özgüvenimi yerine getiren bir zihinsel model oluşturdum. Burada, bunun oldukça sıkıştırılmış bir versiyonunu paylaşıyorum. Bu, her birinin karşılığında sadece birkaç cümle aldığı bir terimler listesi olarak yapılandırılmıştır.

Bu yazıyı okurken, her bir konu hakkında ne kadar özgüvenli hissettiğiniz konusunda zihninizde bir skor tutmaya çalışın. Eğer birçoğunu boş geçerseniz sizi yargılamayacağım! Böyle bir durum söz konusu olursa yazının sonunda bu konuda yardımcı olacak bir şey olacak.

  • Value (Değer): Değer, kavram olarak biraz soyuttur. Bu bir “şeydir”. Matematik için sayı ya da geometri için nokta ne ise JavaScript için de bir değer odur. Programınız çalıştığında dünyası değerler ile dolu olur. 1, 2 ve 420 gibi sayılar birer değerdir fakat bu başka bir şey, mesela "Cows go moo” gibi bir cümle de olabilir. Yine de her şey bir değer değildir. Sayılar birer değerdir ancak bir if koşulu değildir. Aşağıda farklı çeşitlerdeki değerleri inceleyeceğiz.
    Type of Value (Değer Tipi): Farklı “tipte” değer çeşitleri vardır. Örneğin 420 gibi sayılar, "Cows go moo” gibi stringler, objeler ya da birkaç farklı tip daha. Önüne typeof ekleyerek bir değerin tipini öğrenebilirsiniz. Örneğin console.log(typeof 2) konsola "number" yazdıracaktır.
    Primitive Value: Bazı değer tipleri “primitive”dir. Sayıları, stringleri ve birkaç farklı tipi daha kapsarlar. Primitive değerler konusunda tuhaf olan şey onlardan daha fazla oluşturamıyor ya da onları hiçbir şekilde değiştiremiyor olmanızdır. Örneğin her 2 yazdığınızda her seferinde aynı 2 değerini alırsınız. Programınızda başka bir 2 “oluşturamazsınız” ya da 2 değerinin 3 olmasını sağlayamazsınız. Bu stringler için de geçerlidir.
    null ve undefined: Bunlar özel iki değer tipidir. Özeldirler çünkü onlarla yapamayacağınız çok fazla şey mevcut, sıklıkla hatalara sebep olurlar. Çoğunlukla null kasıtlı olarak, undefined ise istemeden eksik kalan değerleri ifade eder. Yine de her ikisinin de nasıl kullanılacağı programı yazan kişiye kalmıştır. Bu iki tipin var olmasının sebebi, bazen bir işlemin başarısız olması, eksik bir değerle devam etmesinden daha iyi olmasından kaynaklanır.
  • Equality (Eşitlik): Değer gibi eşitlik de JavaScript’in temel kavramlarından bir tanesidir. İki değerin eşit olduğunu söylediğimizde onların… aslında, ben böyle bir şeyi asla söylemem. İki değer eşit ise bu onların zaten aynı değer olduğunu gösterir. İki farklı değer değil, bir tanedirler! Örneğin "Cows go moo” === "Cows go moo” ve 2 === 2 olur çünkü 2, 2 ‘dir. Bu eşitlik kavramını JavaScript’te üç eşittir işareti ile gösterdiğimize dikkat edin.
    Strict Equality: Yukarıdakinin aynısı.
    Referential Equality: Yukarıdakinin aynısı.
    Loose Equality: Ah, bu farklı işte! İki eşittir (==) işareti kullandığımız zaman olan şey loose equality’dir. Farklı değerleri işaret ediyor olsalar da benzer şekilde görünen (2 ve "2” gibi) değerler aşağı yukarı eşit sayılabilirler. Zamanında kolaylık sağlaması için JavaScript’e eklendiler ve o günden beri sürekli kafa karışıklığı yaratıyorlar. Bu temel bir kavram değil ancak sık görülen bir hata kaynağı. Kötü günler için öğrenebilirsiniz ancak pek çok insan görmezden gelmeyi tercih ediyor.
  • Literal: Literal, bir değeri programınıza kelimenin tam anlamıyla yazarak işaret etmenizdir. Örneğin 2 number literal, "Banana" ise string literal’dır.
  • Variable (Değişken): Değişkenler, değerlere birer isim vererek işaret etmemize yardımcı olurlar. Örneğin, let message = "Cows go moo” gibi. Artık her seferinde kodunuzda aynı cümleyi tekrar etmektense message yazabilirsiniz. message'ı daha sonra değiştirip message = “I am the walrus" gibi başka bir değere de işaret edebilirsiniz.
    Scope (Kapsam): Tüm programın içerisinde sadece tek bir message değişkeninin olabilmesi çok kötü olurdu. Bunun yerine, bir değişkeni tanımladığınız zaman programınızın bir parçası olur. Bu parçaya da “scope” adı verilir. Scope’un nasıl çalıştığına dair kurallar mevcuttur ancak değişkeninizi tanımladığınız yere en yakın { ve } parantezleri arasına bakabilirsiniz. İşte bu kod “bloku” değişkenin scope’udur.
    Assignment (Atama): message = "I am the walrus" yazdığımız zaman message değişkeninin "I am the walrus" değerine işaret etmesini sağlıyoruz. Buna atama, yazma ya da değişkeni set etmek denir.
    let vs. const vs. var: Genellikle let kullanmak istersiniz. Eğer bu değişkene başka bir atama yapılmasın istiyorsanız const'u kullanabilirsiniz. (Bazı kod yapıları ve çalışanları aşırı detaycı olurlar ve tek bir atama olduğu zamanlarda const kullanmanız konusunda sizi zorlarlar.) Mümkün olduğunca var kullanmaktan kaçının çünkü scope kuralları kafa karıştırıcı olabilir.
  • Object (Obje): Obje, JavaScript’te özel bir değer tipidir. Objelerle ilgili harika olan şey, diğer değerlerle bağlantı kurabilmeleridir. Örneğin, bir {flavor: "vanilla"} objesinin "vanilla" değerine işaret eden bir flavor property’si vardır. Bir objeyi, içinde “bağlantılar” bulunan “size ait” bir değer olarak düşünebilirsiniz.
    Property (Özellik): Property objeden çıkan ve bir değere işaret eden bir tür “bağlantı” gibidir. Size bir değişkeni hatırlatabilir: bir ismi vardır (flavor gibi) ve bir değere işaret eder (vanilla gibi). Ancak bir değişkenin aksine bir property, kodunuzun bir yerlerinde (scope) değil objenin içerisinde “yaşar”. Bir property, objenin parçası olarak kabul edilir ancak işaret ettiği değer bir parça olarak kabul edilmez.
    Object Literal: Object literal, bir objeyi {} ya da {flavor: "vanilla"} gibi doğrudan programınıza yazarak oluşturmanın bir yoludur. {} içerisinde virgüllerle ayırılmış birden fazla property: value çiftleri oluşturabiliriz. Bu, property “bağlantılarının” objemizden nereye işaret edeceğini belirlememizi sağlar.
    Object Identity (Obje Kimliği): Daha önce 2’nin 2’ye eşit olduğundan bahsetmiştik (başka bir deyişle 2 === 2) çünkü ne zaman 2 yazarsak her seferinde aynı değeri “çağırırız”. Ancak her {} yazışımızda farklı bir değer elde ederiz! Bu yüzden {} ile bir başka {} hiçbir zaman eşit değildir. Konsola şunu yazmayı deneyin {} === {} (sonuç false dönecektir). Bilgisayar kodumuzda 2 ile karşılaştığı her zaman bize aynı 2 değerini verir. Ancak object literallar farklıdır: bilgisayar {} ile karşılaştığında her zaman yeni bir değer olan yeni bir obje yaratır. O halde object identity nedir? Bu eşitlik ya da değerlerin aynı-lığı için verilmiş bir başka terimdir. “a ile b aynı kimliğe sahiptir.” dediğimiz zaman a ile b aynı değere işaret ediyor demek isteriz (a === b). “a ile b farklı kimliklere sahiptir.” dediğimiz zaman ise a ile b farklı değere işaret ediyor demek isteriz (a !== b).
    Dot Notation (Noktalı Gösterim): Bir objeden property okumak ya da assign etmek istediğiniz zaman nokta (.) gösterimini kullanabilirsiniz. Örneğin iceCream chocolate'a işaret eden flavor property’si olan bir objeyi işaret eder; iceCream.flavor yazmak size "chocolate" döndürecektir.
    Bracket Notation (Parantezli Gösterim): Bazen okumak istediğiniz property’nin ismini önceden bilmiyor olabilirsiniz. Örneğin, bazen iceCream.flavor bazen de iceCream.taste'i okumak istiyorsunuz. Parantezli ([]) gösterim ismin kendisi bir değişken olduğunda property’yi okumanızı sağlar. Örneğin let ourProperty = 'flavor' dediğimizi düşünelim. Ardından iceCream[ourProperty] bize "chocolate" dönecektir. Tuhaf bir şekilde, bunu objeleri oluştururken de kullanabiliriz: { [ourProperty]: "vanilla" }.
    Mutation: Bir obje, property’si farklı bir değere işaret edecek şekilde değiştirildiğinde o obje “mutate” edilmiş deriz. Örneğin let iceCream = {flavor: "vanilla"} olarak tanımladığımız bir objeyi daha sonra iceCream.flavor = "chocolate" yazarak mutate edebiliriz. iceCream'i const ile tanımlamış olsaydık bile iceCream.flavor'ı yine değiştirebilirdik. Bunun sebebi const'un sadece iceCream değişkeninin kendisini yeniden assign etmeyi engellemesinden kaynaklanır ancak biz objenin işaret ettiği bir property’yi (flavor) mutate ettik. Bazı insanlar bunu kafa karıştırıcı buldukları için const'u kullanmaya tamamen tövbe etmiş durumda.
    Array: Array, bir şeylerin listesini temsil eden bir objedir. ["banana", "chocolate", "vanilla"] şeklinde bir array literal yazdığınız zaman özünde 0 adlı property’si "banana" string değerini, 1 property’si "chocolate" değerini ve 2 property’si "vanilla" değerini işaret eden bir obje oluşturmuş oluyorsunuz. {0: ..., 1: ..., 2: ...} olarak yazmak oldukça can sıkıcı olurdu, bu yüzden de arrayler işe yarar şeylerdir. Aynı zamanda arrayler üzerinde kullanılan map, filter ve reduce gibi bazı gömülü metotlat vardır. Eğer reduce kafa karıştırıcı duruyorsa endişelenmeyin, herkes için öyle.
    Prototype: Var olmayan bir property okumaya çalışırsak ne olur? Örneğin, iceCream.tastegibi (bizim property’miz flavor). Bunun en basit cevabı bahsettiğimiz özel undefined değeri olur. Daha incelikli bir cevabı ise JavaScript’teki tüm objelerin birer “prototype”ının olması olur. Prototype’ı her bir objede, “bir sonraki bakılacak yere” karar veren, “gizli” bir property olarak düşünebilirsiniz. Yani eğer iceCream içerisinde taste property’si yoksa, JavaScript prototype’ında taste property’sine, ardından o objenin prototype’ına bakacaktır diye giderek en sonunda .taste'i bulamayıp “prototype chain’de (prototype zincirinde)” sona yaklaşırsa, undefined değerini dönecektir. Bu mekanizma ile çok nadir karşılaşacaksınız ancak bu, tanımlamadığımız halde iceCream objemizde neden bir toString metodunun olduğunu da açıklar; o da prototype’tan gelmiştir.
  • Function (Fonksiyon): Fonksiyon tek bir amacı olan özel bir değerdir: programınızdaki bazı kodları temsil eder. Fonksiyonlar aynı kodu tekrar tekrar yazmak istemiyorsanız kullanışlı şeylerdir. sayHi() gibi bir fonksiyonu “çağırmak” bilgisayara içindeki kodu çalıştırmasını ve programda neredeyse oraya geri dönmesini söyler. JavaScript’te bir fonksiyon tanımlamanın, yapıkları şeyler konusunda küçük farklılıklar olsa da pek çok yolu vardır.
    Arguments & Parameters (Argümanlar ve Parametreler): Argümanlar fonksiyonu çağırdığınız yere ek bilgi eklemenizi sağlarlar: sayHi("Amelie"). Fonksiyonun içerisinde değişkenlere benzer şekilde davranırlar. Bunlara hangi taraftan okuduğunuza bağlı olarak “argümanlar” ya da “parametreler” denir (fonksiyon bildirimi ya da fonksiyon çağrısı). Ancak terminolojideki bu ayrım aşırı detay içeriyor ve pratikte bu ikisi birbiri yerine kullanılıyor.
    Function Expression (Fonksiyon İfadesi): Daha önce bir değişkeni let message = "I am the walrus" gibi bir string değerine set etmiştik. Görünüşe göre let sayHi = function() { } gibi bir fonksiyonu da bir değişkene set edebiliyoruz. Burada =‘dan sonra gelen kısma function expression denir. Bize kodumuzun bir parçasını temsil eden özel bir değeri (bir fonksiyon) verir, böylece istediğimiz zaman onu çağırabiliriz.
    Function Declaration (Fonksiyon Bildirimi): Her seferinde let sayHi = function() { } yazmak yorucu olacaktır, bu yüzden daha kısa bir yapı kullanabiliriz: function sayHi() { }. Buna function declaration denir. Sol tarafa bir değişken belirlemek yerine bunu function anahtar kelimesinden sonra yazarız. Bu iki kullanım biçimi çoğunlukla birbirlerinin yerine kullanılır.
    Function Hoisting: Normalde, bir değişkeni yalnızca let veya const ile tanımlayıp çalıştırıldıktan sonra kullanabilirsiniz. Bu durum fonksiyonlar ile sorun yaratabilir çünkü birbirlerini çağırmak durumda kalabilirler ve hangi fonksiyonun hangileri tarafından kullanıldığını ya da hangisinin daha önce tanımlanması gerektiğini takip etmek oldukça zordur. Kolaylık olması açısından sadece (sadece!) function declaration yapısını kullandığınız zaman tanımlanma sıralarının bir önemi kalmaz çünkü “hoisting” meydana gelir. Bu, kavramsal olarak hepsinin otomatik olarak scope’un en üstüne taşındığını söylemenin süslü bir yolu. Onları çağırdığınız zaman hepsi zaten tanımlanmış olacaktır.
    this: Muhtemelen en çok yanlış anlaşılan JavaScript kavramı olan this fonksiyon için özel bir çeşit argümandır. Fonksiyona doğrudan siz göndermezsiniz. Bunun yerine, JavaScript fonksiyonu nasıl çağırdığınıza bağlı olarak bunu kendisini gönderir. Örneğin, noktalı . gösterim (iceCream.eat() gibi) bu özel this değerini .'dan önce ne varsa ona göre alır (bizim örneğimizde bu iceCream). Fonksiyon içerisindeki this değeri fonksiyonun nasıl çağırıldığına bağlı şekilde değişir, nerede tanımlandığına göre değil. .bind, .call ve .apply gibi helper metotları bu this değeri üzerinde daha fazla kontrol sahibi olmanızı sağlarlar.
    Arrow Functions: Arrow fonksiyonlar, fonksiyon ifadelerine benzerdir. Şu şekilde tanımlarsınız: let sayHi = () => { }. Kısa ve özdürler ve genellikle one-liners (tek satırlar) için kullanılırlar. Arrow fonksiyonlar, normal fonksiyonlara göre daha kısıtlıdırlar; örneğin, this kavramına sahip değildirler. Arrow fonksiyonu içerisinde this yazdığınız zaman yukarıdaki en yakın “sıradan” fonksiyonun this'ini kullanırlar. Bu, sadece yukarıdaki bir fonksiyonda bulunan bir değişkeni ya da argümanı kullandığınızda olacak şeyle benzerdir. Bu demek oluyor ki, insanlar genelde etrafını saran kod içerisindeki this değerinin aynısını görmek istedikleri zaman arrow fonksiyonlarını kullanırlar.
    Function Binding: Genellikle, bir f fonksiyonunu belirli bir this değerine ve argümanlarına bind etmek (bağlamak), önceden tanımlanmış değerlerle f'i çağıran yeni bir fonksiyon oluşturmak anlamına gelir. JavaScript’in bunu yapması için .bind adında gömülü bir helper metodu var ancak bunu kendiniz de yapabilirsiniz. Bu binding işlemi iç içe geçmiş fonksiyonların, dışarıdaki fonksiyonlarla aynı this değerini “görebilmeleri” için sıkça kullanılan bir yöntemdi. Ancak şimdi bu tür durumlar arrow fonksiyonları ile çözüldüğü için bu işlem eskisi kadar sık kullanılmıyor.
    Call Stack: Bir fonksiyonu çağırmak bir odaya girmeye benzer. Fonksiyonu her çağırışımızda, içindeki değişkenler tekrar tekrar en baştan atanır. Bu sebeple her fonksiyon çağırma kodları ile beraber yeni bir “oda” inşa etmek ve içeri girmek gibidir. Fonksiyonumuzun değişkenleri bu odada “yaşarlar”. Fonksiyon return değerine döndüğü zaman bu “oda” tüm değişkenleri ile beraber yok olur. Bu odaları üst üste sıralanmış odalar yığını olarak düşünebilirsiniz — bir call stack. Fonksiyondan çıktığınız zaman call stack’te “aşağıda” olan fonksiyona geri döneriz.
    Recursion: Recursion, fonksiyonun kendi içerisinde kendisini çağırması anlamına gelir. Bu, fonksiyonunuzla az önce yaptığınız şeyi farklı argümanlarla tekrar yapmak istediğiniz durumlarda kullanışlıdır. Örneğin, web’de gezinen bir arama motoru yazıyorsanız, collectLinks(url) fonksiyonunuz önce bir sayfadan linkleri toplayacaktır ve ardından tüm sayfaları ziyaret edene kadar her link için kendisini tekrar çağıracaktır. Recursion’ın dezavantajı, fonksiyon kendisini sonsuza kadar çağırmaya devam ettiği için hiçbir zaman sona ermeyen bir kod yazma ihtimalinizin oldukça yüksek olmasıdır. Böyle bir durumda JavaScript, “stack overflow” denilen bir hata vererek çalışmayı durduracaktır. Bu şekilde adlandırılmıştır çünkü call stack’imizde çok fazla fonksiyon çağrısı birikmiştir ve kelimenin tam anlamıyla taşmıştır (overflown).
    Higher-Order Function: High-order fonksiyonlar farklı fonksiyonları argüman olarak alarak ya da return değeri olarak döndürerek ele alan fonksiyonlardır. Bu ilk başta kulağa tuhaf geliyor olabilir ancak fonksiyonların da değerler olduklarını ve onları sayılar, stringler ya da objeler gibi kullanabileceğimizi hatırlamalıyız. Bu gereğinden fazla kullanılabilir bir tarz ancak aşırıya kaçılmadığında son derece etkileyicidir.
    Callback: Callback aslında bir JavaScript terimi değil gibi. Daha çok bir modeldir diyebiliriz. Bir fonksiyonu başka bir fonksiyona argüman olarak gönderip, fonksiyonunuzun daha sonra çağırılmasını beklemektir. Bir “call back” beklersiniz. Örneğin, setTimeout bir callback fonksiyonu alır ve… zamanlayıcı sona erdiğinde çağırılır. Ancak bu callback fonksiyonlarının özel bir yanı yoktur. Sıradan fonksiyonlardır ve “callback” dediğimiz zaman yalnızca beklentilerimizden bahsederiz.
    Closure: Normalde bir fonksiyondan çıktığınız zaman tüm değişkenleri “kaybolur”. Bunun sebebi hiçbir şeyin artık onlara ihtiyaç duymamasıdır. Peki ya bir fonksiyon içerisinde başka bir fonksiyon tanımlarsanız ne olur? O zaman içerideki fonksiyon tekrar çağırılabilir ve dıştaki fonksiyonun değişkenlerini okuyabilir. Pratikte bu son derece kullanışlıdır! Ancak bunun işe yaraması için, dıştaki fonksiyonun değişkenlerinin bir yerlerde “takılıp kalması” gerekir. Bu yüzden mevcut durumda, JavaScript normalde yaptığı gibi değişkenleri “unutmak” yerine onları “canlı tutar”. Buna “closure” denir. Closurelar, JavaScript’in sıklıkla yanlış anlaşılmış bir yönü olarak kabul edilse de muhtemelen farkında olmadan onları pek çok kere kullandınız!

JavaScript bu kavramlardan ve çok daha fazlasından oluşturulmuştur. Doğru bir zihinsel model inşa edene kadar JavaScript konusundaki bilgim konusunda son derece endişeliydim ve gelecek nesil geliştiricilerin bu boşluğu daha kısa sürece doldurmasına yardımcı olmak istiyorum.

Bu konuların her birinde daha kapsamlı bir bakış için bana katılmak isterseniz sizin için bir şeyim var. Just JavaScript benim JavaScript’in nasıl çalıştığına dair oluşturduğum saf zihinsel model ve harikulade Maggie Appleton’ın görsel illüstrasyonlarını içerecek. Bu gönderinin aksine daha yavaş bir tempoda ilerleyecek ve her bir detayı takip edebileceksiniz.

--

--