Tại sao các đối tượng không thể lặp lại trong JavaScript?


87

Tại sao các đối tượng không thể lặp lại theo mặc định?

Tôi luôn thấy các câu hỏi liên quan đến việc lặp lại các đối tượng, giải pháp phổ biến là lặp lại các thuộc tính của một đối tượng và truy cập các giá trị trong một đối tượng theo cách đó. Điều này có vẻ quá phổ biến khiến tôi tự hỏi tại sao bản thân các đối tượng lại không thể lặp lại.

Các câu lệnh như ES6 for...ofsẽ được sử dụng cho các đối tượng theo mặc định. Bởi vì các tính năng này chỉ khả dụng cho các "đối tượng có thể lặp lại" đặc biệt không bao gồm {}các đối tượng, chúng tôi phải thực hiện các vòng lặp để làm cho tính năng này hoạt động cho các đối tượng mà chúng tôi muốn sử dụng.

Câu lệnh for ... of tạo ra một vòng lặp Lặp lại các đối tượng có thể lặp lại (bao gồm Mảng, Bản đồ, Tập hợp, đối tượng đối số, v.v.) ...

Ví dụ: sử dụng chức năng tạo ES6 :

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

function* entries(obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
}

for (let [key, value] of entries(example)) {
  console.log(key);
  console.log(value);
  for (let [key, value] of entries(value)) {
    console.log(key);
    console.log(value);
  }
}

Ở trên ghi dữ liệu đúng theo thứ tự tôi mong đợi khi tôi chạy mã trong Firefox (hỗ trợ ES6 ):

đầu ra của hacky cho ... trong tổng số

Theo mặc định, {}các đối tượng không thể lặp lại, nhưng tại sao? Liệu những bất lợi có lớn hơn lợi ích tiềm năng của các đối tượng có thể lặp lại? Những vấn đề liên quan đến điều này là gì?

Ngoài ra, do {}đối tượng khác với "mảng giống như" bộ sưu tập và "iterable đối tượng" như NodeList, HtmlCollectionarguments, họ có thể không được chuyển đổi thành Mảng.

Ví dụ:

var argumentsArray = Array.prototype.slice.call(arguments);

hoặc được sử dụng với các phương thức Mảng:

Array.prototype.forEach.call(nodeList, function (element) {}).

Bên cạnh những câu hỏi mà tôi có ở trên, tôi rất muốn xem một ví dụ làm việc về cách biến {}các đối tượng thành các tệp lặp, đặc biệt là từ những người đã đề cập đến [Symbol.iterator]. Điều này sẽ cho phép các {}"đối tượng có thể lặp lại" mới này sử dụng các câu lệnh như for...of. Ngoài ra, tôi tự hỏi liệu việc tạo các đối tượng có thể lặp lại có cho phép chuyển đổi chúng thành Mảng hay không.

Tôi đã thử mã dưới đây, nhưng tôi nhận được một TypeError: can't convert undefined to object.

var example = {a: {e: 'one', f: 'two'}, b: {g: 'three'}, c: {h: 'four', i: 'five'}};

// I want to be able to use "for...of" for the "example" object.
// I also want to be able to convert the "example" object into an Array.
example[Symbol.iterator] = function* (obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
};

for (let [key, value] of example) { console.log(value); } // error
console.log([...example]); // error

1
Bất cứ thứ gì có thuộc Symbol.iteratortính là có thể lặp lại. Vì vậy, bạn chỉ cần triển khai thuộc tính đó. Một giải thích khả thi cho lý do tại sao các đối tượng không thể lặp lại có thể là điều này ngụ ý rằng mọi thứ đều có thể lặp lại, vì mọi thứ đều là một đối tượng (tất nhiên là ngoại trừ nguyên thủy). Tuy nhiên, việc lặp qua một hàm hoặc một đối tượng biểu thức chính quy có nghĩa là gì?
Felix Kling

7
Câu hỏi thực sự của bạn ở đây là gì? Tại sao ECMA đưa ra các quyết định mà nó đã làm?
Steve Bennett

