JavaScript'te Nesneler Neden Yinelenemez?


87

Nesneler neden varsayılan olarak yinelenemez?

Nesneleri yinelemeyle ilgili her zaman sorular görüyorum, yaygın çözüm bir nesnenin özelliklerini yinelemek ve bir nesne içindeki değerlere bu şekilde erişmektir. Bu o kadar yaygın görünüyor ki, nesnelerin neden yinelenemez olduğunu merak etmeme neden oluyor.

ES6 gibi ifadelerin for...ofvarsayılan olarak nesneler için kullanılması iyi olur. Bu özellikler yalnızca nesneleri içermeyen özel "yinelenebilir nesneler" için mevcut olduğundan {}, bunu kullanmak istediğimiz nesneler için çalışmasını sağlamak için çemberlerden geçmemiz gerekir.

For ... of ifadesi, yinelenebilir nesneler (Array, Map, Set, arguments nesnesi vb. Dahil) üzerinde yinelenen bir döngü oluşturur ...

Örneğin bir ES6 oluşturucu işlevi kullanarak :

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

Yukarıdaki kod, Firefox'ta ( ES6'yı destekleyen ) kodu çalıştırdığımda verileri beklediğim sırayla doğru şekilde günlüğe kaydeder :

için hacky çıktısı ... of

Varsayılan olarak, {}nesneler yinelenemez, ancak neden? Dezavantajlar, nesnelerin yinelenebilir olmasının potansiyel faydalarından daha ağır basıyor mu? Bununla ilgili sorunlar nelerdir?

Buna ek olarak, {}nesneleri "Array benzeri" koleksiyonları ve farklı gibi "iterable nesneleri" NodeList, HtmlCollectionve argumentsbunlar Dizileri dönüştürülebilir olamaz.

Örneğin:

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

veya Dizi yöntemleriyle kullanılabilir:

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

Yukarıda sahip olduğum soruların yanı sıra {}, özellikle [Symbol.iterator]. Bu, bu yeni {}"yinelenebilir nesnelerin" gibi ifadeleri kullanmasına izin vermelidir for...of. Ayrıca, nesneleri yinelenebilir hale getirmenin onların Dizilere dönüştürülmesine izin verip vermediğini merak ediyorum.

Aşağıdaki kodu denedim, ancak bir 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
Symbol.iteratorÖzelliği olan her şey yinelenebilir. Yani sadece o mülkü uygulamanız gerekir. Bir olası nesneler iterable bu ima edeceğini olabilir değil neden için açıklama her şeyi her şeyi (tabii ki ilkel hariç) bir nesne olduğundan, iterable oldu. Bununla birlikte, bir işlev veya normal ifade nesnesi üzerinde yineleme yapmak ne anlama gelir?
Felix Kling

7
Buradaki asıl sorunuz nedir? ECMA neden aldığı kararları aldı?
Steve Bennett

3
Nesnelerin özelliklerinin HİÇBİR garantili sıralaması olmadığından, bu, tahmin edilebilir bir sıraya sahip olmasını beklediğiniz yinelenebilir tanımından kopar mı acaba?
jfriend00

2
"Neden" konusunda güvenilir bir cevap almak için esdiscuss.org'a sormalısınız
Felix Kling

1
@FelixKling - ES6 ile ilgili yazı mı? Muhtemelen hangi sürümden bahsettiğinizi söylemek için düzenlemelisiniz çünkü "ECMAScript’in gelecek sürümü" zamanla pek iyi çalışmıyor.
jfriend00

Yanıtlar:


42

Bunu bir deneyeceğim. ECMA ile bağlantım olmadığına ve karar verme süreçlerine dair hiçbir görünürlüğüm olmadığına dikkat edin, bu nedenle neden hiçbir şey yapıp yapmadıklarını kesin olarak söyleyemem . Ancak, varsayımlarımı ifade edeceğim ve en iyi atışımı yapacağım.

1. Neden for...ofilk olarak bir yapı eklemelisiniz ?

