提到 ECMAScript,可能很多 Web 开发人员会觉得比较陌生。但是提到 JavaScript,大家应该都比较熟悉。实际上,ECMAScript 是标准化组织 ECMA(Ecma International - European association for standardizing information and communication systems)发布的脚本语言规范。现在大家常见的 JavaScript、微软的 JScript 以及 Adobe 的 ActionScript 等语言都是遵循这个规范的,属于 ECMAScript 语言的变体。每个 ECMAScript 规范的变体语言都可能增加自己额外的功能特性。理解 ECMAScript 规范本身,就可以对很多 JavaScript 语言中的复杂特性有比较深入的了解。在笔者之前写的一篇关于 JavaScript 高级特性的文章中(见 参考资料),就提到了 JavaScript 语言的一些复杂难懂的地方,包括 prototype 链、执行上下文和作用域链等。这些概念来源于 ECMAScript 规范,不过这些概念都是基于 1999 年发布的 ECMAScript 规范第三版。
由于互联网的发展,ECMAScript 语言的各种变体得到了广泛的流行,成为了互联网应用的标准脚本语言。这也对 ECMAScript 语言提出了更多的要求,促进了 ECMAScript 语言规范的发展。目前来说,被浏览器广泛支持的是 ECMAScript 规范第三版。在 Web 应用开发中经常会遇到 JavaScript 语言本身的能力无法满足需求的情况。通常需要依靠第三方实现的框架来提供相关的能力。其中的一些功能在大部分框架中都能见到,这反映了 ECMAScript 语言本身功能的缺失。ECMAScript 规范第五版吸纳了一些框架中的常见功能,使之成为规范的一部分。例如,遍历数组时可以直接使用数组对象的 forEach
函数。不过 ECMAScript 规范第五版被不同浏览器广泛支持还需要一定的时间。关于不同浏览器对于 ECMAScript 规范第五版的支持程度,见 参考资料。
下面首先介绍 ECMAScript 规范中的类型。
类型
在 ECMAScript 规范中处理的值都有对应的类型。这些类型进一步细分成两类:一类是 ECMAScript 语言中实际存在的类型,另外一类则是为了描述语言的语义而引入的规范类型。规范类型在 ECMAScript 语言中并不存在,其作用只是为了更好地描述 ECMAScript 语言的实现机制。
ECMAScript 语言中的实际类型共有六种,分别是 Undefined
、Null
、Boolean
、Number
、String
和 Object
。Undefined
类型只有一个值undefined
。如果一个变量没有通过赋值操作指定一个值,那么该变量的值是 undefined
。Null
类型也只有一个值 null
。Boolean
类型有两个值 true
和 false
。
ECMAScript 并不像其他编程语言一样对数值类型进行比较具体的划分。ECMAScript 中并不区分整数和浮点数,也不区分不同长度的整数和浮点数。ECMAScript 中的 Number
类型始终使用 64 位双精度浮点数来表示数值。这一方面使得处理起来变得简单,另外一方面也限制了可以表示的数值的范围。ECMAScript 中 Number
类型的值的个数是 264-253+3。个数后面的“+3”表示的是 Number
类型的 3 个特殊值,分别是NaN
、+Infinity
和 -Infinity
。NaN
的含义是“不是一个数字(Not-A-Number)”。在代码中可以直接通过“NaN
”的方式来引用这个值。代码中与数值相关的计算的结果也可能是 NaN
。一般来说,对于 ECMAScript 语言中的操作符,如果其中一个操作数为 NaN
,那么计算结果为 NaN
。当需要判断一个变量引用 a
是否为 NaN
时,只需要判断 a !== a
是否为 true
即可。+Infinity
和 -Infinity
分别表示正无穷大和负无穷大,可以在代码中直接引用,也可能是某些数值运算的结果。如运算“3 / 0
”的结果是 Infinity
。除了这 3 个特殊值之外,剩下的数值中一半是正数,一半是负数。数值 0 也有正数和负数两种形式,称为正 0 和负 0,分别用 +0
和 -0
来表示。
ECMAScript 中对于 String
类型的处理也比较简单。任何有限长度的 16 位无符号整数的有序序列都是 String
类型的值。当 String
类型对象中包含的是文本数据时,序列中的每个元素的值是 UTF-16 格式的代码单元的值,对应于 Unicode 中的代码点。
在介绍完 ECMAScript 中的类型之后,下面介绍对象和属性。
对象和属性
ECMAScript 语言中的对象的含义比较简单,只是属性(property)的集合。属性一共分成三类,分别是命名数据属性、命名访问器属性和内部属性。前两类属性可以在代码中直接使用,而后面一种属性是规范使用的内部表示。对于前两类属性,可以有获取和设置属性值这两种不同的操作。内部属性则用来描述对象在特定情况下的行为。
命名属性有自己的特性(attribute)来定义该属性本身的行为。对于命名数据属性来说,特性 [[Value]]
表示该属性的值,可以是任何 ECMAScript 语言中定义的类型的值;[[Writable]]
表示该属性的值是否为只读的;[[Enumerable]]
表示该属性是否可以被枚举。可以被枚举的属性可以通过 for-in
循环来获取到;[[Configurable]]
表示该属性是否可以被配置。如果 [[Configurable]]
的值为 true
,则该属性可以被删除、可以被转换为访问器属性、还可以改变除了 [[Value]]
之外的其他特性的值。对于命名访问器属性来说,这类属性没有[[Value]]
和 [[Writable]]
特性,取而代之的是进行属性值获取和设置的 [[Get]]
和 [[Set]]
特性。如果 [[Get]]
和 [[Set]]
特性的值不是 undefined
,那么就必须是一个函数。属性的获取和设置是通过调用这两个函数来完成的。命名访问器属性同样有 [[Enumerable]]
和[[Configurable]]
特性,其含义与命名数据属性的对应特性的含义相同。命名属性可以在 ECMAScript 代码中进行处理。
内部属性的作用是定义 ECMAScript 中的对象在不同情况下的行为。不同类型的对象所包含的内部属性也不尽相同。每个对象都有内部属性[[Prototype]]
,用来引用另外一个对象。被引用的对象的 [[Prototype]]
属性又可以引用另外一个对象。对象之间通过这种引用关系组成一个链条,称为原型链条(prototype chain)。ECMAScript 通过原型链条的方式来实现属性的继承。当尝试获取一个对象中的命名数据属性时,如果在当前对象中没有相应名称的属性,会沿着原型链条往上查找,直到找到该属性或到达原型链条的末尾;当设置命名数据属性时,如果当前对象中不存在相应名称的属性,会在当前对象中创建新的属性。命名访问器属性则始终是继承的。当设置一个命名访问器属性的值时,所设置的是原型链条上定义该属性的对象上的值。
内部属性 [[Class]]
用来声明对象的类别,其作用类似于 Java 语言中对象的类名。通过 Object.prototype.toString
函数可以获取到[[Class]]
属性的值。当需要判断一个对象是否为数组时,可以使用代码 Object.prototype.toString.apply(obj) === ‘[object Array]‘
。
Object
对象是 ECMAScript 中非常重要的一个对象。Object
对象本身是一个函数,因此“typeof Object
”表达式的值是 function
。Object
函数既可以直接调用,也可以作为构造函数来创建新的对象。当把 Object
当成一个普通函数时,如果参数值为 undefined
、null
或未指定,调用的作用相当于以构造函数的方式来使用。所以代码 Object()
的作用相当于 new Object()
;如果提供了具体的参数值,则调用的结果相当于把实际参数的值转换成 Object
类型。如果把 Object
对象当成构造函数来使用时,具体的行为取决于调用时传入的参数。如果参数是 ECMAScript 原生对象,则直接把传入的参数返回;如果参数是 Boolean
、Number
或 String
类型,则调用结果相当于把参数转换成 Object
类型。因此“Object("Hello")
”的调用结果与“new Object("Hello")
”是相同的;如果参数值是 undefined
或 null
或者未指定参数值,则会创建一个新的 ECMAScript 原生对象。新创建对象的内部属性 [[Prototype]]
的值是 ECMAScript 内置提供的 Object
原型对象,[[Class]]
属性的值是 Object
,[[Extensible]]
属性的值是 true
。
Object
本身也是一个对象,也有自己的属性。这些属性可以用来操作对象及其属性。
Object.prototype
:这个属性可以获取 ECMAScript 中内置的Object
原型对象,并访问其中的属性。Object.getPrototypeOf
:这个函数可以获取Object
类型对象的内部属性[[Prototype]]
的值。Object.getOwnPropertyDescriptor
:这个函数用来获取Object
类型对象自身所拥有的属性的描述信息。描述信息是一个Object
类型的对象,其中包含了属性的特性值。如代码“Object.getOwnPropertyDescriptor({val:1}, ‘val‘)
”的执行结果是“{"configurable":true,"enumerable":true,"value":1,"writable":true}
”。Object.getOwnPropertyNames
:该函数用来获取一个包含Object
类型对象自身所拥有的属性名称的数组。如代码“Object.getOwnPropertyNames({a:1, b:2})
”的执行结果是“["a", "b"]
”。Object.defineProperty
:该函数用来在Object
类型对象中创建一个新的属性。在调用时除了属性的名称之外,还需要提供属性的特性值。如果该名称的属性在对象中已经存在,则更新已有的属性。代码清单 1 中给出了Object.defineProperty
函数的使用示例。Object.defineProperties
:该函数的作用类似于Object.defineProperty
,只不过该函数支持同时定义多个属性。比如代码“Object.defineProperties(obj, {a : {}, b : {}})
”定义两个新的属性a
和b
。Object.keys
:调用该函数可以得到一个包含Object
类型对象中所有可被枚举的属性的名称的数组。这个数组中包含的属性与使用for-in
循环所能访问到的属性是相同的。Object.create
:该函数用来创建一个新的对象。新创建对象的内部属性[[Prototype]]
的值由调用时的参数指定。在调用时还可以传入一个包含要定义的属性的对象。这个参数对象会被传递给Object.defineProperties
函数来在新创建的对象中定义属性。通过Object.create
可以很容易地实现基于原型的继承。Object.preventExtensions
和Object.isExtensible
:这两个函数用来设置和获取Object
类型对象的内部属性[[Extensible]]
的值。调用函数Object.preventExtensions
之后会把内部属性[[Extensible]]
的值设为false
。一旦设为false
之后,就无法在代码中重新设置回true
。Object.seal
和Object.isSealed
:调用Object.seal
函数会把内部属性[[Extensible]]
的值设为false
,同时把对象所拥有的每个属性的特性[[Configurable]]
的值设为false
。Object.freeze
和Object.isFrozen
:Object.freeze
函数除了会执行Object.seal
函数中所做的处理之外,还会把对象所拥有的命名数据属性的特性[[Writable]]
的值设为false
。
清单 1. Object.defineProperty 函数的使用示例
var obj = {}; Object.defineProperty(obj, ‘val‘, {}); // 创建一个新属性,特性为默认值 obj.val = 1; Object.defineProperty(obj, ‘CONSTANT‘, {value : 32, writable : false}); // 创建一个只读属性 obj.CONSTANT = 16; // 对属性的修改是无效的,但是不会抛出错误 Object.defineProperty(obj, "newVal", {enumerable: true}); for (var key in obj) { console.log(key); // 可以枚举出 newVal } var initValue = 0; Object.defineProperty(obj, "initValue", { get : function() { return initValue; }, set : function(val) { if (val > 0) { initValue = val; } } });
在 ECMAScript 规范第五版引入 Object
对象中对属性操作的函数之前,开发人员一般使用赋值操作来创建和更新属性。代码清单 2 中给出了相关的示例。使用赋值操作只能创建和更新命名数据属性,并不支持对命名访问器属性进行处理。通过赋值操作创建的属性,其特性[[Configurable]]
、[[Enumerable]]
和 [[Writable]]
的值都是 true
。如果需要设置这些特性的值,就需要使用 Object.defineProperty
函数。Object.preventExtensions
、Object.seal
和 Object.freeze
函数可以用来保护对象,防止被第三方有意或无意的修改。当需要把一个对象传递给其它代码时,可以先调用这三个函数来进行处理。
清单 2. 通过赋值操作创建新属性
var obj = {val : 1}; obj.newVal = "Hello"; Object.seal(obj); Object.defineProperty(obj, ‘anotherVal‘, {}); // 抛出 TypeError 错误
在介绍完对象和属性之后,下面介绍 ECMAScript 中的函数。
函数
函数在 ECMAScript 中有着特殊的地位。作为 ECMAScript 中的一等公民,ECMAScript 中很多特性是基于函数来实现。函数可以通过三种方式来创建:第一种是使用构造函数 Function
,第二种是使用函数声明,第三种是使用函数表达式。比较常见的是后面两种创建方式。第一种方式可以根据不同的代码内容动态创建出相应的函数对象。代码清单 3 中给出了构造函数 Function
的使用示例。当需要计算一个表达式的值时,首先创建一个新的函数,把表达式的内容作为函数体的一部分。再直接调用这个函数就可以得到所需要的结果。
清单 3. 构造函数 Function 使用示例
function calculate(expr) { return new Function("return " + expr).apply(); } calculate("3 * (2 + 4)");
在 Function
的原型对象中包含了一些实用的函数。通过 toString
函数可以查看函数的代码。而 call
和 apply
函数可以在调用函数时指定this
所指向的对象。在 ECMAScript 第五版中,不会对 call
和 apply
函数调用时指定的 this
的值做任何处理:null
和 undefined
不会被转换为全局对象,非 Object
类型的对象也不会被转换为 Object
类型。这是 ECMAScript 规范第五版相对于第三版的一个重要区别。
在调用函数时的一个常见需求是希望可以预先绑定 this
的值,或是预先为部分形式参数指定实际值。在 ECMAScript 规范第五版之前,这样的需求通常由框架来提供,如 Dojo 框架提供的 dojo.hitch
函数。ECMAScript 规范第五版中定义了一个新的函数 bind
,可以实现这样的需求。bind
函数在调用时,需要指定 this
的值,同时还可以指定若干个形式参数的实际值。bind
函数的调用结果是一个新的函数。这个新的函数在调用时的 this
值由 bind
函数来指定,同时形式参数的个数有可能少于之前的函数。代码清单 4 中给出了 bind
函数的使用示例。第一个bind
函数的调用中指定了 this
的值为 obj
。第二个 bind
函数的调用中指定了第一个形式参数的值为 5
。
清单 4. bind 函数的使用示例
var obj = { name : "alex" }; function func() { console.log(this.name); } var func1 = func.bind(obj); func1(); // 输出 alex function add(a, b) { return a + b; } var addBy5 = add.bind(this, 5); addBy5(3); // 值为 8,只有一个形式参数
在介绍完 ECMAScript 中的函数之后,下面介绍数组。
数组
数组是 ECMAScript 中非常重要的一个内置对象。在 ECMAScript 代码中可以看到大量对数组的使用。Array
对象用来表示数组。在 ECMAScript 规范第三版中并没有为 Array
对象提供比较多的实用函数来对数组进行操作。很多 JavaScript 框架对 ECMAScript 规范中的 Array
对象进行增强。ECMAScript 规范第五版中对 Array
对象进行了增强,因此很多功能可以直接依靠运行环境的实现。
Array
对象本身是一个构造函数,可以用来创建新的数组实例。当 Array
对象本身作为一个函数来使用时,其作用相当于作为构造函数来使用。因此“Array(1,2,3)
”的结果与“new Array(1,2,3)
”是相同的。新创建的 Array
对象实例的内部属性 [[Prototype]]
的值是内置的 Array
原型对象,即 Array.prototype
。通过 Array.isArray
函数可以判断一个对象是否为数组。
在 Array.prototype
中增加了一些新的实用函数,具体如下所示:
Array.prototype.indexOf
和Array.prototype.lastIndexOf
:在两个函数用来在数组中查找指定元素。在进行比较判断时,采用的是严格的对象比较算法,即相当于使用===
来进行比较。Array.prototype.every
:该函数用来判断数组中的全部元素是否都满足给定的条件。条件是通过作为参数的另一个函数来指定的。函数的返回值(true
或false
)决定了条件是否满足。Array.prototype.some
:该函数的作用与使用方式类似于Array.prototype.every
,不同的是Array.prototype.some
只要求数组中有任意一个元素满足条件即可。Array.prototype.forEach
:该函数用来遍历数组中的所有元素,对每个元素调用由参数指定的函数。Array.prototype.map
:该函数对数组中的每个元素执行特定的处理,并把处理的结果保存在一个新的数组中。Array.prototype.filter
:该函数根据给定的条件对数组中的元素进行过滤。只有满足条件的元素才会出现在结果数组中。Array.prototype.reduce
和Array.prototype.reduceRight
:这两个函数用来对数组中的元素进行累积操作。上一次操作的结果会作为下一次操作的输入值。两个函数的区别在于reduceRight
是从数组的末尾开始进行处理。
代码清单 5 中给出了 Array.prototype
中函数的使用示例。在使用 every
、some
、forEach
、map
和 filter
函数时,都需要提供一个进行处理的函数。在该函数被调用时,三个实际参数的值分别是当前数组元素的值、元素的序号和数组对象本身。而 reduce
和 reduceRight
函数使用的处理函数则多一个实际参数。第一个实际参数表示的是上一次处理的结果。在调用 reduce
和 reduceRight
函数时,可以额外提供一个参数作为整个处理过程的初始值。
清单 5. Array.prototype 中函数的使用示例
var array = [1, 2, 3, 4, 5]; array.indexOf(3); // 值为 2 array.lastIndexOf(4); // 值为 3 array.every(function(value, index, arr) { return value % 2 === 0; }); // 值为 false array.some(function(value, index, arr) { return value % 2 === 0; }); // 值为 true array.forEach(function(value, index, arr) { console.log(value); }); array.map(function(value, index, arr) { return value * 2; }); // 值为 [2, 4, 6, 8, 10] array.filter(function(value, index, arr) { return value % 2 === 0; }); // 值为 [2, 4] array.reduce(function(preValue, value, index, arr) { return preValue + value; }); // 值为 15 array.reduceRight(function(preValue, value, index, arr) { return preValue * value; }); // 值为 120
实际上,Array.prototype
中的函数并不限制只能对数组对象来使用。这些函数本身是通用的。比较典型的是在函数中对 arguments
对象的处理。arguments
对象本身不是数组类型的,但是一样可以使用 Array.prototype
的函数来进行处理。
在介绍完数组类型之后,下面介绍 JSON 格式。
JSON
在 ECMAScript 代码中,经常会需要与 JSON 格式的数据进行交换。JSON 也通常被用来作为客户端与服务器端之间的数据传输格式。这主要是因为在 ECMAScript 代码中处理 JSON 格式非常自然。JSON 格式数据经过解析之后,可以直接当成 ECMAScript 中的对象来使用。在使用 JSON 格式时的一个重要问题是如何在 ECMAScript 中的对象与文本形式之间进行互相转换。从服务器端通过 HTTP 协议获取的 JSON 文本需要经过解析之后,才能在 ECMAScript 代码中来使用;当需要向服务器端发送数据时,需要先把 ECMAScript 中的对象转换成文本格式。在 ECMAScript 规范第三版中并没有对 JSON 格式数据的转换进行规范,大多数程序都依靠 JavaScript 框架来提供相关的支持。
ECMAScript 规范第五版中定义了 JSON
对象来提供对 JSON 格式数据的处理。JSON
对象中提供了 parse
和 stringify
函数来进行转换操作,其中 parse
函数把 JSON 文本转换成相应的对象,而 stringify
则把 ECMAScript 中的对象转换成 JSON 文本。parse
函数可以接收两个参数。第一个参数是 JSON 文本,需要符合 JSON 格式的要求。第二个参数容易被忽视。该参数是一个函数,可以用来对解析过程中得到的属性名值对进行过滤和转换。该函数在被调用时会提供两个参数,分别是属性的名称和值。该函数的返回值会被作为解析结果对象中使用的值。
代码清单 6 中给出了 parse
函数的使用示例。如果在调用时使用了第二个参数,并且该函数的返回值为 undefined
,则对应的属性不会出现在结果对象中。
清单 6. JSON 对象的 parse 函数的使用示例
var jsonStr = ‘{"a":1, "b":2, "c":3}‘; JSON.parse(jsonStr); JSON.parse(jsonStr, function(key, value) { return typeof value === ‘number‘ ? value * 2 : value; }); // 结果为 {a:2, b:4, c:6} JSON.parse(jsonStr, function(key, value) { return typeof value === ‘number‘ && value % 2 === 0 ? undefined : value; }); // 结果为 {a:1, b:3}
stringify
函数在调用时接收三个参数。第一个参数是待转换的 ECMAScript 对象,第二个参数可以是一个函数或是数组。如果是函数,则转换的结果由该函数来确定;如果是数组,则只有出现在数组中的属性名称,才会出现在转换之后的结果中。第三个参数是用来控制转换结果中文本的缩进,以更好的进行显示。
代码清单 7 中给出了 stringify
函数的使用示例。第一种调用方式是最常见的用法,不会对结果进行修改。第二个调用方式限制了转换结果中只包含名称为 name
的属性。第三种调用方式在属性名称为 email
时用"******"来作为替代,在属性名称为 password
时返回 undefined
,使得属性 password
不会出现在结果文本中。最后一种调用方式使用 4 个空格来进行缩进。
清单 7. JSON 对象中 stringify 函数的使用示例
var user = { name : ‘Alex‘, password : ‘password‘, email : ‘alex@example.org‘ }; JSON.stringify(user); JSON.stringify(user, [‘name‘]); // 输出结果为“{"name":"Alex"}” JSON.stringify(user, function(key, value) { if (key === ‘email‘) { return ‘******‘; } if (key === ‘password‘) { return undefined; } return value; }); // 输出结果为“{"name":"Alex","email":"******"}” JSON.stringify(user, null, 4);
在介绍了 JSON 对象之后,下面介绍 ECMAScript 的代码执行方式。
代码执行
ECMAScript 代码的执行由运行环境来完成。不同的运行环境可能采取不同的执行方式,但基本的流程是相同的。如浏览器在解析 HTML 页面中遇到 <script>
元素时,会下载对应的代码来运行,或直接执行内嵌的代码。在代码中通过 eval
函数也可以指定一段需要执行的代码。代码的基本执行方式是从上到下,顺序执行。在调用函数之后,代码的执行会进入一个执行上下文之中。由于在一个函数的执行过程中会调用其他的函数,执行过程中的活动执行上下文会形成一个堆栈结构。在栈顶的是当前正在执行的代码。当函数返回时,会退出当前的执行上下文,而回到之前的执行上下文中。如果代码执行中出现异常,则可能从多个执行上下文中退出。
在代码执行过程中很重要的一步是标识符的解析。比如当执行过程中遇到语句“alert(val)
”时,首先要做的是解析标识符 val
的值。ECMAScript 不同于 Java 和 C/C++ 等语言,在进行标识符解析时需要利用词法环境并与函数调用方式相关。具体来说,标识符解析由当前代码所对应的执行上下文来完成。为了描述标识符的解析过程,ECMAScript 规范中使用了词法环境的概念来进行描述。一个词法环境描述了标识符与变量或函数之间的对应关系。一个词法环境由两个部分组成:一部分是记录标识符与变量之间的绑定关系的环境记录,另一部分是包围当前词法环境的外部词法环境。环境记录可以看成是一个标识符与变量或函数之间的映射表。不同词法环境之间可以互相嵌套,而内部词法环境会持有一个包围它的外部词法环境的引用。在进行标识符解析时,如果当前词法环境中找不到标识符所对应的变量或函数,则使用外部词法环境来尝试解析。递归查找下去,直到解析成功或外部词法环境为 null
。
具体来说,根据标识符关联方式的不同,环境记录可以进一步分成两类。两种类型分别对应不同的 ECMAScript 中不同的语法结构。当使用这些语法结构时,会对环境记录中的内容产生影响,进而影响标识符的解析过程。第一类环境记录是声明式环境记录。顾名思义,声明式环境记录用来绑定 ECMAScript 代码中的变量声明。当使用 var
声明变量或使用类似 function func(){}
的形式声明函数时,对应的变量或函数会被绑定到相应的环境记录中。另一类环境记录是对象环境记录。对象环境记录并不绑定具体的变量或函数,而是绑定另外一个对象中的属性。对象环境变量主要用来描述 ECMAScript 中 with
操作符的行为。
每个执行上下文会对应两个不同的词法环境。一个是用来进行标识符解析的词法环境,可能随着代码的执行而发生变化;另外一个是包含执行上下文对应的作用域中的变量或函数声明的词法环境。执行上下文还包含一个对象,作为代码中 this
关键词所指向的对象。下面通过一个具体的示例来进行说明。代码清单 8 中的逻辑并不复杂,作用是根据多个参数构造一个字符串。不过在代码中使用了三个变量和两个函数。其中函数 inner
嵌套在函数 outer
中。
清单 8. 演示词法环境的代码示例
var name = "alex"; function outer() { var age = 30; function inner(salutation) { return "Age of " + salutation + name + " is " + age; } return inner("Mr."); } outer();
在 ECMAScript 的代码执行之前,运行环境会负责创建一个全局的词法环境。全局词法环境包含其他的词法环境,是标识符解析过程中最后一个考虑的词法环境。全局词法环境的环境记录是一个绑定了全局对象的对象环境记录。全局对象是 ECMAScript 中定义的一个内置对象。在 ECMAScript 代码中使用的很多值和函数都是全局对象的属性。常见的属性包括 undefined
、parseInt
、encodeURIComponent
以及Object
、Function
、Array
、String
、Number
、Boolean
和 Date
等构造函数。运行环境也可能在全局对象中添加额外的属性。如在浏览器环境中执行的 HTML 页面中的 ECMAScript 代码,可以使用 window
、document
和 alert
等全局变量和函数。程序的代码也可以在全局对象中创建自己的属性。在 代码清单 8中,全局词法环境的环境记录中包含了代码中声明的变量 name
和函数 outer
。在执行代码时,首先进入的是全局执行上下文,对应的是全局词法环境。
在调用 outer
函数时,代码进入一个新的执行上下文中。此时会创建一个对应的新的词法环境。该词法环境的外部词法环境的引用是该函数被调用时的执行上下文所对应的词法环境,即全局词法环境。该词法环境的环境记录中包含了变量 age
和函数 inner
。在 outer
函数中调用inner
函数时,会进入到一个新的执行上下文中。由于 inner
函数声明了形式参数,在 inner
函数执行时的词法环境的环境记录中也会包含对应于形式参数名称的属性,而属性的值则由调用时的实际参数值来确定。
在介绍完 ECMAScript 的代码执行方式之后,下面介绍严格模式。
严格模式
ECMAScript 规范第五版的一个重要新特性是引入了代码执行时的严格模式。在严格模式下,对于 ECMAScript 代码执行时的限制更多。某些使用方式在严格模式下是不允许的。这有利于避免一些潜在的问题,提高代码的鲁棒性。一般来说,框架需要可以在严格模式下能正确运行,而一般的应用程序则可以选择是否使用严格模式。通过在 ECMAScript 代码的最开始使用 "use strict"
或 ‘use strict‘
就可以声明这段代码需要运行在严格模式下。
严格模式会影响 ECMAScript 代码执行的不同方面。具体来说,包括下面几个方面的影响:
- 对一个未声明或无法解析的标识符进行赋值操作时,不会在全局对象中创建新的属性。无法给特性
[[Writable]]
值为false
的数据属性或特性[[Set]]
值为undefined
的访问器属性赋值。如果一个对象的内部属性[[Extensible]]
的值为false
,无法为该对象中不存在的属性赋值。因为这个操作需要创建新的属性。这些对于属性方面的限制,有利于避免由于拼写错误造成的变量引用问题。比如在一个函数执行中的代码valua=10;
。开发人员实际上想要引用的是函数的参数value
。这行赋值操作在非严格模式下会在全局对象中创建一个新的属性valua
。这会造成代码执行时的错误隐患。在严格模式下,则会产生错误。这有利于发现隐含的问题。 - 在严格模式下,不能在函数代码中使用
arguments
对象的callee
和caller
属性。 - 在严格模式下,不能使用
with
语句。 - 严格模式下对于
this
值的处理也更加严格。对于this
所指向的值,如果不是Object
类型,不会被自动转换成Object
类型,如 代码清单 9所示。当this
的值是null
或undefined
时,也不会被自动转换成全局对象。 - 在严格模式下,一个对象字面量中不能声明两个名称相同的属性。一个函数也不能有名称相同的形式参数。
清单 9. 严格模式下 this 的值
"use strict"; function func() { console.debug(typeof this); // 输出为 number } func.apply(10);
小结
ECMAScript 规范第五版在第三版的基础上,吸收了一些流行框架中的特性,增强了 ECMAScript 本身的能力。本文对 ECMAScript 规范第五版中的重点更新做了详细的介绍,包括类型、对象和属性、函数、数组、JSON、代码执行和严格模式等。随着 ECMAScript 规范第五版被广泛支持,利用这些新的增强特性可以更好地开发高质量的应用。