这是同事问的一道Javascript问题,下面的两条语句分别输出什么,为什么?
2.toString(); 2..toString();
先贴出运行结果:
SyntaxError "2"
结果有点儿出人意料,下面细细来看~
1) 2.toString()
对于第一条语句为什么报语法错误,究其原因其实很简单。Javascript解释器在进行词法分析的时候,将
2.toString 切割成 ’2.’ ‘toString’
而不是我们想象中的 ’2′ ‘.’ ‘toString’
这样提取出来的token注定了语法分析时会报出异常。
在Javascript中,允许通过如下语句来定义一个数值:
var a = 5. //试图将a赋值为5.0 var b = 13. //试图将b赋值为13.0
虽然“5.”“13.”后面不接“0”,丑陋了一些,但它们的确是合法的数值字面量,Javascript解释器并不会排斥。于是,Javascript词法分析器会对这种格式的字面量采用了“贪心算法”来尽力匹配。所谓“贪心算法”,就是说词法分析器会尽可能的吃进更多的字符,用以满足当前可能生成的token。因为在解析器的层面无法得知程序员写的代码真正想要表达什么,所以只能才用这样笨拙的匹配方式。
回到文章开头的例子:
“2.toString”
很好,知道为什么是 “2.” “toString” 了吧~因为当词法分析的时候发现“2”后面衔接“.”依然可以组成合法的 NumericLiteral,于是解析器就毫不犹豫的将 “.” 划拨给了“2” …
2) 2..toString()
再来分析2..toString,从执行的结果来看有两个疑问:
- 为什么结果是2,却不是2.0呢?
- 2.是一个值,它怎么会有toString这个方法呢?
下面来逐渐揭开谜底。
首先,对“2..toString”进行词法分析,根据上文的描述,该表达式会被切成:
“2.” “.” “toString”
进行完词法分析后会尝试去构建语法树,这里就不写出其详细的范式推导过程了。
第一个token是”2.” ,根据ECMA的描述,它是一个合乎语法规范的数值字面量”NumericLiteral”。第二个token”.” 可以当做是取属性的运算符, 所取的属性即为第三个token “toString”。其实这里还有两个token没有放进去,就是 “(” 和 “)” ,当遇到这两个token的时候,JS引擎就会认为这是一个函数调用的表达式”CallExpression” 。
如果用JS表示这样一个过程,大体上形如:
var num = 2. ; var foo = num.toString ; //取属性 foo.call(num) ; //函数调用
可以很明显的看出call的调用方式。2..toString()最终会被解析成:
CallExpression : MemberExpression Arguments NumericLiteral . Identifier Arguments
画成的语法树类似于:
不放心的话还可以采用rhino进行验证,最终解析得到的语法树也是这样的。parse出语法树之后,下面一步就是目标代码的生成。语法分析最大的 意义就是在于告诉解析器,当前分析的JS代码是一种怎样的语句,生成的代码该如何执行它。本例中的JS代码就是函数调用表达式“Call Expression”,生成的目标代码也必须要遵循Ecma中对于“Call Expression”的规定。
OK,进一步来看Ecma标准中规定的CallExpression是如何执行的~
CallExpression : MemberExpression Arguments is evaluated as follows: 1.Evaluate MemberExpression. 2.Evaluate Arguments,producing an internal list of argument values. 3.Call GetValue(Result(1)). 4.If Type(Result(3)) is not Object,throw a TypeError exception. 5.If Result(3) does not implement the internal[[Call]] method,throwa TypeError exception. 6.If Type(Result(1)) is Reference,Result(6) is GetBase(Result(1)).Otherwise,Result(6) is null. 7.If Result(6) is an activation object,Result(7) is null.Otherwise,Result(7) is the same as Result(6). 8.Call the [[Call]] method on Result(3),providing Result(7) as the this value and providing the list Result(2) as the argument values. 9.Return Result(8).
又是茫茫长的的一堆…从这里可见,任何一个简单的函数调用,都需要经过9个步骤 – -!这个运行过程看上去着实有点复杂,没办法,只好就地来逐行演算一遍,这样便能弄清楚其中的脉络了。
第一步,先要Evaluate MemberExpression ,其实就是计算 2..toString 的值。对于取属性,标准中有如下描述:
MemberExpression . Identifier 执行方式同于 MemberExpression[<identifier-string>]
接着来找如何利用方括号[ ]取属性:
MemberExpression [Expression]的执行步骤:
1.Evaluate MemberExpression . 2.Call GetValue(Result(1)). 3.Evaluate Expression. 4.Call GetValue(Result(3)). 5.Call ToObject(Result(2)). 6.Call ToString(Result(4)). 7.Return a value of type Reference whose base object is Result(5) and whose property name is Result(6).
第一步又是Evaluate MemberExpression ,这儿的MemberExpression 已经换成了 NumericLiteral,也就是“2. ” 。最后我们来翻阅一下 NumericLiteral 是如何被计算的 ,参阅Ecma -3rd 第7.8.3章节,太长了实在,感兴趣的话可以点下面的链接 :
http://bclary.com/2004/11/07/#a-7.8.3
我没有细看,不过其中最重要的一句话就是:
The MVof DecimalIntegerLiteral . is the MV of Decimal Integer Literal.
MV(mathematicalvalue)就是值,看到这句话,第一个疑惑迎刃而解。
“2.” 会被处理成 “2” ,而不是2.0 ,JS语言标准就这么规定的,妥妥的…
这也是为什么第一节的例子中说试图赋值为5.0、13.0,因为这么写是肯定木有效果的了…
继续来看MemberExpression [Expression] 的7个步骤:
1.Evaluate MemberExpression . //整数2 2.Call GetValue(Result(1)). //依然是2 3.Evaluate Expression. //标识符toString 4.Call GetValue(Result(3)). //标识符toString 5.Call ToObject(Result(2)). //包装类啊包装类…… 6.Call ToString(Result(4)). //“toString” 7.Return a value of type Reference //referenceTypeObject,见下 whose base object is Result(5) and whose property name is Result(6).
注意第6步,它解开了本节开头提出的第二点疑惑,由于在这里将2打包成了一个对象,所以必定会继承得到Object的toString方法…对于一个number数值,toObject的算法如下:
Create a new Number object whose [[value]] property is set to the value of the number.
可以简单认为生成了一个new Number(2) 对象,因此该对象继承了Number.prototype.toString方法。
第7步也很有意思,它生成了Reference Type 对象,这是一种对程序员不可见的中间类型。如果用JS语法来表示,该步骤的结果为 :
var referenceTypeObject =
{
base : new Number(2),
propertyName : toString
}
分析至此,其实才完成了对于2..toString的解析。回到刚开始的函数调用Call expression中:
1.Evaluate MemberExpression. // referenceTypeObject,见上 2.Evaluate Arguments,producing an internal // 神一般的arguments list of argument values. 3.Call GetValue(Result(1)). // 拿到Number.prototype.toString 4.If Type(Result(3)) is not Object, throw a TypeError exception. 5.If Result(3) does not implement the internal // [[call]], 对象和函数区别于此 [[Call]] method,throwa TypeError exception. 6.If Type(Result(1)) is Reference,Result(6) is // num = new Number(2) GetBase(Result(1)).Otherwise,Result(6) is null. 7.If Result(6) is an activation object, // num Result(7) is null.Otherwise, Result(7) is the same as Result(6). 8.Call the [[Call]] method on Result(3),providing // toString.call(num) Result(7) as the this value and providing the list Result(2) as the argument values. 9.Return Result(8).
OK,至此,终于弄明白了2..toString() ~
看似简单的一行代码,却包含了如此多复杂的概念在里面:从词法分析时的贪婪匹配,到运行时包装类、Reference Type、原型继承、函数调用… 越来越不敢说自己懂javascript了






