Почему эти фрагменты JavaScript ведут себя по-разному, даже если оба обнаруживают ошибку?

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

Кевин Аскин
источник
3
@NinaScholz: Я не понимаю. Синтаксической ошибки нет; так что я предположил бы , что b.z = 1и b.e = 1выполнить первое (учитывая правую ассоциативность на =), а затем a.x.y.z = ...выполнить и не; почему bприсвоение выполняется в одном случае, а в другом - нет?
Амадан
3
@NinaScholz Мы согласны с тем, что собственность yне существует на a.x; но это верно в обоих случаях. Почему он предотвращает правостороннее присваивание во втором случае, но не в первом? Чем отличается порядок исполнения? (Я упомянул синтаксическую ошибку, потому что время синтаксической ошибки сильно отличается от времени ошибки времени выполнения.)
Амадан,
@Amadan после запуска кода вы получите сообщение об ошибке, а затем снова введите имя переменной, чтобы увидеть значение
Code Maniac
2
Обнаружил, что это описывает, как Javascript выполняет
Соломон Там
2
Это интересно с теоретической точки зрения, но это определенно относится к категории неожиданного поведения «вот почему вы не пишете такой код».
Джон Монтгомери

Ответы:

152

На самом деле, если вы правильно прочитали сообщение об ошибке, варианты 1 и 2 вызывают разные ошибки.

Дело a.x.y:

Невозможно установить свойство "y" неопределенного значения

Дело a.x.y.z:

Невозможно прочитать свойство "y" неопределенного значения

Думаю, лучше всего описать это пошаговым исполнением на простом английском.

Случай 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)
}

Случай 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)
}

В комментариях Соломон Тэм нашел эту документацию ECMA об операции присваивания .

yqlim
источник
57

Порядок операций становится более ясным, когда вы используете оператор запятой внутри скобок, чтобы увидеть, какие части выполняются, когда:

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

Смотрим на спецификацию :

Производство AssignmentExpression : LeftHandSideExpression = AssignmentExpressionоценивается следующим образом:

  1. Пусть lref будет результатом вычисления LeftHandSideExpression.

  2. Пусть rref будет результатом вычисления AssignmentExpression.

  3. Пусть будет rval GetValue(rref).

  4. Выбрасывать исключение SyntaxError, если ... (неактуально)

  5. Звоните PutValue(lref, rval).

PutValueэто то, что бросает TypeError:

  1. Пусть O будет ToObject(base).

  2. Если результат вызова [[CanPut]]внутреннего метода O с аргументом P ложен, то

    а. Если Throw имеет значение true, выбросить исключение TypeError.

Ничто не может быть присвоено свойству undefined- [[CanPut]]внутренний метод undefinedвсегда будет возвращать false.

Другими словами: интерпретатор анализирует левую часть, затем анализирует правую часть, а затем выдает ошибку, если свойство в левой части не может быть присвоено.

Когда ты делаешь

a.x.y = b.e = 1

Левая часть успешно анализируется , пока не PutValueбудет вызвана; тот факт, что .xсвойство оценивается как значение undefined, не рассматривается до тех пор, пока не будет проанализирована правая часть. Интерпретатор видит это как «Присвойте какое-либо значение свойству« y », равному undefined», а присвоение свойству undefinedтолько значения бросает внутрь PutValue.

Напротив:

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

Интерпретатор никогда не доходит до точки, где он пытается назначить zсвойство, потому что сначала он должен разрешить a.x.yзначение. Если a.x.yразрешить значение (даже до undefined), все будет в порядке - внутри будет выдана ошибка, PutValueкак указано выше. Но при доступе a.x.y возникает ошибка, потому что свойство yнедоступно undefined.

CertainPerformance
источник
20
Хороший трюк с оператором запятой - никогда не думал использовать его таким образом (только для отладки, конечно)!
ecraig12345
2
s / parse /
Берги
3

Рассмотрим следующий код:

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

Грубая схема шагов , необходимых для выполнения кода выглядит следующим образом исх :

  1. Оцените левую сторону. Следует помнить о двух вещах:
    • Оценка выражения - это не то же самое, что получение значения выражения.
    • Оценка ссылки средства доступа к свойству, например, a.x.yвозвращает ссылку ссылки, состоящую из базового значения a.x(undefined) и указанного имени ( y).
  2. Оцените правую сторону.
  3. Получите значение результата, полученного на шаге 2.
  4. Установите значение ссылки, полученной на шаге 1, равным значению, полученному на шаге 3, то есть установите для свойства yundefined значение. Это должно вызвать исключение TypeError ref .
Салман А
источник