3
Vì các đối tượng KHÔNG có thứ tự được đảm bảo về các thuộc tính của chúng, tôi tự hỏi liệu điều đó có phá vỡ định nghĩa về một có thể lặp lại mà bạn mong đợi có một thứ tự có thể dự đoán được không?
jfriend00

2
Để có được một câu trả lời chính thức cho "tại sao", bạn nên tự hỏi tại esdiscuss.org
Felix Kling

1
@FelixKling - đó có phải là bài đăng về ES6 không? Bạn có thể nên chỉnh sửa nó để biết bạn đang nói về phiên bản nào vì "phiên bản sắp tới của ECMAScript" không hoạt động tốt theo thời gian.
jfriend00

Câu trả lời:


42

Tôi sẽ thử cái này. Lưu ý rằng tôi không liên kết với ECMA và không có khả năng hiển thị quá trình ra quyết định của họ, vì vậy tôi không thể nói dứt khoát tại sao họ làm hoặc không làm gì cả. Tuy nhiên, tôi sẽ nêu các giả định của mình và chụp ảnh tốt nhất.

1. Tại sao phải thêm một for...ofcấu trúc ở vị trí đầu tiên?

JavaScript đã bao gồm một for...incấu trúc có thể được sử dụng để lặp lại các thuộc tính của một đối tượng. Tuy nhiên, nó không thực sự là một vòng lặp forEach , vì nó liệt kê tất cả các thuộc tính trên một đối tượng và có xu hướng chỉ hoạt động có thể đoán trước được trong những trường hợp đơn giản.

Nó bị hỏng trong các trường hợp phức tạp hơn (bao gồm cả với mảng, trong đó việc sử dụng nó có xu hướng không được khuyến khích hoặc bị xáo trộn hoàn toàn bởi các biện pháp bảo vệ cần thiết để sử dụng for...invới một mảng một cách chính xác ). Bạn có thể giải quyết vấn đề đó bằng cách sử dụng hasOwnProperty(trong số những thứ khác), nhưng điều đó hơi rườm rà và không phù hợp.

Vì vậy, do đó giả định của tôi là for...ofcấu trúc đang được thêm vào để giải quyết những khiếm khuyết liên quan đến for...incấu trúc và cung cấp tiện ích và tính linh hoạt cao hơn khi lặp lại mọi thứ. Mọi người có xu hướng coi for...innhư một forEachvòng lặp có thể được áp dụng chung cho bất kỳ bộ sưu tập nào và tạo ra kết quả lành mạnh trong bất kỳ bối cảnh nào có thể, nhưng đó không phải là điều xảy ra. Các for...ofsửa lỗi vòng lặp đó.

Tôi cũng giả định rằng điều quan trọng đối với mã ES5 hiện có để chạy trong ES6 và tạo ra kết quả giống như nó đã làm trong ES5, vì vậy không thể thực hiện các thay đổi vi phạm, ví dụ, đối với hành vi của for...incấu trúc.

2. Làm thế nào để for...ofhoạt động?

Tài liệu tham khảo rất hữu ích cho phần này. Cụ thể, một đối tượng được xem xét iterablenếu nó xác định thuộc Symbol.iteratortính.

Định nghĩa thuộc tính phải là một hàm trả về các mục trong bộ sưu tập, từng mục một và đặt cờ cho biết có hay không có nhiều mục để tìm nạp. Các triển khai xác định trước được cung cấp cho một số kiểu đối tượng và tương đối rõ ràng là for...ofchỉ sử dụng đại biểu cho hàm trình vòng lặp.

Cách tiếp cận này rất hữu ích, vì nó giúp bạn cung cấp các trình vòng lặp của riêng bạn rất dễ dàng. Tôi có thể nói rằng cách tiếp cận có thể đã trình bày các vấn đề thực tế do nó phụ thuộc vào việc xác định một thuộc tính mà trước đây không có, ngoại trừ những gì tôi có thể nói rằng đó không phải là trường hợp vì thuộc tính mới về cơ bản bị bỏ qua trừ khi bạn cố tình tìm kiếm nó (tức là nó sẽ không hiển thị trong for...incác vòng lặp dưới dạng khóa, v.v.). Vì vậy, không phải như vậy.

