这是同事问的一道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了

, , , ,

题外话:曾经阅读过一本《java puzzlers》,Joshua Bloch 大大的佳作~读的时候觉得甚为有趣,虽然很多问题实际开发中不常涉及,但是一旦碰到,却也是难以发现。于是就萌生了念头搜集一些 Javascript 中非常 tricky 的问题,并且做出解读。水平有限,不过每个问题我都会尽力从原理的角度讲清楚。

对象字面量

先来看一条js语句:

var foo = {new:1}; // 该语句是否合法?

这本是一个不该出现的问题,理论上来说这样写肯定是不合法的,而且实际开发中,谁会写出这样的对象呢?但是如果将上述语句上放到各个浏览器中试试,竟然都能执行,不仅如此,我们还能写出诸如:

var bar = {
    function:'a',
    undefined:Object,
    100:{},
    NaN:1
}

这些语句放在Chakra,TraceMoney,V8,Rhino中也都是能跑通的。

还是先来看看关于对象字面量的标准,让我们有一个“正确”的认识:

Syntax
ObjectLiteral:
    { }
    { PropertyNameAndValueList }          // 对象属性的键值对列表
PropertyNameAndValueList :
    PropertyName : AssignmentExpression
    PropertyNameAndValueList , PropertyName : AssignmentExpression
PropertyName :
    Identifier                            // Identifier用来做属性名
    StringLiteral
    NumericLiteral
Identifier:
    IdentifierName but not ReservedWord  // 保留字不能当作标识符

从上面的语法来看,JS的对象字面量可以允许三种形式的字符串作为属性名:1标识符 2字符串 3数字 。现在回到刚开始的例子,var foo = {new:1} ,这里定义的对象中new是属性名。令人困惑的是new应该是个保留字,它并不满足标准定义的3种属性名中任意一种。

那它是如何成功被解析的呢?

翻阅Rhino的源码看看,在语法分析的处理中,如果遇到了对象字面量:

private ObjectLiteral objectLiteral() throws IOException
{
  ……
  for (;;) {
    int tt = peekToken();
    switch(tt) {
    case Token.NAME:
    case Token.STRING:
        ……
        break;
    case Token.NUMBER:
        ……
        break;
    default:
        //如果key是一个保留字
        if (compilerEnv.isReservedKeywordAsIdentifier()) {
            // 视图将该保留字转成propertyName, 例如({if: 1})
            propertyName = Token.keywordToName(tt);
            if (propertyName != null) {
                ……
            }
        }
    }
  }
}

从Rhino中对object literal的实现来看,如果遇到保留字,则会做出特殊的处理:试图将这个保留字转成一个合法的Name结点(可以理解为标识符)。可以大胆猜测,在V8或者其他浏览器的JS引擎中也是类似处理的。

>_<这明显是与标准相违背的…

不管你信不信,反正我信了,我只能说,事实就是这样,它确实发生了

附上JS中的保留字(摘自Ecma -262 5rd):

ReservedWord :: See 7.6.1
Keyword
FutureReservedWord
NullLiteral
BooleanLiteral
Keyword
break else new var case finally return void catch
switch while continue function this with default if throw
in try do instanceof typeof for delete debugger
FutureReservedWord
class enum extends super const export import
FutureReservedWord in STRICT MODE
implements let private public interface package protected static yield
NullLiteral
null
BooleanLiteral
true false

 

属性创建访问

var a = new Object
var b = new Object
var c = new Object
c[a]=a
c[b]=b
alert(c[a]===a)     //输出什么
alert(c[a]===b)     //输出什么

这个题目还是很有意思的,它先输出了一个false,随后输出了true …这样也许看不出什么端倪,但是可以推断出如下事实:当运行了c[b]=b这条语句之后,原先的属性c[a]被覆盖了。也就是说,其实c[a]和c[b]指向的是同一个东西~

来看一下 Javascript 对象的属性访问:进行属性有种形式,一种是利用“[ ]”运算符,还有一种是利用“.”运算符。

即 CallExpression. Identifier 或 CallExpression[ Expression ]

注意,在利用“.”的时候,只能后接一个合法的Identifie。但如果利用“[ ]”却可以包含一个表达式进来,本题目中就是利用这种形式去访问object的属性。其实CallExpression. Identifier也会按照CallExpression[ ]的形式去执行。

CallExpression[ Expression ] is evaluated as follows:
1. Evaluate CallExpression.
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).

尤其要注意第6行, 所有方括号中的Expression的值要经过ToString这一步。根据上述步骤,c[a] 会被这样执行:第1&2步骤会返回c的引用,第3&4步骤会返回a的引用,第6步调用 ToString( a ) ,第7步返回一个type Reference object

var referenceObject = {
    base : c
    propertyName : ToString(a)
}

