Tìm hiểu về Execution Context trong JavaScript

Trong bài viết trước mình có đề cập tới khái niệm Hoisting trong JavaScript, bài viết này mình sẽ đề cập tới khái niệm Execution Context, qua đó chúng ta sẽ hiểu hơn về cách thức hoạt động khi chạy code JS cũng như hiểu thêm tại sao có ra khái niệm Hoisting.

Khái niệm Execution Context

Khái niệm này chuyển sang tiếng Việt đọc khá là ngang, vì thế nên mình giữ nguyên bản là tiếng Anh.
Khi code được thực thi, môi trường thực hiện hiện đoạn code đó vô cùng quan trọng, nó có thể nằm trong 3 môi trường sau :

  • Global code : Môi trường mặc định, nơi code được thực thi lần đầu.
  • Function code : Môi trường trong function, khi luồng đi của code chạy trong 1 function nào đó.
  • Eval code : Môi trường khi code được chạy trong hàm eval().

Dưới đây là một ví dụ cụ thể :

alt

Như hình trên, ta có một global context với viền màu tím, 3 function contexts. Sẽ chỉ có duy nhất 1 global context, và nó có thể được truy cập sử dụng từ bất cứ context nào trong chương trình cùng source code.
Mỗi function được thư thi sẽ sinh ra 1 context mới, đó sẽ là nơi mọi khai báo bên trong đó sẽ không thể được gọi trực tiếp từ bên ngoài. Như ví dụ trên hình, context bên trong có thể sử dụng biến của context bao nó, nhưng ngược lại thì context cha không thể sử dụng trực tiếp được biến khai báo trong context mà nó chứa. Lý do là Execution Context Stack.

Execution Context Stack

Trình thông dịch JavaScript trên trình duyệt chỉ sử dụng một luồng, nghĩa là mọi việc sẽ được thực hiện từng cái một, các công việc cần thực hiện sẽ được lưu vào stack gọi là Execution Stack.
Khi trình duyệt load code JS, global execution context sẽ được truy cập đầu tiên nên global execution context sẽ luôn ở cuối trong Context Stack, nếu có function nào được gọi, execution context sẽ được sinh ra và push vào top stack. Giả sử trong function vừa gọi xuất hiện thêm các function khác thì các execution context cũng sẽ được sinh ra và tiếp tục được push vào top stack. Trình duyệt luôn luôn khởi chạy execution context ở trên cùng của stack, sau khi chạy xong, nó sẽ bị đẩy khỏi stack và trao quyền chạy cho execution context tiếp theo ngay bên dưới nó. Đó chính là lý do các execution context con có thể truy cập sử dụng các biến của execution context chứa nó, và các execution context cha sẽ không thể truy cập trực tiếp dữ liều của các execution context con.
5 đặc điểm của execution stack cần nắm :

  • Xử lý đơn luồng.
  • Xử lý đồng bộ.
  • Có duy nhất 1 Global context.
  • Vô hạn function contexts.
  • Mỗi function khi được gọi sẽ sinh ra 1 execution context tương ứng, ngay cả khi nó gọi đến chính nó.

Chi tiết hơn về Execution Context

Hiện tại ta đã nắm chắc mỗi function được gọi, sẽ sinh ra execution context tương ứng. Mỗi execution context được xử lý sẽ có 2 giai đoạn :

  • Khởi tạo : Đây là khi function được gọi, nhưng chưa có bất kỳ đoạn code nào trong function được chạy.
  1. Khởi tạo Scope Chain.
  2. Tạo variable object. Đầu tiên arguments object sẽ được tạo bằng cách check nội dung các tham số, khởi tạo tên và giá trị để sao chép. Sau đó sẽ tiếp tục quét qua từng function được khai báo, mỗi function được quét qua sẽ tạo một thuộc tính với tên chính là tên của function đó vào variable object, giá trị sẽ là con trỏ của function đó. Nếu tên của thuộc tính đã tồn tại, giá trị con trỏ sẽ bị ghi đè thành giá trị mới nhất. Tương tự như vậy với các biến được khai báo, tuy nhiên giá trị của mỗi thuộc tính tạo ra sẽ là undefined và giả sử tên thuộc tính đã tồn tại thì sẽ bỏ qua, không làm gì thuộc tính đã tồn tại đó và tiếp tục quét các biến khác. Cuối là là bước xác định giá trị cho biến đặc biệt this.
  • Khởi chạy : Bước này sẽ gán giá trị cho các biến khởi tạo từ trước và thực thi code.

Để dễ hình dung, đây sẽ là mô hình của Object sau quá trình khởi tạo:

executionContextObj = {
    scopeChain: { /* variableObject + all parent execution context's variableObject */ },
    variableObject: { /* function arguments / parameters, inner variable and function declarations */ },
    this: {}
}

Ví dụ với đoạn code :

function foo(i) {
  var a = "hello";
  var b = function privateB() {};
  function c() {}
}

foo(22);

Khi foo(22) được chạy, ta sẽ có :

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

Sau khi function được chạy xong, ta sẽ có:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

Vậy là ta đã nhận ra tại sao xuất hiện khái niệm hoisting.

Kết luận

Tìm hiểu sau về chi tiết cách thức hoạt động của JavaScript nói chung và Execution Context nói riêng giúp chúng ta nhìn nhận được hoạt động bên trong khi chạy code, tránh việc kết quả đầu ra không như mong muốn cũng như debug được chương trình một cách khoa học hơn là chỉ nhìn nhận từng dòng code bên ngoài.
Bài viết dựa trên việc đọc hiểu một bài viết khác của David Shariff, do kiến thức con hạn chế nên có thể dẫn tới việc hiểu sai một vài vấn đề, mong bạn đọc góp ý để mình hoàn thiện kiến thức. Cám ơn !
Nguồn : What is the Execution Context & Stack in JavaScript?