Mengapa Objek tidak dapat diulang di JavaScript?

87

Mengapa objek tidak dapat diulang secara default?

Saya melihat pertanyaan sepanjang waktu terkait dengan objek iterasi, solusi yang umum adalah mengulang properti objek dan mengakses nilai dalam objek dengan cara itu. Ini tampak sangat umum sehingga membuat saya bertanya-tanya mengapa objek itu sendiri tidak dapat diulang.

Pernyataan seperti ES6 for...ofakan bagus digunakan untuk objek secara default. Karena fitur ini hanya tersedia untuk "objek iterable" khusus yang tidak menyertakan {}objek, kita harus melalui lingkaran untuk membuat ini berfungsi untuk objek yang ingin kita gunakan.

Pernyataan for ... of membuat loop Iterasi di atas objek yang dapat diulang (termasuk Array, Map, Set, objek argumen, dan sebagainya) ...

Misalnya menggunakan fungsi generator 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);
  }
}

Data log di atas dengan benar dalam urutan yang saya harapkan ketika saya menjalankan kode di Firefox (yang mendukung ES6 ):

keluaran hacky untuk ... dari

Secara default, {}objek tidak dapat diulang, tapi mengapa? Apakah kerugiannya lebih besar daripada manfaat potensial dari objek yang dapat diulang? Apa masalah yang terkait dengan ini?

Selain itu, karena {}objek yang berbeda dari "Array-seperti" koleksi dan "iterable objek" seperti NodeList, HtmlCollection, dan arguments, mereka tidak dapat diubah menjadi Array.

Sebagai contoh:

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

atau digunakan dengan metode Array:

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

Selain pertanyaan yang saya miliki di atas, saya ingin melihat contoh kerja tentang cara membuat {}objek menjadi iterable, terutama dari mereka yang telah menyebutkan [Symbol.iterator]. Ini harus memungkinkan {}"objek iterable" baru ini untuk menggunakan pernyataan seperti for...of. Juga, saya bertanya-tanya apakah membuat objek iterable memungkinkannya diubah menjadi Array.

Saya mencoba kode di bawah ini, tetapi saya mendapatkan 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
boombox
sumber
1
Apa pun yang memiliki Symbol.iteratorproperti bersifat iterable. Jadi, Anda hanya perlu menerapkan properti itu. Satu penjelasan yang mungkin mengapa objek tidak dapat diulang bisa jadi bahwa ini menyiratkan semuanya dapat diulang, karena semuanya adalah objek (kecuali primitif tentu saja). Namun, apa artinya mengulang fungsi atau objek ekspresi reguler?
Felix Kling
7
Apa pertanyaan Anda yang sebenarnya di sini? Mengapa ECMA membuat keputusan seperti itu?
Steve Bennett
3
Karena objek TIDAK memiliki jaminan urutan propertinya, saya ingin tahu apakah itu melanggar definisi iterable yang Anda harapkan memiliki urutan yang dapat diprediksi?
jfriend00
2
Untuk mendapatkan jawaban otoritatif untuk "mengapa", Anda harus bertanya di esdiscuss.org
Felix Kling
1
@ FelixKling - apakah itu postingan tentang ES6? Anda mungkin harus mengeditnya untuk mengatakan versi apa yang Anda bicarakan karena "versi ECMAScript yang akan datang" tidak berfungsi dengan baik dari waktu ke waktu.
jfriend00

Jawaban:

42

Saya akan mencoba ini. Perhatikan bahwa saya tidak berafiliasi dengan ECMA dan tidak dapat melihat proses pengambilan keputusan mereka, jadi saya tidak dapat secara pasti mengatakan mengapa mereka telah atau belum melakukan apa pun. Namun, saya akan menyatakan asumsi saya dan melakukan yang terbaik.

1. Mengapa menambahkan for...ofkonstruksi di tempat pertama?

