Câu hỏi JavaScript đóng bên trong vòng - ví dụ đơn giản thực tế


var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

Nó xuất kết quả này:

Giá trị của tôi: 3
  Giá trị của tôi: 3
  Giá trị của tôi: 3

Trong khi tôi muốn nó xuất:

Giá trị của tôi: 0
  Giá trị của tôi: 1
  Giá trị của tôi: 2


Vấn đề tương tự xảy ra khi sự chậm trễ trong việc chạy hàm được gây ra bởi việc sử dụng trình nghe sự kiện:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

… Hoặc mã không đồng bộ, ví dụ: sử dụng lời hứa:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for(var i = 0; i < 3; i++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

Giải pháp cho vấn đề cơ bản này là gì?


2324
2018-04-15 06:06


gốc


Bạn chắc chắn bạn không muốn funcs là một mảng, nếu bạn đang sử dụng các chỉ số số? Chỉ cần một đầu lên. - DanMan
Đây thực sự là vấn đề khó hiểu. Điều này bài viết giúp tôi hiểu nó. Nó cũng có thể giúp người khác. - user3199690
Một giải pháp đơn giản và giải thích khác: 1) Hàm lồng nhaucó quyền truy cập vào phạm vi "ở trên" chúng; 2) một đóng cửa dung dịch... "Một đóng là một hàm có quyền truy cập vào phạm vi cha mẹ, ngay cả sau khi hàm cha đã đóng". - Peter Krauss
Tham khảo liên kết này để biết thêm về Unserstanding javascript.info/tutorial/advanced-functions - Saurabh Ahuja
Trong ES6, một giải pháp tầm thường là khai báo biến tôi với để cho, được đưa vào cơ thể của vòng lặp. - Tomas Nikodym


Các câu trả lời:


Vâng, vấn đề là biến i, trong mỗi hàm ẩn danh của bạn, được liên kết với cùng một biến bên ngoài hàm.

Giải pháp cổ điển: Đóng cửa

Những gì bạn muốn làm là ràng buộc biến trong mỗi hàm thành một giá trị riêng biệt, không thay đổi bên ngoài hàm:

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Vì không có phạm vi khối trong JavaScript - chỉ phạm vi chức năng - bằng cách gói tạo hàm trong một hàm mới, bạn đảm bảo rằng giá trị của "i" vẫn như bạn dự định.


Giải pháp 2015: forEach

Với sự sẵn có tương đối rộng rãi của Array.prototype.forEach chức năng (trong năm 2015), đáng chú ý là trong những tình huống liên quan đến việc lặp lại chủ yếu qua một mảng các giá trị, .forEach() cung cấp một cách tự nhiên, sạch sẽ để có được một đóng cửa riêng biệt cho mỗi lần lặp lại. Đó là, giả sử bạn có một số loại mảng chứa giá trị (tham chiếu DOM, đối tượng, bất kỳ thứ gì) và vấn đề nảy sinh khi thiết lập callbacks cụ thể cho mỗi phần tử, bạn có thể thực hiện điều này:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

Ý tưởng là mỗi lệnh gọi hàm gọi lại được sử dụng với .forEach vòng lặp sẽ là kết thúc của riêng nó. Tham số được truyền vào bộ xử lý đó là phần tử mảng cụ thể cho bước cụ thể đó của phép lặp. Nếu nó được sử dụng trong một cuộc gọi lại không đồng bộ, nó sẽ không va chạm với bất kỳ callbacks nào khác được thiết lập ở các bước khác của phép lặp.

Nếu bạn tình cờ làm việc trong jQuery, $.each() chức năng cung cấp cho bạn một khả năng tương tự.


Giải pháp ES6: let

ECMAScript 6 (ES6), phiên bản mới nhất của JavaScript, hiện đang bắt đầu được triển khai trong nhiều trình duyệt và các hệ thống phụ trợ thường xanh. Ngoài ra còn có transpilers như Babel sẽ chuyển đổi ES6 thành ES5 để cho phép sử dụng các tính năng mới trên các hệ thống cũ hơn.

