Asked May 26th, 2018 10:31 AM 485 0 4
  • 485 0 4
+3

Nhờ giải thích giá trị biến i trong đoạn Code

Share
  • 485 0 4

Mình có đoạn Code này:

for(var i = 0; i < 5; i++){
 setTimeout(function(){
  console.log(i); //scope là giống nhau nên giá trị của i là giá trị cuối cùng của nó
 });
}

Mình không hiểu tại sao lại log ra toàn là giá trị 5. Trong code tác giả có để lại comment nhưng mà mình không hiểu được.

Và nếu ta thay var thành let :

for(let i = 0; i < 5; i++){
 setTimeout(function(){
  console.log(i); 
 });
}

Thì kết quả in được ra từ 0 đến 4.

Mình thực sự chưa hiểu chúng khác nhau thế nào. Nhờ các bạn giải thích gúp mình với. Mình xin cám ơn !

4 ANSWERS


Answered May 26th, 2018 1:43 PM
Accepted
+8

Mình nghĩ có một số lý do như sau:

varlet có scope khác nhau. var có scope là function block còn let chỉ là một block thông thường (ví dụ như for loop có thể coi là một block)

for(let i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // Uncaught ReferenceError: i is not defined
for(var i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // 5

Lý do thứ 2 là do bạn sử dụng setTimeout để wrap console.log(i) statement. Để hiểu rõ hơn nguyên nhân trước hết bạn cần hiểu về Event Loop trong JavaScript. Nói ngắn gọn thì trình duyệt sử dụng event loop để kiểm tra và thực thi các pending events (hay pending messages) trong event queue (hay message queue). setTimeout sẽ đặt quá trình thực thi hàm vào event queue nếu đang có các công việc khác cần phải xử lý.

JavaScript Runtime Demo from the talk...

Trong ví dụ của bạn hàm setTimeout có giá trị delay là 0ms. Tuy nhiên điều đó không có nghĩa là hàm bên trong nó sẽ được thực hiện ngay lập tức. Thời gian delay chỉ là thời gian ngắn nhất mà sau đó event sẽ được xử lý chứ không phải là thời gian chính xác. Khi delay không xác định hoặc có giá trị là 0 thì trình duyệt sẽ cố gắng thực thi hàm đó sớm nhất có thể, cụ thể là nó sẽ được xử lý trong quá trình chúng ta thường gọi là next tick (khái niệm khá quen thuộc trong Node.js)

(function() {
    console.log(1); 
    setTimeout(function(){
       console.log(3);
    }, 0); 
    console.log(4);
})();

Trong ví dụ trên kết quả sẽ là 1 4 3 chứ không phải là 1 3 4

Khi sử dụng var thì for loop đã đi qua tất cả các iteration trước khi hàm bên trong setTimeout được thực thi. Lúc này thì giá trị của i đã là 5 (giá trị của i sẽ bị override sau mỗi iteration - vòng lặp) rồi nên console.log() sẽ chỉ trả về giá trị là 5

Một cách hiểu khác là khi sử dụng từ khóa var, chúng ta ngầm định rằng mỗi vòng lặp của for loop sẽ nhận được một bản sao của itại thời điểm mà vòng lặp đó được thực hiện. NOPE!!! Tuy nhiên nếu bạn tìm hiểu một chút về scope trong JavaScript thì có thể thấy rằng mặc dù các callback function trong setTimeout được định nghĩa riêng biệt nhưng chúng đều chia sẻ một scope chung, trong trường hợp này là global scope. i không được định nghĩa bên trong callback function của setTimeout vì vậy nó sẽ được tìm kiếm ở scope bên ngoài chứa nó, ở đây là global scope. Do không dùng strict mode nên i sẽ là một global variable. Do đó cả 5 lần thực hiện đều dùng chung một reference đến i. Để fix thì chúng ta có thể wrap setTimeout function bên trong một scope mới, cách dễ nhất là sử dụng IIFE và truyền giá trị của i vào. Sử dụng IIFE giúp chúng ta tạo mới một scope đồng thời cũng cho phép callback function của setTimeout ghi nhớ giá trị của i trong scope đó (closure trong JS)

for(var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}
for(var i = 0; i < 5; i++) {
    (function() {
        var j = i;
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })();
}
Share
Answered May 26th, 2018 1:38 PM
+4

ten ten ten tèn. var là scope function block, còn let là enclosing block. Trong 1 scope, thì biến var dc sinh ra và chỉ có 1 ràng buộc được chia sẻ trong một scope và trong vòng lặp, tức là i trong mọi callback của setTimeout đều là trả về 5 (tại sao trả về 5 mà ko phải 4? vì nó lấy thằng loop cuối cùng đã kết thúc) Còn với let, thì cứ mỗi vòng lặp thì nó sẽ tạo 1 ràng buộc mới (ý mình là binding), thế nên nó gắn theo loop, nhận giá trị 0 => 4, vì nó không cho phép truy cập thằng let, trước khi mà biến đó dc khai báo trong block, còn không thì nó sẽ ném exception ra đỏ choé.

Share
Answered May 26th, 2018 1:41 PM
+4

Để giải thích được vấn đề ở trên, bạn cần tìm hiểu về khái niệm Scope ở trong Javascript.

Hiểu đơn giản thì Scope là phạm vi truy cập của một biến, nó sẽ quy định cách thức mà một biến được tạo ra, và phạm vi mà nó có thể được truy cập.

Trong các phiên bản Javascript cũ (trước ES6), thì Javascript chỉ có Function Scope, tức biến được tạo ra trong một function, và có thể dùng trong cả function, chứ không có Block Scope.

Và như bạn biết, thì từ khoá để khai báo biến vẫn được dùng trước đây, là var, chính là để khai báo Function Scope đó.

Như ở ví dụ đầu tiên của bạn:

for(var i = 0; i < 5; i++){
 setTimeout(function(){
  console.log(i); //scope là giống nhau nên giá trị của i là giá trị cuối cùng của nó
 });
}

thì chỉ có một biến i, hay nói cách khác, hàm console.log() gọi đến cùng một biến i. Hàm setTimeout sẽ làm cho callback ở trong đó không được chạy ngay, mà bị delay lại. Lúc callback này được gọi, nó sẽ dùng giá trị của i, lúc đó đã là 5. Thế nên bạn sẽ thấy tất cả sẽ in ra kết quả giống nhau.

Từ phiên bản ES6, Javascript giới thiệu một khái niệm mới là Block Scope, tức là bạn có thể tạo ra một biến với phạm vi sử dụng là một block (bằng cách sử dụng { }).

Và để sử dụng Block Scope, bạn cần dùng đến từ khoá let. Đây chính là sự khác biệt giữa varlet. var tạo ra Function Scope, còn let tạo ra Block Scope.

Ở ví dụ thứ 2 của bạn thì

for(let i = 0; i < 5; i++){
 setTimeout(function(){
  console.log(i); 
 });
}

ở đây do dùng let, nên bạn có thể hiểu đơn giản là ở mỗi một vòng for, nó sẽ tạo ra một biến i riêng, và biến i này chỉ có thể sử dụng trong vòng for đó, ra ngoài (hay chạy vòng for khác) là sẽ mất. Tức ở đây sẽ có 5 biến i khác nhau (mỗi biến tồn tại trong một scope khác nhau), và hàm console.log() sẽ gọi đến các biến i khác nhau ấy. Đó là lý do nó sẽ in ra các kết quả khác nhau 😄

Bạn có thể tìm hiểu thêm về vấn đề này ở một số bài viết khác trên Viblo 😉

P/S: Vấn đề ở trên thì bên cạnh việc sử dụng let, bạn cũng có thể giải quyết bằng cách sử dụng IIFE (Immediately Invoked Function Expression) như sau 😄

for(var i = 0; i < 5; i++){
  (function(i) { 
    setTimeout(function(){
      console.log(i);
     });
   })(i);
}
Share
Answered May 27th, 2018 4:21 AM
+2

Bạn có thể hiểu đơn giản sự khác nhau giữa varletphạm vi mà biến đó còn có thể lấy ra giá trị hoặc thay đổi giá trị của nó.

  • var a = 10: Sau khi khai báo thì biến a có thể lấy ra giá trị hiện tại là 10, hoặc bị thay đổi ở bất cứ đâu. Dù bạn khai báo var a bên trong một function nhưng vẫn còn có thể dùng a ở ngoài function đó. Vì thế nên 5 function sử dụng chung biến a, một trong 5 function thay đổi giá trị của a thì 4 function còn lại cũng sử dụng giá trị đã bị thay đổi.
  • let a = 10: Sau khi khai báo thì biến a chỉ có thể sử dụng ngay bên trong function đầu tiên mà chứa lệnh khai báo biến a. Ngoài function đó, biến a không còn tồn tại.

Trong ví dụ của bạn:

  • var i, i được khởi tạo ở lần lặp đầu tiên, các lần lặp sau sử dụng lại biến i lần lặp trước đó. Cả 5 lần console.log(i) đều hiển thị giá trị của cùng một biến i và đều có thể thay đổi giá trị của i. Vì console.log đặt bên trong setTimeout nên ra khỏi vòng for i tức khi kết thúc lần lặp thứ 5 và i đã bị gán thành 5 mất rồi thì mới hiển thị giá trị đã bị thay đổi i = 5.
  • let i, i sẽ được khởi tạo lại sau mỗi lần lặp. Nên mỗi lần lặp nắm giữ một biến i khác nhau. Kết thúc vòng lặp for, hiển thị giá trị i của từng lần lặp là khác nhau.
Share
Hoang vn @wiliamfeng
May 27th, 2018 7:58 AM

Cám ơn các bạn đã nhiệt tình giúp đỡ mình. Đọc câu giải thích của mọi người mình học và biết được thêm nhiều cái mới. Cám ơn tất cả ạ !

+1
| Reply
Share