分享

JavaScript语言精髓与编程实践20071115 笔记4 第五章JavaScript的动态语言特性

 quasiceo 2013-11-10
p319

avaScript中,动态执行的对象是“代码文本”,它将装载与执行分成
两个阶段。对于后者来说,执行的只是一个字符串文本。
这主要是由eval()方法带来的效果。

JavaScript 的代码总 是要运行在一个闭 包环境中,这样 它才会有一个
ScriptObject用以 访问 当 前 闭 包 中 的 变 量 表 与 内 嵌 函 数 表 (varDecls 和
funDecls),并且能够通过闭包的parent属性访问到外层闭包。


不同的JavaScript引擎对eval()所使用的闭包环境的理解并不相同。

一些引擎在实现eval时,采用一种简省的方法来为eval的代码块创建闭包。
通过将代码包裹成匿名函数的形式,来使得代码可以在当前
函数内部执行,并且可以访问当前函数闭包内的变量。



// (本例建议在mozilla firefox环境中测试)
var i= 100;
function myFunc() {
window.eval('i ="test"');
}
myFunc();
//输出值'test', 表明调用myFunc()时修改了全局闭包中的变量i
alert(i);
应该留意到这里的eval是使用的window.eval方法而非系统的eval函数。
但是事实上差异并非由window对象(或其方法)导致,换做下面的代码效果
也是一样的:
eval.call(window, 'i= "test"');
由于后面这种情况只是指明调用eval()中传入的this 对象为宿主对象。因此事
实上这是Mozilla中的JavaScript引擎针对宿主的一种特殊实现,这种实现也
的确是旨在给eval()提供一种访问全局闭包的能力。

在Internet Explorer中的JScript的eval()就没有这种能力。无
论是使用window.eval调用,还是使用window作为传入的this 实例,都不可
能让eval()得到访问全局闭包的能力。不过在JScript中可以使用另一种方法来
得到完全相同的效果,即在window.execScript()方法中执行的代码“总是”在
全局闭包中执行。例如:
// (本例建议在Internet Explorer环境中测试)
var i= 100;
function myFunc() {
window.execScript('i = "test"');
}
myFunc();
//输出值'test', 表明调用myFunc()时修改了全局闭包中的变量i
alert(i);
由于window是缺省对象,因此下面的代码是等效的:
execScript('i ="test"');

Script引擎使用execScript()来将eval在全局闭包与函数闭包中的不同表
现隔离开来,而Mozilla的JavaScript引擎则使用eval()函数的不同调用形式来
区分它们。二种实现方法确有不同,但对此具有相同理解的是:在全局闭包与
函数闭包中动态执行代码时,应该具有不同的表现和效果。
就目前所知,包括KJS等引擎在内,除了上述的差异之外,在使用eval()
动态执行时,它们都采用第三种方案“在eval中使用当前函数的闭包”。
1.2.1.3. eval使用当前函数的闭包
一般情况下,eval()总是使用当前函数的闭包。基本上来说,这是最理想
的情况.
eval()代码中允许声明局部变量,那么就必须保证代码的语法树能在eval()过程中动态维护

JavaScript代
码解释执行的过程。
在JavaScript中,代码文本是先被解释为语法树,然后按照语法树来执行的;
在每次执行语法树中的一个函数(的实例)时,会将语法树中与该函数相关的
形式参数、函数局部变量、upvalue以及子函数等信息复制到一个结构中,该
结构称为ScriptObject(调用对象);
ScriptObject动态关联到一个闭包,闭包与ScriptObject具有不同的生存周期;
按照语法树来执行函数体中的代码,需要访问变量时,先考察ScriptObject中
的局部变量等,最后考察upvalue。
在这个过程中,闭包是用来创建并存储值的一套系统;ScriptObject是用
来考察标识符和脚本内容的一套系统。而我们在上面这个例子中,由于eval()
试图 在当 前 闭 包 环境 中 新 声 明一 个 变 量 ,因 此 该 变 量名 需 要 被 添加 到
ScriptObject中,而它的一个值则需要被存放在当前闭包。
这显然意味着ScriptObject与闭包都是需要动态维护的。然而,不同的脚
本引擎对此的理解也并不一致。例如在KJS(safari)中,被eval()执行的代码块
中就不能在最外层用var声明一个变量,也不能声明一个命名的局部函数。这
其中的原因,就在于它不能动态地维护ScriptObject中的这个标识符系统。

在JavaScritp中语句是存在返回值的,该值由执行中的最后一
个子句/表达式的值决定(除空语句和语句中的控制子句之外)。由此得到的推
论是:eval()总是试图返回所执行的代码块的值——由代码块的最后一个语句
决定。下面是第一种的情况(没有返回值):
//例1.返回值为undefined
alert(eval('for (var i=0; i<10; i++);'));
这个例子中,循环的代码体(空语句), 以及循环的控制子句(三个表达式)都
不会有返回值,所以 返回undefined。也许部分读者会认为是空语句在返回
undefined。但事实上并不是这样——空语句不影响返回值

,如下例:
//例2.返回值为6
alert(eval('i=6; for (var i=0; i<10; i++);;;;'));
返回值为赋值表达式语句"i=6;"的结果值,也由此可见,循环体中的、以及其
后的空语句都没有影响到该返回值。