ES6 giới thiệu mới let và const các từ khóa được phạm vi khác với varbiến dựa trên. Ví dụ, trong một vòng lặp với một letdựa trên chỉ mục, mỗi lần lặp qua vòng lặp sẽ có một giá trị mới của i trong đó mỗi giá trị được đặt bên trong vòng lặp, vì vậy mã của bạn sẽ hoạt động như bạn mong đợi. Có rất nhiều tài nguyên, nhưng tôi khuyên bạn nên Bài đăng phạm vi khối của 2ality như một nguồn thông tin tuyệt vời.

for (let i = 0; i < 3; i++) {
    funcs[i] = function() {
        console.log("My value: " + i);
    };
}

Tuy nhiên, hãy coi chừng IE9-IE11 và Edge trước hỗ trợ Edge 14 let nhưng nhận sai ở trên (họ không tạo ra một i mỗi lần, vì vậy tất cả các chức năng ở trên sẽ đăng nhập 3 như chúng sẽ sử dụng nếu chúng ta sử dụng var). Edge 14 cuối cùng cũng đúng.


1799
2018-04-15 06:18



không phải function createfunc(i) { return function() { console.log("My value: " + i); }; } vẫn đóng bởi vì nó sử dụng biến i? - アレックス
Thật không may, câu trả lời này là lỗi thời và không ai sẽ nhìn thấy câu trả lời đúng ở phía dưới - sử dụng Function.bind() chắc chắn là thích hợp hơn bây giờ, hãy xem stackoverflow.com/a/19323214/785541. - Wladimir Palant
@ Wladimir: Đề xuất của bạn rằng .bind() Là "câu trả lời đúng" không đúng. Mỗi người đều có nơi riêng của họ. Với .bind() bạn không thể ràng buộc đối số mà không ràng buộc this giá trị. Bạn cũng nhận được một bản sao của i đối số mà không có khả năng đột biến nó giữa các cuộc gọi, đôi khi là cần thiết. Vì vậy, chúng là các cấu trúc hoàn toàn khác nhau, chưa kể đến .bind() việc triển khai đã chậm trong lịch sử. Chắc chắn trong ví dụ đơn giản hoặc là sẽ làm việc, nhưng đóng cửa là một khái niệm quan trọng để hiểu, và đó là những gì câu hỏi đã được về. - cookie monster
Vui lòng ngừng sử dụng các hàm hacks trả lại này, sử dụng [] .forEach hoặc [] .map thay vì chúng tránh sử dụng lại các biến phạm vi tương tự. - Christian Landgren
@ChristianLandgren: Điều đó chỉ hữu ích nếu bạn đang lặp lại một mảng. Những kỹ thuật này không phải là "hack". Chúng là kiến ​​thức cần thiết.


Thử:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Chỉnh sửa (2014):

Cá nhân tôi nghĩ rằng @ Aust's câu trả lời gần đây hơn về việc sử dụng .bind là cách tốt nhất để làm điều này. Ngoài ra còn có lo-dash / underscore's _.partial khi bạn không cần hoặc muốn gây rối bind'S thisArg.


340
2018-04-15 06:10



bất kỳ lời giải thích nào về }(i)); ? - aswzen
@aswzen Tôi nghĩ rằng nó vượt qua i làm đối số index cho hàm. - Jet Blue


Một cách khác mà chưa được đề cập đến là việc sử dụng Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

CẬP NHẬT

Như đã chỉ ra bởi @squint và @mekdev, bạn sẽ có được hiệu suất tốt hơn bằng cách tạo hàm bên ngoài vòng lặp đầu tiên và sau đó ràng buộc các kết quả trong vòng lặp.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


310
2017-10-11 16:41