JavaScript sudah menyertakan for...inkonstruksi yang dapat digunakan untuk mengulang properti suatu objek. Namun, ini sebenarnya bukan perulangan forEach , karena ia menyebutkan semua properti pada sebuah objek dan cenderung hanya bekerja dengan diprediksi dalam kasus sederhana.

Ini rusak dalam kasus yang lebih kompleks (termasuk dengan larik, di mana penggunaannya cenderung tidak disarankan atau sepenuhnya dikaburkan oleh pengamanan yang diperlukan untuk digunakan for...indengan larik dengan benar ). Anda dapat mengatasinya dengan menggunakan hasOwnProperty(antara lain), tetapi itu agak kikuk dan tidak elegan.

Jadi oleh karena itu asumsi saya adalah bahwa for...ofkonstruksi sedang ditambahkan untuk mengatasi kekurangan yang terkait dengan for...inkonstruksi tersebut, dan memberikan utilitas dan fleksibilitas yang lebih besar saat melakukan iterasi. Orang cenderung memperlakukan for...insebagai forEachloop yang secara umum dapat diterapkan pada koleksi apa pun dan menghasilkan hasil yang waras dalam konteks yang memungkinkan, tetapi bukan itu yang terjadi. The for...ofLoop perbaikan itu.

Saya juga berasumsi bahwa penting untuk kode ES5 yang ada untuk berjalan di bawah ES6 dan menghasilkan hasil yang sama seperti di bawah ES5, jadi perubahan yang melanggar tidak dapat dilakukan, misalnya, pada perilaku for...inkonstruksi.

2. Bagaimana cara for...ofkerjanya?

The dokumentasi referensi berguna untuk bagian ini. Secara spesifik, sebuah objek dipertimbangkan iterablejika ia mendefinisikan Symbol.iteratorproperti.

Definisi properti harus berupa fungsi yang mengembalikan item dalam koleksi, satu, oleh, satu, dan menyetel bendera yang menunjukkan apakah ada lebih banyak item untuk diambil atau tidak. Implementasi yang telah ditentukan disediakan untuk beberapa tipe objek , dan cukup jelas bahwa for...ofhanya menggunakan delegasi ke fungsi iterator.

Pendekatan ini berguna, karena membuatnya sangat mudah untuk menyediakan iterator Anda sendiri. Saya dapat mengatakan bahwa pendekatan tersebut dapat menyajikan masalah praktis karena ketergantungannya pada pendefinisian properti yang sebelumnya tidak ada, kecuali dari apa yang dapat saya katakan bukan itu masalahnya karena properti baru pada dasarnya diabaikan kecuali Anda sengaja mencarinya (mis. itu tidak akan muncul dalam for...inloop sebagai kunci, dll.). Jadi bukan itu masalahnya.

Terlepas dari non-masalah praktis, mungkin telah dianggap kontroversial secara konseptual untuk memulai semua objek dengan properti baru yang telah ditentukan sebelumnya, atau secara implisit mengatakan bahwa "setiap objek adalah koleksi".

3. Mengapa objek tidak iterabledigunakan for...ofsecara default?

Dugaan saya adalah bahwa ini adalah kombinasi dari:

  1. Membuat semua objek iterablesecara default mungkin dianggap tidak dapat diterima karena menambahkan properti yang sebelumnya tidak ada, atau karena objek tidak (harus) merupakan koleksi. Seperti yang Felix catat, "apa artinya mengulang fungsi atau objek ekspresi reguler"?
  2. Objek sederhana sudah dapat diulang menggunakan for...in, dan tidak jelas apa implementasi iterator built-in bisa dilakukan secara berbeda / lebih baik dari for...inperilaku yang ada . Jadi, meskipun # 1 salah dan menambahkan properti dapat diterima, mungkin tidak terlihat berguna .
  3. Pengguna yang ingin membuat objeknya iterabledapat dengan mudah melakukannya, dengan mendefinisikan Symbol.iteratorproperti.
  4. Spesifikasi ES6 juga menyediakan tipe Peta , yang secara iterable default dan memiliki beberapa keuntungan kecil lainnya dibandingkan menggunakan objek biasa sebagai Map.