Bỏ qua các vấn đề không thực tế, có thể đã bị coi là gây tranh cãi về mặt khái niệm khi bắt đầu tất cả các đối tượng bằng một thuộc tính mới được xác định trước, hoặc ngầm hiểu rằng "mọi đối tượng là một tập hợp".

3. Tại sao các đối tượng không iterablesử dụng for...oftheo mặc định?

Tôi đoán rằng đây là sự kết hợp của:

  1. Việc tạo tất cả các đối tượng iterabletheo mặc định có thể được coi là không thể chấp nhận được vì nó thêm một thuộc tính mà trước đây không có hoặc vì một đối tượng không (nhất thiết) là một bộ sưu tập. Như Felix lưu ý, "việc lặp qua một hàm hoặc một đối tượng biểu thức chính quy" có nghĩa là gì?
  2. Các đối tượng đơn giản đã có thể được lặp lại bằng cách sử dụng for...invà không rõ việc triển khai trình lặp tích hợp có thể làm gì khác / tốt hơn for...inhành vi hiện có . Vì vậy, ngay cả khi số 1 là sai và việc thêm thuộc tính được chấp nhận, nó có thể không được xem là hữu ích .
  3. Người dùng muốn tạo đối tượng của họ iterablecó thể dễ dàng làm như vậy, bằng cách xác định thuộc Symbol.iteratortính.
  4. Các ES6 đặc tả cũng cung cấp một đồ loại, mà iterable theo mặc định và có một số ưu điểm nhỏ khác trên sử dụng một đối tượng đơn giản như một Map.

Thậm chí còn có một ví dụ được cung cấp cho # 3 trong tài liệu tham khảo:

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

for (var value of myIterable) {
    console.log(value);
}

Cho rằng các đối tượng có thể dễ dàng được tạo ra iterable, chúng đã có thể được lặp lại bằng cách sử dụng for...invà có khả năng không có thỏa thuận rõ ràng về những gì một trình lặp đối tượng mặc định phải làm (nếu những gì nó làm có nghĩa là khác với những gì for...inhiện có), có vẻ hợp lý đủ để các đối tượng không được tạo iterabletheo mặc định.

Lưu ý rằng mã mẫu của bạn có thể được viết lại bằng cách sử dụng for...in:

for (let levelOneKey in object) {
    console.log(levelOneKey);         //  "example"
    console.log(object[levelOneKey]); // {"random":"nest","another":"thing"}

    var levelTwoObj = object[levelOneKey];
    for (let levelTwoKey in levelTwoObj ) {
        console.log(levelTwoKey);   // "random"
        console.log(levelTwoObj[levelTwoKey]); // "nest"
    }
}

... hoặc bạn cũng có thể tạo đối tượng của mình iterabletheo cách bạn muốn bằng cách làm như sau (hoặc bạn có thể tạo tất cả các đối tượng iterablebằng cách gán cho Object.prototype[Symbol.iterator]):

obj = { 
    a: '1', 
    b: { something: 'else' }, 
    c: 4, 
    d: { nested: { nestedAgain: true }}
};

obj[Symbol.iterator] = function() {
    var keys = [];
    var ref = this;
    for (var key in this) {
        //note:  can do hasOwnProperty() here, etc.
        keys.push(key);
    }

    return {
        next: function() {
            if (this._keys && this._obj && this._index < this._keys.length) {
                var key = this._keys[this._index];
                this._index++;
                return { key: key, value: this._obj[key], done: false };
            } else {
                return { done: true };
            }
        },
        _index: 0,
        _keys: keys,
        _obj: ref
    };
};

Bạn có thể chơi với nó tại đây (trong Chrome, cho thuê): http://jsfiddle.net/rncr3ppz/5/

Biên tập

Và để trả lời cho câu hỏi đã cập nhật của bạn, vâng, có thể chuyển đổi một iterablethành một mảng, sử dụng toán tử spread trong ES6.

Tuy nhiên, điều này dường như chưa hoạt động trong Chrome hoặc ít nhất là tôi không thể làm cho nó hoạt động trong jsFiddle của mình. Về lý thuyết, nó phải đơn giản như sau:

