¿Por qué estos fragmentos de JavaScript se comportan de manera diferente a pesar de que ambos encuentran un error?

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
fuente
3
@NinaScholz: No entiendo. No hay ningún error de sintaxis; por lo que asumiría eso b.z = 1y b.e = 1ejecutaría primero (dada la asociatividad derecha activada =), luego a.x.y.z = ...ejecutaría y fallaría; ¿Por qué la basignación pasa en un caso pero no en el otro?
Amadan
3
@NinaScholz Aceptamos que la propiedad yno existe en a.x; pero eso es cierto en ambos casos. ¿Por qué impide la asignación del lado derecho en el segundo caso pero no en el primero? ¿Qué es diferente en el orden de ejecución? (Mencioné el error de sintaxis porque el tiempo en el error de sintaxis es muy diferente al de un error de tiempo de ejecución).
Amadan
@Amadan después de ejecutar el código, obtendrá un error, y luego use que escriba el nombre de la variable nuevamente para ver el valor
Code Maniac
2
Encontré esto describe cómo Javascript procede la operación de asignación ecma-international.org/ecma-262/5.1/#sec-11.13
Solomon Tam
2
Es interesante desde una perspectiva teórica, pero esto definitivamente cae dentro de la categoría de comportamiento inesperado "por eso no escribes código así".
John Montgomery

Respuestas:

152

En realidad, si lee correctamente el mensaje de error, el caso 1 y el caso 2 arrojan errores diferentes.

Caso a.x.y:

No se puede establecer la propiedad 'y' de indefinido

Caso a.x.y.z:

No se puede leer la propiedad 'y' de indefinido

Supongo que es mejor describirlo paso a paso en un inglés sencillo.

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

En los comentarios, Solomon Tam encontró esta documentación de ECMA sobre la operación de asignación .

yqlim
fuente
57

El orden de las operaciones es más claro cuando explota el operador de coma dentro de la notación de corchetes para ver qué partes se ejecutan cuando:

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

Mirando la especificación :

La producción AssignmentExpression : LeftHandSideExpression = AssignmentExpressionse evalúa de la siguiente manera:

  1. Sea lref el resultado de evaluar LeftHandSideExpression.

  2. Sea rref el resultado de evaluar AssignmentExpression.

  3. Sea rval GetValue(rref).

  4. Lanzar una excepción SyntaxError si ... (irrelevante)

  5. Llame PutValue(lref, rval).

PutValuees lo que arroja el TypeError:

  1. Sea O ToObject(base).

  2. Si el resultado de llamar al [[CanPut]]método interno de O con el argumento P es falso, entonces

    a. Si Throw es verdadero, entonces lanza una excepción TypeError.

No se puede asignar nada a una propiedad de undefined: el [[CanPut]]método interno de undefinedsiempre volverá false.

En otras palabras: el intérprete analiza el lado izquierdo, luego analiza el lado derecho, luego arroja un error si no se puede asignar la propiedad del lado izquierdo.

Cuando tu lo hagas

a.x.y = b.e = 1

El lado izquierdo se analiza con éxito hasta que PutValuese llama; el hecho de que la .xpropiedad se evalúe undefinedno se considera hasta que se analiza el lado derecho. El intérprete lo ve como "Asignar algún valor a la propiedad" y "de indefinido", y asignarlo a una propiedad de undefinedsolo arroja dentroPutValue .

A diferencia de:

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

El intérprete nunca llega al punto en el que intenta asignar a la zpropiedad, porque primero debe resolver a.x.ya un valor. Si se a.x.yresuelve en un valor (incluso a undefined), estaría bien: se arrojaría un error dentro PutValuecomo arriba. Pero acceder a.x.y arroja un error, porque yno se puede acceder a la propiedad en undefined.

CertainPerformance
fuente
20
Buen truco del operador de coma: ¡nunca pensé en usarlo de esa manera (solo para depurar, por supuesto)!
ecraig12345
2
s / parse / evaluar /
Bergi
3

Considere el siguiente código:

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

El esquema general de los pasos necesarios para ejecutar el código es el siguiente ref :

  1. Evalúe el lado izquierdo. Dos cosas a tener en cuenta:
    • Evaluar una expresión no es lo mismo que obtener el valor de una expresión.
    • Evaluar una referencia de acceso de propiedad, por ejemplo, a.x.ydevuelve una referencia de referencia que consta de un valor base a.x(indefinido) y un nombre referenciado ( y).
  2. Evalúe el lado derecho.
  3. Obtenga el valor del resultado obtenido en el paso 2.
  4. Establezca el valor de la referencia obtenida en el paso 1 al valor obtenido en el paso 3, es decir, establezca la propiedad yde indefinido en el valor. Se supone que esto arroja una referencia de excepción TypeError .
Salman A
fuente