İçeriğe geç
Educora
İleri22 dk15 / 27

Kapsam ve closure'lar

JavaScript'in değişkenleri nasıl bulduğunu anla: sözcüksel kapsam, closure'lar, IIFE ve modül deseni; ayrıca `this`, `bind`, `call` ve `apply`.

Kendini test et
Bu derste öğreneceklerin
  • Sözcüksel kapsamı, kapsam zincirini, blok kapsamını ve yukarı taşımayı (hoisting) açıklamak
  • Gizli durum saklamak için closure'ları, IIFE'yi ve modül desenini kullanmak
  • Bir çağrıda this'in ne olacağını tahmin etmek ve onu bind, call, apply ya da ok fonksiyonuyla düzeltmek

Bir sitede kaç kez basıldığını hatırlaması gereken bir “Beğen” düğmesi var. Sayacı global bir değişkende tutarsan başka herhangi bir kod onu kazara bozabilir. Peki bir fonksiyon bittikten sonra kendi değişkenini nasıl “hatırlar”? Cevap closure'dır (kapanım): JavaScript'in en güçlü fikirlerinden biri ve mülakatlarda en sık sorulan konulardan biri.

Sözcüksel kapsam ve kapsam zinciri

Kapsam (scope), bir değişkenin görünür olduğu kod bölümüdür. JavaScript sözcüksel (lexical) kapsam kullanır: bir fonksiyonun hangi değişkenleri gördüğü, çağrıldığı yere değil, yazıldığı yere bağlıdır. Her fonksiyon ve let/const içeren her { } bloğu yeni bir kapsam oluşturur. JavaScript bir değişkeni ararken önce o anki kapsama, sonra dıştakine bakar ve global kapsama kadar gider; buna kapsam zinciri denir. Arama yalnızca içeriden dışarıya doğru yapılır, tersi asla.

JavaScript
const city = 'Baku';

function outer() {
  const district = 'Nasimi';

  function inner() {
    const street = 'Nizami';
    console.log(street, district, city);
  }

  inner();
  console.log(typeof street);
}

outer();

if (true) {
  let blockVar = 'inside';
  var functionVar = 'var ignores blocks';
}
console.log(typeof blockVar, functionVar);
▸ Beklenen çıktı
Nizami Nasimi Baku
undefined
undefined var ignores blocks
inner üç kapsamın hepsini görür, outer ise street'i göremez. var bloktan “kaçar”, let ise bloğun içinde kalır.

Motor, kodu çalıştırmadan önce tüm bildirimleri kaydeder; buna yukarı taşıma (hoisting) denir. var ile bildirilen bir değişken, undefined değeriyle kapsamının en üstüne “taşınır”. Bir function bildirimi tamamen taşınır; bu yüzden fonksiyonu yazıldığı satırdan önce çağırabilirsin. let ve const da taşınır, ama kendi satırlarına kadar geçici ölü bölgede (TDZ) kalırlar: onlara erken erişmek ReferenceError fırlatır.

JavaScript
console.log(early);
var early = 'var is hoisted';

try {
  console.log(later);
} catch (error) {
  console.log(error.name);
}
let later = 'let waits in the TDZ';

console.log(sayHi());

function sayHi() {
  return 'Function declarations are hoisted';
}
▸ Beklenen çıktı
undefined
ReferenceError
Function declarations are hoisted

Closure'lar

Tanım
Closure (kapanım)

Bir fonksiyon ile oluşturulduğu kapsamdaki değişkenlerin birlikteliği. Dış fonksiyon bitse bile iç fonksiyon bu değişkenlere erişimini korur.

Normalde bir fonksiyon bitince yerel değişkenleri yok olur. Ama onları kullanan bir iç fonksiyon döndürülmüş ya da bir yere kaydedilmişse motor onları yaşatır. Dış fonksiyonun her çağrısı yeni ve ayrı bir değişken takımı oluşturur; aşağıda likes ile views birbirini hiç etkilemez.

JavaScript
function createCounter(start = 0) {
  let count = start;
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count,
  };
}

const likes = createCounter();
const views = createCounter(100);

likes.increment();
likes.increment();
views.increment();

console.log(likes.value(), views.value());
console.log(likes.count);
▸ Beklenen çıktı
2 101
undefined
count nesnenin bir özelliği değildir; yalnızca closure içinde yaşar, bu yüzden dışarıdan değiştirilemez.

Closure'ların sık görülen bir başka kullanımı fonksiyon fabrikalarıdır: bir parametreyi “hatırlayan” yeni fonksiyonlar üretirsin.

JavaScript
function makeMultiplier(factor) {
  return (x) => x * factor;
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5), triple(5));
console.log([1, 2, 3].map(triple));
▸ Beklenen çıktı
10 15
[ 3, 6, 9 ]
JavaScript
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log('var', i), 0);
}

for (let j = 1; j <= 3; j++) {
  setTimeout(() => console.log('let', j), 0);
}
▸ Beklenen çıktı
var 4
var 4
var 4
let 1
let 2
let 3

IIFE ve modül deseni

ES modüllerinden önce geliştiriciler değişkenleri bir IIFE ile gizlerdi: hemen çağrılan fonksiyon ifadesi (Immediately Invoked Function Expression), yani paranteze alınıp hemen çağrılan bir fonksiyon. Onun değişkenleri global kapsama “sızmaz”. IIFE'yi bir closure ile birleştirirsen modül desenini elde edersin: gizli veriler ve yalnızca seçtiğin metotları içeren açık bir nesne (API). Bugün dosya düzeyinde bu işi ES modülleri yapıyor, ama desen eski kodlarda hâlâ yaygın ve tek bir dosyanın içinde de işe yarar.