ToString(a)会调用到ToPrimitive(a),进而调用a的[[DefaultValue]]方法,在DefaultValue中会调用到a.toString方法。由于a是Object的实例,a.toString 继承自Object.prototype。根据Object.prototype.toString的描述,a.toString( )会返回“[object Object] ”。因此,最终c[a] 被解析成了 c[ "[object Object]” ] ~

//最终相当于c["[object Object]"]=a;
c[a]=a;
//最终相当于c["[object Object]"]=b;
c[b]=b;

到了这儿,谜底已经解开了,c[b]、c[a]、b是完全相等的。事实上,c[c]===b也会输出true~

, , , , , ,

上一篇探讨了依赖 “Last-Modified ” 和 “HTTP 304 响应” 的一种web优化手段,本质上Last-Modified是充分利用了浏览器端的cache,想办法在服务器端判断资源是否已被修改,如资源仍然可用,则直接使用浏览器端的cache。

本篇介绍的Expires依然是基于浏览器端cache的一种手段,不过相比Last-Modified,它的效用往往会更强一些。

Expires的基本介绍

Expires 是浏览器控制cache的一种基本手段, 它是一个http的实体头域(entity-header),通常被包含在server端的响应头中。Expires后面会指定一个日期,它告诉了浏览器这样一个规则:如果超过该日期,则资源被认为无效,否则依然可以被缓存

换句话说,浏览器根据Expires 指定的时间来判断一个资源是否需要重新请求,如果当前时间还未超过Expires 指定的时间,则浏览器直接从cache中获取该资源(因为浏览器认为该资源依然是有效的,无需重新请求)。

但是如果当前的时间超过了Expires 指定的时间,则浏览器会向server发起请求,期望拿到最新的资源。理想状态下,新资源中依然会包含Expires头,这样浏览器会知道新资源需要被cache多久。

交互模型

Expires交互模型

Expires头也是从HTTP 1.0标准就开始支持的。

如果想在Apache中使用Expires,则必须启用mod_expires,下面列出了一份简单的配置,它根据MIME类型指定了css,js,png类型的资源可以再浏览器端被缓存1年。

<IfModule mod_expires.c>
  ExpiresActive on
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType application/javascript "access plus 1 year "
  ExpiresByType image/png "access plus 1 year"
</ifModule>

标准解读

下面的一些注意点是从HTTP1.1标准中提取出来的:

1. 一个资源从server端返回的时候如果带上了Expires头,并不能确定从当前时间 到 Expires时间内,该资源在server上不被修改。

2. Expires头中指定的时间格式是“绝对日期”(absolute date),由3.3.1节中HTTP-date定义,它必须是RFC1123里的日期格式:

Expires=”Expires ” “:”  HTTP-date,例如:

Expires: Thu, 01 Dec 1994 16:00:00 GMT

3. 如果响应头中的Expires日期并不满足规范,则浏览器不能缓存该资源,并且需将该资源视作过期。

4. 当需要将一个资源设置为“永不过期”时,server 须把Expires头指定的日期设为晚于响应发送时间一年左右。HTTP/1.1服务器不应会发送超过将来一年的Expires日期。

5. 如果server端响应中利用Cache-Control头指定max-age时间,则Expires头域中的时间会被Cache-Control所覆盖。否则浏览器会按照Expires指定的日期进行cache。假设用freshness_lifetime表示一个资源的剩余寿命,那么可以有如下计算公式:

如果包含了max-age指令,则max-age优于Expires头域执行:

freshness_lifetime = max_age_value

否则若Expires头域出现在响应里,定义如下:

freshness_lifetime = expires_value – date_value

详细可参考: 过期计算。其中 expires_value 和 date_value 分别表示Expires头和Date头指定的日期。

优缺点

相比Last-Modified,Expires完全可以不必再次请求服务器,连获取304验证都不必了,浏览器本地根据Expires日期直接判断资源是否有效。对于静态资源文件较多的web站点,利用Expires会显著减少网络请求数量,释放了server端的压力。

但是Expires也有美中不足的地方,一是Expires所指定的日期是从server端打上的,可能会与client端的日期不同步。这样可能导致,一是缓存的内容提前过期了,二是过期的资源没有能够及时更新。

启发式Expiration的计算

有个很有意思的现象,假设有如下场景:

你的 apache 并没有挂上mod_expires,那么在返回的response里便不会存在Expires。因此客户来访问你的web页面,浏览器无法根据Expires来进行cache。

但Apache会主动带上Last-Modified,这种情况下,客户再次访问此页面时,还是会发送带有If-Modified-Since的请求去server端判断,假如你的资源没有变动,那么会返回304状态。

事实真的是这样么?实际上,当客户再次打开这张页面,并没有发出预计中带有If-Modified-Since的请求。浏览器甚至都没有考虑去判断是否HTTP 304,就直接拿cache的资源来用了…

为什么?

这里涉及到HTTP标准中描述的“启发式Expires时间的计算”。简单来说,当浏览器碰到没有Expires头的http响应时,它会自动帮你算一个。比如说利用Last-Modified时间戳,去估算一个合理的过期时间。HTTP标准并没有强制规定计算Expires时间的算法,而是由浏览器生产厂商自己去实现。一个典型的计算方法是:

过期时间 = 现在时间 + 0.1 * (Last-Modified到现在的时间差)

事实上,FF正是采用如上的计算方法…

明白了还有启发式Expiration这回事情之后,就不会疑惑诸如“为什么请求比预计的还要少,为什么server没有指定Expires但browser却自己做主”等问题了…

由于HTTP 标准中这块并没有做出具体规定,因此目前各个浏览器表现的行为并不完全一致。从Fiddler抓包的结果来看:

IE9+,Chrome17+ 针对了所有的静态资源进行启发式Expiration,短时间内的再次访问是不会重新请求的。但FF11+却并非如此,它虽然也为资源计算了启发式Expiration,但FF11只对于image类型的资源直接使用cache,其他类型的资源依然会向server发出请求…

 

, , , , , ,

由于工作的需要,最近会去处理一些前端性能的优化。其实我之前并没有接触过这些,作为一个front-end engineer,实在是弱爆了>_<

根据yahoo的经验,前端响应时间中的约80%被用来完成页面组件的下载。换句话说,从页面打开到完全渲染出来,80%的时间消耗在请求js,css,flash,image等各类元素…

试想,如果浏览器每次打开同样的网页,server都需要重复传输相同的 js、css、image…,这不光消耗了server的一部分处理性能,也是对网络流量的一种极大浪费。现实当然不会如此,我们完全有理由采用更加科学的方式向server请求资源。因此引出了这篇笔记——前端优化之Last-Modified。

浏览器端cache

根据大部分时间花在下载上,至少有2个方向值得我们深入探索:

  • 如何加速每一个请求(从域名解析,建立连接直至接受响应)
  • 如何减少请求数量

利用浏览器端的 cache,两个方向都会涉及到。其实cache并非浏览器用来加速网页访问的秘密武器,而是HTTP协议赋予这些浏览器实现厂商/server实现厂商的一种能力。

浏览器端的cache会充分利用HTTP 304 Not Modified状态,这样虽然并没有能够减少请求的数量,但是至少说server无需在响应中包含资源,从而加速了请求的响应。最理想状态下,如果本地已经有了资源的cache,并且能确保该资源没有变动,那么浏览器就无需再次发起请求了…这样可以极大的减少请求的数量。

下面会详细的介绍一些基本的优化,文中的描述仅在apache 2.x下测试过…

1. Last-Modified模型

last-modified是从HTTP1.0版中就开始支持的一种验证模型,他主要采用 Last-Modified + If-Modified-Since 来判断一个资源是否过期。简单来说,last-modified模型采用如下一种交互方式:

Last-Modified

从上图可以看出基本的交互,假设客户端成功执行了GET请求,那么server端判断浏览器请求的资源有没有过期,如果不过期,那么它会返回304状态。client接收到304状态之后,直接从本地的cache中取出资源来使用。

上图并没有表现出如果资源过期的情况,假如一旦server端判断资源已经过期了,那么此时的交互就跟图中的第一次请求类似,server端会返回该资源的最新版本,并且把最新的Last-Modified传给浏览器。

2. Last-Modified时间戳

在http标准中,并没有强制性规定Last-Modified后跟的时间戳该如何计算。不过一般而言,根据请求资源类型的不同,可以有以下方式:

  • 对于文件资源来说,可能是它的最后修改时间
  • 对于包含多个组成部分的实体来说,可能是组成部分中最新的last-modify时间
  • 对数据库网关来说,可能是记录的last-update时间戳。

在HTTP1.1中明确列出需要注意的是:

1) 无论如何,这个Last-modified时间戳都不会比response产生的时间更晚。假如根据某种算法得出的Last-modified时间戳的确比response产生时间晚,则server应该采用response产生的时间替换该时间戳。