//例4.用eval()来获取值的一般方法
alert(eval('true'));
alert(eval('"this isa string."'));
alert(eval('3'));
等等,但是我们不能用同样的代码来得到一个直接量的对象:
//例5.用eval()来获取“对象直接量”的错误方法
alert(eval('{ name: "MyName", value: 1 }'));
要知道,同样的代码如果不是放在eval()中,就会是正常的:
obj ={ name: "MyName", value: 1 };
这其中的原因,就在于eval()其实是将下面代码:
{name: "MyName", value: 1}
中的一对大括号视为一个复合语句的标识,因此接下来:
第一个冒号成了“标签声明”标识符;
(“标签声明”的左操作数)name成了标签;
"MyName"成了字符串直接量;
value成了一个变量标识符;
对第二个冒号不能合理地作语法分析,出现语法分析期异常;

将这里的直接量声明(的单值表达式),变成一
般表达式语句。明确地指出“由大括号引导”的一段代码是“值”的方法,是
使用强制表达式运算符“()”。例如:
//例7.用eval()来获取“对象直接量”的正确方法,返回对象
alert(eval('({value: 1 })'));
这样一来,由于表达式运算符“( )”的操作数必须是表达式或值,因此相当于
强制声明了“由大括号引导”的一段代码表示对象声明。而这里的表达式运算
符,则表明它自己是一个一般表达式。再综合我们前面所讲述过的,eval()就
将这段代码视作了由单个“一般表达式语句”组成的“语句块”。
不过在JScript中存在一个例外:函数直接量(这里指匿名函数)不能通
过这种方式来获得。例如下面的代码:
var func =eval('(function() {})');
//输出'undefined'
alert(typeof func);
这种情况下,可以具名函数来得到它

。例如:
var func;
eval('function func(){ }');
//输出'function'
alert(typeof func);
但是你可能遇到必须使用匿名函数的情况(不打算使用上例那样确定的函
数名),这时就需要稍稍复杂一点的代码(使用表达式运算)。例如:
// varfunc = eval('(function() { }).prototype.constructor');
// varfunc = eval('({$:function() {}}).$');
//或
var func =eval('[function() {}][0]');
//输出'function'
alert(typeof func);

SpiderMonkeyJavaScript正好相反:可以用eval()返回一个匿名函数,而对具名函数却只返回undefined。回
顾前面所讲的内容,这仍然是由两个引擎对“函数声明的语句含义”的理解不同所导致的。

动态方法调用(call与apply)
JavaScript中有三种执行体

。一种是eval()函数入口参数中指定的字符串,
该字符串总是被作为当前函数上下文中的语句来执行(在某些引擎中允许作为
全局代码中的语句执行);第二种是newFunction()中传入的字符串,该字符串
总是被作为一个全局的、匿名函数闭包中的语句行被执行;第三种情况,执行
体就是一个函数,可以通过函数调用运算符“()”来执行。
由于第二种与第三种都是面向某个函数的,因此他们存在一个完全相同的
动态执行机制:除了使用函数调用运算符来执行之外,也可以使用apply()和
call()方法作为动态方法来执行。
下例说明这种执行的一般性用法:
function foo(name) {
alert('hi, ' +name);
}
//示例: call与apply的一般性使用
foo.call(null, 'Guest');
foo.apply(null, ['Guest']);
在这个例子中,函数foo()使用call()与apply()方法来进行动态调用时的效果并
没有什么不同。其实这两个方法的差异也仅仅在于调用时的参数不同——其
中,apply()的第二个参数传入一个数组或arguments值,因此具有相当的灵活
性,而且一般来说效率也较call()方法稍高。

将一个普通函数作为一个对象的方法调用,或者将A对
象的方法作为B对象的方法调用,惟一要做的,也仅是改变一下this 引用。在
JavaScript中,apply()和call()都提供这样的能力——它们的第一个参数用于指
定this 对象。在前面的例子中,我们指定为null值,这表明执行中传入缺省的
宿主对象

