Dlaczego te fragmenty kodu JavaScript zachowują się inaczej, mimo że oba napotykają błąd?

107

var a = {}
var b = {}

try{
  a.x.y = b.e = 1 // Uncaught TypeError: Cannot set property 'y' of undefined
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  a.x.y.z = b.e = 1 // Uncaught TypeError: Cannot read property 'y' of undefined
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

Kevin Askin
źródło
3
@NinaScholz: Nie rozumiem. Nie ma błędu składni; więc założyłbym to b.z = 1i b.e = 1najpierw wykonałbym (biorąc pod uwagę prawostronne skojarzenie =), a następnie a.x.y.z = ...wykonałby i zawiódł; dlaczego bprzypisanie mija w jednym przypadku, a nie w drugim?
Amadan
3
@NinaScholz Zgadzamy się, że nieruchomość ynie istnieje a.x; ale to prawda w obu przypadkach. Dlaczego uniemożliwia przypisanie po prawej stronie w drugim przypadku, a nie w pierwszym? Czym różni się kolejność wykonywania? (Wspomniałem o błędzie składniowym, ponieważ czas wystąpienia błędu składniowego bardzo różni się od czasu błędu wykonania).
Amadan
@Amadan po uruchomieniu kodu pojawi się błąd, a następnie ponownie wpisz nazwę zmiennej, aby zobaczyć wartość
Code Maniac
2
Znaleziono to, opisując, w jaki sposób Javascript wykonuje
Solomon Tam,
2
Jest to interesujące z teoretycznego punktu widzenia, ale zdecydowanie należy to do kategorii nieoczekiwanego zachowania „dlatego nie piszesz takiego kodu”.
John Montgomery

Odpowiedzi:

152

Właściwie, jeśli poprawnie przeczytasz komunikat o błędzie, przypadek 1 i przypadek 2 generują różne błędy.

Sprawa a.x.y:

Nie można ustawić właściwości „y” na wartość undefined

Sprawa a.x.y.z:

Nie można odczytać właściwości „y” wartości undefined

Chyba najlepiej opisać to krok po kroku prostym angielskim.

Przypadek 1

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `b`, gets {}
   *  4. Set `b.z` to 1, returns 1
   *  5. Set `a.x.y` to return value of `b.z = 1`
   *  6. Throws "Cannot **set** property 'y' of undefined"
   */
  a.x.y = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

Przypadek 2

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `a.x.y`, throws "Cannot **read** property 'y' of undefined".
   */
  a.x.y.z = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

W komentarzach Solomon Tam znalazł dokumentację ECMA dotyczącą operacji przypisania .

yqlim
źródło
57

Kolejność operacji jest jaśniejsza, gdy używasz operatora przecinka w notacji nawiasów, aby zobaczyć, które części są wykonywane, gdy:

var a = {}
var b = {}

try{
 // Uncaught TypeError: Cannot set property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  // Uncaught TypeError: Cannot read property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    [console.log('z'), 'z']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

Patrząc na specyfikację :

Produkcja AssignmentExpression : LeftHandSideExpression = AssignmentExpressionjest oceniana w następujący sposób:

  1. Niech lref będzie wynikiem obliczania LeftHandSideExpression.

  2. Niech rref będzie wynikiem obliczania AssignmentExpression.

  3. Niech rval będzie GetValue(rref).

  4. Rzuć wyjątek SyntaxError, jeśli ... (nieistotne)

  5. Zadzwoń PutValue(lref, rval).

PutValueco rzuca TypeError:

  1. Niech O będzie ToObject(base).

  2. Jeśli wynik wywołania [[CanPut]]wewnętrznej metody O z argumentem P jest fałszywy, to

    za. Jeśli Throw ma wartość true, zgłoś wyjątek TypeError.

Nic nie może być przypisane do właściwości undefined- [[CanPut]]wewnętrzna metoda undefinedzawsze zwraca false.

Innymi słowy: tłumacz analizuje lewą stronę, a następnie prawą stronę, a następnie zgłasza błąd, jeśli nie można przypisać właściwości po lewej stronie.

Kiedy to zrobisz

a.x.y = b.e = 1

Lewa strona jest pomyślnie analizowana do momentu PutValuewywołania; fakt, że .xwłaściwość jest oceniana jako, undefinedjest brany pod uwagę dopiero po przeanalizowaniu prawej strony. Interpreter widzi to jako „Przypisz jakąś wartość do właściwości„ y ”z undefined”, a przypisanie do właściwości undefinedtylko wrzuca do środka PutValue.

W przeciwieństwie:

a.x.y.z = b.e = 1

Interpreter nigdy nie dochodzi do punktu, w którym próbuje przypisać zwłaściwość, ponieważ najpierw musi rozstrzygnąć a.x.ywartość. Jeśli zostanie a.x.yrozstrzygnięty na wartość (nawet do undefined), byłoby OK - błąd zostałby wrzucony do środka, PutValuejak powyżej. Jednak dostęp do obiektu a.x.y generuje błąd, ponieważ ynie można uzyskać dostępu do właściwości undefined.

CertainPerformance
źródło
20
Niezła sztuczka z operatorem przecinkowym - nigdy nie pomyślałem, aby użyć go w ten sposób (tylko do debugowania, oczywiście)!
ecraig12345
2
s / parse / assessment /
Bergi
3

Rozważ następujący kod:

var a = {};
a.x.y = console.log("evaluating right hand side"), 1;

Ogólny zarys kroków wymaganych do wykonania kodu jest następujący ref :

  1. Oceń lewą stronę. Należy pamiętać o dwóch rzeczach:
    • Obliczanie wyrażenia to nie to samo, co uzyskiwanie wartości wyrażenia.
    • Oceny właściwości dostępowe ref np a.x.yzwraca odniesienia ref składający się z wartości bazowej a.x(nieokreślony) i nazwa odniesienia ( y).
  2. Oceń prawą stronę.
  3. Uzyskaj wartość wyniku uzyskanego w kroku 2.
  4. Ustaw wartość odniesienia uzyskanego w kroku 1 na wartość uzyskaną w kroku 3, czyli ustaw właściwość yundefined na wartość. Powinno to spowodować zgłoszenie wyjątku TypeError ref .
Salman A
źródło