Bahkan ada contoh yang diberikan untuk # 3 dalam dokumentasi referensi:

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

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

Mengingat bahwa objek dapat dengan mudah dibuat iterable, bahwa mereka sudah dapat diiterasi menggunakan for...in, dan bahwa ada kemungkinan tidak ada kesepakatan yang jelas tentang apa yang harus dilakukan oleh iterator objek default (jika apa yang dilakukannya dimaksudkan untuk entah bagaimana berbeda dari apa yang for...indilakukannya), tampaknya masuk akal cukup sehingga objek tidak dibuat iterablesecara default.

Perhatikan bahwa kode contoh Anda dapat ditulis ulang menggunakan 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"
    }
}

... atau Anda juga dapat membuat objek iterablesesuai keinginan dengan melakukan sesuatu seperti berikut (atau Anda dapat membuat semua objek iterabledengan menetapkannya 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
    };
};

Anda dapat memainkannya di sini (di Chrome, sewa): http://jsfiddle.net/rncr3ppz/5/

Sunting

Dan sebagai tanggapan atas pertanyaan Anda yang telah diperbarui, ya, dimungkinkan untuk mengonversi sebuah iterablemenjadi larik, menggunakan operator sebaran di ES6.

Namun, ini sepertinya belum berfungsi di Chrome, atau setidaknya saya tidak bisa membuatnya berfungsi di jsFiddle saya. Secara teori, ini harus sesederhana:

var array = [...myIterable];
aroth
sumber
Mengapa tidak lakukan saja obj[Symbol.iterator] = obj[Symbol.enumerate]di contoh terakhir Anda?
Bergi
@Bergi - Karena saya tidak melihatnya di dokumen (dan saya tidak melihat properti yang dijelaskan di sini ). Meskipun satu argumen yang mendukung untuk mendefinisikan iterator secara eksplisit adalah bahwa hal itu membuatnya mudah untuk menegakkan urutan iterasi tertentu, jika diperlukan. Jika urutan iterasi tidak penting (atau jika urutan default baik-baik saja) dan pintasan satu baris berfungsi, ada sedikit alasan untuk tidak mengambil pendekatan yang lebih ringkas.
Apakah
Ups, [[enumerate]]bukan simbol yang terkenal (@@ enumerate) tetapi metode internal. Saya harus menjadiobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi
Apa gunanya semua tebakan ini, jika proses diskusi yang sebenarnya didokumentasikan dengan baik? Sangat aneh bahwa Anda akan mengatakan "Jadi oleh karena itu asumsi saya adalah bahwa for ... of construct sedang ditambahkan untuk mengatasi kekurangan yang terkait dengan for ... in construct." Tidak. Itu ditambahkan untuk mendukung cara umum untuk melakukan iterasi atas apa pun, dan merupakan bagian dari serangkaian luas fitur baru termasuk iterable itu sendiri, generator, serta peta dan set. Ini hampir tidak dimaksudkan sebagai pengganti atau peningkatan for...in, yang memiliki tujuan berbeda - untuk beralih di seluruh properti suatu objek.
2
Poin bagusnya menekankan lagi bahwa tidak setiap benda adalah kumpulan. Objek telah digunakan seperti itu untuk waktu yang lama, karena itu sangat nyaman, tetapi pada akhirnya, itu bukan koleksi. Itulah yang kami miliki Mapuntuk saat ini.
Felix Kling
9

Objects tidak mengimplementasikan protokol iterasi dalam Javascript untuk alasan yang sangat bagus. Ada dua tingkat di mana properti objek dapat diiterasi dalam JavaScript:

  • tingkat program
  • tingkat data

Iterasi Tingkat Program