Area.prototype.doCalc = function(v1) {
v1 *=2;
calc_area.apply(this, arguments);//v1就是arguments[0]
}
JavaScript中,形式参数直接引用自arguments。因此修改形式参数(例如
这里的v1)是会直接影响到arguments的。因此下面的代码与之完全相同:
Area.prototype.doCalc = function() { // <--注意这里没有声明形式参数
arguments[0] *=2;
calc_area.apply(this, arguments);

在实现时,类Arguments与Array通常是共享一个父类的——这是因为它
们都有一个需要自维护的length 属性。因此我们事实上也可以将Array原型中
的某些方法apply()在Arguments的实例上。这也是上一小节中
slice.call(arguments, 1)
这行代码能被成功调用的根本原因。

call()与apply()总
是保证函数运行在“函数自己所在的闭包环境”中。
更加简单的说法是:函数总是运行在自己所在的闭包环境中,这与它是否
是通过call()或apply()调用无关。这与上一章所述的内容是一致的:函数的闭
包,是由函数实例和函数在代码文本中的物理位置所决定的。

一个构造器的原型是可以被重写的,重写意味着“此前
的一个原型被废弃”。该构造器的实例中:
旧的实例使用这个被废弃的原型,并受该原型的影响;
新创建的实例则使用重写后的原型,受新原型的影响。

(function Object(){
}).prototype.value =100;
//显示值undefined
var obj =new Object();
alert(obj.value);

在执行期的、将直接量作为表达式的时候——它没有了语法期命名的含义,表达式中声明的标识符即:上面的Object被忽略。
所以事实上:
(function Object(){
// ...
})
完全等效于一个匿名函数:
(function (){ //<--在作为语句的表达式执行时,没有“声明/重写标识符”的副作用
// ...
})
所以代码:
(function Object(){
// ...
}).prototype.value =100;
事实上是在“执行期”修改一个匿名函数的原型(prototype 属性)。而该匿名函
数在该行代码之后没有任何的引用,也就被立即释放了,未对系统有任何的影
响。

var obj ={};
obj.foo =function foo() {
//显示true,代码内容相同
alert(foo.toString() === arguments.callee.toString());
//显示false, foo()函数不是当前的调用者,//注:原书错误错误, 实际测试发现最新的firefox25,显示true.
alert(foo === arguments.callee);
}
obj.foo();


//foo(); // this throws an error
if (true)
function foo() {alert('hi.') }
foo();

“条件化函数声明(conditionalfunctiondeclaration)”,一方面它不具有直接函数声
明(functiondeclaration)在语法解释周期声明标识符的特性,另一方面有具有
在执行期隐式地声明标识符的特性,

被注释的第一行foo()调用会产生异常,因为第三行的foo()函数是有条件的声明的。
当条件为true 时,函数被成功声明,因此最未一行是可以成功执行的;当条件为false 时,最未一行也会抛出异常。
但是从本质上来说,这只是因为支持在运行期对语句中声明的函数作解析。
而if 语句后的这个函数声明被理为“直接量表达式语句”,作为语句的语法元素时,
就能按照函数声明的语法效果了进行理解了。
同样的,即使是在if 语句之后,如果将它作为表达式中的运算元,也是不能达到声明函数的效果的。
例如:
if (true)
(function foo() { alert('hi.') } ); // <--作为连续表达式运算的运算元
foo(); // this throws an error

构造器的原型(prototype属性)不受重写影响

如果重写了Object()——以及其它的内置构造器,那么
当使用“newConstructor()”这样的语法来构造对象系统时,仍然是正常的。
但是,既然我们重写了Object(),那么prototype又从哪里来呢?
答案是:构造器的原型创建自系统内部的对象系统,而不是可被外部覆盖
的标识符Object,因此原型总能被创建。下例说明这一点:
// 1.备份一个系统内部的Object()
var PureObject =Object;
// 2.重写
Object = function() {
}
// 3.声明构造器
function MyObject() {
}
// 4.删除constructor成员,以便访问到父类的同一属性
delete MyObject.prototype.constructor;
// 5.显示true,表明构造器的原型(对象)创建自PureObject;
alert( MyObject.prototype.constructor ==PureObject);
所以构造器——我们这里指任意的构造器函数——的原型属性,并不受
Object()类的重写的影响,它总是创建自一个系统引擎中的对象构造器。

“内部对象系统”不受影响,无论Object是否被重写.
//从对象直接量的构造器中得到系统原始的Object.prototype
function Object() {
// ...
}
Object.prototype = {}.constructor.prototype;

直接量声明总是会调用“内部对象系统”来构造对象。因此,尽管除了
undefined之外的所有直接量,都有对应的构造器,但是构造器与直接量却是
两套系统——只是,在初始时二者是关联的。
当构造器被重写之后,直接量只与重写前的构造器相关,与重写后的就没
什么关系了。因此重写不会对直接量声明构成什么影响。
例如:
// 1.取一个系统缺省的字符串直接量
var str1 ='abc';
// 2.重写String()构造器
String = function() {
}
String.prototype.name = 'myString';
// 3.取重写后的字符串直接量
var str2 ='123';
// 4.如果name成员有值,则证明重写会影响到直接量
alert( str1.name );
alert( str2.name );firefox25均显示undefined!


var obj1 ={};
//Object = function(){
//}
Object.prototype.name = 'myObject';
var obj2 ={};
//如果name成员有值,则证明重写会影响到直接量
alert( obj1.name );
alert( obj2.name );都显示MyObject
对于三种值类型(string、boolean、number)直接量,函数直接量(function),以及
正则表达式对象(RegExp)直接量来说,上例的效果与结论都是一致的。但是,
对于Object与Array来说却并非如此。
在firefox 中测试,则显示:这表明当重写构造器之后,直接量声明将使用“新构造器的原型”来构造对象 ,
但不会影响到旧的、已构造的对象实例。原书此说法错误!
var obj1 ={};
Object = function(){
}
Object.prototype.name = 'myObject';
var obj2 ={};
//如果name成员有值,则证明重写会影响到直接量
alert( obj1.name );
alert( obj2.name );均显示undefined!

//示例2
Object = function() {
this.value = 'myValue';
}
var obj3 ={};
alert(obj3.value);
均显示undefined!


JavaScript给对
象基类添加了一个内置的hasOwnProperty方法。该方法不检测一个实例的父代

与此类似的,重写RegExp的exec()方法也没有什么特殊性——即不会影响SpiderMonkeyJavaScript中的rx()
语法,也不会影响其它方法调用。
类,因此如果“obj.hasOwnProperty('aName')”返回true,就表明aName是在
该对象obj中被重写的——当然如果对象obj是一个构造器的原型,那么也就
可以检测成员是否从原型链上继承而来,或是在当前原型中重写的。

//从对象(以及其原型中)删除属性
function deleteProperty(obj, prop) {
if (prop inobj) {
do {
if (obj.hasOwnProperty(prop)) break;
}
while (obj.constructor &&(obj = obj.constructor.prototype));
}
delete obj[prop];
}

由于原型链上可能有多次重写,所以即使使用该函数,也不能保证在
一次就能删除掉该对象的指定成员。因此某些情况下会需要一个加强版本:
function deleteProperty(obj, prop, forced) {
do {
if (!(prop in obj)) break;
do {
if (obj.hasOwnProperty(prop)) break;
}
while (obj.constructor &&(obj = obj.constructor.prototype));
delete obj[prop];
}
while (forced);
}

//示例1:重写变量,其它变量对它的引用不受影响
var v1= {name: 'MyName' };
var v2= v1;
// 1.重写v1
v1 =100;
// 2.显示'MyName', 表明v2仍然是被引用的对象,但与(当前的)v1已经不同了
alert( v2.name );

在JScript中,一个隐式声明的位置在window对象之外。而按照约定,使
用'varName in window'检测应当返回真值。于是,JScript 采用了一种策略来维
护这种关系:变量们于global_object的对象闭包内,同时通过一个引用(或内
部变量)作为window对象的成员。
JScript采用这种(必须维护window与变量名之间的关系的)机制的原因 ,
是因为window对象对引擎来说其实是一个外部对象。JScript脚本引擎来自一
个名为ActiveSciption的系统,而window来自浏览器的DOM系统。其中的区
别之一在于:JScript 引擎中的对象成员都可以删除,例如JScript 的全局对象
的成员其实是可以删除的:
//删除global object的成员
delete parseInt;
delete eval;
...
而widnow对象的成员(包括动态添加的成员)不可以删除。例如下面的代码
导致异常:
//删除window对象的成员导致异常(这由不同的宿主对象自行决定)
delete window.self;
delete alert;


隐式声明的变量被动态地添加到闭包(这里指调用对象ScriptObject块)的varDelcs中,这些动态添加的
内容是可以被delete运算清除的。而使用var显式声明的变量在语法解释期就被添加到varDelcs( 的 前 端 ),
这个结构在运行前即被创建好,因此不是动态的,也不能用delete动态删除。

使用“window.XXX”方式添加的成员“可
以用for..in 列举,但不能删除”,而隐式变量声明的全局变量“不能用for..in
列,但能删除”,其根本原因在于它们位于不同的对象闭包域中。


而为了使”in 运算符在这些不同的域中表现一致”,JScript 强加了一层外部关
系。然而在delete删除过程中,JScript 未能清除上述关系。所以在JScript 中,
在“deletevarName”操作之后,一方面即可以检测到“varNamein window”
仍为真值,另一方面又存取异常(因为它实际上已经被删除了)。

语句语法中的重写
大多数语句本身不会导致重写

。尽管在其语法元素中使用赋值表达式可
以带来重写效果,但这是赋值表达式导致的,而非语句本身导致的重写行为。
一些语句本身有“暂存对象引用”的行为,最明显的有如with语句,就
是用于操作一个对象闭包的。而在“暂存对象引用”的影响下,在语句中重写
对象的行为将变得很特殊。例如:
var obj ={
name:'myName',
value:'hi'
}
// 1.暂存:for语句暂存了obj对象的一个引用
for (var iin obj) {
// 2.重写:该重写不会影响到for语句暂存的obj对象引用,以及它对该对象的列举效果
obj ={};
}
除了with语句以及for、for..in、while、do..while等循环语句之外,switch

这也是在章节“1.6.2.2对象闭包带来的可见性效果”中我们使用self属性名,而不是this作为属性名的原
因。

反之,例如下一小节将讲到的catch()子句自身就具有重写行为。
语句也会暂存对象引用。但特殊的是,switch语句中只有一次成功检测case
分支的机会,因此下面这个示例构造得令人困惑:
1 var obj =obj1 ={}
2 var obj2 ={};
3 switch (obj) {
4 case obj =obj2:alert('obj2');break;
5 case obj1: alert('obj1'); break; //<--跳转到该分支
6 }
7 //显示true
8 alert(obj === obj2);
这个示例主要检测obj引用是obj1还是obj2。从初始状态到第3行,obj引用
都指向obj1,而第3行是一个switch()语句,因此它缓存了一个obj的引用。
第4行代码完成了一次重写,使它指向了obj2。如果这次重写对switch的缓存
是有效的,那么第4行的case分支就会检测成功,因为分支检测的正是obj2。
然而我们看到结果仍将显示'obj1'。所以第4行的重写并没有影响到缓存。
如果从如此细致的粒度来考查语句,那么if 语句也有类似的性质。例如:
9 // (续上例)
10 if (obj ==(obj = obj1)) {
11 alert('obj1');
12 }
在第10行中,表达式“obj=obj1”将有两个语句效果:
将obj重写为obj1的引用
返回obj1引用
但在外部的if()语句中,由于if 是缓存了obj引用,因此它仍是重写前的值“obj2
的引用”。所以上面的第11行代码不会执行到。
语句“暂存对象引用”的效果只针对引用本身,而不包括它的成员。



在结构化异常处理的catch(exception) {... }块中,我们可以 在exception
位置声明任意一个变量名。由于这个变量名是被动态地声明的,因此也可能重
写当前闭包中的、或闭包链上的标识符。ECMA规范对此约定:该变量将仅在
catch块中的catchStatements部分使用,并在退出该块时从闭包中移除。

function foo(x) {
try {
return x;
}
finally {
x= x*2;
}
}
//显示值100
alert( foo(100) );

答案是“否”——上例返回的并不是x*2的值。如果用户试图使finally{...}中
的修改有效,那么应该在finally 块中使用return子句,例如:
...
finally {
x= x*2;
return x;
}
但是这个问题还有更加复杂的细节:在上面这个例子中,我们使用的是一
个值类型数据(数值100)。这种情况下,从执行过程来说,finally 语句“挂起”
的将会是“returnx”这整个语句,而变量x是一个值类型数据,因此在“挂起”
时就已经被取值了,于是我们在finally{...}中的代码对变量x的修改不会影响
到它(这个已经被取走的值)。然而,如果我们在这里使用对象并修改它的成
员,那么由于“挂起”时取走的只是引用,所以finally{...}中的代码仍然会造
成影响

。例如:
function foo(x) {
try {
return x;
}
finally {
x.push(100);
}
}
//显示返回数组字符串形式'1,2,3,100'
alert( foo([1,2,3]) );

//示例1:分析对象运算过程中,运算是否产生包装行为
var data =100;
// 1.instanceof运算不对原数据进行"包装"
data instanceof Number
// 2.如下导致异常,因为不能对元数据做in运算
'constructor' indata
而在做下面的运算时,它就需要通过包装先将元数据变成对象:
// (续上例)
// 3.成员存取运算时,"包装"行为发生在存取行为过程中
data.constructor
data['constructor']
// 4.所谓方法调用,其实是成员存储后的函数调用运算,因此"包装"行为发生的时期同上
data.toString()
// 5.做delete运算时, "包装"行为仍然发生在成员存取时
delete data.toString
综合上述3~5的例子,我们可以知道,所谓元数据到对象的“隐式包装”,
其实总是发生在成员存取运算符中


对string、number与boolean的v1值检测的结果表明:
进行typeof 检测,都不是'object',这表明“元数据类型”不是对象;
元类型 直接量(v1) 包装类(v2)
特性
typeof <value>
<value>instanceof
<class>
v1 v2 v1 v2
undefine
d
undefined
'undefin
ed'
(*)
string '', "" String 'string'
'object' false
true
number 数值 Number 'number' true
boolean true, false Boolean 'boolean' true
进行instanceof 检测,值都是false,表明它们都不是通过对象系统(构造器)
创建的。
这两项特征真实地反映了“元数据类型”或“值类型”(相对于引用类型)数
据的特性。

包装类是JavaScript用来应对“可以调用值类型数据的方法,使它看起来
象是对象调用”的处理技术。这与后来在.NET 中出来的“装箱(boxing)”是一
样的,只是JavaScript将这种技术称为“包装(warping)”而已。

准确的说是在该对象的
生存周期结束时),临时创建的包装对象也将被清理掉。
显然变量n1并不等同于包装后的“newNumber(n1)”,因此“值类型数据
的方法调用”其实是被临时地隔离在另外的一个对象中完成。而同样的原因,

