JavaScript 中并没有 Java 或是 C++ 中的类(class)的概念,而是采用构造器(constructor)的方式来创建对象。在 new 表达式中使用构造器就可以创建新的对象。由构造器创建出来的对象有一个隐含的引用指向该构造器的 prototype 。
所有的构造器都是对象,但并不是所有的对象都能成为构造器。能作为构造器的对象必须实现隐含的[[Construct]方法。如果 new 操作符后面的对象并不是构造器的话,会抛出 TypeError 异常。
new 操作符会影响 function 调用中 return 语句的行为。当 function 调用的时候有 new 作为前缀,如果 return 的结果不是一个对象,那么新创建的对象将会被返回。在代码清单 6中,functionanotherUser中通过 return 语句返回了一个对象,因此u2引用的是返回的那个对象;而 functionuser并没有使用 return 语句,因此u1引用的是新创建的user对象。
清单 6. new 操作符对 return 语句行为的影响
function user(name) {
this.name = name;
}
function anotherUser(name) {
this.name = name;
return {"badName" : name};
}
var u1 = new user("Alex");
alert(typeof u1.name); // 结果为 string
var u2 = new anotherUser("Alex");
alert(typeof u2.name); // 结果为 undefined
alert(typeof u2.badName); // 结果为 string
eval
JavaScript 中的 eval 可以用来解释执行一段 JavaScript 程序。当传给 eval 的参数的值是字符串的时候,该字符串会被当成一段 JavaScript 程序来执行。
隐式的 eval
除了显式的调用 eval 之外,JavaScript 中的有些 function 能接受字符形式的 JavaScript 代码并执行,这相当于隐式的调用了 eval 。这些 function 的典型代表是setTimeout和setInterval。具体请见代码清单 7。由于 eval 的性能比较差,所以在使用setTimeout和setInterval等 function 的时候,最好传入 function 的引用,而不是字符串。
清单 7. 隐式的 eval 示例
var obj = {
show1 : function() { alert(" 时间到! "); },
show2 : function() { alert("10 秒一次的提醒! "); };
};
setTimeout(obj.show1, 1000);
setTimeout("obj.show1();", 2000);
setInterval(obj.show2, 10000);
setInterval("obj.show2();", 10000);
eval 的潜在危险
在目前的 Ajax 应用中,JSON 是一种流行的浏览器端和服务器端处之间传输数据的格式。服务器端传过来的数据在浏览器端通过 JavaScript 的 eval 方法转换成可以直接使用的对象。然而,在浏览器端执行任意的 JavaScript 会带来潜在的安全风险,恶意的 JavaScript 代码可能会破坏应用。对于这个问题,有两种解决方式:
带注释的 JSON(JSON comments filtering)和带前缀的 JSON(JSON prefixing)
这两种方法都是 Dojo 中用来避免 JSON 劫持(JSON hijacking)的。带注释的 JSON 指的是从服务器端返回的 JSON 数据都是带有注释的,浏览器端的 JavaScript 代码需要先去掉注释的标记,再通过 eval 来获得 JSON 数据。这种方法一度被广泛使用,后来被证明并不安全,还会引入其它的安全漏洞。带前缀的 JSON 是目前推荐使用的方法,这种方法的使用非常简单,只需要在从服务器端的 JSON 字符串之前加上{} &&,再调用 eval 。关于这两种方法的细节,请看参考资料。
对 JSON 字符串进行语法检查
安全的 JSON 应该是不包含赋值和方法调用的。在 JSON 的 RFC 4627 中,给出了判断 JSON 字符串是否安全的方法,是通过两个正则表达式来实现的。具体见代码清单 8。关于 RFC 4627 的细节,请看参考资料。
清单 8. RFC 4627 中给出的检查 JSON 字符串的方法
var my_JSON_object = !(/[^,:{}[]0-9.-+Eaeflnr-u nrt]/.test(
text.replace(/"(.|[^"])*"/g, ''))) &&
eval('(' + text + ')');
执行上下文(execution context)和作用域链(scope chain)
执行上下文(execution context)是 ECMAScript 规范(请看参考资料)中用来描述 JavaScript 代码执行的抽象概念。所有的 JavaScript 代码都是在某个执行上下文中运行的。在当前执行上下文中调用 function 的时候,会进入一个新的执行上下文。当该 function 调用结束的时候,会返回到原来的执行上下文中。如果 function 调用过程中抛出异常,并没有被捕获的话,有可能从多个执行上下文中退出。在 function 调用过程,也可能调用其它的 function,从而进入新的执行上下文。由此形成一个执行上下文栈。
每个执行上下文都与一个作用域链(scope chain)关联起来。该作用域链用来在 function 执行时求标识符(Identifier)的值。在该链中包含多个对象。在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。如”protype 链与继承“中所述,在每个对象中进行属性查找的时候,会使用该对象的 prototype 链。在一个执行上下文中,与其关联的作用域链只会被with语句和 catch 子句影响。
在进入一个新的执行上下文的时候,会按顺序执行下面的操作:
创建激活(Activation)对象
激活对象是在进入新的执行上下文的时候被创建出来的,并与新的执行上下文关联起来。在初始化的时候,该对象包含一个名为arguments的属性。激活对象在变量初始化的时候也会被使用。 JavaScript 代码不能直接访问该对象,但是可以访问该对象里面的成员(如 arguments)。
创建作用域链
接下来的操作是创建作用域链。每个 function 都有一个内部属性[[scope]],它的值是一个包含多个对象的链。该属性的具体值与 function 的创建方式和在代码中的位置有很大关系(见“function 对象的创建方式”)。这个步骤中的主要操作是将上一步中创建的激活对象添加到 function 的[[scope]]属性对应的链的前面。
变量初始化
该步骤对 function 中需要使用的变量进行初始化。初始化时使用的对象是第一步中所创建的激活对象,不过被称之为变量(Variable)对象。会被初始化的变量包括 function 调用时的实际参数、内部 function 和局部变量。在这个步骤中,对于局部变量,只是在变量对象中创建了同名的属性,但是属性的值为 undefined,只有在 function 执行过程中才会被真正赋值。
全局 JavaScript 代码是在全局执行上下文中运行的,该上下文的作用域链只包含一个全局对象。
图 2中给出了 function 执行过程中的作用域链的示意图,其中的虚线表示作用域链。
图 2. 作用域链示意图
function a() {}、var a = function() {} 与 var a = new Function()
在 JavaScript 中,function 对象的创建方式有三种:function 声明、function 表达式和使用 Function 构造器。通过这三种方法创建出来的 function 对象的[[scope]]属性的值会有所不同,从而影响 function 执行过程中的作用域链。下面具体讨论这三种情况。
function 声明
function 声明的格式是function funcName() {}。使用 function 声明的 function 对象是在进入执行上下文时的变量初始化过程中创建的。该对象的[[scope]]属性的值是它被创建时的执行上下文对应的作用域链。
function 表达式
function 表达式的格式是var funcName = function() {}。使用 function 表达式的 function 对象是在该表达式被执行的时候创建的。该对象的[[scope]]属性的值与使用 function 声明创建的对象一样。
Function 构造器
对于 Function 构造器,大家可能比较陌生。声明一个 function 时,通常使用前两种方式。该方式的格式是var funcName = new Function(p1, p2,..., pn, body),其中 p1、p2 到 pn 表示的是该 function 的形式参数,body 是 function 的内容。使用该方式的 function 对象是在构造器被调用的时候创建的。该对象的[[scope]]属性的值总是一个只包含全局对象的作用域链。