VAR, LET và CONST - Hoisting và Scope

Mở đầu

JavaScript, như mọi ngôn ngữ lập trình hiện đại khác, cung cấp cho chúng ta nhiều phương thức khai báo biến khác nhau. Javascript có các keywords var, letconst dùng cho việc khai báo biến và chúng được sử dụng trong các trường hợp khác nhau. Bài viết này được viết với mục đích đi sâu vào những chú ý và những điểm khác nhau mà bạn sẽ có thể gặp phải khi sử dụng các phương thức khai báo biến này.

Để việc theo dõi bài viết được dễ hơn một chút, chúng ta hãy xem thử định nghĩa của các keyword theo như ECMAMDN.

Var :

"Var keyword declares a variable which is scoped to its current execution context, optionally initializing to a value."

Keyword var khai báo một biến trong phạm vị context thực thi hiện tại, có thể cùng lúc khởi tạo nó với một giá trị.

Let :

"Let keyword declares a block scoped variable, optionally initializing it to a value."

Keyword let khai báo một biến trong phạm vi block, có thể cùng lúc khởi tạo nó với một giá trị.

Const :

"Const keyword declares constants which are block scoped much like variables defined using let but the value of a constant cannot change. The const declaration creates a read-only reference to a value."

Keyword const khai báo một hằng số trong phạm vị block (giống như khai báo biến sử dụng let nhưng giá trị của hằng số không thể thay đổi). Khai báo bằng const tạo một tham chiếu read-only tới một giá trị.


HOISTING

Về mặt ý tưởng, hoisting dùng để chỉ việc khai báo biến và hàm được chuyển lên bên trên cùng code của bạn. Về mặt kỹ thuật, điều đó không thực sự xảy ra mà việc khai báo biến và hàm sẽ được đưa vào bộ nhớ trong giai đoạn biên dịch (compile), còn trong code của bạn, vị trí của chúng vẫn ở chính xác nơi mà bạn viết chúng. Điểm quan trọng chính của hoisting là nó cho phép chúng ta có thể sử dụng hàm trước khi khai báo chúng trong code.

Bạn chỉ cần nhớ những điểm chính về hoisting như sau:

  • Thứ được di chuyển là việc khai báo biến và hàm. Việc gán giá trị hoặc khởi tạo biến không bao giờ được di chuyển cả.

  • Việc khai báo không thực sự được chuyển lên trên đầu code mà chính xác thì chúng được đưa vào bộ nhớ.

Trong JavaScript, tất cả các biến được khai báo bằng keyword var sẽ có giá trị ban đầu là undefined. Chúng có giá trị như vậy chính là do hoisting, việc khai báo các biến được đưa vào bộ nhớ và chúng được khởi tạo với giá trị là undefined. Chúng ta có thể thấy việc này qua ví dụ như sau. Ta có file UntitledDocument.js:

Chạy file này, ta sẽ được kết quả như sau:

Tuy nhiên, biến được khai báo bằng keyword let hoặc const khi hoisting ta sẽ thấy chúng không được khởi tạo với giá trị undefined. Thay vào đó, chúng ở trong một trạng thái gọi là Temporal Dead Zone, trạng thái này kéo dài từ khi vào scope cho đến khi khai báo xong, trong khoảng này chúng sẽ không thể truy xuất/tham chiếu tới được. Trạng thái TDZ kết thúc sau khi việc khai báo kết thúc.

Đoạn code tiếp theo sẽ thể hiện cho chúng ta thấy việc hoisting của các biến letconst:

Dòng thứ 3 sẽ báo ReferenceError vì x ở dòng 4 đã được hoist lên đầu ở trong block:

Biến x được định nghĩa bằng keyword let ở trong block sẽ được hoist và được ưu tiên hơn là biến x được định nghĩa bằng var. Tuy nhiên, nó vẫn ở trong Temporal Dead Zone khi được tham chiểu tới tại console.log(x) nên sẽ báo lỗi reference.


SCOPE

Các biến được định nghĩa với keyword var có phạm vị trong context thực thi hiện tại. Vì phạm vị của chúng không bị giới hạn trong block nên chúng có thể được truy cập từ bên ngoài block mà chúng được định nghĩa, miễn là vẫn trong phạm vi context thực thi của nó. Khái niệm này được thể hiện như dưới đây:

  • Lỗi ở dòng 9 do tham chiếu tới biến y ở ngoài scope của nó.

  • Lỗi ở dòng 11 do tham chiếu tới biến y ở ngoài scope của nó.

  • Lỗi ở dòng 12 do tham chiếu tới biến x ở ngoài scope của nó.

Thêm nữa, khi bạn khai báo nột biến global bằng keyword var, biến đó sẽ được gắn với một global context (context này là window với trình duyệt và global với node). Còn các biến khai báo bằng letconst thì không.


Lưu ý

  • Khi bạn không khai báo biến mà gán giá trị cho biến, biến sẽ được tạo ra và gắn với global context (window với trình duyệt và global với node). Tuy nhiên điều này là vô cùng không nên vì nó khiến cho việc debug trở nên rất khó khăn.

  • Các biến được khai báo với keyword var có thể được khai báo lại tại bất kì vị trí nào trong code, kể cả trong cùng một context. Các biến được định nghĩa bằng letconst thì không như vậy, chúng chỉ có thể được khai báo một lần trong scope của chúng.

Đặc biệt chú ý không sử dụng let hoặc const khai báo cũng một biến trong các case khác nhau khi sử dụng switch:

Tất nhiên là có thể tránh lỗi trên bằng cách thêm ngoặc nhọn quanh các case để tạo ra các block khác nhau, nhưng code khi đó chắc hẳn sẽ cần phải được refactor:

  • Một điểm nữa cần lưu ý về const đó là dù cho giá trị của chúng không thể bị gán lại nhưng nó vẫn có thể thay đổi được. Trường hợp này có thể thấy trong thực tế khi giá trị của const là một object, vì thuộc tính của object thì có thể sửa đổi được:


Vậy là qua bài viết trên, mình đã tổng hợp các định nghĩa và các điểm cần lưu ý khi sử dụng các phương thức khai báo biến khác nhau trong JavaScript, cảm ơn các bạn đã đọc bài viết 🙇