JavaScript
const bank = (function () {
  let balance = 0;
  const history = [];

  return {
    deposit(amount) {
      balance += amount;
      history.push(`+${amount}`);
    },
    withdraw(amount) {
      if (amount > balance) return 'Not enough money';
      balance -= amount;
      history.push(`-${amount}`);
      return 'OK';
    },
    report: () => ({ balance, history: [...history] }),
  };
})();

bank.deposit(100);
console.log(bank.withdraw(30), bank.withdraw(500));
console.log(bank.report());
console.log(typeof balance);
▸ Beklenen çıktı
OK Not enough money
{ balance: 70, history: [ '+100', '-30' ] }
undefined
balance ve history yalnızca deposit, withdraw ve report aracılığıyla değişir. report, geçmişin dışarıdan bozulmaması için dizinin bir kopyasını döndürür.

this, bind, call ve apply

Sıradan değişkenlerin aksine this sözcüksel değildir: normal bir fonksiyonda değeri, fonksiyonun nasıl çağrıldığına bağlıdır ve çağrı anında belirlenir. Ok fonksiyonlarının kendi this'i yoktur; onu çevredeki koddan alırlar.

Çağrıthis nedir
obj.method()noktanın solundaki nesne — obj
fn()katı modda (sınıflar, modüller) undefined, eski katı olmayan kodda global nesne
new Fn()yeni oluşturulan nesne
fn.call(obj, a, b), fn.apply(obj, [a, b])obj — fonksiyon hemen çağrılır
fn.bind(obj)this'i her zaman obj olan yeni bir fonksiyon
ok fonksiyonuçevredeki kodun this'i
JavaScript
function introduce(greeting, punctuation) {
  return `${greeting}, I am ${this.name}${punctuation}`;
}

const aysel = { name: 'Aysel' };
const murad = { name: 'Murad' };

console.log(introduce.call(aysel, 'Hi', '!'));
console.log(introduce.apply(murad, ['Hello', '.']));

const ayselIntro = introduce.bind(aysel, 'Hey');
console.log(ayselIntro('!!'));

console.log(Math.max.apply(null, [3, 9, 4]));
▸ Beklenen çıktı
Hi, I am Aysel!
Hello, I am Murad.
Hey, I am Aysel!!
9
call argümanları tek tek, apply ise dizi olarak alır. bind fonksiyonu çağırmaz; this'i (istersen ilk argümanları da) sabitlenmiş yeni bir fonksiyon döndürür.

Bir metodu callback olarak verdiğinde metot nesnesinden “kopar”. Sınıf metotları her zaman katı modda çalışır; bu yüzden this undefined olur ve bir özelliği okumak TypeError fırlatır. Çözümler: bind, bir ok fonksiyonu sarmalayıcısı ya da sınıf alanı olarak yazılmış bir ok fonksiyonu.

JavaScript
class Player {
  score = 0;

  addPoint() {
    this.score++;
  }

  addPointArrow = () => {
    this.score++;
  };
}

const player = new Player();
const detached = player.addPoint;

try {
  detached();
} catch (error) {
  console.log('Lost this:', error.name);
}

[1, 2].forEach(player.addPoint.bind(player));
[1, 2, 3].forEach(player.addPointArrow);
console.log(player.score);
▸ Beklenen çıktı
Lost this: TypeError
5
Alıştırma

createIdGenerator(prefix) fonksiyonunu yaz: her çağrıda bir sonraki kimliği veren bir fonksiyon döndürsün: user-1, user-2… Sayacı bir closure içinde tut ve her üretecin kendi sayacı olsun.

Alıştırma · JavaScript
function createIdGenerator(prefix) {
  // keep a counter here
  return () => {
    // increase the counter and return `${prefix}-${count}`
  };
}

const userId = createIdGenerator('user');
const orderId = createIdGenerator('order');

console.log(userId());
console.log(userId());
console.log(orderId());
console.log(userId());
▸ Beklenen çıktı
user-1
user-2
order-1
user-3
Alıştırma

finalPrice fonksiyonu indirimi this.discount'tan alır. 1) call ile shop nesnesi için 200 manatlık bir ürünün fiyatını yazdır. 2) bind ile shop'a bağlı bir fonksiyon oluştur ve [50, 80] fiyatlarını dönüştürmek için onu map'e ver.

Alıştırma · JavaScript
const shop = { discount: 10 };

function finalPrice(price) {
  return price - (price * this.discount) / 100;
}

// 1) finalPrice for 200 with call
// 2) bind to shop and use it in map for [50, 80]
▸ Beklenen çıktı
180
[ 45, 72 ]

Önemli noktalar

  • Sözcüksel kapsam: fonksiyon yazıldığı yerin değişkenlerini görür; arama kapsam zinciri boyunca dışarıya doğru ilerler.
  • let/const blok kapsamlıdır ve kendi satırlarına kadar TDZ'dedir; var fonksiyon kapsamlıdır ve undefined olarak taşınır.
  • Closure, bir fonksiyon ve hatırladığı değişkenlerdir; dış fonksiyonun her çağrısı yeni bir takım oluşturur.
  • IIFE ve modül deseni gizli verileri saklar ve yalnızca açık bir API döndürür.
  • this çağrılma biçimine bağlıdır: call/apply onu bir kez verir, bind kalıcı olarak sabitler, ok fonksiyonu ise dışarıdan alır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
function outer() { let n = 0; return () => ++n; } veriliyor. const a = outer(); const b = outer(); a(); a(); satırlarından sonra b() ne döndürür?