JavaScript没有类似于“拆箱(unboxing)”的过程,因为它的函数形式参数不支持值的传出。
无论我们如何修改这个新对象的成员,这种修改也会不影响到原来的值:
//声明值类型数据,并修改它的成员
var str ='abc';
str.toString =function() { // <--这里的重写是无意义的
return '';
}
//显示'abc', 表明对str包装后对象的修改,不会传递到原变量
alert(str);
alert(str.toString());

这个例子是可以预期的。然而有些函数对入口参数值也会做类似的处理,
其效果就会变成不可预期,如下例(参见1.2.1.1值类型与引用类型):
var str ='abcde';
function newToString() {
return 'hello, world!';
}
function func(val) {
//在这里,如果val是对象则修改它的成员,否则修改值类型包装的结果对象(并随后废弃该对象)
val.toString =newToString;
}
//试图显示'hello, world',但实际仍然显示'abcde'
func(str);
alert(str);

JavaScript中的值类型数据包括number、boolean与string,这其中只有
boolean是序数的,其它两种则是非序数的(number在JavaScript中实现为浮

几乎没有什么书籍提及到这个奇怪的特性及其成因。我为此专门阅读了SpiderMonkeyJavaScript的源代码,
其中的注释表明,这是为兼容旧版而保留的一项特性。因此我完全不建议读者使用该特性,但是在写有关
SpiderMonkeyJavaScript的兼容代码时,必须将这项特性考虑进去,以避免在使用typeof 时出现误判。

另一种常见的区分方法是动态数组与静态数据,是按照数组对存储的使用方式来区分的。从这个角度上来
说,JavaScript的数组是一种动态数组,这也是适合于实现关联数组的一种存储策略。
点数)。因此JavaScript必然以关联数组为基础,来实现“(使用索引存取的)
数组”这种对象类型。
1.6.1. 关联数组是对象系统的基础
使用 关联 数组作 为实 现方案 的另 一个原 因——也许 是更 加本质 的原
因——则是在JavaScript中,关联数组其实是实现对象系统的基础。也就是说,
早在有Array类型之前,系统已经为Object类型实现好了关联数组,而Array
只是这种特性的一种应用罢了。
关联数组下标是非序数的,所以它看起来更象是一张“表”,这大概是它
在C++中被称为map,或在python中被称为字典的原因了。JavaScript的对象
的“表特性”非常明显:可以使用“[ ]”来作为成员存取符,而且成员可以是
任意的字符串——而无论该字符串是否可以作为标识符。
更进一步确指地说,JavaScript中对象(的原型)所持有的属性表①
,就是
一个关联数组的内存表达。因而:
所谓属性存取,其实就是查表;
继续从这样的实现细节来考察JavaScript的对象,那么:
所谓对象实例,其实就是一个可以动态添加元素的关联数组;
所谓原型继承,其实就是在继承链的关联数组中查找元素。
——由此看来,JavaScript的内部实现并不怎么神秘。
1.6.2. 用关联数组实现的索引数组
JavaScript中的数组(Array对象类型),首先是一个对象。因此它的属性存
取也是查表,例如:
aArray["length"]
便是进入了一个关联数组存取的过程。同样,我们去看它作为索引数组时候的
特性:
aArray[0]
其实也与前面完全一样、毫无二致。如果你非要说有什么不同,那大概是将字
符串"length"换成了数值0。当然,事实上你也可以使用字符串0:

