前言
最近准备归纳总结一下前端的知识,突然看到了这样一道题:
var foo = {n: 1};
var bar = foo;
foo.x = foo = {n: 2};
console.log(foo.x);
console.log(bar);
乍一看,脑子确实一下转不过来,仔细一看也似懂非懂的。那么接下来我们就一起来解析一下这道题目。
赋值运算符的运算顺序
运算符的优先级决定了表达式中运算执行的先后顺序。
我们这里只分析与本题相关的运算符:赋值
。想了解更多的同学可以查看MDN。
赋值
运算顺序:从右到左
我们先来看赋值运算符
:
'use strict'
const bar = new Proxy({ obj: { n: 1 } }, {
get: (_obj, prop) => {
console.log('get-----bar');
return _obj[prop];
},
});
foo = bar.obj;
首先我们申明了一个使用Proxy
代理的bar
对象,然后将bar
对象的obj
属性赋值给了一个未声明的foo
变量。
运行后,可以看到,首先打印了Proxy
中get
方法的日志,然后报错提示了foo
变量没有定义。
这里验证了赋值运算符的运算顺序的确是从右到左
。
那么使用连续赋值也是一样吗?我们再来看一个例子:
'use strict'
const bar = new Proxy({ obj: { n: 1 } }, {
get: (_obj, prop) => {
console.log('get-----bar');
return _obj[prop];
},
});
foo = test = bar.obj;
我们以上面的例子为基础,在最后赋值的时候使用了连续赋值,将bar.obj
赋值给了两个未声明的变量:test
,foo
。运行一下,如图:
可以看到,首先还是打印了Proxy
中get
方法的日志,然后报错提示了test
变量没有定义。
从这个例子中我们可以看出,在连续赋值操作中,运算的顺序还是从右到左
。可以看做这样:
A = (B = C)
解析题目
再来看题目
'use strict'
var foo = {n: 1}; // A
var bar = foo;
foo.x = foo = {n: 2}; // B
console.log(foo.x);
console.log(bar);
我们先来分析一下题目:
- 创建了一个对象A并赋值给了foo变量
- 将foo变量赋值给了bar
- 然后创建了新的对象B进行连续赋值
首先我们知道foo
变量持有的是一个引用类型的内存地址,将foo
赋值给了bar
,bar
也持有了同一个内存地址。
在最后进行了连续赋值操作,通过上面我们知道,连续赋值的操作运算顺序是从右到左的,可以转化为这样:
foo.x = (foo = {n: 2});
根据ECMScript规范:
LeftHandSideExpression = AssignmentExpression
-
Let
lref
be the result of evaluating LeftHandSideExpression. -
Let
rref
be the result of evaluating AssignmentExpression. -
Let
rval
be ? GetValue(rref
). -
Perform ? PutValue(
lref
,rval
). -
Return
rval
.
我们根据规范分析一下:
- 首先会将左手边的表达式,赋值给
lref
,像这样:lref = foo.x
- 然后将右手边的赋值表达,赋值给
rref
(此时并没有开始计算),像这样:rref = (foo = {n: 2})
- 然后通过
GetValue(rref)
获取到右手边赋值表达式的结果赋值给rval
,像这样:rval = {n: 2} = (foo = {n: 2})
- 然后使用
PutValue
方法,将rval
的结果赋值给lref
,像这样:
lref = rval
也就是:
foo.x = {n: 2}; // 此时foo和bar(指向的同一个对象A)都等于:{n: 1, x: {n: 2}}
- 最后返回
rval
,第一个赋值表达式结束。 - 开始第二个赋值表达式的运算,也就是小括号中的:
foo = {n: 2} // 此时,foo变量重新赋值对象B,与对象A切断了联系
最终连续赋值的结果为:
bar = {n: 1, x: {n: 2}};
foo = {n: 2};
console.log(foo.x); // undefined
console.log(bar); // {n: 1, x: {n: 2}}
可以使用运算符优先级解答?
在查看一些资料的时候,也看到其中一种解法是根据运算符优先级来的。
运算符顺序:
运算符 | 优先级 |
---|---|
. | 20 |
= | 3 |
运算符优先级越大优先级越高。想了解更多的同学可以查看MDN。
由于访问成员.
的优先级大于赋值的优先级,所以赋值时这样的:
foo.x = foo = {n: 2}
转为:
foo.x = {n: 2};
foo = {n: 2};
这样的解法,最后结果确实是对的,但是我持怀疑的态度,进行了实验。
实验1:
'use strict'
m.n = g = 1;
对两个未声明的变量进行了连续赋值,如果是使用优先级赋值的话,那么肯定会先调用m.n
,由于m
并没有声明,所以肯定会提示m没有定义的错误。结果如下:
这里证明了,的确会先调用优先级更高的运算符。
实验2:
let a = new Proxy({n: 1}, {
set: (obj, prop, value) => {
console.log('set-----a');
obj[prop] = value;
return true;
},
});
const b = a;
a.x = a = { n: 2 };
console.log('a.x', a.x);
console.log('b', b);
这个例子中,我们使用Proxy
对a变量指向的对象进行了包装,当调用a变量的属性赋值时,则会打印一段日志。
假设赋值顺序为这样:
a = {};
b = a;
a = b = c
顺序为:
先 b = c
后 a = (b = C)
a和c都重新被复制为新的对象c,与原对象的联系被切断,那么Proxy
中的日志便不会打印。
那么我们来看下结果:
最终日志被打印了,说明赋值的顺序为:
先对a.x赋值: a.x = { n: 2 };
再对a赋值: a = { n: 2 };
结果证明,使用运算符优先级也是可以正确解答本题。
总结
本题可以使用两种方法解答:
- 从ECMscript规范:
- 首先会将左手边的表达式,赋值给
lref
,像这样:lref = foo.x
- 然后将右手边的赋值表达,赋值给
rref
(此时并没有开始计算),像这样:rref = (foo = {n: 2})
- 然后通过
GetValue(rref)
获取到右手边赋值表达式的结果赋值给rval
,像这样:rval = {n: 2} = (foo = {n: 2})
- 然后使用
PutValue
方法,将rval
的结果赋值给lref
- 最后返回
rval
,第一个赋值表达式结束。 - 然后又从第一步开始,开启第二个赋值表达式的运算。
- 首先会将左手边的表达式,赋值给
- 使用运算符优先级:
运算符 | 优先级 |
---|---|
. | 20 |
= | 3 |
成员访问.
的优先级比赋值优先级的优先级更高,会先对成员访问的表达式进行赋值。