Məzmuna keç
Educora
İrəli22 dəq15 / 27

Görünmə sahəsi və qapanmalar

JavaScript-in dəyişənləri necə tapdığını anla: leksik görünmə sahəsi, qapanmalar, IIFE və modul şablonu, həmçinin `this`, `bind`, `call` və `apply`.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Leksik görünmə sahəsini, sahələr zəncirini, blok sahəsini və qaldırılmanı (hoisting) izah etmək
  • Gizli vəziyyət saxlamaq üçün qapanmalardan, IIFE-dən və modul şablonundan istifadə etmək
  • Çağırışda this-in nə olacağını təxmin etmək və onu bind, call, apply və ya ox funksiyası ilə düzəltmək

Saytda «Bəyən» düyməsi var və o, neçə dəfə basıldığını yadda saxlamalıdır. Sayğacı qlobal dəyişəndə saxlasan, istənilən başqa kod onu təsadüfən poza bilər. Bəs funksiya bitəndən sonra da öz dəyişənini necə «xatırlaya» bilər? Cavab qapanmadır (closure) — JavaScript-in ən güclü ideyalarından biri və müsahibələrdə ən çox soruşulan mövzulardan biri.

Leksik görünmə sahəsi və sahələr zənciri

Görünmə sahəsi kodun dəyişənin göründüyü hissəsidir. JavaScript leksik sahədən istifadə edir: funksiyanın hansı dəyişənləri gördüyü onun harada yazıldığından asılıdır, harada çağırıldığından yox. Hər funksiya, let/const olan hər { } bloku yeni sahə yaradır. Dəyişən axtarılanda JavaScript əvvəlcə cari sahəyə, sonra xarici sahəyə baxır və qlobal sahəyə qədər gedir — buna sahələr zənciri deyilir. Axtarış yalnız içəridən çölə gedir, əksinə heç vaxt.

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);
▸ Gözlənilən nəticə
Nizami Nasimi Baku
undefined
undefined var ignores blocks
inner üç sahənin hamısını görür, outer isə street-i görmür. var blokdan «qaçır», let isə blokun içində qalır.

Kod icra olunmazdan əvvəl mühərrik bütün elanları qeydə alır — buna qaldırılma (hoisting) deyilir. var ilə elan olunmuş dəyişən sahənin əvvəlinə «qaldırılır» və undefined qiyməti alır. function elanı bütövlükdə qaldırılır, ona görə onu yazıldığı yerdən əvvəl çağırmaq olar. let və const da qaldırılır, amma öz sətrinə qədər müvəqqəti ölü zonada (TDZ) qalır: onlara vaxtından əvvəl müraciət ReferenceError verir.

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';
}
▸ Gözlənilən nəticə
undefined
ReferenceError
Function declarations are hoisted

Qapanmalar

Tərif
Qapanma (closure)

Funksiya ilə onun yarandığı sahədəki dəyişənlərin birliyi. Xarici funksiya işini bitirsə belə, daxili funksiya həmin dəyişənlərə çıxışı saxlayır.

Adətən funksiya bitəndə onun lokal dəyişənləri yox olur. Amma həmin dəyişənlərdən istifadə edən daxili funksiya qaytarılıbsa və ya harasa yazılıbsa, mühərrik onları yaşadır. Xarici funksiyanın hər çağırışı yeni, ayrıca dəyişənlər dəsti yaradır — aşağıda likes və views bir-birinə mane olmur.

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);
▸ Gözlənilən nəticə
2 101
undefined
count obyektin xassəsi deyil — o, yalnız qapanmada yaşayır, ona görə kənardan onu dəyişmək mümkün deyil.

Qapanmaların başqa tez-tez rast gəlinən istifadəsi fabrik funksiyalardır: parametri «yadda saxlayan» yeni funksiyalar yaradırsan.

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));
▸ Gözlənilən nəticə
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);
}
▸ Gözlənilən nəticə
var 4
var 4
var 4
let 1
let 2
let 3

IIFE və modul şablonu