JavaScript zaten for...inbir nesnenin özelliklerini yinelemek için kullanılabilecek bir yapı içerir . Ancak, bir nesnedeki tüm özellikleri numaralandırdığı ve yalnızca basit durumlarda tahmin edilebilir şekilde çalışma eğiliminde olduğu için bu gerçekten bir forEach döngüsü değildir .

Daha karmaşık durumlarda (diziler dahil, kullanımının cesaretinin kırıldığı veyafor...in bir diziyle doğru bir şekilde kullanılması için gereken güvenlik önlemleri tarafından tamamen karıştırıldığı durumlarda) bozulur . hasOwnProperty(Diğer şeylerin yanı sıra) kullanarak bunun üstesinden gelebilirsiniz , ancak bu biraz hantal ve uygunsuz.

Dolayısıyla benim varsayımım, yapının for...ofyapıyla ilişkili eksiklikleri gidermek for...inve şeyleri yinelerken daha fazla fayda ve esneklik sağlamak için ekleniyor olmasıdır . İnsanlar genellikle herhangi bir koleksiyona uygulanabilen ve olası herhangi bir bağlamda mantıklı sonuçlar üreten for...inbir forEachdöngü gibi davranma eğilimindedir , ancak olan bu değildir. for...ofDöngü giderdiği.

Ayrıca, mevcut ES5 kodunun ES6 altında çalışmasının ve ES5 altında olduğu gibi aynı sonucu üretmesinin önemli olduğunu varsayıyorum, bu nedenle, örneğin yapının davranışında kırma değişiklikleri yapılamaz for...in.

2. Nasıl for...ofçalışır?

Referans belgeleri bu bölümü için yararlıdır. Özellikle, özelliği iterabletanımlıyorsa bir nesne dikkate alınır Symbol.iterator.

Özellik tanımı, koleksiyondaki öğeleri tek tek döndüren ve getirilecek daha fazla öğe olup olmadığını gösteren bir bayrak ayarlayan bir işlev olmalıdır. Bazı nesne türleri için önceden tanımlanmış uygulamalar sağlanır for...ofve yineleyici işlevi için basit temsilcilerin kullanılması nispeten açıktır .

Bu yaklaşım, kendi yineleyicilerinizi sağlamayı çok kolaylaştırdığı için kullanışlıdır. Yaklaşımın, daha önce hiç bulunmayan bir mülkü tanımlamaya dayanması nedeniyle pratik sorunlar ortaya koymuş olabileceğini söyleyebilirim, ancak bunun böyle olmadığını söyleyebilirim, çünkü siz kasıtlı olarak aramaya gitmedikçe, yeni mülk esasen göz ardı edilir. for...indöngülerde anahtar olarak bulunmaz , vb.). Yani durum bu değil.

Pratik olmayan konular bir yana, tüm nesneleri yeni bir önceden tanımlanmış özellik ile başlatmak veya örtük olarak "her nesne bir koleksiyondur" demek kavramsal olarak tartışmalı olarak değerlendirilebilirdi.

3. Nesneler neden varsayılan olarak iterablekullanmıyor for...of?

Benim tahminim bu bir kombinasyonu olmasıdır:

  1. Tüm nesneleri iterablevarsayılan olarak yapmak, daha önce hiç bulunmayan bir özelliği eklediğinden veya bir nesnenin (zorunlu olarak) bir koleksiyon olmadığı için kabul edilemez olarak değerlendirilebilir. Felix'in belirttiği gibi, "bir işlev veya normal ifade nesnesi üzerinde yineleme yapmak ne anlama gelir"?
  2. Basit nesneler kullanılarak zaten yinelenebilir for...inve yerleşik bir yineleyici uygulamasının mevcut for...indavranıştan farklı / daha iyi ne yapabileceği açık değildir . Dolayısıyla, # 1 yanlış olsa ve özelliği eklemek kabul edilebilir olsa bile, yararlı olarak görülmemiş olabilir .
  3. Nesnelerini yapmak isteyen kullanıcılar iterable, Symbol.iteratorözelliği tanımlayarak bunu kolayca yapabilirler .
  4. ES6 Spec ayrıca sağlayan Harita türünü olduğu iterable varsayılan ve bir düz nesneyi kullanımına kıyasla bazı diğer küçük avantajları vardır Map.

