12/08/2018, 13:06

Tìm hiểu về Javascript Hoisting

Hoisting là một khái niệm khá hay trong JavaScript tuy nhiên lại ít được mọi người để ý do nó có đôi chút phức tạp cũng như mọi người ít quan tâm đến cách thức hoạt động bên trong ngôn ngữ lập trình. Trong bài viết này mình, mình sẽ chỉ ra khái niệm cũng như cách thức hoạt động bên trong. Do kiến ...

Hoisting là một khái niệm khá hay trong JavaScript tuy nhiên lại ít được mọi người để ý do nó có đôi chút phức tạp cũng như mọi người ít quan tâm đến cách thức hoạt động bên trong ngôn ngữ lập trình.
Trong bài viết này mình, mình sẽ chỉ ra khái niệm cũng như cách thức hoạt động bên trong. Do kiến thức còn hạn chế nhiều nên sẽ có nhiều lỗi, mong các bạn góp ý để mình hoàn thiện kiến thức.

JavaScript has function-level scope

Hãy xem đoạn code bằng C sau

#include <stdio.h>
int main() {
	int x = 1;
    printf("%d, ", x); // 1
    if (1) {
    	int x = 2;
        printf("%d, ", x); // 2
    }
    printf("%d
", x); // 1
}

Kết quả nhận được lần lượt là 1, 2, 1 bới vì họ hàng nhà C có thuộc tính block-level scope, vì thế trong đoạn lệnh if, một scope mới được tạo ra, biến x được tạo ra trong scope mới mà không ảnh hưởng tới biến x nằm ở scope bên ngoài.
Tuy nhiên, với JavaScript :

var x = 1;
console.log(x); // 1
if (true) {
	var x = 2;
    console.log(x); // 2
}
console.log(x); // 2

Kết quả nhận được lần lượt là 1, 2, 2 bởi vì JavaScript chỉ có function-level scope (hay không có block-level scope ), trong đoạn lệnh if sẽ không được tạo scope mới mà sẽ dùng chung với scope bên ngoài, khiến cho biến x bị thay đổi.

JavaScript Compilation

Được xử lý bởi JavaScript engine, bao gồm 3 bước :

  • Tokenizing/Lexing
  • Parsing
  • Code-Generation

Cụ thể ở bước này mình sẽ viết trong bài riêng sắp tới.
Trong bước này, bạn có thể hiểu thông qua ví dụ :

var foo = "bar";
function bar() {
	var foo = "bar";
}
  • Yêu cầu Global scope khai báo biến có tên là foo.
  • Yêu cầu Global scope khai báo hàm có tên là bar.
  • Yêu cầu bar scope khai báo biến có tên là foo.

JavaScript Execution

Có 2 khái niệm cần biến ở bước này, đó là :

  • LHS : Left hand side.
  • RHS : Right hand side.

Hiểu đơn giản, gọi là LHS reference nếu như ta có biến nằm ở bên trái ký tự gán = và ngược lại với RHS reference (Nếu không có LHS reference thì ta coi đó là RHS reference). Một cách khác phân biệt LHS và RHS là RHS is target, RHS is source.

Ta có thể xem cuộc đối thoại vui giữa Engine và Scope khi thực hiện đoạn code var foo = "bar"; như sau :

  • Engine : Hey Global scope, I have an LHS reference for a variable named foo. Ever heard of it?
  • Scope : The global scope has because foo was registered on line 1 in the compilation phase, so the assignment occurs.

JavaScript Compilation

Ở trên bước Execution ta có nhắc tới Compilation, đây cũng chính là bước sinh ra khái niệm Hoisting ta cần tìm hiểu.
Về cơ bản, Hosting là thao tác chuyển tất cả các khai báo biến lên trên cùng của hàm. Xem qua ví dụ dưới đây để hiểu rõ :
Code được viết :

console.log(a);
var a = 123;
console.log(a);

Mặc dù console.log(a); được viết trước khi a được khai báo nhưng ta không hề bị ReferenceError. Lý do chính là Hoisting, đoạn code sẽ trở thành như sau trong quá trình Compilation:

var a;
console.log(a);
a = 123;
console.log(a);

Nên nhớ, Hoisting chỉ chuyển việc khai báo các biến lên trên cùng, còn các phép gán vẫn giữ nguyên vị trí. Vậy nên ta thấy đầu ra của đoạn code trên lần lượt là undefined và 123.
Chú ý rằng việc khai báo hàm và biến luôn luôn sử dụng Hoisting, hơn nữa JavaScript không có block-level scope khiến cho khái niệm Hoisting trở nên nguy hiểm nếu ta không hiểu rõ và khái báo bừa bãi. Đây là một ví dụ :

var foo = 10;
function bar() {
	if (!foo) {
    	var foo = 100;
    }
    console.log(foo);
}
bar();

Bạn thử đoán xem kết quả hiện ra là bao nhiêu ? Là 100 ! Tại sao câu lệnh trong câu điều kiện if lại được thực hiện? Rõ ràng foo = 10 => !foo == false mà.
Như ta đã biết, các khai báo biến sẽ được đưa lên trên cùng và JavaScript không phải block-level scope, vì thế sẽ xảy ra hoisting cho var foo = 100; trong scope tạo bởi function bar() nên xảy ra việc giá trị của foo khi thực hiện if (!foo) sẽ là undefined thay vì 10, vậy nên !foo == true và foo = 100 được thực hiện, kết quả in ra ngoài sẽ là 100.

Đây chỉ là một ví dụ vô cùng đơn giản để cho chúng ta thấy sự nguy hiểm của Hoisting.

Kết luận

Thực tế khi dự án lớn với việc khai báo hàm và biến vô cùng nhiều, việc xuất hiện bug là không khó xảy ra. Những bug dạng này lại càng khó để phát hiện và xử lý, vì vậy ta nên hiểu rõ các khái niệm, luồng xử lý cũng như luôn khai báo hàm/biến ở trên cùng thay vì để chúng nó tự Hoisting.

0