12/08/2018, 16:43

Javascript - What is scope? - Do you really understand about it?

I. Introduce Một trong những mô hình cơ bản nhất trong hầu hết các ngôn ngữ lập trình là khả năng lưu trữ values của biến, và sau đó lấy lại hoặc xác định các giá trị này. Nếu không có khái niệm này, một chương trình vẫn có thể thực hiện một vài công việc, nhưng chúng sẽ rất là hạn chế và ...

I. Introduce

  • Một trong những mô hình cơ bản nhất trong hầu hết các ngôn ngữ lập trình là khả năng lưu trữ values của biến, và sau đó lấy lại hoặc xác định các giá trị này.
  • Nếu không có khái niệm này, một chương trình vẫn có thể thực hiện một vài công việc, nhưng chúng sẽ rất là hạn chế và không được thú vị cho lắm. Việc đưa biến vào trong chương trình của chúng ta gây ra một số câu hỏi như là:
    • Những biến này được sống ở đâu?
    • Và quan trọng hơn cả là cách mà chương trình của chúng ta tìm ra chúng khi nó cần chúng?
  • Những câu hỏi này nói lên sự cần thiết của các quy tắc được xác định rõ ràng để lưu trữ các biến ở một số vị trí, và để tìm các biến đó vào một thời điểm sau. Chúng ta sẽ gọi bộ quy tắc đó là: Scope

II. Compiler Theory

  • Javacript Engine thực hiện rất nhiều bước giống nhau, theo cách phức tạp và tinh vi hơn chúng ta có thể nhận thức được của bất kỳ ngôn ngữ biên dịch truyền thống nào.
  • Trong tiến trình ngôn ngữ biên dịch truyền thống, một mảnh của source code sẽ trải qua 3 bước trước khi nó được xử lý, gọi là "compilation"
    1. Tokenizing/Lexing: bẻ gãy các ký tự của một string thành các các mảnh có nghĩa, gọi là tokens. Ví dụ, xem xét chương trình:
      var a = 2;
      
      Chương trình này sẽ được bẻ gãy thành các mảnh: var, a, =;. Khoảng trắng có thể là token, cũng có thể không tùy vào việc nó có nghĩa hay không.
    2. Parsing: lấy một mảng các tokens và biến đổi nó thành một tree các phần tử lồng nhau, nó đại diện cho cấu trúc của chương trình. Cây này được gọi là "AST" (Abstract Syntax Tree). Tree for var a = 2; có thể bắt đầu với một node top-level gọi là VariableDeclaration, với một node con gọi là Identifier (giá trị của nó là a), và một node con khác là AssignmentExpression, bản thân nó có một node con là NumericLiteral (giá trị của nó là 2). Ví dụ tree của một statement 2 * 7 + 3
    3. Code-Generation: tiến trình lấy AST và biến nó thành mã code có khả năng xử lý. Phần này rất khác nhau tùy thuộc vào ngôn ngữ, nền tảng mà nó nhắm đến, etc. Note: Chi tiết cách mà engine này quản lý tài nguyên hệ thống là sâu hơn rất nhiều chúng ta có thể đào bới, vì vậy chúng ta sẽ chỉ lấy nó để thừa nhận rằng nó có khả năng tạo mới và lưu trữ biến nếu cần.

III. Understanding Scope