Đây là những gì tôi làm những ngày này quá, tôi cũng thích lo-dash / gạch dưới _.partial - Bjorn Tipling
.bind() sẽ phần lớn lỗi thời với các tính năng của ECMAScript 6. Bên cạnh đó, điều này thực sự tạo ra hai hàm cho mỗi lần lặp. Đầu tiên là ẩn danh, sau đó được tạo bởi .bind(). Sử dụng tốt hơn sẽ là tạo ra nó bên ngoài vòng lặp, sau đó .bind() nó bên trong.
Điều này không kích hoạt JsHint - Đừng tạo ra các hàm trong vòng lặp. ? Tôi đã đi xuống con đường này cũng nhưng sau khi chạy các công cụ chất lượng mã của nó một không đi .. - mekdev
@squint @mekdev - Cả hai đều đúng. Ví dụ ban đầu của tôi đã được viết một cách nhanh chóng để chứng minh làm thế nào bind Được sử dụng. Tôi đã thêm một ví dụ khác cho mỗi đề xuất của bạn. - Aust
Tôi nghĩ thay vì lãng phí tính toán qua hai vòng lặp (n), chỉ cần làm cho (var i = 0; i <3; i ++) {log.call (điều này, i); } - user2290820


Sử dụng một Biểu thức hàm được gọi ngay lập tức, cách đơn giản và dễ đọc nhất để kèm theo một biến chỉ mục:

for (var i = 0; i < 3; i++) {

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

Điều này sẽ gửi trình lặp i vào chức năng ẩn danh mà chúng tôi định nghĩa là index. Điều này tạo ra một đóng cửa, trong đó biến i được lưu để sử dụng sau này trong bất kỳ chức năng không đồng bộ nào trong IIFE.


234
2017-10-11 18:23



Để có thêm khả năng đọc mã và tránh nhầm lẫn i là gì, tôi đổi tên tham số hàm thành index. - Kyle Falconer
Bạn sẽ sử dụng kỹ thuật này như thế nào để xác định mảng funcs được mô tả trong câu hỏi gốc? - Nico
@Nico Cách tương tự như được hiển thị trong câu hỏi ban đầu, ngoại trừ bạn sẽ sử dụng index thay vì i. - JLRishe
@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); } - Nico
@Nico Trong trường hợp cụ thể của OP, họ chỉ đang lặp qua các con số, vì vậy đây sẽ không phải là một trường hợp tuyệt vời cho .forEach(), nhưng rất nhiều thời gian, khi một người bắt đầu với một mảng, forEach() là một lựa chọn tốt, như: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; }); - JLRishe


Bit trễ cho bữa tiệc, nhưng tôi đã khám phá vấn đề này ngày hôm nay và nhận thấy rằng nhiều câu trả lời không hoàn toàn giải quyết cách Javascript xử lý phạm vi, mà về cơ bản những gì này nhọt xuống.

Như nhiều người khác đã đề cập, vấn đề là hàm bên trong tham chiếu i biến. Vậy tại sao chúng ta không tạo một biến cục bộ mới cho mỗi lần lặp và thay vào đó có tham chiếu hàm bên trong?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Cũng giống như trước đây, trong đó mỗi hàm bên trong xuất ra giá trị cuối cùng được gán cho i, bây giờ mỗi hàm bên trong chỉ xuất ra giá trị cuối cùng được gán cho ilocal. Nhưng không phải mỗi lần lặp lại đều có riêng ilocal?

Hóa ra, đó là vấn đề. Mỗi lần lặp lại chia sẻ cùng một phạm vi, do đó, mỗi lần lặp lại sau lần đầu tiên chỉ là ghi đè ilocal. Từ MDN:

Quan trọng: JavaScript không có phạm vi khối. Các biến được giới thiệu với một khối được đưa vào phạm vi hàm hoặc kịch bản có chứa, và các hiệu ứng của việc thiết lập chúng tồn tại bên ngoài chính khối đó. Nói cách khác, các câu lệnh chặn không giới thiệu một phạm vi. Mặc dù các khối "độc lập" là cú pháp hợp lệ, bạn không muốn sử dụng các khối độc lập trong JavaScript, bởi vì chúng không làm những gì bạn nghĩ, nếu bạn nghĩ chúng làm bất cứ thứ gì như các khối như vậy trong C hoặc Java.

Được nhắc lại để nhấn mạnh:

JavaScript không có phạm vi khối. Các biến được giới thiệu với một khối được sắp xếp theo hàm hoặc tập lệnh có chứa

Chúng ta có thể thấy điều này bằng cách kiểm tra ilocal trước khi chúng tôi khai báo nó trong mỗi lần lặp:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

Đây chính là lý do tại sao lỗi này rất phức tạp. Mặc dù bạn đang redeclaring một biến, Javascript sẽ không ném một lỗi, và JSLint thậm chí sẽ không ném một cảnh báo. Đây cũng là lý do tại sao cách tốt nhất để giải quyết điều này là tận dụng lợi thế của các bao đóng, về cơ bản là ý tưởng trong Javascript, hàm bên trong có quyền truy cập vào các biến bên ngoài vì phạm vi bên trong "bao bọc" các phạm vi bên ngoài.

Closures

Điều này cũng có nghĩa là các hàm bên trong "giữ" các biến bên ngoài và giữ chúng sống, ngay cả khi hàm bên ngoài trả về. Để sử dụng điều này, chúng ta tạo và gọi hàm bao bọc hoàn toàn để tạo một phạm vi mới, khai báo ilocaltrong phạm vi mới và trả về hàm bên trong sử dụng ilocal (giải thích thêm bên dưới):

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Việc tạo hàm bên trong bên trong một hàm bao bọc cung cấp cho hàm bên trong một môi trường riêng tư mà chỉ nó có thể truy cập, một "đóng cửa". Do đó, mỗi khi chúng ta gọi hàm bao bọc, chúng ta tạo ra một hàm bên trong mới với môi trường riêng của nó, đảm bảo rằng ilocal các biến không va chạm và ghi đè lên nhau. Một vài tối ưu hóa nhỏ cung cấp câu trả lời cuối cùng mà nhiều người dùng SO khác đã đưa ra:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

Cập nhật

Với ES6 giờ đây, chúng ta có thể sử dụng let từ khóa để tạo các biến có phạm vi khối:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

Hãy xem nó thật dễ dàng như thế nào! Để biết thêm thông tin, xem câu trả lời nàymà thông tin của tôi dựa trên.


127
2018-04-10 09:57



Tôi thích cách bạn giải thích cách IIFE. Tôi đang tìm kiếm điều đó. Cảm ơn bạn. - CapturedTree
Bây giờ có một điều như là phạm vi khối trong JavaScript bằng cách sử dụng let và const từ khóa. Nếu câu trả lời này được mở rộng để bao gồm điều đó, nó sẽ hữu ích hơn nhiều trên toàn cầu theo ý kiến ​​của tôi. - Tiny Giant
@ TinyGiant điều chắc chắn, tôi đã thêm một số thông tin về let và liên kết một lời giải thích đầy đủ hơn - woojoo666
@ woojoo666 Câu trả lời của bạn cũng có thể hoạt động để gọi hai URL thay thế trong một vòng lặp như sau: i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (có thể thay thế window.open () bằng getelementbyid ......) - nutty about natty
@nuttyaboutnatty xin lỗi về câu trả lời muộn như vậy. Có vẻ như mã trong ví dụ của bạn đã hoạt động. Bạn không sử dụng i trong các chức năng thời gian chờ của bạn, vì vậy bạn không cần đóng - woojoo666


