რა არის Scope?

(არა, პლებეური ცრუ ღმერთი არასწორია) Scope - განსაზღვრის არე არის წესების ნაკრები, ცვლადის მისი სახელის (იდენტიფიკატორის) მიხედვით მოსაძებნად.

თუკი თქვენ რომელიმე კლასიკურ დაპროგრამების ენას ფლობთ (მაგ: C++, Java, C# და ა.შ.) ალბათ გსმენიათ ლოკალურ და გლობალურ ცვლადებზე:

var globalVar; // ეს ცვლადი გლობალურია
int main(){ // #1 ბლოკი
    var mainVar; // ეს ცვლადი ლოკალურია main ფუნქციაში
    someVar = 2; // გამოიწვევს შეცდომას
    { // #2 ბლოკი
        var tmpVar; // ეს ცვლადი ლოკალურია ამ ბლოკში
    } // tmpVar იშლება მეხსიერებიდან
    tmpVar = 3; // გამოიწვევს შეცდომას
    return 0;
} // mainVar იშლება მეხსიერებიდან
// პროგრამის დასრულების შემდეგ globalVar წაიშლება მეხსიერებიდან

აქ ყველაფერი მარტივადაა: ცვლადები ბლოკებს შორის, იმავე ბლოკისათვის ლოკალურია და ხელმისაწვდომია ყველა შიგნით ჩასმული ბლოკისათვის. (მაგ: globalVar-ს შეგვიძლია მივწვდეთ main-დან, mainVar-ს კი - #2 ბლოკიდან)

ანუ ცვლადების განსაზღვრის არე - Scope იმ ბლოკებით გამოიხატება, რომელშიც ინიციალიზებულია.

სამწუხაროდ თუ საბედნიეროდ, ჯავასკრიპტში მსგავსი მიდგომა ყოველთვის არ ამართლებს და განსაზღვრის არე ანუ Scope სულ სხვანაირად მუშაობს.

მიუხედავად იმისა, რომ ჯავასკრიპტი დინამიურ, ინტრერპრეტირებად ენად მიიჩნევა, მისი ინტერპრეტირების პროცესი ძალიან ახლოსაა კომპილაციასთან და შეიძლება სამ ნაწილად დაიყოს:

  1. ტოკენიზირება - ანუ კოდის პატარ-პატარა, კომპილერისათვის გასაგებ ნაკუწებად (ტოკენებად) დაყოფა, მაგალითად var myidveli = 200 დაიყოფა ნაწილებად: var, myidveli, =, 200
  2. პარსირება - ტოკენების მასივის წაკითხვა და ბრძანებების გადატანა უფრო კომპლექსურ მონაცემთა სტრუქტურაში.(Abstract Syntax Tree)
  3. კოდის გენერირება - მაგიის მეშვეობით ბრძანებების მანქანურ კოდში გადაყვანა.

თუმცა კომპილერი და განსაზღვრის არეები არ არის საკმარისი ჯავასკრიპტის გასაშვებად, კიდევ ერთი კომპონენტი - ძრავი (Engine) არის საჭირო.

იმისათვის, რათა გავიგოთ როგორ მუშაობს ჯავასკრიპტი, საჭიროა, რომ ვიფიქროთ როგორც ძრავმა.

მაგალითად, განვიხილოთ მაგალითი: var myidveli = 200; ლოგიკური იქნება თუ ვიფიქრებთ, რომ, კომპილირების შედეგად მივიღებთ ესეთ ფსევდო-ბრძანებას: გამოყავი მეხსიერება, მონიშნე ის სახელით myidveli და ჩაწერე 200 მასში

var myidveli = 200; თითქოს ერთი ბრძანებაა, თუმცა, ძრავი მას ორად აღიქვამს:

  1. var myidveli - რომელსაც კომპილერი დაამუშავებს შემდეგნაირად: ის შეამოწმებს, არის თუ არა მოცემულ Scope-ში ცვლადი სახელით myidveli თუ კი, გადავა შემდეგ ბრძანებაზე, თუ არა - შექმნის მას.
  2. myidveli = 200 - რომელსაც ძრავი დაამუშავებს: კომპილერის მიერ Scope-ში შექმნილ ცვლადს მოძებნის და მიანიჭებს 200-ს.

როგორ ეძებს ძრავი ცვლადებს? სწორედ ძებნის პრინციპი არის ჩვენთვის ყველაზე საინტერესო. ჯავასკრიპში, ცვლადების მოძებნა ორი შესაძლო მეთოდით ხდება:

  • მარცხენა (LHS, Left-hand side lookup)
  • მარჯვენა (RHS, Right-hand side lookup)

LHS არის ცვლადის, როგორ კონტეინერის, მეხსიერების უჯრედის ძებნა, RHS კი ცვლადის მნიშვნელობის.

განვიხილოთ კოდის მონაკვეთი:

//...
a = b + 2
//...

აქ a LHS-ა რადგან ვეძებთ a სახელის მქონე უჯრედს, რათა მასში ჩავწეროთ b+2 რომელიც RHS ძებნის შედეგად მოგვცემს მნიშვნელობას, რადგან ჩვენ არ გვაინტერესებს სად წერია b, არამედ გვაინტერესებს რა წერია(value) მასში.

ჩადგმული (Nested) Scope: განვიხილოთ კოდის ეს მონაკვეთი:

function foo(a) {
    function bar(){
        console.log( a + b + c );
    }
    c = 3;
    bar();
}

var b = 2;

foo( 2 ); // ??

იმისათვის, რომ გავიგოთ რას დააბრუნებს კოდის ბოლო ხაზი, როგორც ზემოთ აღვნიშნე, საჭიროა ვიაზროვნოთ როგორ ძრავმა. ძრავის მუშაობა კი, შეიძლება ასეთი დიალოგით აღვწეროთ:

Engine: გამარჯობა გლობალურო Scope, RHS მითითება foo-ზე, ხომ არ გინახავს? Scope: კი, გლობალურ Scope-ში აღწერა ფუნქციად კომპილერმა.

აქ ძრავი გამოიძახებს foo ფუნქციას, მნიშვნელობა 2, მიენიჭება foo ფუნქციის ლოკალურ ცვლადს a, ძრავი მიადგება ბრძანებას c = 3

Engine: გამარჯობა foo-ს Scope, LHS მითითება მაქვს c-ზე, ხომ არ გინახავს? Scope: არა, მოძებნე. ამ დროს ძრავი 1-ით მაღალ განსაზღვრის არეში, ანუ გლობალურში გადავა Engine: გამარჯობა გლობალურო Scope, კიდევ ერთხელ. c ხომ არ გინახავს? Scope: არა

რადგან გლობალური არე ყველაზე “ზედაა” და მასშიც ვერ მოიძებნა c ცვლადი, ძრავი მას შექმნის და მნიშვნელობას მიანიჭებს. ძრავი მიადგება bar() ბრძანებას, მოძებნის foo-ს Scope-ში მას, შემდეგი შესრულდება console.log( a + b + c ); ბრძანება. მსგავსი დიალოგის მეშვეობით, ძრავი foo-ს განსაზღვრის არეში იპოვის c-ს, ხოლო გლობალურში a და b-ს, საბოლოოდ კი დაიბეჭდება 2+2+3=7.

scopes img

Scope-ებში ცვლადების ძებნა სართულებზე ბინის ძებნასავითაა, ვიწყებთ ქვედა სართულებიდან და ნელ-ნელა ზემოთ ავდივართ, სანამ არ ვიპოვით.

სინტაქსური შეცდომები, RHS vs LHS

განვიხილოთ კოდის მონაკვეთი:

function foo(a) {
    console.log( a + b );
    b = a;
}

foo( 2 );

მე-2 ხაზზე, ძრავი b-ს RHS ძებნას დაიწყებს, foo-ს არედან გადავა გლობალურ არეში, და როდესაც იქაც ვერ იპოვის b-ს, მოხდება ReferenceError.

თუმცა ამ შემთხვევაში:

function foo(a) {
    b = a;
}

foo( 2 );

მოხდება b-ს LHS ძებნა და მას შემდეგ, რაც ის არც გლობალურ Scope-ში აღმოჩნდება, ძრავი შექმნის გლობალურ b-ს და მასში ჩაწერს 2-ს.

შეჯამება:

Scope ანუ განსაზღვრის არე, არის წესების ერთობლიობა, რომელიც განაპირობებს სად და როგორ უნდა მოიძებნოს ცვლადი. ეს ძებნა შესაძლოა გვჭირდებოდეს იმისათვის, რომ მივანიჭოთ ცვლადს (LHS), ან მივიღოთ მისი მნიშვნელობა (RHS).

ჯავასკრიპტის ძრავი კოდს აკომპილირებს გაშვებამდე. ამიტომ მსგავსი ბრძანებები: var a=2 იყოფა შუაზე:

  • var a; კომპილერი ქმნის ცვლადს scope-ში
  • a=2; ძრავი ეძებს ცვლადს, (LHS) და ანიჭებს მას, თუ მოიძებნა.

ორივე ძებნის მეთოდი, (LHS, RHS) იწყებს ძებნას (current) Scope-დან და ადის ნელ-ნელა ზემოთ, ჩადგმულ Scope-ებში, ერთი Scope(სართული) ერთ ბიჯზე. სანამ გლობალურს არ მიაღწევს და გაჩერდება.

დაუკმაყოფილებელი(თუ ცვლადი ვერ მოიძებნა) RHS ძებნა, გამოიწვევს referenceError-ს, LHS კი - შექმნის ცვლადს გლობალურ განსაზღვრის არეში.

წყარო: You don’t know JS, Eloquent Javascript