ES6: var, let and const — The battle between function scope and block scope

Như chúng ta đã biết, trước thời kỳ của ES6, chỉ có 1 cách duy nhất để khai báo biến trong Javascript - đó là sử dụng var. var luôn có hào quang của 1 quan niệm sai lầm - điều này có lẽ do các biến khai báo với var khác biệt ở hầu hết các ngôn ngữ lập trình. Với điều này, tất cả đều được giải thích với 1 thuật ngữ khá tự nhiên - scope. varfunction scoped. Loại scope này có đôi chút khác biệt với loại scope được sử dụng nhiều hơn là block scoped Chúng ta hãy xem xét xem điều này có ý nghĩa gì nhé!

var — function scope

Như đã đề cập, một biến được khai báo sử dụng var sẽ được gọi là function scoped, có nghĩa là nó sẽ tồn tại trong phạm vi của function được khai báo.

function myFunc() {  
  var name = 'Luke'
  console.log(name); // 'Luke'
}

myFunc();

console.log(name); // name is not defined  

Như bạn thấy, biến khai báo với var bên trong hàm myFunc, không thể truy cập từ bên ngoài.

Với điều đó, các loại khối khác - chẳng hạn như câu lệnh if, vòng ... sẽ không được coi là scope.

if(true) {  
  var name = 'Luke'
}

console.log(name); // 'Luke'  

Sử dụng var, chúng ta có thể truy cập biến name bên ngoài if trong khi nó được khai báo bên trong if. Điều này là do chúng nằm trong cùng scope.

Tuy nhiên, với việc giới thiệu ES6, hiện nay chúng ta đã có thêm hai cách khai báo biến mới.

let and const — the introduction of block scope

Trong ES6, letconst đã được giới thiệu như là các cách khác để khai báo các biến - cả hai đều bị hạn chế trong phạm vi scope.

Điều này có thể sẽ giúp tiếp cận tốt hơn đối với những bạn đã quen với bất kỳ ngôn ngữ nào khác ngoài JavaScript.

Trong block scope, bất kỳ block nào sẽ là một scope. Điều này sẽ cung cấp cho một hành vi nhất quán hơn.

const

const dùng để khai báo một hằng số - là một giá trị không thay đổi được trong suốt quá trình chạy.

const A = 5;
A = 10; // Lỗi Uncaught TypeError: Assignment to constant variable

let

let tạo ra một biến chỉ có thể truy cập được trong block bao quanh nó, khác với var - tạo ra một biến có phạm vi truy cập xuyên suốt function chứa nó.

function myFunc() {  
  let name = 'Luke'
  console.log(name); // 'Luke'
}

myFunc();

console.log(name); // name is not defined  

Khi sử dụng let các block khác cũng được coi là scope - ví dụ như câu lệnh if

if(true) {  
  let name = 'Luke'
}

console.log(name); // name is not defined  

Ngoài ra, khi ở global scope (tức là không nằm trong một function nào cả), từ khóa var tạo ra thuộc tính mới cho global object (this), còn let thì không:

var x = 'global';
let y = 'global';
console.log(this.x); // "global"
console.log(this.y); // undefined

When function scope gets confusing

Bây giờ chúng ta đã nhận được sự khác biệt giữa phạm vi của function scopeblock scope - chúng ta hãy xem tại sao điều này nhanh chóng có thể gây nhầm lẫn.

Hãy đặt 1 câu hỏi như thế này: Sẽ như thế nào nếu chúng ta có 1 biến local trong phạm vi scope trùng tên với biến bên ngoài scope đó ? Để trả lời hãy cùng xem ví dụ dưới đây:

var name = 'Luke';

const func = () => {  
  var name = 'Phil';
  console.log(name); // 'Phil'
}

func();

console.log(name); // 'Luke' 

Như mong đợi, biến name trong scope bên ngoài giữ giá trị khai báo ban đầu 'Luke' ngay cả sau khi hàm func chứa một biến cục bộ được đặt tên giống nhau - đã được thực thi.

Tuy nhiên vấn đề là vì function scope chỉ bao gồm các function chứ không phải các loại block khác, chúng ta sẽ có một kết quả khá khác khi thực hiện ví dụ này với các block khác.

var name = 'Luke';

if (true) {  
  var name = 'Phil';
  console.log(name); // 'Phil'
}

console.log(name); // 'Phil'  

Trong trường hợp này biến name sẽ bị ghi đè trong cậu lệnh if và kết quả là ở cả 2 scope ta đều nhận được kết quả là 'Phil'.

Như bạn có thể tưởng tượng, với sự phức tạp ngày càng tăng có thể nhanh chóng trở thành một vấn đề thực sự đau đầu.

Bringing consistency with blocked scope

Vẫn là ví dụ như trên nhưng thay việc dùng var bằng cách dùng let để khai báo biến name thì vấn đề đã được giải quyết và clear hơn rất nhiều.

let name = 'Luke';

const func = () => {  
  let name = 'Phil';
  console.log(name); // 'Phil'
}

func();

console.log(name); // 'Luke'  
let name = 'Luke';

if (true) {  
  let name = 'Phil';
  console.log(name); // 'Phil'
}

console.log(name); // 'Luke' 

Callback và let

Có một trường hợp dùng let rất hiệu quả đó là sử dụng callback trong một vòng lặp. Nếu dùng var:

for (var i = 0; i < 5; i++) {
   setTimeout(function(){ 
      console.log('Yo! ', i);
   }, 1000);
}

Kết quả sẽ ra gì nào?

Yo! 5
Yo! 5
Yo! 5
Yo! 5
Yo! 5

Giá trị của biến i bên trong hàm callback luôn là giá trị cuối cùng của i trong vòng lặp.

Để giải quyết vấn đề này, chúng ta thay var bằng let:

for (let i = 0; i < 5; i++) {
   setTimeout(function(){ 
      console.log('Yo! ', i);
   }, 1000);
}

Và kết quả sẽ đúng như mong đợi:

Yo!  0
Yo!  1
Yo!  2
Yo!  3
Yo!  4

Kết luật

Qua bài viết này chúng ta rút ra 3 điều (và chỉ áp dụng với ES6 thôi nhé ^^):

  • Không dùng var trong bất kì mọi trường hợp
  • Thay vào đó thì dùng let
  • Dùng const khi cần định nghĩa một hằng số

Tham khảo


All Rights Reserved