ES modullarından əvvəl proqramçılar dəyişənləri IIFE ilə gizlədirdilər — dərhal çağırılan funksiya ifadəsi (Immediately Invoked Function Expression): mötərizəyə alınmış və elə oradaca çağırılan funksiya. Onun dəyişənləri qlobal sahəyə «sızmır». IIFE qapanma ilə birləşəndə modul şablonu alınır: gizli verilənlər və yalnız lazım olan metodları olan açıq obyekt (API). Bu gün fayllar səviyyəsində bu işi ES modulları görür, amma şablon köhnə kodda hələ də geniş yayılıb və bir faylın içində faydalıdır.

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);
▸ Gözlənilən nəticə
OK Not enough money
{ balance: 70, history: [ '+100', '-30' ] }
undefined
balance və history yalnız deposit, withdraw və report vasitəsilə dəyişir. report massivin surətini qaytarır ki, kənardan tarixçəni pozmaq olmasın.

this, bind, call və apply

Adi dəyişənlərdən fərqli olaraq this leksik deyil: adi funksiyada onun qiyməti funksiyanın necə çağırıldığından asılıdır və çağırış anında müəyyən olunur. Ox funksiyalarının isə öz this-i yoxdur — onlar this-i ətrafdakı koddan götürür.

Çağırışthis nədir
obj.method()nöqtədən soldakı obyekt — obj
fn()sərt rejimdə (siniflər, modullar) undefined, köhnə qeyri-sərt kodda qlobal obyekt
new Fn()yeni yaradılan obyekt
fn.call(obj, a, b), fn.apply(obj, [a, b])obj — funksiya dərhal çağırılır
fn.bind(obj)this-i həmişəlik obj olan yeni funksiya
ox funksiyasıətrafdakı 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]));
▸ Gözlənilən nəticə
Hi, I am Aysel!
Hello, I am Murad.
Hey, I am Aysel!!
9
call arqumentləri tək-tək, apply massiv kimi alır. bind funksiyanı çağırmır — this-i (və istəsən, ilk arqumentləri) bağlanmış yeni funksiya qaytarır.

Metodu callback kimi ötürəndə o, obyektindən «qopur». Sinif metodları həmişə sərt rejimdə işləyir, ona görə this undefined olur və xassəni oxumaq TypeError verir. Çarələr: bind, ox funksiyası ilə sarğı və ya sinif sahəsi kimi yazılmış ox funksiyası.

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);
▸ Gözlənilən nəticə
Lost this: TypeError
5
Tapşırıq

createIdGenerator(prefix) funksiyasını yaz: o, hər çağırışda növbəti identifikatoru qaytaran funksiya qaytarsın: user-1, user-2... Sayğac qapanmada saxlanmalıdır və hər generatorun öz sayğacı olmalıdır.

Tapşırıq · 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());
▸ Gözlənilən nəticə
user-1
user-2
order-1
user-3
Tapşırıq

finalPrice funksiyası endirimi this.discount-dan götürür. 1) call ilə shop obyekti üçün 200 manatlıq malın qiymətini çap et. 2) bind ilə shop-a bağlanmış funksiya yarat və onu map-ə ötürərək [50, 80] qiymətlərini çevir.

Tapşırıq · 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]
▸ Gözlənilən nəticə
180
[ 45, 72 ]

Əsas fikirlər

  • Leksik sahə: funksiya yazıldığı yerin dəyişənlərini görür, axtarış sahələr zənciri ilə içəridən çölə gedir.
  • let/const blok sahəlidir və öz sətrinə qədər TDZ-dədir; var funksiya sahəlidir və undefined kimi qaldırılır.
  • Qapanma — funksiya və onun xatırladığı dəyişənlərdir; xarici funksiyanın hər çağırışı yeni dəst yaradır.
  • IIFE və modul şablonu gizli verilənləri saxlayır və yalnız açıq API qaytarır.
  • this çağırış üsulundan asılıdır: call/apply onu bir dəfəlik verir, bind həmişəlik bağlayır, ox funksiyası çöldən götürür.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
function outer() { let n = 0; return () => ++n; } verilib. const a = outer(); const b = outer(); a(); a(); sətirlərindən sonra b() nə qaytarır?