2)Last-Modified值应该尽量靠近服务器产生响应的Date值。这允许接收者对实体修改时间作出准确的估计,特别是如果实体的改变时间接近响应产生的时间

注:原文如下(HTTP1.1  14.29)

An origin server SHOULD obtain the Last-Modified value of the entity as close as possible to the time that it generates the Date value of its response. This allows a recipient to make an accurate assessment of the entity’s modification time, especially if the entity changes near the time that the response is generated.

3)最后,HTTP1.1中规定server只要有可能,就应该在response中带上Last-Modified。

3. 缺点:

尽管这样已经大大提高了系统的响应速度(server无需真正把文件传给前端),但是仍然有美中不足的地方。

  • 依然需要发送的请求。但是如果一张页面包含特别多的元素,每次加载页面时,依然会发送请求给server,受浏览器并发的限制,还是会产生影响。
  • 不够灵活,假设对于文本资源test.js,如果仅仅是修改时间产生了变动,或者例如仅仅多了一个空行,但其实质性的内容并没有产生变动,此时server并不希望把整个文件传输给前端,但是根据Last-Modified的判断规则,此时server不得不重新传输一遍。
  • 【引自他人】Last-Modified只能精确到秒级别的粒度,如果资源在秒级别内会变动多次,则Last-Modified无法体现。

 

 

参考文章:

Best Practices for Speeding Up Your Web Site

HTTP1.0标准

HTTP1.1标准

, , , ,

依然是 javascript,这篇文章会继续纠结比较tricky的问题。

下面这道题是同事出的:

题目

下面三条语句的执行结果是什么:

null == 0
null > 0
null < 0

由于是弱类型,并且 “==” 仅仅是作值比较,因此会有同学想当然的认为 null 和0 值相等。但是上述语句的执行结果都是false …来看一看为什么吧~

分析

还是有问题,找标准。参阅 Ecma-3rd 11.9.3 章节,其中有关于 “==” 操作的详尽描述。从Ecma标准上来看,==的判断最多需要经过多达 22 个步骤 = =| ,这里无需一一列出。

找一下其中与 null 相关的部分:

1. If Type(x) is different from Type(y),goto step 14.
14.If x is null and y is undefined,return true.
15.If x is undefined and y is null,return true.
16.If Type(x) is Number and Type(y) is String,
return the result of the comparison x==ToNumber(y).
17.If Type(x) is String and Type(y)isNumber,
return the result of the comparison ToNumber(x)==y.
18.If Type(x) is Boolean,
return the result of the comparison ToNumber(x)==y.
19.If Type(y) is Boolean,
return the result of the comparison x ==ToNumber(y).
20.If Type(x) is either String or Number and Type(y) is Object,
return the result of the comparison x==ToPrimitive(y).
21.If Type(x) is Object and Type(y) is either String or Number,
return the result of the comparison ToPrimitive(x)==y.
22.Return false.

