Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

复杂表达式与未定义行为

  脚本语言和编译语言的一个重大差别就是在表达式的解析上。脚本语言通常是从左到右来解析表达式,相当于把每个子表达式都变成单独的语句来运行。而编译型的语言会把整个表达式编译到一起。比如“a=b=1”在编译型语言中可以被这样编译: mov eax,1
mov [a],eax
mov [b],eax
  而在脚本语言中不可能把它们混在一起,“a=b=1”显然是两个表达式嵌套而成的。在脚本语言中就会把它们拆开做单独的解释。正是因为脚本语言的表达式会被拆开来单独解析,所以就不容易出现未定义行为(Undefined Behavior)。下面这个测试,在C语言中会因为编译器的不同的产生不同的结果。但是在JavaScript中它的运行结果就是唯一的。 var a=0;
a=(a++)+(a++);
alert(a);
  按照运算符优先级来看,“a++”最先被运行。根据从左到右的运行法则是先运行左边的“a++”,得到0,之后a递加变成1。然后运行右边的“a++”,得到1,之后a递加变成2。再运行中间的“+”把两次得到的结果加起来就是“0+1”。最后运行“=”就是“a=1”。无论a在这个过程中改变了多少次,赋值运算在最后运行,a的最终结果就是1。从这个运行过程可以看出,JavaScript是把表达式分解成许多个语句来执行的,所以在复杂的表达式中不容易产生未定义行为,但是也因此效率和编译型的语言差很多。再看一个例子吧
var o={};
o.n=o={};
alert(o.n);
  你能猜出这个运行结果吗?根据运算符优先级,最先运行的是“.”,得到o的n属性的地址(或者说是o中的n这个变量)。然后解释器就遇到麻烦了,根据从左到右的运行法则,“o.n=o”要先被运行,可是如果它们先被运行就会变成类似“(o.n=o)={};”。这是有语法错误的,这变成了给常量赋值。因为“=”这个运算符的返回值是一个常量(运算符中只有“[]”和“.”的返回值是变量)。聪明的解释器就避开这个错误,从后面先运行。这一样以来就变成了类似“o.n=(o={});”,先是运行了“o={}”o被赋值了。接着运行“o.n=”,由于“o.n”一开始就被解析为了一个地址,所以这里的“o.n”实际上是指向var语句中赋值的那个对象。而刚才o已经被重新赋值过了,所以这个“o.n=”的操作不会影响到现在的o。所以最后输出的“o.n”应该是undefined。
  这两个例子在JavaScript中不会变成未定义行为,只是解读上有点吃力而已,在ECMA中对这些东西还是有定义的。虽然JavaScript不容易产生未定义行为,但也不是没有。比如下面这个例子。 function a(){return 1};
function b(){return 2};
alert(a(a=b));
  这个程序看似很简单嘛,有两个函数a和b。它们调用时候分别返回1和2。如果我单独调用a,肯定是返回1的。可是我在调用a的参数中写入a=b又会怎么解析呢?按照前面的逻辑去分析没错,但是有一个问题是没有定义的。参数列表括号内的表达式是调用函数时解析还是调用函数前解析?因此这个代码在不同的JavaScript引擎上会有不同的结果。
    输出1:FireFox、Opera、IE9+
    输出2:Chrome、IE8-
  这就是一个典型的例子,要在JavaScript中找到这样一个例子还真不容易。如果是C语言,这样的例子一抓一大把。正常写代码都应该避开这些未定义行为,但是我觉得它们偶尔也可以作为hack来使用。比如当年判断IE的代码“!-[1,]”,这个代码在IE8-上的运行结果是true。其它浏览器上是false。只是现在对于JavaScript的hack用的越来越少了,它们的存在已经没啥重大意义了。不过对于一个Web开发者来说,稍微了解这些东西还是很有必要的。
网名:
34.203.213.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^