参见:XXXXXXXXXX
aArray["0"]
这与上面的存取效果没有任何的不同。
所以JavaScript中的索引数组,只是用数字的形式(内部仍然是字符串的
形式)来存取的一个关联数组。由于在这一特性上,它与普通对象没有任何的
区别,因此你完全可以用"in"运算,或for... in 语句来考察它的成员——我这
里指的是数组下标/元素。例如:
var aArray = ['a', 'b', 'c', 'd'];
//显示true
alert('1' in aArray);
//列举元素0~3
for (iin aArray) {
alert( i +'=> '+ aArray[i] );
}
一般来说,我们对索引数组中的元素进行考察时,应使用递增/减序的for
语句。例如通常实现在数组中查找对象的indexOf 方法:
Array.prototype.indexOf =function(obj) {
for (var i=0, imax=this.length; i<imax; i++) {
if (this[i] === obj) return i;
}
return -1;
}
var aArray = ['a', 'b', 'c', 'd'];
alert( aArray.indexOf('b') );
如同前面所讲述的,由于JavaScript中的索引数组并不是真正“连续分配”
的有序个元素,因此这样的索引方法并不会真正地带来什么效率——也许这在
其它高级语言中是更有效的,亦或有特殊的优先方案。而且当数组变得更加无
序、自由存取时,这种列举的效率可能会更差。例如我们可能声明一个数组为
一百万个(或更多
①)元素大小,但事实上却没有一个元素被存入数组——
JavaScript的数组在存储特性上是动态数组,是即需即分配的。因此对这种数
组做上述列举,会产生大量的虚耗:
var aArray = newArray(100000);
for (var i=0, imax=aArray.length; i<imax; i++) {

JavaScript的数组的元素数的理论上限是一个无符号32位整数大小(大约40亿 )。
//列举aArray.length次
}
然而我们应该记得这个数组本质上仍然是一个关联数组,因此它并非必须
使用这个增/减序列举的方法。所以你也可以使用一下for..in 来解决问题:
var aArray = newArray(100000);
for (var iin aArray) {
//列举次数=真实存在的元素数+少数可被列举的重写或修改后的原型成员
}
但在使用这种方法的时候,我们需要注意的是,所列举的元素将不再是某种特
定的序列(例如上例中的0..100000-1)。关联数组是无序的,因此我们不能保
证for..in是准确地按照索引数组那样的增/减序列来列举。
在Array对象类型的实现上,JavaScript主要是保障了length 属性和数组
元素维护。基于数组这种数据结构本身的特性,JavaScript在以下情况中隐式
地维护length 属性:
使用push、pop、shift、unshift方法在数组首尾增删元素时;
使用splice方法在数组中删除元素时;
又因为JavaScript的数组是动态数组,所以具有如下的特性:
给大于或等于length 值的指定下标赋值时,会隐式地重设length 属性值;
可以显式地重写length 属性来调整数组大小。
然而,JavaScript数组的“基于关联数组实现”的这一事实上,带来了更
多的疑问。其中最关键的一项是:如果数组是不连续的,那么在变更数组内部
成员(及其顺序时),是否要为“索引数组”维护某种连续性?
这取决于“索引数组”的效果是一种真实存取值,还是一种运算规则的假
象——我想这样的陈述过于坚涩,而我的意思不过是“JavaScript的索引数组
的连续性,只是一种表现效果”。
在一般语言中,对于一个索引数组来说,如果某个下标没有存放值,那么
它的取值应当是空值(null、nil或空串等)。在JavaScript中,这个效果被解释
为“应当是undefined值”——这样的设定与一般语言是没有什么差异的。但
是一般语言为了实现这个效果,得将该数组长度所示的内存区间填以0值,因
此这是一种实现效果。而JavaScript并不为该特性做出任何专门的实现,按照
对象系统所约定的“访问不存在的对象成员的值为undefined”,所以数组自然
便有了上面的效果,因而我们说这只是一种表现效果。
正因为这是一种表现效果,所以用for 语句增/减序列举时,它就“表现
为”索引数组;用for..in 列举时,它就“是”关联数组。而更进一步的推论就
是,根本不必为数组“维护某种连续性”——那只是不同语句的“表现效果”。
然而有趣的地方就在这里。我们设定下面这样一个数组:
var aArray = newArray(1000*10000); // 1千万个元素
aArray[1] =3;
aArray[3] =1;
aArray[5] =5;
aArray[9999] =99999;
接下来我们问:如果用内部方法sort()排序该数组,那么会有多少个元素参与
运算呢?答案是:仅有上面的四个元素参与运算。