由于

type(null) is Null and type(0) is Number

不难发现其中第22步正是我们想要寻找的 = =| ,悲剧的是,标准并没有给出什么解释,仅仅是很武断的说,OK,这就是false…

按照惯性思维来说,如果y是一个数字,x是null,那么我们想办法把null转化成数值去做比较,类似于:

ToNumber(null)==0

那么ToNumber之后的 null 的确为0.很可惜,标准里头并没有这样定义…

心有不甘,翻阅了一下rhino中实现 “==” 的代码,发现的确是这么回事:

public static boolean eq(Object x, Object y)
{
    if (x == null || x == Undefined.instance) {
        if (y == null || y == Undefined.instance) {
            return true;
        }
        if (y instanceof ScriptableObject) {
            Object test = ((ScriptableObject)y).equivalentValues(x);
            if (test != Scriptable.NOT_FOUND) {
                return ((Boolean)test).booleanValue();
            }
        }
        //本例中直接跳到了此处
        return false;
    }
    ……
}
,

new Object():

上一篇没有彻底写完。这篇会来具体比较一下两种不同方式~首先来谈一谈new Object()

scope chain

由于Object是一个顶级对象,我们假设在js引擎本身本初始化的时候,Object就已经被生成好,并且置于全局域中。那么其实在第二步骤中“在scope中寻找名叫Object的函数”的开销,无非就是取决于从scope chain 的某一层开始,逐层向上寻找,直至全局域所花费的时间。

Rhino里在scope中寻找一个对象的底层实现为:org.mozilla.javascript.ScriptRuntime.name

我们来仔细研究一下这个函数:

public static Object name(Context cx, Scriptable scope, String name){
    Scriptable parent = scope.getParentScope();
    if (parent == null) {
        Object result = topScopeName(cx, scope, name);
        if (result == Scriptable.NOT_FOUND) {
            throw notFoundError(scope, name);
        }
        return result;
    }
    // 如果不是顶级作用域,则去 parent scope中寻找
    return nameOrFunction(cx, scope, parent, name, false);
}

这个很好理解,如果在函数中我们覆盖了Object的定义,例如:

function foo(){
    var Object = function(){}
    var obj = new Object;
}
foo();

则我们在调用foo的时候,首先会在foo函数的作用域中寻找Object对象,当然此时是找不到的,因此会沿着scope chain向上追溯,寻找Global中的Object对象。

举这个例子不是很恰当,应为在我们目前的case里(上一篇所作的测试),并没有这样向上寻找的过程。也就是说,其实该例中并不存在类似的性能消耗。

不过可以假想,如果在一个嵌套层次很深的 scope chain中,这样做是否也是一种开销呢 ?当然作为js引擎,可以进一步优化,使得执行时跳过 foo内这一层,而直接去Global 中寻找 Object 。

扯远了… = =|

Operator “new”

下面着重来论述一下 new 关键字。想象一下 new Object 的时候发生了什么,Object 是一个构造器,就如同 js 中的Date、RegExp、String、Array 、Function …,那么 new Object 不就是一个普通的 new 构造器 的过程么?

参考 Ecma-262 标准的11.2.2章节,该小节明确描述了 应该如何处理NewExpression:

The production NewExpression : new NewExpression is evaluated as follows:
1. Evaluate NewExpression.
2. Call GetValue(Result(1)).
3. If Type(Result(2)) is not Object, throw a TypeError exception.
4. If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
5. Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).
6. Return Result(5).

在Rhino中我们可以找到其对应的实现:org.mozilla.javascript.ScriptRuntime.newObject

根据标准的表述,每一个构造器都必须要拥有内部方法 [[construct]],当通过new去操作这个构造器时,只不过是调用了构造器内部的 [[construct]]方法。作为Object,也不会例外。

我们可以大胆猜测一下,Object.construct 做了些什么?

  1. 首先它会创造一个空的 js 对象,并无任何属性,此时这个对象的 _proto_ 为空
  2. 然后我们将这个空对象的 _proto_ 赋值为 Object.prototype

事实上,Rhino 的实现也大体如此。这里我们贴出Rhino中的代码,被我简化过了,可以很直观的看出 Object.construct 是怎么回事:

public Scriptable construct(Context cx, Scriptable scope, Object[] args){
    Object val = new NativeObject();

    result = (Scriptable)val;
    if (result.getPrototype() == null) {
        result.setPrototype(getClassPrototype());
    }

    if (result.getParentScope() == null) {
        Scriptable parent = getParentScope();
        if (result != parent) {
            result.setParentScope(parent);
        }
    }

    return result;
}

上述代码比我们的猜想仅仅多了一步 :设置新对象的 parent scope ,别的并无二致。

var a = { }

上面说了很多关于 new Object 的实现,现在看看{ } 对象字面量是怎么回事。Rhino的底层实现中,有一个关于字面量对象生成的接口: org.mozilla.javascript.ScriptRuntime.newObjectLiteral

newObjectLiteral 处理过程如下:

