JavaScript | Scopes, Closure
JavaScript-ში დამწყებებისთვის ერთ-ერთი ყველაზე დამაბნეველი თემა Scope (სეგმენტი) და Clouse-რე არის. ამ სტატიაში ვეცდები მარტივად ავხსნა თუ რას ნიშნავს ეს ტერმინები.
Scope
JavaScript-ში Scope გვაქვს - ლექსიკური (Lexical Scope), ფუნქციონალური (Functional Scope), მოდულარული (Module Scope), ბლოკის (Block Scope) და გლობალური (Global Scope) სეგმენტები, ასევე არსებობს დინამიური სეგმენტი (Dynamic Scope) რომელიც JavaScript-ში არ არის.
სეგმენტი შეგვიძლია როგორც კონტეინერი წარმოვიდგინოთ ცვლადისთვის სადაც მისი სიცოცლხის დრო და ხილვადობა განისაზღვრება სხვა ფუნქციებისათვის ან ცვლადებისთვის.
Global Scope
ცვლადები/ფუნქციები რომლებიც გამოცხადებულია ფუნქციის, ბლოკის ან მოდულის გარეთ მიეკუთვნებიან გლობალურ სეგმენტს, ისინი კოდში ნებისმიერ ადგილას არიან ხელმისაწვდომები.
ბრაუზერისთვის გლობალური ობიექტია window
ხოლო node
-სთვის global
.
index.js
function globalFunction() {
console.log(this)
}
window.globalFunction()
კოდის შედეგი იქნება:
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
რადგან globalFunction
გლობალურ სეგმენტშია გამოცხადებული ის window
ობიექტს მიეკუთვნება.
Module Scope
მოდულში გამოცხადებული ცვლადები მხოლოდ ამ მოდულისთვის იქნება ხელმისაწვდომი, თუ გვინდა რომ ცვლადი სხვა მოდულში მივიღოთ მაშინ ეს ცვლადი უნდა დავაექსპორტოთ.
a.js
const a = 1;
export { a };
და import
-ით მივიღოთ ცვლადი სხვა მოდულში.
b.js
import { a } from './a.js';
რადგან მოდული იმპორტის დროს შექმნის ფუნქციას სადაც მოათავსებს მთელ კოდს და განუსაზღვრავს სეგმენტს. მაგალითად NodeJS-სი მოდულს ასე გარდაქმნის:
index.js
console.log(arguments);
იმპორტის დროს გარდაიქმნება:
funtion(exports, module, require, __filename, __dirname) {
console.log(arguments);
return module.exports
}
Function Scope
ფუნქციონალურ სეგმენტში გამოცხადებული ცვლადები და ფუნქციის პარამეტრები მხოლოდ ამ ფუნქციის შიგნით იქნება ხელმისაწვდომი.
function foo() {
const variable = 1;
}
console.log(variable) // ReferenceError: variable is not defined
var vs let and const
ცვლადებს, რომლებიც გამოცხადებულია var
-ით, მხოლოდ ფუნქციური სეგმენტი გააჩნიათ და კომპილაციის დროს ფუნქციის თავში თავსდებიან.
function foo() {
console.log(x) //undefined
var x = 1;
}
დაიბეჭდება undefined
რადგან var-ით გამოცხადებული ცვლადი ფუნქციის თავში ინაცვლებს კომპილაციისას, რასაც Hoisting
ეწოდება.
function foo() {
var x = undefined;
console.log(x) //undefined
x = 1;
}
Hoisting არის JavaScript-ის მექანიზმი სადაც გამოცხადებული ცვლადები და ფუნქციები მისი სეგმენტის თავში თავსდებიან.
თუ ცვლადი გამოცხადებულია var
-ით, ფუნქცისს გარეთ მისი მნიშვნელობა global
ობიექტში ჩაიწერება.
var foo = 1;
window.foo // 1
let
და const
-ით გამოცხადებული ცვლადების სეგმენტი არის ბლოკი (Block Scope) თუ შევქმნით ცვლადს ფუნქციების გარეთ, მათი მნიშვნელობა global
ობიექტში არ ჩაიწერება.
console.log(foo) //ReferenceError: foo is not defined
let foo = 1;
ასევე var
შეგვიძლია გადავადეკლარიროთ let/const
განსხვავებით.
var x = 1;
var x = 2;
console.log(x) // 2
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
Block Scope
ბლოკის სეგმენტი იქმნება ფიგურულ ბრჭყალებს შორის { }
. როგორც ავღნიშნეთ let
და const
-ით გამოცხადებულ ცვლადებს გააჩნიათ ბლოკის სეგმენტი და ისინი მხოლოდ იმ ბლოკში არიან ხელმისაწვდომები სადაც გამოცადდნენ.
let x = 1;
{
let x = 2;
}
console.log(x) // 1
var x = 1;
{
var x = 2;
}
console.log(x) // 2
ავიღოთ მაგალითსთვის შემდეგი კოდი:
function loop(){
for(var i=0; i<5; i++){
setTimeout(function logValue(){
console.log(i); //5
}, 100);
}
};
loop();
ეს კოდი console
-ში დაბეჭდავს 5
-ს ყოველ იტერაციაზე. რადგან setTimeout
-ი 100 მილიწამიანი დაგვიანებით სრულდება, მისი შესრულების დროს i
-ცვლადის მნიშვნელობა უკვე 5
იქნება.
function loop(){
for(let i=0; i<5; i++){
setTimeout(function logValue(){
console.log(i); // 0, 1, 2, 3, 4
}, 100);
}
};
loop();
ხოლო let
ყოველ ჯერზე ახალ ლოკალურ ცვლადს შექმნის, ამიტომ დაიბეჭდება 0, 1, 2, 3, 4
Lexical Scope
ლექსიკურ სეგმენტს ასევე უწოდებენ სტატიკურ სეგმენტს ის ცვლადისთვის განსაზღვრავს ადგილს (კონტეინერს) სადაც ის შეიქმნა. მაგალითად თუ ცვლადი შეიქმნა გლობალურ სეგმენტში მიდი ლექსიკური სეგმენტი იქნება გლობალური, თუ ცვლადი შეიქმნა ფუნქციაში მისი ლექსიგური სეგმენტი იქნება ის ფუნქცია სადაც ცვლადი გამოცხადდა.
function foo() {
console.log( a );
}
function bar() {
var a= 3;
foo();
}
var a=2;
bar();
რა იქნება a
ცვლადის მნიშვნელობა ამ შემთხვევაში? როგორც ავღნიშნეთ ლექსიკური სეგმენტი ცვლადის სეგმენტს განსაზღვრავს იმ ადგილით სადაც ეს ცვლადი იყო გამოცხადებული, var a=2
-ს გამოცხადებულია გლობალურ სეგმენტში, ამიტომ მისი ლექსიკური სეგმენტი იქნება გლობალური და console-ში დაიბეჭდება 2
Lexical Scope vs Dynamic Scope
თუ სტატიკური სეგმენტი (Lexical Scope) ცვლადის მნიშვნელობას განსაზღვრავს სეგმენტიდან (Scope-იდან) სადაც ის შეიქმნა, დინამიური სეგმენტი (Dynamic Scope) ცვლადის მნიშვნელობას განსაზღვრავს ადგილიდან საიდანაც იყო ის გამოძახებული.
განვიხილოთ წინა მაგალითი და წარმოვიდგინოთ რომ JavaScript-ში ლექსიკურის მაგივრად დინამიური სეგმენტი გვაქვს, რა იქნება დაიბეჭდება console-ში?
function foo() {
console.log( a );
}
function bar() {
var a= 3;
foo();
}
var a=2;
bar();
a
ცვლადი foo
ფუნქციაში არ გვაქვს, ლექსიკური სეგმენტის შემთხვევაში კომპილატორი ნახულობს რომელ სეგმენტშია შექმნილი ფუნქცია და იმ სეგმენტიდან იღებს მნიშვნელობას, ხოლო დინამიური სეგმენტის შემთხვევაში კომპილატორი Call Stack-ს (Call Stack-ზე სტატია შეგიძლიათ წაიკითხოთ აქ აყვება და ნახავს სად არის ფუნქცია გამოძახებული, რადგან foo
გამოძახებულია bar
-ში და მის სეგმენტში ცვლადის მნიშვნელობა არის 3
კონსოლში დაიბეჭდება 3
.
Scope Chain
ყველა სეგმენტს აქვს ლინკი მის მშობელ სეგმენტზე, ამიტომ თუ JavaScript-ი ვერ იპოვის ცვლადს სეგმენტში სადაც კოდი სრულდება ის მოძებნის მშობელ სეგმენტში სანამ გლობალურ სეგმენტს არ მიაღწევს, თუ გლობალურ სეგმენტშიც არ მოიძებნება ცვლადი, მივიღებთ ReferenceError
-ს.
Closure
Closure არის როცა შიდა ფუნქცია იმახსოვრებს გარე ფუნქციის ცვლადებს.
JavaScript-ში ფუნქცია ყოველი გამოძახებისას ახლიდან ქმნის მის ცვლადებს:
function foo() {
let counter = 0;
counter += 1;
console.log(counter);
}
foo() //1
foo() //1
ხოლო ამ შემთხვევაში:
function parent() {
let counter = 0;
return function child() {
counter += 1;
console.log(counter);
}
}
const fn = parent();
fn() // 1
fn() // 2
child
ფუნქცია იმახსოვრებს მის მშობელ ფუნქციაში გამოცხადებულ ცვლადს, ამას ეწოდება Closure
.
function parent() {
let counter = 0;
return function child() {
counter += 1;
console.log(counter);
}
}
const fn = parent();
const fn2 = parent();
fn() // 1
fn2() // 1
fn() // 2
fn2() // 2
როცა fn2
-ის შემდეგ გამოვიძახეთ fn
არ მივიღეთ 3
რადგან fn
-ს და fn2
-ს თავის Closure აქვთ, შესაბამისად counter
ცვლადიც თავისი აქვთ Closure-ში.
ფოტოს წყაროები:
- https://javascript.plainenglish.io/how-scope-chain-is-determined-in-javascript-b180eceae002