Từ khóa "let", "const" và "var" trong JavaScript ES6

Từ khóa "let", "const" và "var" trong JavaScript ES6


Trước khi phiên bản JavaScript ES6 (ECMAScript 2015) được giới thiệu, chúng ta chỉ có một từ khóa để khai báo biến, đó là "var". Tuy nhiên, ES6 đã giới thiệu hai từ khóa mới, "let""const", để cung cấp các phương thức khai báo biến linh hoạt hơn trong JavaScript. Trong bài viết này, chúng ta sẽ tìm hiểu về từ khóa "let", "const""var" trong ES6 và điểm khác biệt giữa chúng.

Nếu bạn mới học và sử dụng Javascript thì có một vài điều bạn sẽ từng nghe là:
  • "var""let" có thể gán lại giá trị khác.
  • "const" không thể gán lại giá trị.
  • Developer không nên sử dụng "var" nữa. Thay vào đó là "let" "const".
Tính chất của "var", "let" "const" thì không có gì để bàn cãi. Vậy tại sao lại không nên sử dụng "var" hay khi nào nên sử dụng "let""const"?. Hi vọng bài viết này sẽ có ý nghĩa với bạn.

"var", "let" và "const". Sự khác nhau là gì?

Để phân tích sự khác nhau giữa chúng thì mình sẽ sử dụng 3 yếu tố:
  • Phạm vi truy cập (scope).
  • Tính khai báo lại, gán lại giá trị (redeclaration and reassignment).
  • Hoisting.
Hãy bắt đầu với từ khóa "var".

Var

"var" là từ khóa truyền thống để khai báo biến trong JavaScript trước ES6. Biến khai báo bằng "var" có phạm vi toàn cục (global scope) hoặc phạm vi hàm (function scope) tùy thuộc vào việc khai báo biến xảy ra ở đâu. Điều này có nghĩa là biến khai báo bằng "var" có thể truy cập từ bất kỳ đâu trong phạm vi toàn cục hoặc phạm vi hàm, dù sau khi khai báo.

Tính chất

Biến được khai báo bằng "var" có phạm vi toàn cục hoặc phạm vi hàm (function scope). Nghĩa là biến có thể truy cập từ bất kỳ đâu trong phạm vi toàn cục hoặc phạm vi hàm mà nó được khai báo.

Ví dụ 1: 

Biến number có phạm vi global – nó được khai báo các hàm bên ngoài trong phạm vi global – vì vậy bạn có thể truy cập nó ở mọi nơi (bên trong và bên ngoài các hàm).

var number = 50

function print() {
  var square = number * number
  console.log(square)
}

console.log(number) // 50

print() // 2500

Ví dụ 2: 

Ở đây, chúng ta đã khai báo biến number trong hàm print, vì vậy nó có phạm vi là local. Điều này có nghĩa là biến chỉ có thể được truy cập bên trong hàm đó. Mọi cố gắng truy cập vào biến bên ngoài hàm nơi nó được khai báo sẽ dẫn đến lỗi tham chiếu đến biến không được xác định (a variable is not defined reference error).

function print() {
  var number = 50
  var square = number * number
  console.log(square)
}

print() // 2500

console.log(number)
// ReferenceError: number is not defined

Khai báo, gán lại giá trị (redeclare and reassign)

Bạn có thể gán lại giá trị mới cho biến "var" bất cứ lúc nào trong phạm vi của nó.

Ví dụ: 

var number

console.log(number)
// undefined

Biến number được khai báo với từ khóa là "var" và không được gán giá trị. Giá trị mặc định của biến number sẽ là undefined.

var number = 50

Bây giờ thì giá trị khởi tạo của biến number sẽ là 50.

Từ khóa "var" cho phép chúng ta khai báo lại ở bất kỳ đâu trong phạm vi của nó. Hãy xem ví dụ sau đây:

var number = 50
console.log(number) // 50

var number = 100
console.log(number) // 100

Bạn có thể gán lại giá trị khi đã khởi tạo biến bằng từ khóa "var".

var number = 50
console.log(number) // 50

number = 100
console.log(number) // 100

number = 200
console.log(number) // 200

Hoisting

Các biến được khai báo với "var" hoisted lên đầu phạm vi hàm hoặc global của chúng, giúp chúng có thể truy cập được trước dòng chúng được khai báo.

Ví dụ:

console.log(number) // undefined

var number = 50

