CodeMash 2012 için 'Wat' konuşmasında bahsedilen bu tuhaf JavaScript davranışlarının açıklaması nedir?


753

CodeMash 2012 'Wat' konuşma temelde Ruby ve JavaScript ile birkaç tuhaf tuhaflıklar işaret ediyor.

Ben de sonuçlarının bir JSFiddle yaptık http://jsfiddle.net/fe479/9/ .

JavaScript'e özgü davranışlar (Ruby'yi bilmediğim gibi) aşağıda listelenmiştir.

JSFiddle'da bazı sonuçlarımın videodakilerle uyuşmadığını gördüm ve neden olduğundan emin değilim. Bununla birlikte, her durumda JavaScript'in perde arkasında nasıl çalıştığını bilmek istiyorum.

Empty Array + Empty Array
[] + []
result:
<Empty String>

+JavaScript'te dizilerle kullanıldığında operatörü çok merak ediyorum . Bu, videonun sonucuyla eşleşir.

Empty Array + Object
[] + {}
result:
[Object]

Bu, videonun sonucuyla eşleşir. Burada neler oluyor? Bu neden bir obje. Ne geliyor +operatör mı?

Object + Empty Array
{} + []
result:
[Object]

Bu videoyla eşleşmiyor. Video sonucun 0 olduğunu öne sürerken, [Object] elde ederim.

Object + Object
{} + {}
result:
[Object][Object]

Bu da videoyla eşleşmiyor ve bir değişkenin çıktısı nasıl iki nesneye neden oluyor? Belki JSFiddle'ım yanlış.

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

Wat + 1 yapmak wat1wat1wat1wat1...

Bu sadece bir dize bir sayı çıkarmaya çalışırken NaN sonuçlarına yol açan basit bir davranış olduğundan şüpheleniyorum.


4
{} + [], Temelde burada açıkladığım gibi zor ve uygulamaya bağımlı olan tek şeydir, çünkü bir ifade veya ifade olarak ayrıştırılmasına bağlıdır. Hangi ortamda test yapıyorsunuz (Firefow ve Chrome'da 0 bekleniyor, ancak NodeJ'lerde "[object Object]" var)?
hugomg

1
Firefox 9.0.1'i Windows 7'de çalıştırıyorum ve JSFiddle bunu [Object]
NibblyPig

@missingno Düğümde 0 alıyorumJS REPL
OrangeDog

41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson

1
@missingno Buraya bir soru gönderdi , ama {} + {}.
Ionică Bizău

Yanıtlar:


1479