undefined值是不参与sort()运算的。



难以避免一个对象被污染,因此在一些解决方案中才会提供
一个名为“safed<in>operator”的函数,以使得用户代码可以有效地检测对象
成员。下例说明这种方案:
//示例3: safed <in> operator
// (使用上例中的obj对象)
$in =function(p, o){
var $o= {};
return function(p, o){
return !(p in $o) &&(p ino);
}
}();
//检则对象obj,显示值false
alert( $in('vv', obj) );
然而我们仍然不能确保obj在声明时就一定“不包括'vv' 成员”。所以上面
这种方案仍然是不够安全的。


解决这个问题的方法之一,是放弃使用in 运算,而使用
hasOwnProperty():



元类型其实只
有两类:值类型和引用类型。在这个类型系统中,只有函数与对象是引用类型
的,JavaScript的对象系统衍生自元类型object,函数式语言特性则基于元类型
function。
回顾“1.3.2.1 运算的实质,是值运算”,我们注意到引用类型自身其实并
不参与值运算。对于运算系统来说,引用类型的价值是:
标识一组值数据;
提供一组存取值数组的规则;
在函数中传递与存储引用(标识)。
所以我们必然面临的问题是:所谓的类型转换,其实是指
值类型之间的转换


将引用类型转换为值类型(以参与运算)
这样的两种情况。而这,就是JavaScript中类型转换的全部了。