console.log(number) // 50

Biến number được khai báo ở phạm vi toàn cục (global) nên sẽ hoisted lên đầu phạm vi. Nghĩa là chúng ta có thể truy cập biến number trước dòng mà chúng ta khai báo nó mà không bị lỗi.
Nhưng khi biến hoisted sẽ được gán giá trị mặc định là undefined.

Hãy xem ví dụ dưới đây:

function print() {
  var square1 = number * number
  console.log(square1)

  var number = 50

  var square2 = number * number
  console.log(square2)
}

print()
// NaN
// 2500

Trong hàm print ở trên, biến number được khai báo trong phạm vi hàm và hoisted lên đầu phạm vi hàm đó. Chúng ta có thể truy cập được biến number trước dòng khai báo.

Như vậy, chúng ta thấy trong square1, chúng ta gán number number . Vì số được hoisted  với giá trị mặc định là undefined, nên hình square1 sẽ undefined * undefined nên không xác định dẫn đến NaN.

Sau khi dòng khai báo có giá trị ban đầu được thực thi, number sẽ có giá trị là 50. Vậy trong square2 là  number * number sẽ là 50 * 50, kết quả là 2500.

Let

"let" là từ khóa ES6 để khai báo biến có phạm vi khối (block scope). Biến được khai báo bằng "let" chỉ có thể truy cập được trong phạm vi khối bao quanh nó (ví dụ: trong một khối if, vòng lặp, hoặc một khối mã nguồn khác). Điều này giúp giảm thiểu xung đột và nhầm lẫn trong việc sử dụng biến. Bên cạnh đó, bạn có thể gán lại giá trị cho biến được khai báo bằng "let".

Tính chất

Biến được khai báo bằng "let" có phạm vi khối (block scope). Nghĩa là biến chỉ có thể truy cập được trong phạm vi khối bao quanh nó (ví dụ: trong một khối if, vòng lặp, hoặc một khối mã nguồn khác). 

Hãy xem ví dụ bên dưới:

let number = 50

function print() {
  let square = number * number

  if (number < 60) {
    var largerNumber = 80
    let anotherLargerNumber = 100

    console.log(square)
  }

  console.log(largerNumber)
  console.log(anotherLargerNumber)
}

print()
// 2500
// 80
// ReferenceError: anotherLargerNumber is not defined

Trong ví dụ này, chúng ta có một biến phạm vi global là number và một biến phạm vi local là square. Ngoài ra còn có biến phạm vi block là otherLargerNumber vì nó được khai báo bằng let trong một khối.

Mặt khác, largeNumber - mặc dù được khai báo trong một khối tuy nhiên nó được khai báo bằng từ khóa "var" nên nó hoisted lên đầu phạm vi hàm print và  được truy xuất ở mọi nơi trong phạm vi hàm đó. Vì vậy, largeNumber có phạm vi cục bộ (phạm vi hàm) như được khai báo trong hàm print.

Chúng ta có thể truy cập number ở khắp mọi nơi. Chúng ta chỉ có thể truy cập square và largeNumber trong hàm vì chúng có phạm vi cục bộ (phạm vi hàm). Nhưng việc truy cập AnotherLargerNumber bên ngoài khối sẽ gây ra lỗi anotherLargerNumber is not defined.

Khai báo, gán lại giá trị (redeclare and reassign)

Giống như "var", biến được khai báo bằng từ khóa "let" có thể được gán lại một giá trị khác nhưng không thể khai báo lại.

let number = 50
console.log(number) // 50

number = 100
console.log(number) // 100

Ở đây, mình đã gán lại một giá trị khác 100 sau khi khai báo ban đầu là 50.

Nhưng khai báo lại một biến với let sẽ gây ra lỗi:

let number = 50

let number = 100
// SyntaxError: Identifier 'number' has already been declared

Chúng ta sẽ gặp lỗi như sau: Identifier 'number' has already been declared.

Hoisting

Các biến được khai báo bằng "let" được hoist lên đầu phạm vi global, local hoặc block của chúng, nhưng cách hoist của chúng hơi khác so với biến được khai báo bằng từ khóa "var".

Các biến "var" được nâng lên với giá trị mặc định là undefined, giúp chúng có thể truy cập được trước dòng khai báo của chúng (như chúng ta đã thấy ở trên).