Với ES6 hiện được hỗ trợ rộng rãi, câu trả lời hay nhất cho câu hỏi này đã thay đổi. ES6 cung cấp let và const từ khóa cho trường hợp chính xác này. Thay vì lộn xộn xung quanh với đóng cửa, chúng tôi chỉ có thể sử dụng let để đặt biến phạm vi vòng lặp như sau:

var funcs = [];
for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value: " + i); 
    };
}

val sau đó sẽ trỏ đến một đối tượng cụ thể cho lượt quay vòng cụ thể đó và sẽ trả lại giá trị chính xác mà không có ký hiệu đóng bổ sung. Điều này rõ ràng đơn giản hóa vấn đề này.

const tương tự như let với giới hạn bổ sung là tên biến không thể được phục hồi thành một tham chiếu mới sau khi gán ban đầu.

Hỗ trợ trình duyệt hiện có ở đây cho những người nhắm mục tiêu các phiên bản trình duyệt mới nhất. const/let hiện được hỗ trợ trong Firefox, Safari, Edge và Chrome mới nhất. Nó cũng được hỗ trợ trong Node và bạn có thể sử dụng nó ở mọi nơi bằng cách tận dụng các công cụ xây dựng như Babel. Bạn có thể xem ví dụ làm việc tại đây: http://jsfiddle.net/ben336/rbU4t/2/

Tài liệu tại đây:

Tuy nhiên, hãy coi chừng IE9-IE11 và Edge trước hỗ trợ Edge 14 let nhưng nhận sai ở trên (họ không tạo ra một i mỗi lần, vì vậy tất cả các chức năng ở trên sẽ đăng nhập 3 như chúng sẽ sử dụng nếu chúng ta sử dụng var). Edge 14 cuối cùng cũng đúng.


121
2018-05-21 03:04



Thật không may, 'let' vẫn chưa được hỗ trợ đầy đủ, đặc biệt là trên thiết bị di động. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - MattC
Tính đến ngày 16 tháng 6, để cho được hỗ trợ trong tất cả các phiên bản trình duyệt chính ngoại trừ iOS Safari, Opera Mini và Safari 9. Các trình duyệt Evergreen hỗ trợ nó. Babel sẽ transpile nó một cách chính xác để giữ cho hành vi mong đợi mà không có chế độ compliancy cao bật. - Dan Pantry
@DanPantry yeah về thời gian cho một bản cập nhật :) Cập nhật để phản ánh tốt hơn trạng thái hiện tại của sự vật, bao gồm thêm một đề cập đến const, liên kết doc và thông tin tương thích tốt hơn. - Ben McCormick
Đây không phải là lý do tại sao chúng tôi sử dụng babel để transpile mã của chúng tôi để các trình duyệt không hỗ trợ ES6 / 7 có thể hiểu những gì đang xảy ra? - pixel 67


Một cách khác để nói rằng đó là i trong hàm của bạn bị ràng buộc tại thời điểm thực hiện hàm, không phải lúc tạo hàm.

Khi bạn tạo ra sự đóng cửa, i là một tham chiếu đến biến được xác định trong phạm vi bên ngoài, không phải là bản sao của nó như khi bạn tạo xong việc đóng. Nó sẽ được đánh giá tại thời điểm thực hiện.

Hầu hết các câu trả lời khác cung cấp các cách để làm việc xung quanh bằng cách tạo một biến khác không thay đổi giá trị cho bạn.

Chỉ nghĩ rằng tôi sẽ thêm một lời giải thích cho sự rõ ràng. Đối với một giải pháp, cá nhân, tôi muốn đi với Harto vì nó là cách tự giải thích nhất để làm điều đó từ các câu trả lời ở đây. Bất kỳ mã nào được đăng sẽ hoạt động, nhưng tôi muốn chọn một nhà máy đóng cửa phải viết một đống nhận xét để giải thích lý do tôi tuyên bố biến mới (Freddy và 1800) hoặc có cú pháp đóng mở lạ (apphacker).


77
2018-04-15 06:48