var array = [...myIterable];

Tại sao không chỉ làm obj[Symbol.iterator] = obj[Symbol.enumerate]trong ví dụ cuối cùng của bạn?
Bergi

@Bergi - Vì tôi không thấy điều đó trong tài liệu (và không thấy thuộc tính đó được mô tả ở đây ). Mặc dù một đối số ủng hộ việc xác định trình lặp một cách rõ ràng là nó giúp dễ dàng thực thi một thứ tự lặp cụ thể, nếu điều đó được yêu cầu. Tuy nhiên, nếu thứ tự lặp lại không quan trọng (hoặc nếu thứ tự mặc định ổn) và phím tắt một dòng hoạt động, thì có rất ít lý do để không thực hiện cách tiếp cận ngắn gọn hơn.
aroth

Rất tiếc, [[enumerate]]không phải là một biểu tượng nổi tiếng (@@ enumerate) mà là một phương pháp nội bộ. Tôi sẽ phải như vậyobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi

Tất cả những phỏng đoán này có ích gì, khi quá trình thảo luận thực tế được ghi chép đầy đủ? Thật kỳ lạ khi bạn nói "Vì vậy, giả định của tôi là cấu trúc for ... of đang được thêm vào để giải quyết những thiếu sót liên quan đến cấu trúc for ... in." Không. Nó đã được thêm vào để hỗ trợ một cách chung để lặp lại bất kỳ thứ gì và là một phần của một loạt các tính năng mới bao gồm bản thân các vòng lặp, trình tạo, bản đồ và tập hợp. Nó hầu như không có nghĩa là thay thế hoặc nâng cấp for...in, có mục đích khác - để lặp lại trên các thuộc tính của một đối tượng.

2
Một điểm tốt nữa là nhấn mạnh rằng không phải mọi đối tượng đều là một tập hợp. Các đối tượng đã được sử dụng như vậy trong một thời gian dài, bởi vì nó rất tiện lợi, nhưng cuối cùng, chúng không thực sự là bộ sưu tập. Đó là những gì chúng tôi có Mapcho bây giờ.
Felix Kling

9

Objects không triển khai các giao thức lặp trong Javascript vì những lý do rất chính đáng. Có hai cấp độ mà các thuộc tính đối tượng có thể được lặp lại trong JavaScript:

  • cấp độ chương trình
  • mức dữ liệu

Lặp lại mức chương trình

Khi bạn lặp lại một đối tượng ở cấp độ chương trình, bạn sẽ kiểm tra một phần cấu trúc của chương trình. Đó là một hoạt động phản chiếu. Hãy minh họa câu lệnh này với một kiểu mảng, kiểu này thường được lặp lại ở cấp dữ liệu:

const xs = [1,2,3];
xs.f = function f() {};

for (let i in xs) console.log(xs[i]); // logs `f` as well

Chúng tôi chỉ kiểm tra cấp độ chương trình của xs. Vì mảng lưu trữ chuỗi dữ liệu, chúng tôi thường chỉ quan tâm đến mức dữ liệu. for..inRõ ràng là không có ý nghĩa gì khi kết nối với mảng và các cấu trúc "hướng dữ liệu" khác trong hầu hết các trường hợp. Đó là lý do tại sao ES2015 đã giới thiệu for..ofvà giao thức có thể lặp lại.

Lặp lại mức dữ liệu

Điều đó có nghĩa là chúng ta có thể đơn giản phân biệt dữ liệu từ cấp độ chương trình bằng cách phân biệt các hàm với các kiểu nguyên thủy? Không, vì các hàm cũng có thể là dữ liệu trong Javascript:

  • Array.prototype.sort ví dụ: mong đợi một hàm thực hiện một thuật toán sắp xếp nhất định
  • Thunks như () => 1 + 2chỉ là trình bao bọc chức năng cho các giá trị được đánh giá một cách lười biếng

Bên cạnh các giá trị nguyên thủy cũng có thể đại diện cho cấp chương trình:

  • [].lengthví dụ là một Numbernhưng đại diện cho độ dài của một mảng và do đó thuộc về miền chương trình

