Nguyên lý Javascript thực thi chương trình
Nguyễn Dương 20-06-2024Javascript là ngôn ngữ lập trình bậc cao, hướng
đối tượng, đa mô hình, có cơ chế quản lý bộ nhớ, được thông dịch hoặc biên dịch
JIT, có tính động, đơn luồng, có các hàm first-class và mô hình đồng thời
non-blocking event loop.
Mọi chương trình chạy trên máy tính của bạn đều
cần một số tài nguyên phần cứng, chẳng hạn như bộ nhớ và CPU. Các ngôn ngữ cấp
thấp, chẳng hạn như C thì chúng ta phải yêu cầu máy tính cung cấp bộ nhớ để tạo
một biến mới. Mặt khác, có các ngôn ngữ cấp cao như JavaScript và Python, nơi
chúng ta không phải quản lý tài nguyên vì những ngôn ngữ này có các phần trừu
tượng được gọi và loại bỏ tất cả công việc đó khỏi chúng ta. Điều này làm cho
ngôn ngữ bậc cao dễ học và dễ sử dụng hơn, nhưng nhược điểm là các chương trình
bậc cao sẽ không bao giờ nhanh hoặc được tối ưu hóa như chương trình C.
Cơ chế quản lý bộ nhớ (garbage collected) là một
thuật toán bên trong công cụ Javascript tự động loại bỏ các đối tượng cũ, đối
tượng lâu không sử dụng khỏi bộ nhớ máy tính. Nó giống như một công cụ dọn dẹp,
dọn sạch bộ nhớ theo thời gian.
Cơ chế thông dịch hoặc biên dịch JIT (biên dịch
trong khi chạy): Bộ xử lý của máy tính chỉ có thể hiểu 0 và 1. Vì vậy mọi
chương trình cần được viết bằng 0, 1 hay còn gọi là mã máy (machine code). Với
ngôn ngữ lập trình, cụ thể là Javascript, code được viết ra để con người có thể
hiểu được nên nó là một sự trừu tượng hóa dựa trên mã máy. Tuy nhiên code
Javascript vẫn phải được dịch sang mã máy để máy có thể hiểu được, quá trình
này gọi là thông dịch hoặc biên dịch.
- Paradigm là một cách tiếp cận và tư duy tổng
thể về cấu trúc code. Chúng ta sẽ định hướng phong cách và kỹ thuật viết code
trong một dự án dựa trên một mô hình nhất định. 3 mô hình lập trình phổ biến hiện
nay là:
+ Lập trình thủ tục - Procedural programming: Sắp
xếp code một cách tuyến tính, từ trên xuống dưới
+ Lập trình hướng đối tượng - Object oriented
programming OOP
+ Lập trình hướng hàm - Functional Programming
- Ngoài ra, có thể phân loại paradigm thành 2
loại: Imperative và Declarative.
- Hầu hết mọi thứ trong Javascript đều là đối
tượng ngoại trừ các giá trị nguyên thủy.
- Mảng cũng là một đối tượng, việc chúng ta có
thể gọi phương thức push() trên mảng là do kế thừa theo prototype. Prototype giống
như một template, array prototype chứa tất cả các phương thức của mảng.
Tính dynamic ở đây thực ra là dynamic-typed
(cho phép thay đổi kiểu biến tùy biến), trong Javascript chúng ta không chỉ định
được kiểu dữ liệu cho các biến, thay vào đó chúng chỉ được chỉ định khi JS thực
thi code. Ngoài ra kiểu dữ kiệu cũng dễ dàng thay đổi khi chúng ta gán lại dữ
liệu.
- JS là ngôn ngữ lập trình đơn luồng, có nghĩa
là chúng chỉ có thể làm từng việc một nên chúng cần có mô hình Concurrency
model.
- Concurrency model (mô hình đồng thời) là cách
mà công cụ Javascript xử lý nhiều tác vụ cùng một lúc.
- Với đơn luồng, nếu một nhiệm vụ kéo dài nó sẽ
chặn cả luồng đơn đang chạy, khi đó chúng ta cần trạng thái non-blocking (không
dừng).
- Event loop thực hiện nhiều tác vụ lâu dài và
thực thi nó ở background sau đó đưa chúng trở lại luồng chính sau khi chúng
hoàn thành.
Tóm lại JS là một mô hình đồng thời có
non-blocking event loop với một luồng duy nhất.
Javascript Engine
- Javascript Engine là một chương trình máy
tính thực thi code Javascript.
- Mỗi trình duyệt hiện nay đều có Javascript Engine, công cụ được biết đến nhiều nhất là Google V8.
- Bất kỳ JS engine nào cũng chứa callstack và
heap.
- CallStack là nơi code được thực thi bằng cách
sử dụng Execution Context.
- Heap là một vùng nhớ (memory pool) không có cấu
trúc, lưu trữ tất cả các đối tượng mà ứng dụng cần.
Phân biệt giữa thông dịch
và biên dịch
- Trong quá trình biên dịch, toàn bộ code được
chuyển đổi thành mã máy cùng một lúc, sau đó mã máy này được viết thành một
file di động có thể thực thi trên bất kỳ máy tính nào.
- Quá trình thông dịch chạy code và thực hiện từng
dòng một, code được đọc và thực thi tất cả cùng một lúc. Code javascript vẫn được
chuyển sang mã máy nhưng sau đó được thực thi luôn
JS kết hợp sử dụng cả
thông dịch và biên dịch
- JS đã từng là một ngôn ngữ thông dịch thuần
túy nhưng vấn đề của trình thông dịch là chúng chậm hơn nhiều lần so với biên dịch,
với web hiện đại ngày nay điều đó không được chấp nhận nữa. JS ngày nay kết hợp
vừa thông dịch vừa biên dịch được gọi là just-in-time compilation.
- JIT compiler hiểu cơ bản là biên dịch toàn bộ
code thành mã máy cùng một lúc sau đó thực thi nó ngay lập tức. Điều này khiến
JS thực thi nhanh hơn so với thông dịch trước đây.
Trình biên dịch
just-in-time
1. Phân tích cú pháp (Pharsing): Khi một đoạn
code JS đi vào JS Engine code sẽ được phân tích thành một cấu trúc dữ liệu được
gọi là cây cú pháp trừu tượng (Abstract Syntax Tree), bước này cũng kiểm tra
xem code có lỗi cú pháp nào không. Cây kết quả sẽ được sử dụng để tạo Mã máy.
Lưu ý: Cây cú pháp trừu tượng không liên quan
gì đến cây DOM.
2. Biên dịch (Compilation) lấy AST được tạo ra
và biên dịch nó thành mã máy.
3. Thực thi mã máy (Execution), việc thực thi
diễn ra trong Call Stack.
Sau bước 3 JS Engine đã tạo một phiên bản đầu
tiên, sau đó JS Engine thực hiện thêm một số chiến lược tối ưu hóa thực hiện
trong background và biên dịch lại trong quá trình thực thi chương trình đã chạy.
JS runtime
Javascript Runtime trong trình duyệt như một hộp,
thùng chứa lớn chứa tất cả những thứ cần thiết để sử dụng Javascript. Bao gồm:
- JS Engine (callstack và heap)
- Web APIs
- Callback Queue
Web APIs
- JS Runtime bao gồm Web APIs (các chức năng được
cung cấp cho engine).
- JS có quyền truy cập vào các API này thông
qua đối tượng global window.
Callback Queue
- Callback Queue là cấu trúc dữ liệu có chứa tất
cả các hàm callback sẵn sàng để thực thi.
Ví dụ: Hàm được truyền vào để xử lý sự kiện
trên một phần từ DOM cũng là một hàm callback.
- Sau khi một event xảy ra, hàm callback được đặt
vào Callback Queue, sau đó khi stack rỗng, hàm callback được đặt vào callstack
để nó thực thi, quá trình này xảy ra theo cơ chế event loop.
Event Loop
- Event Loop nhận các hàm callback từ Callback
Queue và đưa chúng vào Call Stack để thực thi
JS không chỉ chạy trên
trình duyệt
- Tồn tại nhiều kiểu JS Runtime khác nhau và JS
không chỉ chạy trên trình duyệt.
Định nghĩa this
- Biến this là một biến đặc biệt được tạo trong
mọi ngữ cảnh thực thi (Execution Context) và cho mọi function nào.
- This trỏ tới giá trị của chủ sở hữu của hàm
nơi mà this được gọi.
- This không phải static, nó phụ thuộc vào cách
mà hàm được gọi - giá trị của this chỉ được gán khi hàm thực sự được gọi.
Quá trình thực thi chi
tiết của 1 chương trình JS
Sau khi quá trình biên dịch hoàn tất, mã máy được
đưa vào quá trình thực thi:
1. Tạo ra global execution context (ngữ cảnh thực
thi toàn cục) - dành cho những code thuộc top-level (code thuộc top-level là những
code không nằm trong bất kỳ function nào)
- Execute context là ngữ cảnh thực thi định
nghĩa là môi trường thực thi đoạn code JS, nó giống như một chiếc hộp lưu giữ tất
cả các thông tin cần thiết để một code được thực thi như là các biến cục bộ hay
các đối số truyền vào.
- Code JS luôn chạy trong một Execution
Context.
- Trong bất kỳ project JS nào cũng chỉ có một
global execution context là context mặc định để thực thi các code thuộc top
level.
2. Thực thi top level code bên trong GEC
(Global Execution Context)
3. Thực thi các hàm và chờ callback
- Với mỗi hàm sẽ có một execution context.
- Các EC này kết hợp vs nhau tạo thành Call
Stack.
Bên trong Execution
Context bao gồm:
1. Variable Environment chứa các biến, khai báo
hàm và cũng có một object argument đặc biệt (chứa tất cả các đối số được truyền
vào hàm thuộc về EC hiện tại)
2. Scope Chain bao gồm các tham chiếu đến các
biến nằm ngoài hàm hiện tại
3. Từ khóa this
Lưu ý: EC thuộc về hàm mũi tên không lấy được
arguments và cũng không có this.
- Call Stack là nơi các Execution Context xếp
chồng lên nhau, EC ở trên cùng của stack là thứ hiện đang chạy và khi chạy xong
nó sẽ bị xóa khỏi stack.
- Việc thực thi diễn ra theo nguyên lý của
Stack - Last In First Out.