Gördüğünüz (ve görmeniz gereken) sonuçların açıklamalarının listesi. Kullandığım referanslar ECMA-262 standardına aittir .

  1. [] + []

    Toplama operatörünü kullanırken, önce hem sol hem de sağ işlenenler ilkellere dönüştürülür ( §11.6.1 ). §9.1 uyarınca , bir nesneyi (bu durumda bir dizi) bir ilkeye dönüştürmektoString() , geçerli bir yönteme sahip nesneler için çağrının sonucu olan varsayılan değerini döndürür object.toString()( §8.12.8 ). Diziler için bu çağrı ile aynıdır array.join()( §15.4.4.2 ). Boş bir diziye katılmak boş bir dizeyle sonuçlanır, bu nedenle toplama operatörünün # 7 no'lu adımı, boş dize olan iki boş dizenin birleşimini döndürür.

  2. [] + {}

    Benzer şekilde [] + [], her iki işlenen de ilkel olarak dönüştürülür. "Nesne nesneleri" (§15.2) için, bu yine object.toString()boş olmayan, tanımsız nesneler için "[object Object]"( §15.2.4.2 ) olan çağrının sonucudur .

  3. {} + []

    {}Burada bir nesne olarak çözümlenen, ancak bunun yerine (boş bir bloğu olarak değil §12.1 uzun bir ifade olarak bu ifadeyi zorla değil herkese açıktır ancak daha bu konuda daha geç en azından). Boş blokların dönüş değeri boştur, bu nedenle bu ifadenin sonucu ile aynıdır +[]. Tekli +operatör ( §11.4.6 ) geri döner ToNumber(ToPrimitive(operand)). Biz zaten bildiği gibi, ToPrimitive([])boş dize ve uygun §9.3.1 , ToNumber("")0'dır.

  4. {} + {}

    Önceki duruma benzer şekilde, birincisi {}boş dönüş değeri olan bir blok olarak ayrıştırılır. Yine +{}aynı ToNumber(ToPrimitive({}))ve ToPrimitive({})bir "[object Object]"(bakınız [] + {}). Sonuç almak için dizeye +{}başvurmamız gerekiyor . §9.3.1'deki adımları uygularken , sonuç olarak elde ederiz :ToNumber"[object Object]"NaN

    Dilbilgisi bir genişleme olarak Dize yorumlamak yapamıyorsanız StringNumericLiteral ardından sonucudur ToNumber olan NaN .

  5. Array(16).join("wat" - 1)

    Gereğince §15.4.1.1 ve §15.4.2.2 , Array(16)katılma argüman değerini almak için uzunluğu 16 ile yeni bir dizi oluşturur, §11.6.2 biz a hem işlenen dönüştürmek zorunda 5. ve 6. gösteri adımlar numarası kullanarak ToNumber. ToNumber(1)sadece 1 ( §9.3 ), buna karşın ToNumber("wat")yine NaNgöre §9.3.1 . Aşama 7 takip §11.6.2 , §11.6.3 belirlediğini

    Her iki işlenen de NaN ise, sonuç NaN olur .

    İçin argüman Yani Array(16).joinDİR NaN. §15.4.4.5 ( Array.prototype.join) 'i takiben ToString, "NaN"( §9.8.1 ) argümanını çağırmalıyız :

    Eğer m olan NaN , String dönmek "NaN".

    15.4.4.5'in 10. adımından sonra , "NaN"gördüğünüz sonuca eşit olan birleştirme ve boş dizenin 15 tekrarını alırız . Kullanırken "wat" + 1yerine "wat" - 1bağımsız değişken olarak, ek operatör dönüştürür 1yerine dönüştürülmesi için bir dizeye "wat"bir dizi, etkin bir çağrı bu yüzden Array(16).join("wat1").

{} + []Vaka için neden farklı sonuçlar gördüğünüze ilişkin olarak: Bir işlev bağımsız değişkeni olarak kullanırken, ifadeyi bir ExpressionStatement olmaya zorlarsınız , bu da {}boş blok olarak ayrıştırmayı imkansız hale getirir , bunun yerine boş bir nesne olarak ayrıştırılır değişmezi.


2
Öyleyse neden [] +1 => "1" ve [] -1 => -1?
Rob Elsner

4
@RobElsner []+1hemen hemen rhs operand []+[]ile aynı mantığı takip eder 1.toString(). İçin []-1bir açıklama bakınız "wat"-1unutmayın 5. nokta ToNumber(ToPrimitive([]))0 (nokta 3) 'dir.
Ventero

4
Bu açıklama eksik / birçok ayrıntıyı atlıyor. Örneğin, "bir nesneyi (bu durumda bir dizi) bir ilkeye dönüştürmek, varsayılan değerini döndürür; bu değer, geçerli bir toString () yöntemine sahip nesneler için object.toString () yönteminin çağrılmasının sonucudur; önce çağrılır, ancak dönüş değeri bir ilkel olmadığından (bir dizidir), bunun yerine [] öğesinin toString'i kullanılır. Bunun yerine gerçek derinlik açıklaması için bakmanızı tavsiye ederim 2ality.com/2012/01/object-plus-object.html
jahav 14:17

30

Bu, bir yanıttan çok bir yorumdur, ancak nedense sorunuza yorum yapamam. JSFiddle kodunuzu düzeltmek istedim. Ancak, bunu Hacker News'a gönderdim ve birisi burada yeniden göndermemi önerdi.

JSFiddle kodundaki sorun ({})(parantez içindeki parantezleri açma) {}(parantezlerin bir kod satırının başlamasıyla parantez açma ) ile aynı olmamasıdır . Yazarken Yani out({} + [])sen zorluyor {}yazarken değil mi duyulacak bir şey {} + []. Bu, Javascript'in genel 'vat'ının bir parçasıdır.

Temel fikir, JavaScript'in şu formların her ikisine de izin vermek istediği basit bir fikirdi:

if (u)
    v;

if (x) {
    y;
    z;
}

Bunu yapmak için, açılış ayracı üzerinde iki yorum yapılmıştır: 1. gerekli değildir ve 2. herhangi bir yerde görünebilir .

Bu yanlış bir hamleydi. Gerçek kod hiçbir yerin ortasında görünen bir açılış ayracı yoktur ve gerçek kod da ikinci formdan ziyade ilk formu kullandığında daha kırılgan olma eğilimindedir. (Son işimde yaklaşık her ay bir kez, kodumdaki değişiklikler çalışmadığında bir iş arkadaşının masasına çağrılırdım ve sorun, "if" öğesine kıvırcık eklemeden bir satır eklemeleriydi Sonunda sadece bir satır yazarken bile kıvırcık parantezlerin her zaman gerekli olduğu alışkanlığını benimsedim.)

Neyse ki birçok durumda eval (), JavaScript'in tam kalitesini çoğaltacaktır. JSFiddle kodu şöyle olmalıdır:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[Ayrıca uzun yıllar boyunca ilk defa document.writeln yazdım ve hem document.writeln () hem de eval () ile ilgili her şeyi yazarken biraz kirli hissediyorum.]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Katılmıyorum (bir çeşit): Geçmişte sıklıkla C'deki değişkenleri kapsamak için böyle bloklar kullandım . Bu alışkanlık, yığındaki değişkenlerin yer kapladığı gömülü C yapılırken bir süre geri alındı, bu yüzden artık ihtiyaç duyulmuyorsa, bloğun sonunda alanın serbest bırakılmasını istiyoruz. Ancak, ECMAScript yalnızca function () {} blokları içinde yer alır. Bu yüzden, kavramın yanlış olduğunu kabul etmeme rağmen JS'deki uygulamanın ( muhtemelen ) yanlış olduğunu kabul ediyorum .
Jess Telford

4
@JessTelford ES6'da, letblok kapsamındaki değişkenleri bildirmek için kullanabilirsiniz .
Oriol

19

Ventero'nun çözümünü ikinci olarak kullandım. İsterseniz +, işlenenlerini nasıl dönüştürdüğü hakkında daha ayrıntılı bilgi edinebilirsiniz .

Birinci aşama (§9.1): (ilkel değerler ilkellere iki işlenen dönüştürmek undefined, null, boolean, sayılar, dizi, diğer tüm değerler diziler ve fonksiyonları da dahil olmak üzere, nesneleridir). Bir işlenen zaten ilkel ise, işiniz bitti demektir. Değilse, bir nesnedir objve aşağıdaki adımlar gerçekleştirilir:

  1. Arayın obj.valueOf(). Bir ilkel döndürürse, işiniz bitti demektir. Doğrudan örnekleri Objectve diziler kendilerini döndürür, bu yüzden henüz işiniz bitmedi.
  2. Arayın obj.toString(). Bir ilkel döndürürse, işiniz bitti demektir. {}ve []her ikisi de bir dize döndürür, böylece işiniz biter.
  3. Aksi takdirde, a TypeError.

Tarihler için adım 1 ve 2 değiştirilir. Dönüşüm davranışını aşağıdaki gibi gözlemleyebilirsiniz:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Etkileşim ( Number()önce ilkele sonra sayıya dönüştürülür):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

İkinci adım (§11.6.1): İşlenenlerden biri bir dize ise, diğer işlenen de dizeye dönüştürülür ve sonuç iki dizeyi birleştirerek üretilir. Aksi takdirde, her iki işlenen de sayılara dönüştürülür ve sonuç eklenerek üretilir.

Dönüşüm sürecinin daha ayrıntılı açıklaması: “ JavaScript'te {} + {} nedir?


13

Spesifikasyona başvurabiliriz ve bu harika ve en doğrudur, ancak vakaların çoğu aşağıdaki ifadelerle daha anlaşılır bir şekilde açıklanabilir:

  • +ve -operatörler yalnızca ilkel değerlerle çalışır. Daha spesifik olarak +(toplama), dizgiler veya sayılarla çalışır ve +(tekli) ve -(çıkarma ve tekli) yalnızca sayılarla çalışır.
  • Bağımsız değişken olarak ilkel değer bekleyen tüm yerel işlevler veya işleçler, önce bu bağımsız değişkeni istenen ilkel türe dönüştürür. Herhangi bir nesnede bulunan valueOfveya ile yapılır toString. Bu tür işlevlerin veya işleçlerin nesneler üzerinde çağrıldığında hata atmamasının nedeni budur.