public static Scriptable newObjectLiteral(Object[] propertyIds, Object[] propertyValues, int [] getterSetters, Context cx, Scriptable scope){
    Scriptable object = cx.newObject(scope);

    // 对于空的对象字面量,不会进入下面的for循环
    for (int i = 0, end = propertyIds.length; i != end; ++i) {
        Object id = propertyIds[i];
        int getterSetter = getterSetters[i];
        Object value = propertyValues[i];
        if (id instanceof String) {
            if (getterSetter == 0) {
                if (isSpecialProperty((String)id)) {
                    specialRef(object, (String)id, cx).set(cx, value);
                } else {
                    ScriptableObject.putProperty(object, (String)id, value);
                }
            } else {
                Callable fun;
                String definer;
                if (getterSetter < 0)   // < 0 means get foo() ...
                    definer = "__defineGetter__";
                else
                    definer = "__defineSetter__";
                fun = getPropFunctionAndThis(object, definer, cx);
                // Must consume the last scriptable object in cx
                lastStoredScriptable(cx);
                Object[] outArgs = new Object[2];
                outArgs[0] = id;
                outArgs[1] = value;
                fun.call(cx, scope, object, outArgs);
            }
        } else {
            int index = ((Integer)id).intValue();
            ScriptableObject.putProperty(object, index, value);
        }
    }
    return object;
}

然后我们进一步查看cx.newObject(scope)方法:

NativeObject result = new NativeObject();
ScriptRuntime.setBuiltinProtoAndParent(result,scope,TopLevel.Builtins.Object);
return result;

看着是不是很眼熟…先创建一个NativeObject,然后设置它的 _proto_ 和 parentScope

是的…他们的确是一回事情

而且无论是 new Object 还是 { } 方式,新对象的 _proto_ 总是被指向 Object.prototype ,这个 Object.prototype 对象可以想象为在JS引擎启动时,伴随一系列顶级对象生成好的 。

结论:

那它们的性能差距究竟在哪儿呢 …

单纯从{ } 和 new Object 这个case来看~仅仅是因为Rhino的实现机制导致Object.construct中多了一些抽象一些判断,仅此而已…

BTW,不同的 JS 引擎会有不同的实现。对于 Rhino 来说,我也可以放弃使用newObjectLiteral ,转而将对象的生成方式转到 Object.construct ,只是这样还是需要再进入for循环来附上一个个属性。假如当时的newObjectLiteral 这样实现,那么{ }的速度也就不会比使用new Object() 来的快了,甚至或许会更慢…

回到文章最初的问题,对于现代浏览器来讲,这种优化是有道理的。从测试结果来看,字面量的方式的确是要快一些。但是从JS标准以及JS引擎的执行原理上讲,这种结论是毫无依据的。并且我个人不是很支持这类优化,在我看来,这种结论更偏向于经验性,而非原理。

因此,其实这篇文章是很蛋疼的…

,

引子:

在javascript中创建Object实例~通常有2种方式:

var obj = new Object();
var obj = { };

经常在各类前端优化的文章中看到推荐使用后者进行对象创建,而不推荐使用前者,原因无外乎js字面量执行效率高,而new Object则相对耗性能。

但是却没有一篇文章深究其原因:为何执行new Object会比对象字面量慢?

前阵子和同事讨论到这个问题,于是抽空研究了下。

小测试:

为了突出它们之间的性能差异,拿IE6作了个测试。下面列出了两种方式分别生成100W个对象所需的耗时:

var t1 = new Date().getTime();
for(var i=0;i<1000000;i++)
{
    var obj = {};
}
var t2 = new Date().getTime();
document.write(t2-t1);                   //打印1766

var t3 = new Date().getTime();
for(var i=0;i<1000000;i++)
{
    var obj = new Object();
}
var t4 = new Date().getTime();
document.write(t4-t3);                   //打印5000

果然在古董级别的浏览器里,它们之间有着非常明显的效率差异。令人更加惊异的是,似乎IE6中通过对象字面量来创建object是所有浏览器中最快的……..

标准里的描述:

有问题,找标准。带着标准上的描述去思考,绝大部分问题都会迎刃而解~很可惜,这次例外。

在ECMA262-3rd  11.1.5章节中有如下描述:

The production ObjectLiteral : {}  is evaluated as follows:
1.Create a new object as if by the expression new Object().
2.Return Result(1).

上述文字描述了js引擎是如何解析一个空对象字面量的。此处的as if 很值得玩味,如果按照标准,{ } 会尽力保持同 new Object() 一致的处理方式。那性能上的差距是如何产生的呢?无外乎两点可能性:

1,将js代码编译成目标代码时,所需的开销不同
2,在目标代码的执行期,执行的效率不同

上面说了两点可能性。

第一点比较容易理解,因为字面量很好识别,将 { } 看成一个表达式,那确定该表达式只需要读两个字符;另一方面,new Object( ) 的识别需要费一些周折。

第二点,在运行期间,js引擎并没有把 { } 简单的翻译成 new Object , 对待两种不同的object生成方式,js引擎内部有着不同的执行流程。

在这里暂时无视第一种可能性,上述试验中仅仅测试了执行期的性能差距,而并没有体现代码编译的效率影响。

利用rhino进行探究:

OK~闲话不说,为了一窥js解析器究竟做了些什么,先用rhino分别对文章开头列举的两条语句进行编译。rhino的原理在这里不作赘述,感兴趣的可以参考:http://www.mozilla.org/rhino/

首先写两段JS代码,然后试图将它们编译成java byte code:

  1. 代码1:var obj = { };
  2. 代码2:var obj = new Object();

由于生成的字节码比较长,其实无关代码较多,因此这里只摘出真正与js执行相关的部分:

代码1生成的 java byte code:

private static java.lang.Object _c_script_0(test arg0, org.mozilla.javascript.Context arg1, org.mozilla.javascript.Scriptable arg2, org.mozilla.javascript.Scriptable arg3, java.lang.Object[] arg4);
 0  aload_0 [arg0]
 1  aload_3 [arg3]
 2  aload_1 [arg1]
 3  aload_2 [arg2]
 4  iconst_0
 5  invokestatic org.mozilla.javascript.ScriptRuntime.initScript(org.mozilla.javascript.NativeFunction, org.mozilla.javascript.Scriptable, org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, boolean) : void [85]
 8  getstatic org.mozilla.javascript.Undefined.instance : java.lang.Object [91]
11  astore 5
13  aload_1 [arg1]
14  aload_2 [arg2]
15  ldc <String "obj"> [72]
17  invokestatic org.mozilla.javascript.ScriptRuntime.bind(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.String) : org.mozilla.javascript.Scriptable [95]
20  getstatic org.mozilla.javascript.ScriptRuntime.emptyArgs : java.lang.Object[] [98]
23  getstatic org.mozilla.javascript.ScriptRuntime.emptyArgs : java.lang.Object[] [98]
26  iconst_0
27  newarray int [10]
29  aload_1 [arg1]
30  aload_2 [arg2]
31  invokestatic org.mozilla.javascript.ScriptRuntime.newObjectLiteral(java.lang.Object[], java.lang.Object[], int[], org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable) : org.mozilla.javascript.Scriptable [102]
34  aload_1 [arg1]
35  aload_2 [arg2]
36  ldc <String "obj"> [72]
38  invokestatic org.mozilla.javascript.ScriptRuntime.setName(org.mozilla.javascript.Scriptable, java.lang.Object, org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.String) : java.lang.Object [106]
41  pop
42  aload 5
44  areturn

代码2生成的 java byte code:

private static java.lang.Object _c_script_0(test arg0, org.mozilla.javascript.Context arg1, org.mozilla.javascript.Scriptable arg2, org.mozilla.javascript.Scriptable arg3, java.lang.Object[] arg4);
 0  aload_0 [arg0]
 1  aload_3 [arg3]
 2  aload_1 [arg1]
 3  aload_2 [arg2]
 4  iconst_0
 5  invokestatic org.mozilla.javascript.ScriptRuntime.initScript(org.mozilla.javascript.NativeFunction, org.mozilla.javascript.Scriptable, org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, boolean) : void [85]
 8  getstatic org.mozilla.javascript.Undefined.instance : java.lang.Object [91]
11  astore 5
13  aload_1 [arg1]
14  aload_2 [arg2]
15  ldc <String "obj"> [72]
17  invokestatic org.mozilla.javascript.ScriptRuntime.bind(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.String) : org.mozilla.javascript.Scriptable [95]
20  aload_1 [arg1]
21  aload_2 [arg2]
22  ldc <String "Object"> [97]
24  invokestatic org.mozilla.javascript.ScriptRuntime.name(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.String) : java.lang.Object [101]
27  aload_1 [arg1]
28  aload_2 [arg2]
29  getstatic org.mozilla.javascript.ScriptRuntime.emptyArgs : java.lang.Object[] [104]
32  invokestatic org.mozilla.javascript.ScriptRuntime.newObject(java.lang.Object, org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object[]) : org.mozilla.javascript.Scriptable [108]
35  aload_1 [arg1]
36  aload_2 [arg2]
37  ldc <String "obj"> [72]
39  invokestatic org.mozilla.javascript.ScriptRuntime.setName(org.mozilla.javascript.Scriptable, java.lang.Object, org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.String) : java.lang.Object [112]
42  pop
43  aload 5
45  areturn

虽然看上比较复杂,但是仔细比较一下他们之间的差别,还是很清晰的 : )

rhino内部执行 var obj = { } 生成对象的步骤是:

  1. 生成一个undefined对象 ,并且与obj变量进行绑定
  2. 调用newObjectLiteral方法
  3. 在scope中将obj赋值为newObjectLiteral生成的对象

同样,rhino执行var obj = new Object() 的步骤是:

  1. 生成一个undefined对象 ,并且与obj变量进行绑定
  2. 在scope中寻找名叫“Object”的函数
  3. 调用newObject方法
  4. 在scope中将obj赋值为newObject生成的对象

从表象上来看,new Object 与 { } 分别利用不同的函数来生成 js对象,并且new Object还多了一个“寻找Object函数”的步骤。

,

好久不写blog了,淘宝上买了这个域名,感觉还不错~

echo "hello driftcloudy"

感谢晓枫推荐的theme

感谢金华提供的host

本章是该系列最后一篇,打算看一下 exit 函数中究竟做了些什么。

main函数的返回值

在第(5)篇里完成了_cinit() 的分析之后,mainCRTStartup中接下来代码是:

__initenv = _environ;
mainret = main(__argc, __argv, _environ);
exit(mainret);

很显然, 其实main函数是可以接受第三个参数的,_environ是一个环境变量的指针,只不过一般情况下写程序的时候用不到。从代码中可以看出,调用完main函数后,其返回值mainret会被传递给exit 用作参数。

这里首先要解决一个问题,如果main函数的返回值类型是void呢?

其实准确说写成void main是不对的T T…根据C99的规定,main的返回类型必须是int,并且如果 main 函数的最后没有写 return 语句,编译器要自动加入 return 0 ,表示程序正常退出。例如:

#include <stdio.h>
void main()
{
	printf("%d",100);
}

利用VS2010进行build,OD进入main函数:

注意倒数第二行,这里将EAX清0。其实 main 函数也是一个标准的__cdecl 函数,其return的值会存放在EAX中,因此这里等于会返回一个0 。可见VS2010 这点上还是满足C99 标准的,即使程序员写的是 void main,它依然悄悄的在最后添上 return 0。

来看看VC 6,如果用VC 6来build同样一段代码,则main函数为:

很显然,这里并没有将EAX的值清0再retn,但是接下来依然会从EAX 中拿值赋给mainret 。换句话说,用VC6 编译的时候,main函数并不会有默认的返回值,真正传进exit函数的还是main调用完后的EAX值,不过鬼知道这个时候EAX 是什么。这里可以看出 VC6并没有遵循C99的规范,貌似VC6是98年出来的,想想也算情有可原了…

exit   _exit   _cexit   _c_exit

由于有一系列和 exit 类似的函数,这里一起顺便看下~

void __cdecl exit ( int status )
{
        doexit(status, 0, 0); /* full term, kill process */
}

void __cdecl _exit ( int status)
{
        doexit(status, 1, 0); /* quick term, kill process */
}

void __cdecl _cexit ( void )
{
        doexit(0, 0, 1);    /* full term, return to caller */
}

void __cdecl _c_exit ( void )
{
        doexit(0, 1, 1);    /* quick term, return to caller */
}

在crt0dat.c中定义了上面四个乍一看名字让人很纠结的函数。根据代码中的注释,它们的大概作用为:

  • exit 函数先进行清理工作(比如析构处理、关闭所有标准IO流),然后利用main 函数返回的status 来终结当前进程
  • _exit 函数用于快速终结进程,它并不进行那些“高层次”的清理
  • _cexit 同exit 函数一样执行清理,它并不终结进程
  • _c_exit 同_exit 一样执行清理,它并不终结进程

用通俗的话说,exit 是 _exit 的安全增强版,_cexit是_c_exit 的安全增强版。不过从它们的实现上看,本质上都是 doexit 函数在起作用。在doexit 的内部负责进行各种清理,然后再终结进程或者返还控制权给程序。

来看一下doexit 的大概实现,这里忽略了一些条件编译:

// 是否需要终结进程,0表示终结当前进程,1表示返回控制权给程序
char _exitflag = 0;

/*
 * 两个标志
 * 一旦进入了doexit ,_C_Termination_Done会被设置为true
 * 在doexit 完成了所有清理工作后(进入内核之前),_C_Exit_Done 会被设置为true
 */
int _C_Termination_Done = FALSE;
int _C_Exit_Done = FALSE;

static void __cdecl doexit ( int code, int quick, int retcaller )
{
        if (_C_Exit_Done == TRUE)                          /*如果doexit()被递归的调用*/
                TerminateProcess(GetCurrentProcess(),code);/*直接TerminateProcess终结当前进程*/
        _C_Termination_Done = TRUE;

        /* 在执行其他清理的时候可能会用到retcaller,因此先将它赋值给全局变量_exitflag */
        _exitflag = (char) retcaller;  /* 0 = term, !0 = callable exit */

        if (!quick) {
            /*
             * 如果该程序曾经利用_onexit 或者 atexit  注册过函数,那么在退出前需要执行这些函数。
             * 执行的顺序与被注册的顺序相反,即采用LIFO的模式。
             * 利用atexit 来注册函数的时候,内存中会生成一张函数指针列表,
             * __onexitbegin 和__onexitend 分别指向列表的头部和尾部。
             *
             * 注意:
             * 是先从__onexitend指针开始,逐渐向前遍历,直到__onexitbegin,
             * 这样就能确保LIFO的调用顺序。
             */

            if (__onexitbegin) {
                _PVFV * pfend = __onexitend;

                while ( --pfend >= __onexitbegin )
                /*
                 * if current table entry is non-NULL,
                 * call thru it.
                 */
                if ( *pfend != NULL )
                    (**pfend)();
            }

            /*
             * 会进行endstdio之类的操作,进行清理
             */
            _initterm(__xp_a, __xp_z);
        }

        /*
         * 调用C terminators,貌似实际上没调用什么函数
         */
        _initterm(__xt_a, __xt_z);

        /* 如果定义了retcaller,那么需要将控制权返回 */
        if (retcaller) {
            return;
        }

        _C_Exit_Done = TRUE;

        /* 结束进程 */
        ExitProcess(code);
}

从上述实现可以看出,如果是对于正常的退出,doexit 进行4个步骤操作:

1. 执行 _onexit 或者 atexit 中已经注册了的函数

2. _initterm(__xp_a, __xp_z)

3. _initterm(__xt_a, __xt_z)

4. ExitProcess(code)

析构

如果对象是定义在一个函数的内部,相当于局部变量,那么在函数调用结束之前,会自动析构该对象。

如果是一个全局对象,那么析构其实运行在上面4个步骤中的第1步,即调用_onexit、atexit 注册过的函数时发生。

可以用一段简单的示例代码来说明这些问题:

#include <stdio.h>
#include <stdlib.h>

typedef struct foo1 {
	foo1() { printf("1"); }
	~foo1() { printf("2"); }
	static void bar() { printf("3"); }
} Foo1;

typedef struct foo2 {
	foo2() { printf("4"); }
	~foo2() { printf("5"); }
} Foo2;

Foo1 f1;

void main()
{
	Foo2 f2;
	atexit(&Foo1::bar);
}

这是一段C++代码,因为C中的struct是不被允许定义方法的。最终的输出结果是:14532