var x= {};
alert( x[1,2,3,4] );
这个例子中的“[]”是对象成员存取运算,因此“1,2,3,4”被作为表达式理解,
并隐式地要求将结果值转换为字符串。因此这里最终的运算是:x['4']

var x= y={};
x+ y== ? // <--结果将是一个字符串
由于x、y是对象,因此引擎先检测其
valueOf()返回值的类型,并随后调用了toString()方法来转换为字符串类型。因
此事实上该运算变成了:
"[object Object]' +'[object Object]'

with语句总是试图打开一个对象的对象闭包,因此如果它作用的表达式返
回一个值类型数据,那么with语句会通过包装类(参见“1.5包装类:面向对
象的妥协”),将值包装为对象并打开它的闭包。与此类似的,for..in语句也会
有这样的过程。由于包装类的实质是将值类型转换为同类的对象类型,因此这
里也 可 以 称 为 一 个 隐 式 的 类 型 转 换 过 程 。 相 同 的 原 因 , 也 可 以 认 为
“(2).prototype”这个存取运算中也存在一个包装类的类型转换过程。
最后对switch语句作一些补充。switch()语句试图对表达式求值,并用该
值与case分支中的值进行比较运算。在比较中采用的是类似于“===”操作符
的运算,亦即是说是优先进行类型检测的,因此这里不会发生类型转换过程。
因此下面的示例不会进入到case分支:
var obj =new Number(2);
switch ( obj ){
case 2: alert('hello');
}


undefined能转换为特殊数字值NaN,因此它与数字值的运算结果将会是
NaN,而不会导致异常。例如:
//示例: undefined的转换
var value =undefined;
//显示NaN
alert(10 +value);
undefined能转换为字符串'undefined'与布尔值false。它总是恒定的得到这
两个值。

任何值都可以被转换到number类型的值。如果转换得不到一个有效的数
值,那么结果会是一个NaN。而NaN其实是一个可以参与数值运算的值,这
样处理的目的,是使得表达式可以尽量求值,而不是弹出一个异常。
Number值转换到布尔值时,非零值都转为true,零与NaN都转为false。

boolean值的true 和false 总是被转换为数值1和0。

如果字符串由'0x'(零和x|X)开始作为前缀,且由0~9、A~F、a~f这些字
符(不包括小数点)构成,则它总是可以被作为十六进制数转换为数值的。也
许一些人会因此推论出“以0为前缀则表示八进制”,但事实上下面的代码证
明这种推论是错误的:
/**
*例1
*前缀0可以用在直接量中,以表示8进制
*结果值输出:12
*/
alert(033 -15);
/**
*例2
*前缀0用在字符串中,在(隐式)转换将被忽略
*结果值输出:18
*/
alert('033' - 15);
尽管你能将boolean值转换为字符串“true”和“false”,但是反过来你却
不能把这两个字符串转换对应的boolean值。在字符串中,有且仅有空字符串
能被转换为布尔值false,其它的(任何有值的)字符串在转换后都会得到true。

