coffeescript

CoffeeScript有个语法叫解构赋值(Destructuring Assignment),可以将一个对象的不同成员一次性赋值给多个的变量。官网中给了下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
futurists =
sculptor: "Umberto Boccioni"
painter: "Vladimir Burliuk"
poet:
name: "F.T. Marinetti"
address: [
"Via Roma 42R"
"Bellagio, Italy 22021"
]
{poet: {name, address: [street, city]}} = futurists
alert name + " — " + street

运行结果自然是 “F.T. Marinetti — Via Roma 42R”,因为coffee将其翻译为下面的JS:

1
2
3
4
5
6
7
8
9
10
11
var city, futurists, name, street, _ref, _ref1;
futurists = {
sculptor: "Umberto Boccioni",
painter: "Vladimir Burliuk",
poet: {
name: "F.T. Marinetti",
address: ["Via Roma 42R", "Bellagio, Italy 22021"]
}
};
_ref = futurists.poet, name = _ref.name, (_ref1 = _ref.address, street = _ref1[0], city = _ref1[1]);
alert(name + " — " + street);

这个语法跟Erlang的模式匹配有点类似,不同的是,Erlang会严格匹配等号两边,不赋值的要用_作为占位符,否则运行时将抛出异常,而coffee则不会,对于不存在的成员,值为undefined:

1
2
3
{poet: {nameX, address: [streetX, city]}} = futurists
# nameX = "undefined"
# streetX = "F.T. Marinetti"

当然,对一个不存在的成员继续解析还是会抛出异常的:

1
2
{poetX:{a}} = futurists
# TypeError: Cannot read property 'a' of undefined

另外,和JS一样,coffee也可以连续赋值:

1
2
3
a=b=100
# a=100
# b=100

假如将上面两种语法组合在一起,会怎样呢?就像下面的代码,最终d=?

1
2
a={b:1,c:2,x:3,y:4}
d={b,c}=a

简单分析下: 赋值语句的结合顺序是从右到左,所以d={b,c}=a等价于d=({b,c}=a) 我们还知道,赋值表达式的值是其本身,那么{b,c}=a的值是什么呢,{b,c}还是a? 可以尝试下,coffee会将语句{b,c}=a转成下面的JS:

1
b = a.b, c = a.c;

这样看来,d最终的值会是2,这也太奇怪了吧?

还是看一下coffee把d={b,c}=a到底翻译成什么吧:

1
d = (b = a.b, c = a.c, a);

结果在意料之内,d的值不是2 结果在意料之外,d的值不是{b,c}

我一直以为,像{b,c}=a这样的表达式,返回值将会是{b,c}而不是a,然后d={b,c}=a可以按过滤器的方式执行。但是,我错了,{b,c}=a这个表达式的值是a,再写几行代码验证一次:

1
f = (a) -> {b,c}=a

coffee将上面的代码翻译成

1
2
3
4
f = function(a) {
var b, c;
return b = a.b, c = a.c, a;
};

好吧,解析赋值表达式的值确实是等号右边的值。 至于{b,c}=a为何会被译成b = a.b, c = a.c;估计是因为该表达式的值不产生副作用,所以coffee把该表达式的值抛弃了。

那么,在JS里,普通赋值表达式的值,是不是也是等号右边的值呢?下面的JS语句,result最终应该等于expr1还是expr2呢?

1
result = expr1 = expr2

乍眼一看,可能会觉得result==expr1且result==expr2,等于哪个都一样。

是的,在大多情况下,这都是成立的。但是,为什么对于上面的{b,c}=a这类语句,就不一定成立了呢?

这是因为,不成立的原因是:执行dest = src后,dest 不一定等于 src

javascript

JS中有类似的情形吗?答案当然是肯定的,看下面的JS代码:

1
2
3
4
5
6
7
8
9
10
var obj, result;
obj = {};
Object.defineProperties(obj, {
x: {
set: function() {},
get: function() {
return 10;
}
}
});

将obj.x的读和写分离,就能产生类似的效果。

1
2
3
obj.x = 11;
result = obj.x;
console.log(result); // 10

回到正题,继续测试:

1
2
result = obj.x = 11;
console.log(result); // 11

好吧,JS的赋值表达式的值也是等于等号右边的值。 当最右值的读取包含副作用时,会怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
var obj, r1, r2, r3;
obj = {_x:1};
Object.defineProperties(obj, {
x: {
set: function() {},
get: function() {
return ++_x;
}
}
});
r1 = r2 = r3 = obj.x
console.log(r1,r2,r3); // 2 2 2

嗯…我知道,在get方法包含带副作用的行为是不对的,这段代码只是测试而已~ 结果表明,连续赋值时最右值只计算一次。其实,从AST的执行过程来分析,最右值只计算一次,是合理的。

ECMA文档是这么描述赋值语句的:

11.13.1 Simple Assignment ( = )
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. Let rref be the result of evaluating AssignmentExpression.
  3. Let rval be GetValue(rref).
  4. 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”
  1. Call PutValue(lref, rval).
  2. 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属性吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Hole
{
public Hole(string name,int value)
{
_name = name;
_value = value;
}
private string _name;
private int _value;
public int Value
{
get { Console.WriteLine("get {0} {1}", _name, _value); return _value; }
set { Console.WriteLine("set {0}={1}", _name, value); }
}
}
static void Main(string[] args)
{
var a = new Hole("a", 10);
var b = new Hole("b", 11);
var c = new Hole("c", 12);
var d = c.Value = b.Value = a.Value;
Console.WriteLine("d={0}",d);
//get a 10
//set b=10
//set c=10
//d=10
}

跟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的模式匹配属于严格匹配,左右值必然严格相同,并且变量不可变,似乎不会有上面的歧义。

1
{_,Y}={X,_}={a,b}. % {a,b}

对于涉及到副作用的代码,像时间戳/随机值,就难说了:

1
A=B=random:uniform(1234567890). % okey

没出现模式匹配错误,说明erlang中最右值也只计算一次。

原因

赋值表达式的值为何总是右值而不是左值?

我估计,按赋值表达式的语意,等号的左边总是“可写”的,右边总是“可读”的。如果赋值表达式的值是左边的值,那么程序需要重新计算等号左边的值才能返回,这带来两个问题,一是性能,二是左边的对象不一定可读(例如那些可写但不可读的属性)。于是,几乎所有语言都将“所赋的值”作为赋值语句的返回值,所以连续赋值语句的行为总是“计算最右值一次,从右向左赋值多次”。

但是,对于coffee的解构赋值表达式,情况则有点不同,因为coffee必须将该表达式拆分成两个,当c是一个包含副作用的属性时,a=b=c和b=c;a=c;并不是等价的,所以,对于解构赋值表达式来说,等号右边的表达式实际被求值了多次。