Điều đó có nghĩa là chúng ta không thể phân biệt chương trình và mức dữ liệu chỉ bằng cách kiểm tra các loại.


Điều quan trọng là phải hiểu rằng việc triển khai các giao thức lặp lại cho các đối tượng Javascript cũ thuần túy sẽ phụ thuộc vào mức dữ liệu. Nhưng như chúng ta vừa thấy, không thể phân biệt được đáng tin cậy giữa dữ liệu và sự lặp lại cấp độ chương trình.

Với Arrays, sự khác biệt này là không đáng kể: Mọi phần tử có khóa giống số nguyên đều là một phần tử dữ liệu. Objects có một tính năng tương đương: Bộ enumerablemô tả. Nhưng có thực sự nên dựa vào điều này? Tôi tin rằng nó không phải là! Ý nghĩa của bộ enumerablemô tả quá mờ.

Phần kết luận

Không có cách nào có ý nghĩa để triển khai các giao thức lặp cho các đối tượng, bởi vì không phải mọi đối tượng đều là một tập hợp.

Nếu các thuộc tính đối tượng là có thể lặp lại theo mặc định, thì mức chương trình và dữ liệu sẽ được trộn lẫn. Vì mọi kiểu phức hợp trong Javascript đều dựa trên các đối tượng thuần túy nên điều này cũng sẽ áp dụng cho ArrayMapcũng như.

for..in, Object.keys, Reflect.ownKeysVv có thể được sử dụng cho cả phản ánh và dữ liệu lặp, một sự phân biệt rõ ràng là thường xuyên không thể. Nếu không cẩn thận, bạn sẽ nhanh chóng kết thúc với lập trình meta và các phụ thuộc kỳ lạ. Kiểu Mapdữ liệu trừu tượng kết thúc hiệu quả sự kết hợp giữa chương trình và mức dữ liệu. Tôi tin rằng đó Maplà thành tích quan trọng nhất trong ES2015, ngay cả khi Promisecác trận đấu thú vị hơn nhiều.


3
+1, tôi nghĩ rằng "Không có cách nào có ý nghĩa để triển khai các giao thức lặp cho các đối tượng, bởi vì không phải mọi đối tượng đều là một tập hợp." Tổng cộng lại.
Charlie Schliesser 24/09/18

1
Tôi không nghĩ đó là một lập luận tốt. Nếu đối tượng của bạn không phải là một bộ sưu tập, tại sao bạn lại cố gắng lặp lại nó? Không quan trọng là mọi đối tượng đều là một bộ sưu tập, bởi vì bạn sẽ không cố gắng lặp lại những đối tượng không phải.
BT

Trên thực tế, mọi đối tượng một bộ sưu tập, và việc quyết định bộ sưu tập có mạch lạc hay không phụ thuộc vào ngôn ngữ. Mảng và Bản đồ cũng có thể thu thập các giá trị không liên quan. Vấn đề là bạn có thể lặp lại các khóa của bất kỳ đối tượng nào bất kể việc sử dụng chúng như thế nào, vì vậy bạn chỉ còn một bước nữa là có thể lặp lại các giá trị của chúng. Nếu bạn đang nói về một ngôn ngữ nhập tĩnh các giá trị mảng (hoặc bất kỳ bộ sưu tập nào khác), bạn có thể nói về những hạn chế như vậy, nhưng không phải JavaScript.
Manngo

Lập luận rằng mọi đối tượng không phải là một tập hợp là không có ý nghĩa. Bạn đang giả định rằng một trình lặp chỉ có một mục đích (để lặp lại một tập hợp). Trình lặp mặc định trên một đối tượng sẽ là một trình lặp các thuộc tính của đối tượng, bất kể các thuộc tính đó đại diện cho điều gì (cho dù là một bộ sưu tập hay một cái gì đó khác). Như Manngo đã nói, nếu đối tượng của bạn không đại diện cho một tập hợp, thì lập trình viên không nên coi nó như một tập hợp. Có lẽ họ chỉ muốn lặp lại các thuộc tính trên đối tượng cho một số đầu ra gỡ lỗi? Có rất nhiều lý do khác ngoài một bộ sưu tập.
jfriend00