这段示例代码中定义了两个变量,全局变量f1和局部变量f2,并且利用atexit注册了一个函数bar 。

根据第(5)篇中的描述,f1 的初始化工作在_cinit 函数中调用_initterm( __xc_a, __xc_z )时完成,至于f2 的初始化,肯定是在运行至main函数中Foo2 f2 一句时才开始进行。当main函数中的语句都执行完毕(此时尚未退出main函数),开始对f2 执行析构。析构完毕随后就退出main 调用,进入exit—-> doexit,开始上述的4个步骤。在第1步中会运行注册的bar函数,然后调用f1 的析构函数,在第2步中调用endstdio 关闭IO,第3步没做啥,第4步ExitProcess。

因此从 cinit —-> main —-> exit 大概发生的事情顺序如下所示:

 

, , , ,

_cinit

在完成了_setargv() 以及_setenvp() 之后,进入到_cinit 函数。该函数的注释很短,就一句“do C data initialize”,让人完全摸不着头脑。不过不用着急,可以阅读_cinit 函数的实现来加以分析。

_cinit 函数很短,大致上分为三个步骤:

1. _fpmath() 或者 (*_FPinit)();

2. _initterm( __xi_a, __xi_z );

3. _initterm( __xc_a, __xc_z );

第一步 是可选的,_FPinit 主要用来初始化浮点运算。只有当用户写的代码中出现了浮点运算,_FPinit 才会被定义。关于_FPinit 由于MSDN 上没相关资料,在此不做深究。

第二步和第三步 是分别对C和C++程序做初始化。_initterm 接受两个指针作为参数,这两个指针中间的内存区域是一张函数指针表。_initterm 会从第一个指针开始,慢慢向后寻找,直到第二个指针结束,中间如果找到了一块内存表示一个函数指针,则执行该函数。

/*
 * pointers to initialization sections
 */

extern _PVFV __xi_a[], __xi_z[];    /* C initializers */
extern _PVFV __xc_a[], __xc_z[];    /* C++ initializers */

void _initterm ( _PVFV * pfbegin, _PVFV * pfend )
{
        /*
         * walk the table of function pointers from the bottom up, until
         * the end is encountered.  Do not skip the first entry.  The initial
         * value of pfbegin points to the first valid entry.  Do not try to
         * execute what pfend points to.  Only entries before pfend are valid.
         */
        while ( pfbegin < pfend )
        {
            /*
             * if current table entry is non-NULL, call thru it.
             */
            if ( *pfbegin != NULL )
                (**pfbegin)();
            ++pfbegin;
        }
}

_initterm( )

先来看看第二步、第三步中都做了什么。这里继续沿用一段空的C代码(main函数中没有任何东西)来build成exe,随后运行该exe,并且OD到_cinit 内部。

这里的两个call 分别表示调用了 _initterm( __xi_a, __xi_z ) 和_initterm( __xc_a, __xc_z ) 。对应的有:

__xi_a = 00406008

__xi_z = 00406010

__xc_a = 00406000

__xc_z = 00406004

继续跟进可以发现,在00406008 至 00406010之间仅有一个函数指针,指向__initmbctable()函数。 __initmbctable() 在第(4)篇_setargv 中曾经有过介绍,它会创建一个新的 创建一个新的multibyte code page,前提是之前并没有被创建。但是 _setargv 中已经调用过了该函数, 并将__mbctype_initialized 被设置为1 ,因此这里 __initmbctable()实际上不会重复创建。

在00406000 至 00406004 之间没有函数指针,实际上什么也不执行。

因此,对于空的C程序:

  • _initterm( __xi_a, __xi_z )  ——> 调用__initmbctable() ——> 实际上没做什么
  • _initterm( __xc_a, __xc_z ) ——> 不产生调用

现在来换一段C程序:

#include <stdio.h>
#include <math.h>

void main()
{
	int a = 5;
	double b = sqrt(a);
	printf("%f\n",b);
}

跟踪的结果为:

  • _initterm( __xi_a,  __xi_z )  ——> 先后调用__initstdio ,__initmbctable
  • _initterm( __xc_a, __xc_z ) ——> 不产生调用

这里由于调用了stdio标准库,因此为了stdio能够正确的工作,需要进行初始化。暂时没有找到关于initstdio 函数的资料,暂时不作深究。

至于为什么第二个initterm 不会产生函数调用,这是因为第二个initterm 是用于C++ data initializations,所以在C 程序中毫无作为。来看一段简单的C++ 代码:

int foo(){
	int a=1;
	int b=9;
	return a+b;
}

int a = foo();

void main()
{

}

注意这里的全局变量a,C语言里是不会允许这种写法的,在C中全局变量只能用常量进行赋值。准确说C中的全局变量在编译期就需要被确定,链接器会把所有的全局变量都放进PE的data区域。说白了,这些全局变量都是直接写死在PE中的。

但是C++ 中确允许像上面那样动态赋值,原因就在于第二次调用 initterm 时,会调用foo函数为A进行初始化。对于上面的C++代码:

  • _initterm( __xi_a, __xi_z )  ——> 调用__initmbctable() ——> 实际上没做什么
  • _initterm( __xc_a, __xc_z ) ——> 调用foo()函数

利用_initterm( __xc_a, __xc_z ) 对C++ data 进行 initialization 有点儿复杂,这里不讨论。

另参考:

http://blog.donews.com/x140yu/archive/2005/05/26/399256.aspx

,