提到 ECMAScript,可能很多 Web 开发人员会觉得比较陌生。但是提到 JavaScript,大家应该都比较熟悉。实际上,ECMAScript 是标准化组织 ECMA发布的脚本语言规范。现在大家常见的 JavaScript、微软的 JScript 以及 Adobe 的 ActionScript 等语言都是遵循这个规范的,属于 ECMAScript 语言的变体。每个 ECMAScript 规范的变体语言都可能增加自己额外的功能特性。理解 ECMAScript 规范本身,就可以对很多 JavaScript 语言中的复杂特性有比较深入的了解。
初次听到ES5、ES6,我有点儿懵了。好吧!我承认我不是一个合格的程序员。所以,我开始重新认识ES5、ES6。
一、什么是ES?
ES全称为:ECMAScript,是一种由Ecma国际(前身为欧洲计算机制造商协会,英文名称是European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言,至今为止有六个版本。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。
二、ECMAScript关键字的完整列表:
Undefined
、Null
、Boolean
、Number
、String
和 Object
。在此,我就不对每个类型进行赘叙了。 命名属性有自己的特性(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.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; } } });
通过赋值操作创建新属性
var obj = {val : 1}; obj.newVal = "Hello"; Object.seal(obj); Object.defineProperty(obj, 'anotherVal', {}); // 抛出 TypeError 错误
数组
数组是 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 中函数的使用示例
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
在 ECMAScript 代码中,经常会需要与 JSON 格式的数据进行交换。JSON 也通常被用来作为客户端与服务器端之间的数据传输格式。这主要是因为在 ECMAScript 代码中处理 JSON 格式非常自然。JSON 格式数据经过解析之后,可以直接当成 ECMAScript 中的对象来使用。在使用 JSON 格式时的一个重要问题是如何在 ECMAScript 中的对象与文本形式之间进行互相转换。从服务器端通过 HTTP 协议获取的 JSON 文本需要经过解析之后,才能在 ECMAScript 代码中来使用;当需要向服务器端发送数据时,需要先把 ECMAScript 中的对象转换成文本格式。在 ECMAScript 规范第三版中并没有对 JSON 格式数据的转换进行规范,大多数程序都依靠 JavaScript 框架来提供相关的支持。
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}
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);
代码执行
ECMAScript 代码的执行由运行环境来完成。不同的运行环境可能采取不同的执行方式,但基本的流程是相同的。如浏览器在解析 HTML 页面中遇到 <script>
元素时,会下载对应的代码来运行,或直接执行内嵌的代码。在代码中通过 eval
函数也可以指定一段需要执行的代码。代码的基本执行方式是从上到下,顺序执行。在调用函数之后,代码的执行会进入一个执行上下文之中。由于在一个函数的执行过程中会调用其他的函数,执行过程中的活动执行上下文会形成一个堆栈结构。在栈顶的是当前正在执行的代码。当函数返回时,会退出当前的执行上下文,而回到之前的执行上下文中。如果代码执行中出现异常,则可能从多个执行上下文中退出。
演示词法环境的代码示例
var name = "alex"; function outer() { var age = 30; function inner(salutation) { return "Age of " + salutation + name + " is " + age; } return inner("Mr."); } outer();
四、ES5中新增的Array方法
ES5中新增的不少东西,比如数组这块,我们可能就不需要去有板有眼地for
循环了。
ES5中新增了写数组方法,如下:
- forEach (js v1.6)
- map (js v1.6)
- filter (js v1.6)
- some (js v1.6)
- every (js v1.6)
- indexOf (js v1.6)
- lastIndexOf (js v1.6)
- reduce (js v1.8)
- reduceRight (js v1.8)
浏览器支持
- Opera 11+
- Firefox 3.6+
- Safari 5+
- Chrome 8+
- Internet Explorer 9+
对于让人失望很多次的IE6-IE8浏览器,Array原型扩展可以实现以上全部功能,例如forEach
方法:
// 对于古董浏览器,如IE6-IE8 if (typeof Array.prototype.forEach != "function") { Array.prototype.forEach = function () { /* 实现 */ }; }
下面,我就选取其中一个方法进行示范:
forEach
forEach
是Array新方法中最基本的一个,就是遍历,循环。例如下面这个例子:
[1, 2 ,3, 4].forEach(alert);
等同于下面这个传统的for
循环:
var array = [1, 2, 3, 4]; for (var k = 0, length = array.length; k < length; k++) { alert(array[k]); }
Array在ES5新增的方法中,参数都是function
类型,默认有传参,这些参数分别是?见下面:
[1, 2 ,3, 4].forEach(console.log); // 结果: // 1, 0, [1, 2, 3, 4] // 2, 1, [1, 2, 3, 4] // 3, 2, [1, 2, 3, 4] // 4, 3, [1, 2, 3, 4]
显而易见,forEach
方法中的function
回调支持3个参数,第1个是遍历的数组内容;第2个是对应的数组索引,第3个是数组本身。
因此,我们有:
[].forEach(function(value, index, array) { // ... });
对比jQuery中的$.each
方法:
$.each([], function(index, value, array) { // ... });
会发现,第1个和第2个参数正好是相反的,大家要注意了,不要记错了。后面类似的方法,例如$.map
也是如此。
现在,我们就可以使用forEach
卖弄一个稍显完整的例子了,数组求和:
var sum = 0; [1, 2, 3, 4].forEach(function (item, index, array) { console.log(array[index] == item); // true sum += item; }); alert(sum); // 10
再下面,更进一步,forEach
除了接受一个必须的回调函数参数,还可以接受一个可选的上下文参数(改变回调函数里面的this
指向)(第2个参数)。
array.forEach(callback,[ thisObject])
例子更能说明一切:
var database = { users: ["张含韵", "江一燕", "李小璐"], sendEmail: function (user) { if (this.isValidUser(user)) { console.log("你好," + user); } else { console.log("抱歉,"+ user +",你不是本家人"); } }, isValidUser: function (user) { return /^张/.test(user); } }; // 给每个人法邮件 database.users.forEach( // database.users中人遍历 database.sendEmail, // 发送邮件 database // 使用database代替上面标红的this ); // 结果: // 你好,张含韵 // 抱歉,江一燕,你不是本家人 // 抱歉,李小璐,你不是本家
如果这第2个可选参数不指定,则使用全局对象代替(在浏览器是为window
),严格模式下甚至是undefined
.
另外,forEach不会遍历纯粹“占着官位吃空饷”的元素的,例如下面这个例子:
var array = [1, 2, 3]; delete array[1]; // 移除 2 alert(array); // "1,,3" alert(array.length); // but the length is still 3 array.forEach(alert); // 弹出的仅仅是1和3
综上全部规则,我们就可以对IE6-IE8进行仿真扩展了,如下代码:
// 对于古董浏览器,如IE6-IE8 if (typeof Array.prototype.forEach != "function") { Array.prototype.forEach = function (fn, context) { for (var k = 0, length = this.length; k < length; k++) { if (typeof fn === "function" && Object.prototype.hasOwnProperty.call(this, k)) { fn.call(context, this[k], k, this); } } }; }
五、ES6新特性
ES6(ECMAScript 6)是即将到来的新版本JavaScript语言的标准。
箭头操作符
如果你会C#或者Java,你肯定知道lambda表达式,ES6中新增的箭头操作符=>便有异曲同工之妙。它简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值Inputs=>outputs。
我们知道在JS中回调是经常的事,而一般回调又以匿名函数的形式出现,每次都需要写一个function,甚是繁琐。当引入箭头操作符后可以方便地写回调了。请看下面的例子。
var array = [1, 2, 3]; //传统写法 array.forEach(function(v, i, a) { console.log(v); }); //ES6 array.forEach(v = > console.log(v));
类的支持
ES6中添加了对类的支持,引入了class关键字(其实class在JavaScript中一直是保留字,目的就是考虑到可能在以后的新版本中会用到,现在终于派上用场了)。JS本身就是面向对象的,ES6中提供的类实际上只是JS原型模式的包装。现在提供原生的class支持后,对象的创建,继承更加直观了,并且父类方法的调用,实例化,静态方法和构造函数等概念都更加形象化。
下面代码展示了类在ES6中的使用。
//类的定义 class Animal { //ES6中新型构造器 constructor(name) { this.name = name; } //实例方法 sayName() { console.log('My name is '+this.name); } } //类的继承 class Programmer extends Animal { constructor(name) { //直接调用父类构造器进行初始化 super(name); } program() { console.log("I'm coding..."); } } //测试我们的类 var animal=new Animal('dummy'), wayou=new Programmer('wayou'); animal.sayName();//输出 ‘My name is dummy’ wayou.sayName();//输出 ‘My name is wayou’ wayou.program();//输出 ‘I'm coding...’
增强的对象字面量
对象字面量被增强了,写法更加简洁与灵活,同时在定义对象的时候能够做的事情更多了。具体表现在:
- 可以在对象字面量里面定义原型
- 定义方法可以不用function关键字
- 直接调用父类方法
这样一来,对象字面量与前面提到的类概念更加吻合,在编写面向对象的JavaScript时更加轻松方便了。
//通过对象字面量创建对象 var human = { breathe() { console.log('breathing...'); } }; var worker = { __proto__: human, //设置此对象的原型为human,相当于继承human company: 'freelancer', work() { console.log('working...'); } }; human.breathe();//输出 ‘breathing...’ //调用继承来的breathe方法 worker.breathe();//输出 ‘breathing...’
字符串模板
字符串模板相对简单易懂些。ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。如果你使用过像C#等后端强类型语言的话,对此功能应该不会陌生。
//产生一个随机数 var num=Math.random(); //将这个数字输出到console console.log(`your num is ${num}`);
解构
自动解析数组或对象中的值。比如若一个函数要返回多个值,常规的做法是返回一个对象,将每个值做为这个对象的属性返回。但在ES6中,利用解构这一特性,可以直接返回一个数组,然后数组中的值会自动被解析到对应接收该值的变量中。
var [x,y]=getVal(),//函数返回值的解构 [name,,age]=['wayou','male','secrect'];//数组解构 function getVal() { return [ 1, 2 ]; } console.log('x:'+x+', y:'+y);//输出:x:1, y:2 console.log('name:'+name+', age:'+age);//输出: name:wayou, age:secrect
参数默认值,不定参数,拓展参数
默认参数值
现在可以在定义函数的时候指定参数的默认值了,而不用像以前那样通过逻辑或操作符来达到目的了。
function sayHello(name){ //传统的指定默认参数的方式 var name=name||'dude'; console.log('Hello '+name); } //运用ES6的默认参数 function sayHello2(name='dude'){ console.log(`Hello ${name}`); } sayHello();//输出:Hello dude sayHello('Wayou');//输出:Hello Wayou sayHello2();//输出:Hello dude sayHello2('Wayou');//输出:Hello Wayou
不定参数
不定参数是在函数中使用命名参数同时接收不定数量的未命名参数。这只是一种语法糖,在以前的JavaScript代码中我们可以通过arguments变量来达到这一目的。不定参数的格式是三个句点后跟代表所有不定参数的变量名。比如下面这个例子中,…x代表了所有传入add函数的参数。
//将所有参数相加的函数 function add(...x){ return x.reduce((m,n)=>m+n); } //传递任意个数的参数 console.log(add(1,2,3));//输出:6 console.log(add(1,2,3,4,5));//输出:15
拓展参数
拓展参数则是另一种形式的语法糖,它允许传递数组或者类数组直接做为函数的参数而不用通过apply。
var people=['Wayou','John','Sherlock']; //sayHello函数本来接收三个单独的参数人妖,人二和人三 function sayHello(people1,people2,people3){ console.log(`Hello ${people1},${people2},${people3}`); } //但是我们将一个数组以拓展参数的形式传递,它能很好地映射到每个单独的参数 sayHello(...people);//输出:Hello Wayou,John,Sherlock //而在以前,如果需要传递数组当参数,我们需要使用函数的apply方法 sayHello.apply(null,people);//输出:Hello Wayou,John,Sherlock
let与const 关键字
可以把let看成var,只是它定义的变量被限定在了特定范围内才能使用,而离开这个范围则无效。const则很直观,用来定义常量,即无法被更改值的变量。
for (let i=0;i<2;i++)console.log(i);//输出: 0,1 console.log(i);//输出:undefined,严格模式下会报错
for of 值遍历
我们都知道for in 循环用于遍历数组,类数组或对象,ES6中新引入的for of循环功能相似,不同的是每次循环它提供的不是序号而是值。
var someArray = [ "a", "b", "c" ]; for (v of someArray) { console.log(v);//输出 a,b,c }