Cách chúng ta sẽ tiếp cận scope là tìm hiểu về quá trình của một cuộc trò chuyện giữa chúng. Nhưng, ai đang trong cuộc trò chuyện?

  1. The cast a. Engine: chịu trách nhiệm start-to-finish compilation và xử lý chương trình Javascript b. Compiler: một người bạn của Engine, xử lý tất cả những công việc bẩn thỉu trong quá trình parsing và code-generation c. Scope: một người bạn khác của Engine, tổng hợp và duy trì một danh sách tra cứu các biến, thi hành một bộ quy tắc nghiêm ngặt để xử lý code.

  2. Back & Forth Khi bạn nhìn vào chương trình var a = 2; hầu hết bạn sẽ nghĩ đó chỉ là một statement. Nhưng đó không phải là cách mà Engine nhìn. Trên thực tế, Engine sẽ nhìn thành 2 statement riêng biệt, 1 là cái mà Compiler xử lý trong quá trình compilation, và một là cái mà Engine sẽ xử lý trong quá trình excution. Vì vậy, hãy chia nhỏ cách Engine và bạn bè của nó sẽ tiếp cận chương trình var a = 2; Điều đầu tiên mà Compiler sẽ làm với chương trình này là thực hiện lexing để bẻ gãy nó thành các tokens sau đó nó sẽ phân tích các tokens này thành 1 tree, và tiếp tục thực hiện như sau:
    1. Gặp var a, Compiler sẽ hỏi Scope nếu a đã tồn tại trong một tập phạm vi riêng biệt thì nó sẽ bỏ qua việc khai báo và đi tiếp, ngược lại nó sẽ yêu cầu Scope khai báo một biến a. 2. Compiler sau đó sẽ sản xuất code cho Engine để xử lý a = 2 assignment. Đoạn code Engine chạy đầu tiên sẽ hỏi Scope nếu có một biến gọi là a có khả năng truy cập trong scope hiện tại, nếu có nó sẽ xử dụng biến này, nếu không, nó sẽ tìm ở một nơi khác. Nếu Engine tìm thấy một biến, nó sẽ gán giá trị 2 cho nó. Nếu không, Engine sẽ raise lên một lỗi.

  3. Compiler Speak Trong trường hợp này, Engine sẽ thực hiện một LHS(Left-hand Side) look-up cho biến a. Một loại khác của look-up là RHS(Right-hand Side). Việc tra cứu LHS được thực hiện khi một biến xuất hiện ở phía bên tay trái của một assignment, và một RHS look-up được thực hiện khi một biến xuất hiện ở phía bên tay phải của một assignment. Thực tế, một RHS look-up là rất khó để phân biệt, từ cái nhìn đơn giản là một look-up các giá trị của một vài biến, trong khi LHS đang cố gắng để tìm biến chứa bản thân nó, để nó có thể assign giá trị. Trong trường hợp này, RHS không thực sự có nghĩa là "right-hand side of an assignment", mà đơn giản chỉ có nghĩa là "not left-hand side". Chúng ta sẽ đào sâu hơn để hiểu về nó. Khi ta nói:

    console.log(a)
    

    tham chiếu tới a là một RHS , bởi vì không có gì được assign cho a ở đây, thay vào đó chúng ta sẽ tra cứu để tìm giá trị của a. Tương phản:

    a = 2;
    

    tham chiếu tới a ở đây là một LHS, bởi vì chúng ta không quan tâm đến giá trị hiện tại của a, chúng ta chỉ đơn giản tìm biến đó cho = 2 assignment. Xem xét chương trình này, nó sẽ bao gồm cả LHSRHS

    function foo(a) { console.log(a) };
    foo( 2 );
    

    Dòng cuối cùng gọi foo(...) như một function yêu cầu gọi một RHS tham chiếu tới foo và tìm giá trị của nó. Xa hơn, (..) nghĩa là giá trị của foo nên được xử lý. Ngoài ra còn một LHS khi assign ngầm giá trị 2 cho a, một RHS tham chiếu tới a khi pass đến console.log(..). console.log(..) cũng cần một tham chiếu để xử lý, đó là một RHS cho console object để call method log. Note: Bạn có thể khái niệm hóa khai báo function function foo(a) {... như một khai báo biến và assign giá trị như bình thường, chẳng hạn var foo và foo = function(a) {... => sẽ cần một LHS look-up.

  4. Engine/Scope Conversation

     function foo(a) { console.log(a) };
     foo( 2 );
    

    Hãy tưởng tượng sự trao đổi ở trên như một cuộc trò chuyện và nó sẽ đại loại như sau:

IV. Nested Scope

Chúng ta nói rằng Scope là một tập các quy tắc cho việc tìm biến bằng tên của chúng. Tuy nhiên chúng ta có nhiều hơn 1 Scope để xem xét. Nếu một biến không thể được tìm thấy ngay lập tức trong scope này, Engine sẽ hỏi bên ngoài scope này và tiếp tục tìm hoặc cho đến khi ra đến scope ngoài cùng là global.
Xem xét:

     function foo(a) {
         console.log( a + b );
     }; 
     var b = 2;
     foo( 2 ); // 4

Tham chiếu RHS cho b sẽ không thể giải quyết trong function foo, nhưng nó có thể được giải quyết trong Scope xung quanh nó (ở đây là global). Và đây là convention giữa Engine và Scope.

V. Building on Metaphors

Hình ảnh này biểu diễn tập quy tắc nested Scope. Tầng đầu tiên là biểu diễn scope đang xử lý hiện tại. Tầng trên cùng luôn luôn là global scope. Bạn giải quyết tham chiếu LHS và RHS bằng cách nhìn vào tầng hiện tại của bạn, và nếu bạn không tìm thấy, hãy đưa thang máy lên tầng kế tiếp tìm ở đó, tiếp tục tấng kế tiếp nếu không tìm thấy cho đến khi đến tầng cuối cùng.

VI. Errors

Xem xét:

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

Khi RHS look-up đến với b lần đầu tiên, nó sẽ không được tìm thấy. Đây được gọi là biến "undeclared", bởi vì nó không được tìm thấy trong scope. Nếu một RHS look-up không thể tìm thấy một biến, bất cứ nơi đâu trong các nested Scope, kết quả là một ReferenceError được ném ra bởi Engine. Hoặc, nếu một biến được tìm thấy bởi RHS look-up nhưng bạn cố tình gán một vài giá trị mà nó không có khả năng lưu trữ, Engine sẽ raise ra 1 lỗi khác gọi là TypeError.

0