8

Tôi đoán câu hỏi nên được "tại sao không có built-in đối tượng lặp đi lặp lại?

Việc thêm khả năng lặp vào bản thân các đối tượng có thể hình dung ra những hậu quả không mong muốn và không, không có cách nào để đảm bảo thứ tự, nhưng việc viết một trình lặp đơn giản như

function* iterate_object(o) {
    var keys = Object.keys(o);
    for (var i=0; i<keys.length; i++) {
        yield [keys[i], o[keys[i]]];
    }
}

Sau đó

for (var [key, val] of iterate_object({a: 1, b: 2})) {
    console.log(key, val);
}

a 1
b 2

1
cảm ơn torazaburo. tôi đã sửa lại câu hỏi của mình. tôi rất thích xem một ví dụ sử dụng [Symbol.iterator]cũng như nếu bạn có thể mở rộng về những hậu quả không mong muốn đó.
boombox

4

Bạn có thể dễ dàng làm cho tất cả các đối tượng có thể lặp lại trên toàn cầu:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});

3
Không thêm toàn cục các phương thức vào các đối tượng gốc. Đây là một ý tưởng khủng khiếp sẽ cắn bạn và bất kỳ ai sử dụng mã của bạn, vào mông.
BT

2

Đây là cách tiếp cận mới nhất (hoạt động trong chrome canary)

var files = {
    '/root': {type: 'directory'},
    '/root/example.txt': {type: 'file'}
};

for (let [key, {type}] of Object.entries(files)) {
    console.log(type);
}

entriesbây giờ là một phương thức là một phần của Đối tượng :)

biên tập

Sau khi xem xét kỹ hơn, có vẻ như bạn có thể làm như sau

Object.prototype[Symbol.iterator] = function * () {
    for (const [key, value] of Object.entries(this)) {
        yield {key, value}; // or [key, value]
    }
};

vì vậy bây giờ bạn có thể làm điều này

for (const {key, value:{type}} of files) {
    console.log(key, type);
}

sửa2

Quay lại ví dụ ban đầu của bạn, nếu bạn muốn sử dụng phương thức nguyên mẫu ở trên, nó sẽ như thế này

for (const {key, value:item1} of example) {
    console.log(key);
    console.log(item1);
    for (const {key, value:item2} of item1) {
        console.log(key);
        console.log(item2);
    }
}


0

Về mặt kỹ thuật, đây không phải là câu trả lời cho câu hỏi tại sao? nhưng tôi đã điều chỉnh câu trả lời của Jack Slocum ở trên theo nhận xét của BT thành một thứ có thể được sử dụng để làm cho một Đối tượng có thể lặp lại.

var iterableProperties={
    enumerable: false,
    value: function * () {
        for(let key in this) if(this.hasOwnProperty(key)) yield this[key];
    }
};

var fruit={
    'a': 'apple',
    'b': 'banana',
    'c': 'cherry'
};
Object.defineProperty(fruit,Symbol.iterator,iterableProperties);
for(let v of fruit) console.log(v);

Không hoàn toàn thuận tiện như lẽ ra phải có, nhưng nó hoàn toàn khả thi, đặc biệt nếu bạn có nhiều đối tượng:

var instruments={
    'a': 'accordion',
    'b': 'banjo',
    'c': 'cor anglais'
};
Object.defineProperty(instruments,Symbol.iterator,iterableProperties);
for(let v of instruments) console.log(v);

Và, bởi vì mọi người đều có quyền đưa ra ý kiến, tôi không thể hiểu tại sao Đối tượng cũng không thể lặp lại được. Nếu bạn có thể polyfill chúng như trên hoặc sử dụng for … inthì tôi không thể thấy một đối số đơn giản.

Một gợi ý khả thi là những gì có thể lặp lại là một loại đối tượng, vì vậy có thể khả năng lặp được giới hạn trong một tập con các đối tượng đề phòng một số đối tượng khác phát nổ trong lần thử.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.