Por que esses snippets de JavaScript se comportam de maneira diferente, embora ambos encontrem um erro?

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
fonte
3
@NinaScholz: Não entendo. Não há erro de sintaxe; então eu assumiria isso b.z = 1e b.e = 1executaria primeiro (dada a associatividade à direita ativada =), depois a.x.y.z = ...executaria e falharia; por que a batribuição é aprovada em um caso, mas não no outro?
Amadan
3
@NinaScholz Concordamos que a propriedade ynão existe em a.x; mas isso é verdade em ambos os casos. Por que impede a atribuição do lado direito no segundo caso, mas não no primeiro? O que é diferente na ordem de execução? (Eu mencionei o erro de sintaxe porque o tempo no erro de sintaxe é muito diferente daquele de um erro de tempo de execução.)
Amadan
@Amadan depois de executar o código, você obterá um erro e, em seguida, use que digite o nome da variável novamente para ver o valor
Código Maniac
2
Esta descrição descreve como Javascript prossegue com a operação de atribuição ecma-international.org/ecma-262/5.1/#sec-11.13
Solomon Tam
2
É interessante de uma perspectiva teórica, mas definitivamente se enquadra na categoria de comportamento inesperado "é por isso que você não escreve um código assim".
John Montgomery

Respostas:

152

Na verdade, se você ler a mensagem de erro corretamente, o caso 1 e o caso 2 geram erros diferentes.

Caso a.x.y:

Não é possível definir a propriedade 'y' de indefinido

Caso a.x.y.z:

Não é possível ler a propriedade 'y' de indefinido

Acho que é melhor descrevê-lo pela execução passo a passo em um inglês fácil.

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

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

Nos comentários, Solomon Tam encontrou esta documentação ECMA sobre a operação de atribuição .

yqlim
fonte
57

A ordem das operações é mais clara quando você explora o operador vírgula dentro da notação de colchetes para ver quais partes são executadas quando:

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

Olhando para as especificações :

A produção AssignmentExpression : LeftHandSideExpression = AssignmentExpressioné avaliada da seguinte forma:

  1. Seja lref o resultado da avaliação de LeftHandSideExpression.

  2. Seja rref o resultado da avaliação de AssignmentExpression.

  3. Deixe rval ser GetValue(rref).

  4. Lança uma exceção SyntaxError se ... (irrelevante)

  5. Ligue PutValue(lref, rval).

PutValueé o que lança o TypeError:

  1. Deixe O ser ToObject(base).

  2. Se o resultado da chamada do [[CanPut]]método interno de O com o argumento P for falso, então

    uma. Se Throw for true, lance uma exceção TypeError.

Nada pode ser atribuído a uma propriedade de undefined- o [[CanPut]]método interno de undefinedsempre retornará false.

Em outras palavras: o intérprete analisa o lado esquerdo, a seguir analisa o lado direito e, a seguir , lança um erro se a propriedade do lado esquerdo não puder ser atribuída.

Quando você faz

a.x.y = b.e = 1

O lado esquerdo é analisado com sucesso até que PutValueseja chamado; o fato de a .xpropriedade ser avaliada como undefinednão é considerado até que o lado direito seja analisado. O intérprete vê isso como "Atribuir algum valor à propriedade" y "de undefined", e atribuir a uma propriedade undefinedapenas joga dentro PutValue.

Em contraste:

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

O intérprete nunca chega ao ponto em que tenta atribuir à zpropriedade, porque primeiro deve resolver a.x.ypara um valor. Se a.x.yresolvido para um valor (mesmo para undefined), estaria tudo bem - um erro seria lançado dentro PutValuecomo acima. Mas o acesso a.x.y gera um erro, porque a propriedade ynão pode ser acessada em undefined.

CertainPerformance
fonte
20
Bom truque do operador de vírgula - nunca pensei em usá-lo dessa forma (apenas para depuração, é claro)!
ecraig12345
2
s / parse /
Evalu
3

Considere o seguinte código:

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

O esboço das etapas necessárias para executar o código é o seguinte ref :

  1. Avalie o lado esquerdo. Duas coisas para ter em mente:
    • Avaliar uma expressão não é o mesmo que obter o valor da expressão.
    • Avaliar um acessador de propriedade ref, por exemplo, a.x.yretorna uma referência ref consistindo de valor base a.x(indefinido) e nome referenciado ( y).
  2. Avalie o lado direito.
  3. Obtenha o valor do resultado obtido na etapa 2.
  4. Defina o valor da referência obtida na etapa 1 para o valor obtido na etapa 3, ou seja, defina a propriedade yde undefined para o valor. Isso deve lançar uma referência de exceção TypeError .
Salman A
fonte