Ketika Anda melakukan iterasi pada sebuah objek pada level program, Anda memeriksa sebagian dari struktur program Anda. Ini adalah operasi reflektif. Mari kita ilustrasikan pernyataan ini dengan tipe array, yang biasanya diulangi pada tingkat data:

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

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

Kami baru saja memeriksa tingkat program xs. Karena array menyimpan urutan data, kami secara teratur hanya tertarik pada level data. for..injelas tidak masuk akal dalam kaitannya dengan array dan struktur "berorientasi data" lainnya dalam banyak kasus. Itulah alasan mengapa ES2015 diperkenalkan for..ofdan protokol yang dapat diulang.

Iterasi Tingkat Data

Apakah itu berarti bahwa kita dapat dengan mudah membedakan data dari level program dengan membedakan fungsi dari tipe primitif? Tidak, karena fungsi juga dapat berupa data dalam Javascript:

  • Array.prototype.sort misalnya mengharapkan suatu fungsi untuk melakukan algoritma pengurutan tertentu
  • Thunks seperti () => 1 + 2hanyalah pembungkus fungsional untuk nilai yang dievaluasi dengan malas

Selain itu nilai primitif dapat mewakili level program juga:

  • [].lengthmisalnya a Numbertetapi mewakili panjang sebuah array dan dengan demikian menjadi milik domain program

Artinya kita tidak bisa membedakan program dan level data hanya dengan memeriksa jenis.


Penting untuk dipahami bahwa penerapan protokol iterasi untuk objek Javascript lama yang biasa akan bergantung pada tingkat data. Namun seperti yang baru saja kita lihat, perbedaan yang dapat diandalkan antara data dan iterasi tingkat program tidak dimungkinkan.

Dengan Arrays perbedaan ini sepele: Setiap elemen dengan kunci seperti integer adalah elemen data. Objects memiliki fitur yang setara: enumerableDeskriptor. Tetapi apakah benar-benar disarankan untuk mengandalkan ini? Saya yakin tidak! Arti enumerabledeskriptornya terlalu kabur.

Kesimpulan

Tidak ada cara yang berarti untuk mengimplementasikan protokol iterasi untuk objek, karena tidak setiap objek adalah kumpulan.

Jika properti objek dapat diulang secara default, program dan tingkat data akan tercampur. Karena setiap tipe komposit dalam Javascript didasarkan pada objek biasa, ini akan berlaku untuk Arraydan Mapjuga.

for..in, Object.keys, Reflect.ownKeysDll dapat digunakan untuk kedua refleksi dan data iterasi, perbedaan yang jelas secara teratur tidak mungkin. Jika Anda tidak berhati-hati, Anda akan berakhir dengan cepat dengan pemrograman meta dan ketergantungan yang aneh. The Maptipe data abstrak secara efektif mengakhiri conflating program dan tingkat data. Saya yakin Mapini adalah pencapaian paling signifikan di ES2015, meskipun Promisejauh lebih menarik.


sumber
3
+1, menurut saya "Tidak ada cara yang berarti untuk mengimplementasikan protokol iterasi untuk objek, karena tidak setiap objek adalah kumpulan." meringkasnya.
Charlie Schliesser
1
Saya tidak berpikir itu argumen yang bagus. Jika objek Anda bukan koleksi, mengapa Anda mencoba mengulanginya? Tidak masalah bahwa tidak semua objek adalah koleksi, karena Anda tidak akan mencoba mengulang yang bukan.
BT
Sebenarnya, setiap objek adalah sebuah koleksi, dan tidak tergantung pada bahasanya untuk memutuskan apakah koleksinya koheren atau tidak. Array dan Peta juga dapat mengumpulkan nilai yang tidak terkait. Intinya adalah Anda dapat mengulang kunci objek apa pun terlepas dari penggunaannya, jadi Anda selangkah lagi untuk mengulang nilainya. Jika Anda berbicara tentang bahasa yang secara statis mengetik nilai array (atau kumpulan lainnya), Anda dapat berbicara tentang batasan tersebut, tetapi bukan JavaScript.
Manngo
Argumen bahwa setiap objek bukanlah koleksi tidak masuk akal. Anda mengasumsikan bahwa iterator hanya memiliki satu tujuan (untuk mengulang koleksi). Iterator default pada sebuah objek akan menjadi iterator dari properti objek, tidak peduli apa yang direpresentasikan oleh properti tersebut (apakah koleksi atau yang lainnya). Seperti yang dikatakan Manngo, jika objek Anda tidak mewakili sebuah koleksi, maka terserah programmer untuk tidak memperlakukannya seperti koleksi. Mungkin mereka hanya ingin mengulang properti pada objek untuk beberapa keluaran debug? Ada banyak alasan lain selain koleksi.
jfriend00
8