Tuy nhiên, các biến khai báo bằng "let" được hoist lên mà không cần khởi tạo mặc định. Vì vậy, khi bạn cố gắng truy cập các biến như vậy, thay vì gặp lỗi undefined hoặc variable is not defined, bạn sẽ nhận được lỗi cannot access variable before initialization. Hãy xem một ví dụ:

console.log(number)
// ReferenceError: Cannot access 'number' before initialization

let number = 50

Ở đây, chúng ta có một biến global, number được khai báo bằng "let". Bằng cách cố gắng truy cập biến này trước dòng khai báo, chúng ta nhận được lỗi ReferenceError: Cannot access variable before initialization.

Đây là một ví dụ khác với biến phạm vi local:

function print() {
  let square = number * number

  let number = 50
}

print()
// ReferenceError: Cannot access 'number' before initialization

Ở đây, chúng ta có một biến local (phạm vi hàm), number được khai báo bằng "let". Bằng cách cố gắng truy cập biến này trước dòng khai báo, chúng ta cũng nhận được lỗi ReferenceError: Cannot access variable before initialization.

Const

"const" cũng là một từ khóa ES6 để khai báo biến, nhưng khác với "let", biến được khai báo bằng "const" là hằng số (constant). Một hằng số không thể gán lại giá trị mới sau khi đã được khởi tạo. Điều này đảm bảo rằng giá trị của hằng số không thay đổi trong quá trình thực thi chương trình. Một hằng số cũng có phạm vi khối (block scope) như "let".

Tính chất

Biến được khai báo bằng "const" cũng có phạm vi khối (block scope) tương tự như "let". Tuy nhiên, biến được khai báo bằng "const" là hằng số (constant) và không thể gán lại giá trị mới sau khi đã được khởi tạo.

Các biến được khai báo với const tương tự như let về phạm vi. Các biến như vậy có thể có phạm vi global, local hoặc block.

Dưới đây là ví dụ:

const number = 50

function print() {
  const square = number * number

  if (number < 60) {
    var largerNumber = 80
    const anotherLargerNumber = 100

    console.log(square)
  }

  console.log(largerNumber)
  console.log(anotherLargerNumber)
}

print()
// 2500
// 80
// ReferenceError: anotherLargerNumber is not defined

Đây là từ ví dụ trước của chúng ta, nhưng mình đã thay let bằng const. Như bạn có thể thấy ở đây, biến number có phạm vi global, biến square có phạm vi local (được khai báo trong hàm print) và otherLargeNumber có phạm vi block (được khai báo bằng const).

Ngoài ra còn có largeNumber, được khai báo trong một block. Nhưng vì nó được khai báo với từ khóa var nên biến chỉ có phạm vi cục bộ (phạm vi hàm). Do đó, nó có thể được truy cập bên ngoài block (if).

Bởi vì AnotherLargeNumber có phạm vi là block, nên việc truy cập nó bên ngoài blokc sẽ xảy ra lỗi anotherLargerNumber is not defined.

Khai báo, gán lại giá trị (redeclare and reassign)

Về điểm này, const khác với var let. const được sử dụng để khai báo các biến hằng – là các biến có giá trị không thể thay đổi. Vì vậy, các biến như vậy không thể được khai báo lại và chúng cũng không thể được gán lại cho các giá trị khác. Khi cố tình hay vô tình gán lại giá trị thì sẽ xảy ra lỗi.

Hãy xem một ví dụ với khai báo lại:

const number = 50

const number = 100

// SyntaxError: Identifier 'number' has already been declared

Tại đây, bạn có thể thấy lỗi cú pháp SyntaxError: Identifier 'number' has already been declared.

Bây giờ, hãy xem một ví dụ với việc gán lại:

const number = 50

number = 100

// TypeError: Assignment to constant variable

Tại đây, bạn có thể thấy lỗi TypeError: Assignment to constant variable.

Hoisting

Các biến được khai báo với const, giống như let, được hoist lên đầu phạm vi global, local hoặc block của chúng – nhưng không có khởi tạo giá trị mặc định.

Các biến var, như bạn đã thấy trước đó, được hoist lên với giá trị mặc định là undefined để có thể truy xuất chúng trước khi khai báo mà không gặp lỗi. Truy cập một biến được khai báo với const trước dòng khai báo sẽ đưa ra lỗi cannot access variable before initialization.

Hãy xem một ví dụ:

console.log(number)
// ReferenceError: Cannot access 'number' before initialization

const number = 50

