Why do these snippets of JavaScript behave differently even though they both encounter an error?
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
Solution 1:
Actually, if you read the error message properly, case 1 and case 2 throw different errors.
Case a.x.y
:
Cannot set property 'y' of undefined
Case a.x.y.z
:
Cannot read property 'y' of undefined
I guess it's best to describe it by step-by-step execution in easy English.
Case 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)
}
Case 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)
}
In comments, Solomon Tam found this ECMA documentation about assignment operation.
Solution 2:
The order of operations is clearer when you exploit the comma operator inside bracket notation to see which parts are executed when:
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
Looking at the spec:
The production
AssignmentExpression : LeftHandSideExpression = AssignmentExpression
is evaluated as follows:
Let lref be the result of evaluating LeftHandSideExpression.
Let rref be the result of evaluating AssignmentExpression.
Let rval be
GetValue(rref)
.Throw a SyntaxError exception if... (irrelevant)
Call
PutValue(lref, rval)
.
PutValue
is what throws the TypeError
:
Let O be
ToObject(base)
.If the result of calling the
[[CanPut]]
internal method of O with argument P is false, thena. If Throw is true, then throw a TypeError exception.
Nothing can be assigned to a property of undefined
- the [[CanPut]]
internal method of undefined
will always return false
.
In other words: the interpreter parses the left-hand side, then parses the right-hand side, then throws an error if the property on the left-hand side can't be assigned to.
When you do
a.x.y = b.e = 1
The left hand side is successfully parsed up until PutValue
is called; the fact that the .x
property evaluates to undefined
is not considered until after the right-hand side is parsed. The interpreter sees it as "Assign some value to the property "y" of undefined", and assigning to a property of undefined
only throws inside PutValue
.
In contrast:
a.x.y.z = b.e = 1
The interpreter never gets to the point where it tries to assign to the z
property, because it first must resolve a.x.y
to a value. If a.x.y
resolved to a value (even to undefined
), it would be OK - an error would be thrown inside PutValue
like above. But accessing a.x.y
throws an error, because property y
cannot be accessed on undefined
.
Solution 3:
Consider the following code:
var a = {};
a.x.y = console.log("evaluating right hand side"), 1;
The rough outline of steps required to execute the code is as follows ref:
- Evaluate the left hand side. Two things to keep in mind:
- Evaluating an expression is not the same as getting the value of expression.
- Evaluating a property accessor ref e.g.
a.x.y
returns a reference ref consisting of base valuea.x
(undefined) and referenced name (y
).
- Evaluate the right hand side.
- Get the value of result obtained in step 2.
- Set the value of the reference obtained in step 1 to the value obtained in step 3 i.e. set property
y
of undefined to the value. This is supposed to throw a TypeError exception ref.