如果不指定radix,那么将采用数值直接量声明中使用的规则:将有前缀
“0x”的处理成16进制字符串,有前缀“0”的处理为8进制字符串。然而令
人大跌眼镜的是,前缀“0”在隐式转换中却并不被识别为8进制字符串。所
以下面的两个例子的结果并不一致:
/**
*例3
*(同上,隐式转换)
*结果值输出:18
*/
alert( parseInt('033' - 15) );
/**
*例4
*显式地转换中,parseInt()将前缀“0”识别为8进制
*结果值输出:12
*/
alert( parseInt('033') -15 );
parseInt()和parseFloat()另一项特性在于总是尽可能地得到转换结果。即使
字符串中只有(前缀的)部分能被转换,那么该转换也将成功进行。

如果该一个数据是引用类型,且该数据需要进行值运算,那么引擎
将先调用它(或它经过包装后的对象)的valueOf()方法求值,然后以该值参与
值运算。

有一部分引用类型数据,通过它的valueOf()方法还是不能得到一
个有效的值。例如表格中的Object、Error、RegExp对象,以及Function对象。
在这种“valueOf()返回引用类型”的情况下,引擎会再次调用toString()方法以
取得一个字符串值。字符串总是可以以值类型的形式参与运算的,这保证了
JavaScript内部的任意数据总是可以直接参与值运算。

元类型中的)值类型数据在类型转换
中并不会并调用toString()或valueOf()方法,也不会为此进行包装类的操作。

如果数据是值类型,则直接参与值运算;否则,
如果valueOf()返回一个值类型数据,则以该数据参与值运算;否则,
使用toString()返回的字符串参与运算。
在JavaScript中的任何一种数据类型,都可以(显式或隐式地)转换
到字符串类型。提供这种能力的前提是:JavaScript约定一切都是对象(undefined
值除外),因而必然存在toString()方法,也就可以取得当前对象的字符串表示
值。事实上,隐式转换其实也是通过调用toString()方法达到转换数据的目的,
只不过是由JavaScript引擎来负责调用这个方法而已。
在ECMAScriptv3标准中,对象还有一个toLocaleString()方法。它的含义
是根据本地规范来显示某种格式化的字符串,它被应用于Array、Date、Number、
Object等对象,它的效果与引擎的、操作系统环境的设置有关。在缺省情况下
它 与toString() 的 效 果 是 一 致 的 , 且 隐 式 转 换 使 用toString() 而 不 是
toLocaleString()

在进行Number到字符串的转换时,还存在两个问题:指定小数点的位置
与如何启用指数记数法。这需要使用到Number对象的另外三个方法,分别是 :
toFixed(digits):使用定点计数法,返回串的小数点后有digits位数字;
toExponential(digits):使用指数计数法,使返回串的小数点后有digits位数字;
toPrecision(precision):使用定点计数法,返回串包括precision个有效数字(如
果整数部分少于precision位,则在小数部分用0补齐)。如果整数部分多于
precision位,则使用指数计数法,并使小数点后有precision-1个数字。

Array的toString()
不能支持多维数组,也不能有效的处理元素中的引用类型;
大多数情况下我们并不能用toString()来实现序列化。用函数uneval()来做这件事。

为了实现uneval()函数,引擎为
每个对象定义了一个名为toSource()的方法。它类同于toString(),但只用于
uneval()函数

。因此上述代码的效果完全等义于:
// (续上例,适用于SpiderMonkey JavaScript引擎,等义于调用uneval()函数)
alert( arr.toSource() );
alert( foo.toSource() );

序列化时处理的是值本身,而如果将这个序列化结果直接
用于eval(),会存在语法问题——这在“1.2.2动态执行过程中的语句、表达式
与值”中已经讲述过了。因此JSON

采用在字符串外面套上一对“()”表明强
制表达式运算,并交由eval()处理

特性、由字符串而非标识符进行成员存取,使对象系统可以任意扩展
特性、数据结构的解绑(例如字符串、数组、结构与对象等不需要连续存储:结构化编程建
立在一个基本假设之上:存取连续数据比存取离散数据高效。但动态语言中并没有在内存中
实际连续的数据结构。)
特性、动态绑定与动态类型转换(动态语言)
特性、由eval()带来的运算、语句与代码块
特性、object.apply()与object.call()方法带来的动态方法调用
特性、弱类型识别:例如用对象模拟数组,以及arguments的数组特性
特性、编译器只解释声明,不执行运算,即使运算是可预测的(例如立即值或立即值的运
算)——强调:只有函数是声明即有值的,也是唯一在立即值声明的同时可以声明标识符的。
特性、valueOf()会影响到比较运算,其根源在于类型转换。Date()能参与序列检测的根源。
特性、语法解释与表达式执行是分阶段的。试以此观点解释“vari=100”和“i=100”的不同。
特性、重写构造器带来的问题,尤其是重写Object和call()方法带来的问题。
特性、eval对函数调用栈的破坏:重要的是指出eval()中的代码在列举栈时,ff与ie 呈现的不
同效果。





    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多