Ở đây, số là một biến có phạm vi global được khai báo bằng const. Bằng cách cố gắng truy cập biến này trước dòng khai báo, chúng ta sẽ nhận được ReferenceError: Cannot access 'number' before initialization. Điều tương tự cũng sẽ xảy ra nếu nó là một biến có phạm vi local.

Tóm lại

Dưới đây là hình ảnh tóm tắt nội dung:


Trong bài viết này đã giải thích rõ các yêu tố đóng vai trò quyết đinh trong việc khai báo các biến trong Javascript.

Hành vi hoisting có thể gây ra các lỗi không mong muốn trong ứng dụng của bạn. Đó là lý do tại sao các developer thường được khuyên nên tránh sử dụng biến var và nên sử dụng với let const.

Lưu ý khi khai báo biến trong Javascript

Trong Javascript có 4 cách để khai báo một biến:
  • Automatically
  • Sử dụng var
  • Sử dụng let
  • Sử dụng const
Các cách sử dụng từ khóa như var, let và const thì các bạn đã biết ở trên. Vậy automatically declared là như thế nào?

Nghĩa là chúng ta có thể sử dụng biến đó trong lần đầu mà không cần khai báo.

Ví dụ:

x = 5;
y = 6;
z = x + y;
console.log(z); // 11

Có thể thấy, mình không hề khai báo các biến x, y, z bằng các từ khóa var, let const. Tuy nhiên, ta không nhận một cái lỗi nào và đoạn code vẫn cho ra kết quả.

Vậy bạn có biết những biến x, y, z sẽ có những tính chất gì không? Cùng khám phá thử nha!

Chúng ta vẫn dựa trên những yêu tố sau:
  • Phạm vi truy cập (scope).
  • Tính khai báo lại, gán lại giá trị (redeclaration and reassignment).
  • Hoisting.

1. Phạm vi truy cập (scope)

Xét ví dụ dưới đây:

number = 50

function print() {
  square = number * number
  console.log(square)
}

console.log(number) // 50

print() // 2500

Trong ví dụ trên, biến number được khai báo ở phạm vi global, biến square được khai báo trong phạm vi là local (phạm vi hàm). Kết quả cho thấy có thể truy cập đến biến number kể cả bên trong và bên ngoài phạm vi hàm. 

Thêm một ví dụ nữa: 

function print() {
  number = 50
  square = number * number
  console.log(square)
}

print() // 2500

console.log(number) // 50

Trong ví dụ trên, biến number và biến square được khai báo trong phạm vi là local (phạm vi hàm). Kết quả cho thấy có thể truy cập đến biến number kể cả bạn khai báo bên trong phạm vi hàm. 

Hãy thử comment dòng sử dụng hàm print lại và bạn sẽ thấy lỗi là ReferenceError: number is not defined.

2. Khai báo, gán lại giá trị (redeclare and reassign)

Bạn có thể gán lại giá trị mới cho biến automatically declared bất cứ lúc nào, bất kỳ đâu trong chưng trình của bạn.

Ví dụ: 

function print() {
    x = 3;
}

print()

console.log(x) // 3

x = 4;

console.log(x) // 4

Có thể thấy, mặc dù mình khai báo biến x trong hàm print tuy nhiên mình vẫn có thể truy xuất và gán lại giá trị cho biến x ở bên ngoài phạm vi hàm print.

3. Hoisting

Biến được khai báo dưới dạng automatically declared sẽ không được hoisting lên đầu phạm vi như var, let const.

Hãy xem ví dụ dưới đây:

console.log(x)
x = 3
// ReferenceError: x is not defined

Mình cố truy cập biến x trước dòng khai báo của nó và nhận được một lỗi như sau: ReferenceError: x is not defined.

Nếu biến x được hoist lên đầu phạm vi thì ta sẽ nhận một lỗi là ReferenceError: Cannot access 'x' before initialization.

Điều này xảy ra tương tự với phạm vi local block.

Kết luận

Biến được khai báo dưới dạng automatically declared sẽ có những đặc điểm sau:
  • Có thể truy xuất đến biến đó ở bất kỳ đâu trong chương trình.
  • Có thể gán lại và khai báo lại.
  • Không được hoisting ở bất kì phạm vi nào.
Nếu bạn thắc mắc điều gì ở bài viết, hãy đừng gần ngại comment phía bên dưới để mình cùng thảo luận nha.

No comments:

Powered by Blogger.