4.1 קלוז׳רים וסקופ הרצאה
סקופ - scope¶
סקופ הוא ה"תחום" שבו משתנה חי ונגיש. ב-JS יש שלושה סוגי scope:
סקופ גלובלי - global scope¶
משתנה שמוגדר מחוץ לכל פונקציה או בלוק - נגיש מכל מקום בקוד:
const globalVar = "I'm global";
function test() {
console.log(globalVar); // accessible here
}
if (true) {
console.log(globalVar); // accessible here too
}
console.log(globalVar); // and here
- בפייתון אותו דבר - משתנה ברמה העליונה של הקובץ נגיש מכל מקום
- הימנעו ממשתנים גלובליים כמה שאפשר - הם יוצרים בעיות
סקופ של פונקציה - function scope¶
משתנה שמוגדר בתוך פונקציה נגיש רק בתוכה:
function greet() {
const message = "Hello";
console.log(message); // works
}
greet();
// console.log(message); // ReferenceError - not accessible outside
זה נכון ל-var, let, ו-const - כולם מוגבלים לפונקציה שבה הם מוגדרים.
סקופ בלוק - block scope¶
בלוק הוא כל מה שבתוך סוגריים מסולסלים { } - כולל if, for, while:
if (true) {
let blockVar = "I'm block-scoped";
const alsoBlock = "Me too";
var notBlock = "I escape blocks!";
}
// console.log(blockVar); // ReferenceError
// console.log(alsoBlock); // ReferenceError
console.log(notBlock); // "I escape blocks!" - var ignores block scope!
ההבדל הקריטי:
- let ו-const - מוגבלים לבלוק (block-scoped)
- var - מוגבל לפונקציה בלבד (function-scoped), מתעלם מבלוקים
function example() {
if (true) {
var x = 10; // function-scoped - belongs to example()
let y = 20; // block-scoped - belongs to this if block only
}
console.log(x); // 10 - var is visible in the entire function
// console.log(y); // ReferenceError - let is limited to the block
}
בפייתון אין block scope בכלל - משתנה שמוגדר ב-if או for נגיש מחוץ לבלוק:
ב-JS עם let/const זה לא עובד ככה. זה יותר בטוח כי משתנים לא דולפים ממקומות לא צפויים.
סקופ לקסיקלי - lexical scope¶
זה הכלל הכי חשוב להבנת closures:
פונקציה רואה את המשתנים מהמקום שבו היא הוגדרה, לא מהמקום שבו היא נקראת.
const name = "Global";
function outer() {
const name = "Outer";
function inner() {
console.log(name); // where does JS look for 'name'?
}
return inner;
}
const fn = outer();
fn(); // "Outer" - not "Global"!
למרות ש-fn נקראת מה-scope הגלובלי, היא רואה את name מהמקום שבו היא הוגדרה - בתוך outer. זה סקופ לקסיקלי.
בפייתון עובד בדיוק אותו דבר:
# name = "Global"
#
# def outer():
# name = "Outer"
# def inner():
# print(name) # "Outer" - same behavior
# return inner
#
# fn = outer()
# fn() # "Outer"
שרשרת הסקופ - scope chain¶
כש-JS צריך למצוא משתנה, היא מחפשת מהסקופ הפנימי ביותר החוצה:
const a = "global";
function outer() {
const b = "outer";
function middle() {
const c = "middle";
function inner() {
const d = "inner";
// inner can see everything:
console.log(d); // "inner" - found in own scope
console.log(c); // "middle" - found in middle's scope
console.log(b); // "outer" - found in outer's scope
console.log(a); // "global" - found in global scope
}
inner();
}
middle();
}
outer();
סדר החיפוש (scope chain):
1. הסקופ של הפונקציה עצמה
2. הסקופ של הפונקציה העוטפת
3. הסקופ של הפונקציה שעוטפת את העוטפת
4. ... וכן הלאה עד ה-scope הגלובלי
5. אם לא נמצא בכלל - ReferenceError
קלוז׳ר - closure¶
עכשיו שאנחנו מבינים scope, אפשר להגדיר closure:
קלוז׳ר היא פונקציה ש"זוכרת" את המשתנים מהסקופ החיצוני שלה, גם אחרי שהפונקציה החיצונית סיימה לרוץ.
function createGreeter(greeting) {
// greeting lives in createGreeter's scope
return function(name) {
// this inner function "remembers" greeting
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
// createGreeter has finished running, but the returned functions
// still have access to 'greeting':
sayHello("Alice"); // "Hello, Alice!"
sayHello("Bob"); // "Hello, Bob!"
sayHi("Charlie"); // "Hi, Charlie!"
מה קורה כאן:
1. קוראים ל-createGreeter("Hello") - נוצר scope עם greeting = "Hello"
2. הפונקציה מחזירה פונקציה פנימית שמשתמשת ב-greeting
3. createGreeter סיימה לרוץ, אבל הפונקציה הפנימית עדיין מחזיקה הפניה ל-scope שלה
4. כשקוראים ל-sayHello("Alice"), היא ניגשת ל-greeting דרך ה-closure
דוגמה - מפעל מונים - counter factory¶
function createCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => { count = start; }
};
}
const counter1 = createCounter();
const counter2 = createCounter(100);
counter1.increment();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 3
counter2.decrement();
console.log(counter2.getCount()); // 99
// each counter has its own independent 'count'
console.log(counter1.getCount()); // still 3
כל קריאה ל-createCounter יוצרת scope חדש עם count עצמאי. הפונקציות שחוזרות סוגרות (close over) על ה-count הזה - ומכאן השם closure.
דוגמה - ממואיזציה - memoization¶
function createMemoized(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("Returning cached result");
return cache[key];
}
console.log("Computing new result");
const result = fn(...args);
cache[key] = result;
return result;
};
}
const memoizedAdd = createMemoized((a, b) => a + b);
memoizedAdd(1, 2); // "Computing new result" -> 3
memoizedAdd(1, 2); // "Returning cached result" -> 3
memoizedAdd(3, 4); // "Computing new result" -> 7
ה-cache נשמר ב-closure - הוא פרטי ולא נגיש מבחוץ, אבל הפונקציה המוחזרת יכולה לגשת אליו.
דוגמה - מצב פרטי - private state¶
function createBankAccount(initialBalance) {
let balance = initialBalance; // private!
const transactions = []; // private!
return {
deposit(amount) {
if (amount <= 0) {
console.log("Amount must be positive");
return;
}
balance += amount;
transactions.push({ type: "deposit", amount });
},
withdraw(amount) {
if (amount > balance) {
console.log("Insufficient funds");
return;
}
balance -= amount;
transactions.push({ type: "withdraw", amount });
},
getBalance() {
return balance;
},
getHistory() {
return [...transactions]; // return a copy
}
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.getHistory());
// [{ type: "deposit", amount: 500 }, { type: "withdraw", amount: 200 }]
// can't access balance directly:
// console.log(account.balance); // undefined - it's not a property!
אין דרך לגשת ל-balance או ל-transactions ישירות - הם קיימים רק בתוך ה-closure. זה אנקפסולציה אמיתית.
תבנית המודול - module pattern¶
לפני ש-ES6 הביא לנו modules (שנלמד בשיעור 4.4), השתמשו ב-closures כדי ליצור מודולים:
const Calculator = (function() {
// private variables and functions
let history = [];
function addToHistory(operation, result) {
history.push({ operation, result, time: new Date() });
}
// public API - returned object
return {
add(a, b) {
const result = a + b;
addToHistory(`${a} + ${b}`, result);
return result;
},
subtract(a, b) {
const result = a - b;
addToHistory(`${a} - ${b}`, result);
return result;
},
getHistory() {
return [...history];
},
clearHistory() {
history = [];
}
};
})();
Calculator.add(5, 3); // 8
Calculator.subtract(10, 4); // 6
console.log(Calculator.getHistory());
// [{ operation: "5 + 3", result: 8, ... }, { operation: "10 - 4", result: 6, ... }]
התבנית הזו משלבת IIFE עם closures: הפונקציה רצה מיד, יוצרת scope פרטי, ומחזירה אובייקט עם ממשק ציבורי.
קלוז׳רים בלולאות - הבעיה הקלאסית¶
זו אחת הבעיות הכי מפורסמות ב-JS:
// THE CLASSIC BUG - using var in a loop
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// expected: 0, 1, 2
// actual: 3, 3, 3
למה? כי var הוא function-scoped, לא block-scoped. יש רק i אחד ששייך לכל הלולאה. עד שה-setTimeout רץ, הלולאה כבר סיימה ו-i הוא 3. כל שלוש הפונקציות מצביעות על אותו i.
הפתרון עם let¶
// FIXED - using let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// prints: 0, 1, 2
let הוא block-scoped - כל איטרציה של הלולאה מקבלת i משלה. כל closure סוגר על i אחר.
הפתרון הישן עם IIFE¶
לפני let, השתמשו ב-IIFE כדי ליצור scope חדש בכל איטרציה:
// OLD FIX - IIFE creates a new scope per iteration
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
// prints: 0, 1, 2
ה-IIFE מקבלת את i הנוכחי כפרמטר j, ויוצרת scope חדש עם עותק של הערך. היום אין צורך בזה - פשוט משתמשים ב-let.
IIFE - ביטוי פונקציה שמופעל מיידית¶
כבר ראינו IIFE בשיעור על פונקציות, אבל עכשיו אנחנו מבינים למה היא שימושית - היא יוצרת scope חדש:
// basic IIFE
(function() {
const secret = "hidden";
console.log(secret); // works
})();
// console.log(secret); // ReferenceError
// IIFE with arrow function
(() => {
const temp = computeSomething();
// use temp...
})();
// IIFE that returns a value
const result = (() => {
const x = 10;
const y = 20;
return x + y;
})();
console.log(result); // 30
שימושים נפוצים של IIFE:
- יצירת scope פרטי (בעיקר לפני let/const)
- תבנית המודול (module pattern)
- הרצת קוד async ב-top level (לפני top-level await)
// async IIFE - useful when you can't use top-level await
(async () => {
const response = await fetch("/api/data");
const data = await response.json();
console.log(data);
})();
איסוף זבל - garbage collection וקלוז׳רים¶
ב-JS, המנוע מנקה אוטומטית משתנים שאף אחד לא מתייחס אליהם יותר (garbage collection). אבל closures יכולים למנוע ניקוי:
function createHeavyFunction() {
const bigArray = new Array(1000000).fill("data"); // takes memory
return function() {
// this closure keeps bigArray alive!
console.log(bigArray.length);
};
}
const heavy = createHeavyFunction();
// bigArray can't be garbage collected because heavy() still references it
// to free the memory:
// heavy = null; // now bigArray can be collected
כלל חשוב: closure שומר הפניה לכל ה-scope החיצוני. גם אם הפונקציה הפנימית לא משתמשת בכל המשתנים, המנוע עשוי לשמור אותם בזיכרון (תלוי בהמנוע ובאופטימיזציות שלו).
function example() {
const usedVar = "I'm used";
const unusedVar = "I'm not used"; // may still be kept alive
return function() {
console.log(usedVar);
};
}
טיפ: אם יש לכם closure שמחזיק מידע כבד, שחררו אותו כשאתם לא צריכים אותו יותר.
השוואה לפייתון¶
Closures עובדים באופן דומה מאוד בפייתון. ההבדל העיקרי הוא מילת המפתח nonlocal:
// JavaScript - can modify outer variables freely
function createCounter() {
let count = 0;
return () => {
count++; // can modify count directly
return count;
};
}
# Python - need 'nonlocal' to modify outer variables
# def create_counter():
# count = 0
# def increment():
# nonlocal count # required! otherwise Python thinks count is local
# count += 1
# return count
# return increment
בפייתון, אם רוצים לשנות (ולא רק לקרוא) משתנה מ-scope חיצוני, חייבים להכריז עליו עם nonlocal. ב-JS אין צורך - אפשר לשנות משתנים חיצוניים חופשי.
הבדל נוסף - ב-JS, let ב-for loop יוצר scope חדש בכל איטרציה. בפייתון אין לזה מקבילה ישירה:
# Python has the same closure-in-loop problem:
# functions = []
# for i in range(3):
# functions.append(lambda: print(i))
#
# for f in functions:
# f() # prints 2, 2, 2 (not 0, 1, 2!)
#
# Fix in Python - use default parameter:
# functions = []
# for i in range(3):
# functions.append(lambda i=i: print(i)) # i=i captures current value
סיכום¶
- שלושה סוגי scope: גלובלי, פונקציה, ובלוק
let/constהם block-scoped,varהוא function-scoped - העדיפו תמידlet/const- סקופ לקסיקלי: פונקציה רואה משתנים מהמקום שבו הוגדרה, לא מהמקום שבו נקראת
- שרשרת הסקופ: חיפוש מהסקופ הפנימי החוצה עד הגלובלי
- קלוז׳ר: פונקציה שזוכרת את הסקופ החיצוני שלה גם אחרי שהפונקציה החיצונית סיימה
- שימושים: counter factory, memoization, מצב פרטי, תבנית המודול
- בעיה קלאסית: closures בלולאות עם
var- הפתרון:let - IIFE יוצרת scope חדש - שימושי לתבנית המודול
- נזהרים מ-closures שמחזיקים זיכרון מיותר
- בפייתון closures עובדים דומה, אבל צריכים
nonlocalכדי לשנות משתנים חיצוניים