Saya kira pertanyaan harus "mengapa tidak ada built-in objek iterasi?

Menambahkan iterabilitas ke objek itu sendiri dapat menimbulkan konsekuensi yang tidak diinginkan, dan tidak, tidak ada cara untuk menjamin keteraturan, tetapi menulis iterator sesederhana

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

Kemudian

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

a 1
b 2

sumber
1
terima kasih torazaburo. saya telah merevisi pertanyaan saya. Saya ingin melihat contoh penggunaan [Symbol.iterator]serta jika Anda dapat memperluas konsekuensi yang tidak diinginkan tersebut.
boombox
4

Anda dapat dengan mudah membuat semua objek dapat diulang secara global:

Object.defineProperty(Object.prototype, Symbol.iterator, {
    enumerable: false,
    value: function * (){
        for(let key in this){
            if(this.hasOwnProperty(key)){
                yield [key, this[key]];
            }
        }
    }
});
Jack Slocum
sumber
3
Jangan menambahkan metode ke objek asli secara global. Ini adalah ide buruk yang akan menggigit Anda, dan siapa pun yang menggunakan kode Anda, di pantat.
BT
2

Ini adalah pendekatan terbaru (yang berfungsi di chrome canary)

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

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

Ya entriessekarang adalah metode yang merupakan bagian dari Objek :)

edit

Setelah melihat lebih dalam, sepertinya Anda bisa melakukan hal berikut

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

jadi Anda sekarang bisa melakukan ini

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

edit2

Kembali ke contoh awal Anda, jika Anda ingin menggunakan metode prototipe di atas, akan seperti ini

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);
    }
}
Chad Scira
sumber
2

Saya juga merasa terganggu dengan pertanyaan ini.

Kemudian saya mendapatkan ide untuk menggunakan Object.entries({...}), ini mengembalikan yang Arraymana adalahIterable .

Juga, Dr. Axel Rauschmayer memposting jawaban yang sangat bagus tentang ini. Lihat Mengapa benda biasa TIDAK dapat diulang

Romko
sumber
0

Secara teknis, ini bukanlah jawaban atas pertanyaan mengapa? tetapi saya telah mengadaptasi jawaban Jack Slocum di atas berdasarkan komentar BT menjadi sesuatu yang dapat digunakan untuk membuat Object dapat berulang.

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);

Tidak senyaman yang seharusnya, tetapi dapat diterapkan, terutama jika Anda memiliki banyak objek:

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

Dan, karena setiap orang berhak mendapatkan opini, saya tidak bisa melihat mengapa Object juga belum dapat diulang. Jika Anda dapat mem-polyfill seperti di atas, atau gunakanfor … in maka saya tidak dapat melihat argumen sederhana.

Salah satu saran yang mungkin adalah bahwa apa yang dapat di-iterable adalah jenis objek, jadi ada kemungkinan bahwa iterable telah dibatasi pada subset objek untuk berjaga-jaga jika beberapa objek lain meledak dalam upaya tersebut.

Manngo
sumber