Referans belgelerde # 3 için verilen bir örnek bile var:

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

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

Nesnelerin kolayca yapılabileceği iterable, kullanılarak yinelenebilecekleri for...inve varsayılan bir nesne yineleyicinin ne yapması gerektiği konusunda muhtemelen net bir fikir birliği olmadığı göz önüne alındığında (eğer yaptığı şey, yapandan bir şekilde farklı olacaksa for...in), makul görünmektedir. nesnelerin iterablevarsayılan olarak yapılmaması yeterlidir .

Örnek kodunuzun aşağıdakiler kullanılarak yeniden yazılabileceğini unutmayın 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"
    }
}

... veya iterableaşağıdaki gibi bir şey yaparak nesnenizi istediğiniz şekilde de oluşturabilirsiniz (veya bunun yerine atayarak tüm nesneleri oluşturabilirsiniz ):iterableObject.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
    };
};

Bununla burada oynayabilirsiniz (Chrome'da, kiralanabilir): http://jsfiddle.net/rncr3ppz/5/

Düzenle

Ve güncellenmiş sorunuza yanıt olarak, evet, ES6'daki yayılma operatörünüiterable kullanarak bir diziye dönüştürmek mümkündür .

Ancak, bu henüz Chrome'da çalışmıyor gibi görünüyor veya en azından jsFiddle'ımda çalışmasını sağlayamıyorum. Teorik olarak şu kadar basit olmalıdır:

var array = [...myIterable];

Neden obj[Symbol.iterator] = obj[Symbol.enumerate]son örneğinizde yapmıyorsunuz ?
Bergi

@Bergi - Çünkü bunu belgelerde görmedim (ve burada açıklanan özelliği göremiyorum ). Yineleyiciyi açıkça tanımlamanın lehine olan bir argüman, gerekli olması durumunda belirli bir yineleme sırasını uygulamaya koymayı kolaylaştırmasıdır. Yineleme sırası önemli değilse (veya varsayılan sıra uygunsa) ve tek satırlık kısayol çalışıyorsa, daha kısa yaklaşımı benimsememek için çok az neden vardır.
aroth

Hata, [[enumerate]]iyi bilinen bir sembol (@@ numaralandır) değil, dahili bir yöntemdir. Ben olurduobj[Symbol.iterator] = function(){ return Reflect.enumerate(this) }
Bergi

Gerçek tartışma süreci iyi bir şekilde belgelendiğinde tüm bu tahminler ne işe yarar? "Öyleyse benim varsayımım, for ... in yapısıyla ilişkili eksiklikleri gidermek için for ... yapısının eklenmesi." Demeniz çok garip. Hayır. Herhangi bir şey üzerinde yinelemenin genel bir yolunu desteklemek için eklendi ve yinelenenlerin kendileri, oluşturucular, haritalar ve kümeler dahil olmak üzere çok çeşitli yeni özelliklerin bir parçasıdır. Bir nesnenin özelliklerinifor...in yinelemek için farklı bir amacı olan bir değiştirme veya yükseltme anlamına gelmez .

2
Her nesnenin bir koleksiyon olmadığını tekrar vurgulamak güzel bir nokta. Nesneler uzun zamandır bu şekilde kullanıldı, çünkü çok kullanışlıydı, ancak sonuçta bunlar gerçekten koleksiyon değiller. MapŞimdilik elimizde olan bu .
Felix Kling

9

Objectjavascript'te yineleme protokollerini çok iyi nedenlerle uygulamayın. JavaScript'te nesne özelliklerinin yinelenebileceği iki düzey vardır:

  • program seviyesi
  • veri seviyesi

Program Seviyesi Yineleme

Program düzeyinde bir nesnenin üzerinde yineleme yaptığınızda, programınızın yapısının bir bölümünü incelersiniz. Yansıtıcı bir işlemdir. Bu ifadeyi, genellikle veri düzeyinde yinelenen bir dizi türüyle gösterelim:

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

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

Sadece program seviyesini inceledik xs. Diziler veri dizilerini depoladığından, düzenli olarak yalnızca veri düzeyiyle ilgileniyoruz. for..inÇoğu durumda diziler ve diğer "veri yönelimli" yapılarla bağlantılı olmanın hiçbir anlamı olmadığı açıktır. ES2015'in tanıtılmasının for..ofve yinelenebilir protokolün nedeni budur .

Veri Seviyesi Yineleme

Bu, fonksiyonları ilkel türlerden ayırarak verileri program seviyesinden kolayca ayırt edebileceğimiz anlamına mı geliyor? Hayır, çünkü işlevler Javascript'te de veri olabilir:

  • Array.prototype.sort örneğin bir işlevin belirli bir sıralama algoritması gerçekleştirmesini bekler
  • Thunks gibi () => 1 + 2şeyler, tembel olarak değerlendirilen değerler için işlevsel sarmalayıcılardır

İlkel değerlerin yanı sıra, program seviyesini de temsil edebilir:

  • [].lengthörneğin bir Numberama bir dizinin uzunluğunu temsil eder ve bu nedenle program alanına aittir.

Bu, programı ve veri seviyesini yalnızca türleri kontrol ederek ayırt edemeyeceğimiz anlamına gelir.


Düz eski Javascript nesneleri için yineleme protokollerinin uygulanmasının veri düzeyine bağlı olduğunu anlamak önemlidir. Ancak az önce gördüğümüz gibi, veri ve program düzeyinde yineleme arasında güvenilir bir ayrım yapmak mümkün değildir.

Bununla birlikte, Arraybu ayrım önemsizdir: Tamsayı benzeri bir anahtara sahip her öğe bir veri öğesidir. Objects eşdeğer bir özelliğe sahiptir: enumerableTanımlayıcı. Ama buna güvenmeniz gerçekten tavsiye edilir mi? Ben öyle olmadığına inanıyorum! enumerableTanımlayıcının anlamı çok bulanık.

Sonuç

Nesneler için yineleme protokollerini uygulamanın anlamlı bir yolu yoktur, çünkü her nesne bir koleksiyon değildir.

Nesne özellikleri varsayılan olarak yinelenebiliyorsa, program ve veri düzeyi karışıktır. Javascript'teki her bileşik tür düz nesnelere dayandığından, bu hem için Arrayhem Mapde geçerlidir .

for..in, Object.keys, Reflect.ownKeysVs., hem yansıma ve veri tekrarlama için kullanılabilir, açık bir ayrım düzenli mümkün değildir. Dikkatli olmazsanız, hızlı bir şekilde meta programlama ve garip bağımlılıklarla karşılaşırsınız. MapSoyut veri türü etkin bir program ve veri seviyesinin conflating sona erer. Çok daha heyecan verici Mapolsa da ES2015'teki en önemli başarı olduğuna inanıyorum Promise.


3
+1, bence "Nesneler için yineleme protokollerini uygulamanın anlamlı bir yolu yoktur, çünkü her nesne bir koleksiyon değildir." özetliyor.
Charlie Schliesser

1
Bunun iyi bir argüman olduğunu sanmıyorum. Nesneniz bir koleksiyon değilse, neden onun üzerinden geçmeye çalışıyorsunuz? Her nesnenin bir koleksiyon olmaması önemli değil, çünkü olmayanları yinelemeye çalışmayacaksınız.
BT

Aslında, her nesne olan bir koleksiyon ve toplama tutarlı olup olmadığına karar vermek kadar dile değildir. Diziler ve Haritalar, ilgisiz değerleri de toplayabilir. Önemli olan, kullanımlarından bağımsız olarak herhangi bir nesnenin anahtarlarını yineleyebilmenizdir, böylece değerleri üzerinde yinelemekten bir adım uzaktasınız. Dizi (veya başka bir koleksiyon) değerlerini statik olarak yazan bir dilden bahsediyor olsaydınız, bu tür kısıtlamalar hakkında konuşabilirdiniz, ancak JavaScript'ten bahsedemezdiniz.
Manngo

Her nesnenin bir koleksiyon olmadığı argümanı mantıklı değil. Bir yineleyicinin yalnızca tek bir amacı olduğunu varsayarsınız (bir koleksiyonu yinelemek). Bir nesnedeki varsayılan yineleyici, bu özelliklerin neyi temsil ettiğine bakılmaksızın (bir koleksiyon veya başka bir şey) nesnenin özelliklerinin bir yineleyicisi olacaktır. Manngo'nun dediği gibi, nesneniz bir koleksiyonu temsil etmiyorsa, onu bir koleksiyon gibi ele almamak programcıya kalmıştır. Belki sadece bazı hata ayıklama çıktıları için nesnedeki özellikleri yinelemek istiyorlardır? Bir koleksiyon dışında pek çok neden var.
jfriend00

8

Sanırım soru şu olmalı "neden yerleşik nesne yinelemesi yok?

Nesnelerin kendilerine yinelenebilirlik eklemek, muhtemelen istenmeyen sonuçlara yol açabilir ve hayır, düzeni garanti etmenin bir yolu yoktur, ancak bir yineleyici yazmak kadar basittir.

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

Sonra

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

a 1
b 2

1
teşekkürler torazaburo. sorumu revize ettim. [Symbol.iterator]Bu istenmeyen sonuçları genişletebilirmiş gibi kullanarak bir örnek görmeyi çok isterim.
boombox

4

Tüm nesneleri global olarak kolayca yinelenebilir hale getirebilirsiniz:

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

3
Yerel nesnelere genel olarak yöntemler eklemeyin. Bu, sizi ve kodunuzu kullanan herkesi kıçından ısıracak korkunç bir fikir.
BT

2

Bu, en son yaklaşımdır (chrome canary'de işe yarar)

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

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

Evet entriesartık Object'in bir parçası olan bir yöntem :)

Düzenle

Daha fazlasını inceledikten sonra, aşağıdakileri yapabilirsiniz gibi görünüyor

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

yani şimdi bunu yapabilirsin

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

düzenle2

Orijinal örneğinize geri dönelim, yukarıdaki prototip yöntemini kullanmak isterseniz, bunu beğenirsiniz.

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

2

Ben de bu sorudan rahatsız oldum.

Sonra bir kullanma fikri buldum Object.entries({...}), bir Arrayolan bir olan bir Iterable.

Ayrıca Dr. Axel Rauschmayer bu konuda mükemmel bir yanıt yayınladı. Bkz. Düz nesneler neden yinelenemez


0

Teknik olarak, bu neden sorusunun cevabı değil ? ancak BT'nin yorumları ışığında Jack Slocum'un yukarıdaki cevabını, bir Nesneyi yinelenebilir hale getirmek için kullanılabilecek bir şeye uyarladım.

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

Olması gerektiği kadar kullanışlı değil, ancak uygulanabilir, özellikle birden fazla nesneniz varsa:

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

Ve herkesin bir görüş alma hakkı olduğu için, Nesnelerin neden yinelenemediğini de anlayamıyorum. Bunları yukarıdaki gibi for … inçoklu doldurabilirseniz veya kullanırsanız, o zaman basit bir argüman göremiyorum.

Olası bir öneri, yinelenebilir olanın bir nesne türü olduğudur , bu nedenle, denemede bazı diğer nesnelerin patlaması durumunda yinelenebilirin bir nesne alt kümesiyle sınırlı olması mümkündür.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.