Yani şunu söyleyebiliriz:

  • [] + []aynıdır String([]) + String([])ile aynı olan '' + ''. Yukarıda +(ekleme) sayılar için de geçerli olduğunu belirttim, ancak JavaScript'te bir dizinin geçerli bir sayı temsili yok, bu nedenle dizelerin eklenmesi kullanılıyor.
  • [] + {}ile aynı String([]) + String({})olanla aynıdır'' + '[object Object]'
  • {} + []. Bu daha fazla açıklamayı hak ediyor (bakınız Ventero cevabı). Bu durumda, kıvırcık parantez bir nesne olarak değil, boş bir blok olarak kabul edilir, bu yüzden aynı olduğu ortaya çıkar +[]. Unary +yalnızca sayılarla çalışır, bu nedenle uygulama bir sayı çıkarmaya çalışır []. İlk olarak valueOf, diziler durumunda aynı nesneyi döndürdüğünü dener, bu nedenle son çare dener: bir toStringsonucun bir sayıya dönüştürülmesi. Biz olarak yazabilirsiniz +Number(String([]))aynı şekilde olduğu +Number('')aynı olan +0.
  • Array(16).join("wat" - 1)çıkarma -: bu yüzden aynı şey, sadece numaraları ile çalışır Array(16).join(Number("wat") - 1)gibi "wat"geçerli bir numaraya dönüştürülemez. Biz almak NaNve üzerinde herhangi bir aritmetik işlem NaNile sonuçları NaNelimizdeki yüzden: Array(16).join(NaN).

0

Daha önce paylaşılanları desteklemek.

Bu davranışın altında yatan neden kısmen JavaScript'in zayıf yazılan doğasından kaynaklanmaktadır. Örneğin, işlenen türlerine (int, string) ve (int int) dayalı iki olası yorum olduğu için 1 + “2” ifadesi belirsizdir:

  • Kullanıcı iki dizeyi birleştirmeyi amaçlar, sonuç: “12”
  • Kullanıcı iki sayı eklemek istiyor, sonuç: 3

Böylece değişen girdi türleri ile çıktı olanakları artar.

Ekleme algoritması

  1. İşlenenleri ilkel değerlere zorlama

JavaScript temel öğeleri dize, sayı, null, undefined ve boolean (Yakında ES6'da sembol geliyor). Başka herhangi bir değer bir nesnedir (örneğin, diziler, işlevler ve nesneler). Nesneleri ilkel değerlere dönüştürmek için zorlama işlemi şu şekilde açıklanmaktadır:

  • Object.valueOf () çağrıldığında ilkel bir değer döndürülürse, bu değeri döndürün, aksi takdirde devam edin

  • Object.toString () çağrıldığında ilkel bir değer döndürülürse, bu değeri döndürün, aksi takdirde devam edin

  • Bir TypeError atın

Not: Tarih değerleri için, sipariş valueOf öğesinden önceString işlevini çağırmaktır.

  1. Herhangi bir işlenen değeri bir dize ise, bir dize birleştirmesi yapın

  2. Aksi takdirde, her iki işleneni de sayısal değerlerine dönüştürün ve ardından bu değerleri ekleyin

JavaScript'teki türlerin çeşitli zorlama değerlerini bilmek, kafa karıştırıcı çıktıları daha net hale getirmeye yardımcı olur. Aşağıdaki baskı tablosuna bakın

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

Birden fazla + işlemi içeren çıktının ne olacağını belirlemek için JavaScript + operatörünün sol ilişkisel olduğunu bilmek de iyidir.

Böylece 1 + "2" nin kullanılması "12" verecektir, çünkü bir dize içeren herhangi bir ekleme her zaman dize birleştirme için varsayılan olacaktır.

Bu blog yazısında daha fazla örnek okuyabilirsiniz (sorumluluk reddi yazdım).

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.