coffeescript
CoffeeScript有个语法叫解构赋值(Destructuring Assignment),可以将一个对象的不同成员一次性赋值给多个的变量。官网中给了下面一个例子:
运行结果自然是 “F.T. Marinetti — Via Roma 42R”,因为coffee将其翻译为下面的JS:
这个语法跟Erlang的模式匹配有点类似,不同的是,Erlang会严格匹配等号两边,不赋值的要用_作为占位符,否则运行时将抛出异常,而coffee则不会,对于不存在的成员,值为undefined:
当然,对一个不存在的成员继续解析还是会抛出异常的:
另外,和JS一样,coffee也可以连续赋值:
假如将上面两种语法组合在一起,会怎样呢?就像下面的代码,最终d=?
简单分析下: 赋值语句的结合顺序是从右到左,所以d={b,c}=a等价于d=({b,c}=a) 我们还知道,赋值表达式的值是其本身,那么{b,c}=a的值是什么呢,{b,c}还是a? 可以尝试下,coffee会将语句{b,c}=a转成下面的JS:
这样看来,d最终的值会是2,这也太奇怪了吧?
还是看一下coffee把d={b,c}=a到底翻译成什么吧:
结果在意料之内,d的值不是2 结果在意料之外,d的值不是{b,c}
我一直以为,像{b,c}=a这样的表达式,返回值将会是{b,c}而不是a,然后d={b,c}=a可以按过滤器的方式执行。但是,我错了,{b,c}=a这个表达式的值是a,再写几行代码验证一次:
coffee将上面的代码翻译成
好吧,解析赋值表达式的值确实是等号右边的值。 至于{b,c}=a为何会被译成b = a.b, c = a.c;估计是因为该表达式的值不产生副作用,所以coffee把该表达式的值抛弃了。
那么,在JS里,普通赋值表达式的值,是不是也是等号右边的值呢?下面的JS语句,result最终应该等于expr1还是expr2呢?
乍眼一看,可能会觉得result==expr1且result==expr2,等于哪个都一样。
是的,在大多情况下,这都是成立的。但是,为什么对于上面的{b,c}=a这类语句,就不一定成立了呢?
这是因为,不成立的原因是:执行dest = src后,dest 不一定等于 src
javascript
JS中有类似的情形吗?答案当然是肯定的,看下面的JS代码:
将obj.x的读和写分离,就能产生类似的效果。
回到正题,继续测试:
好吧,JS的赋值表达式的值也是等于等号右边的值。 当最右值的读取包含副作用时,会怎样呢?
嗯…我知道,在get方法包含带副作用的行为是不对的,这段代码只是测试而已~ 结果表明,连续赋值时最右值只计算一次。其实,从AST的执行过程来分析,最右值只计算一次,是合理的。
ECMA文档是这么描述赋值语句的:
11.13.1 Simple Assignment ( = )
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 the following conditions are all true:
- Type(lref) is Reference is true
- IsStrictReference(lref) is true
- Type(GetBase(lref)) is Environment Record
- GetReferencedName(lref) is either “eval” or “arguments”
- Call PutValue(lref, rval).
- Return rval.
NOTE When an assignment occurs within strict mode code, its LeftHandSide must not evaluate to an unresolvable
reference. If it does a ReferenceError exception is thrown upon assignment. The LeftHandSide also may not be a
reference to a data property with the attribute value {[[Writable]]:false}, to an accessor property with the attribute value
{[[Set]]:undefined}, nor to a non-existent property of an object whose [[Extensible]] internal property has the value false. In
these cases a TypeError exception is thrown.
赋值表达式的返回值是等号右边的值,连续赋值表达式的值自然就是最右值了,而且只会计算一次。
那么,其他语言呢?
C#
C#也支持连续赋值,再加上坑爹的DateTime.Now,这个问题似乎也会有! 不过嘛,如果写a=b=DateTime.Now能得到a!=b的概率似乎极小,写个坑爹的get属性吧:
跟JS类似,C#的连续赋值里,赋值表达式的值也是等号右边的值,最右值也只会计算一次。dynamic呢?也一样。
C# 4.0语言规范 7.17.1 这么描述赋值表达式:
The result of a simple assignment expression is the value assigned to the left operand. The result has the same type as the left operand and is always classified as a value.
Erlang
因为erlang的模式匹配属于严格匹配,左右值必然严格相同,并且变量不可变,似乎不会有上面的歧义。
对于涉及到副作用的代码,像时间戳/随机值,就难说了:
没出现模式匹配错误,说明erlang中最右值也只计算一次。
原因
赋值表达式的值为何总是右值而不是左值?
我估计,按赋值表达式的语意,等号的左边总是“可写”的,右边总是“可读”的。如果赋值表达式的值是左边的值,那么程序需要重新计算等号左边的值才能返回,这带来两个问题,一是性能,二是左边的对象不一定可读(例如那些可写但不可读的属性)。于是,几乎所有语言都将“所赋的值”作为赋值语句的返回值,所以连续赋值语句的行为总是“计算最右值一次,从右向左赋值多次”。
但是,对于coffee的解构赋值表达式,情况则有点不同,因为coffee必须将该表达式拆分成两个,当c是一个包含副作用的属性时,a=b=c和b=c;a=c;并不是等价的,所以,对于解构赋值表达式来说,等号右边的表达式实际被求值了多次。