第24章 ECMAScript 6

第24章 ECMAScript 6

ECMAScript 6是继ECMAScript 5之后的一次主要改进,语言规范由ECMAScript 5.1时代的245页扩充至600页。ECMAScript 6增添了许多必要的特性,如模块和类,以及一些实用特性,如Maps、Sets、Promises、生成器(Generators)等。尽管ECMAScript 6做了大量的更新,但是它依旧完全向后兼容以前的版本,标准化委员会决定避免由不兼容版本语言导致的Web体验破碎。因此所有老代码都可以正常运行,整个过渡也显得更为平滑,但随之而来的问题是,开发者们抱怨了多年的老问题依然存在。

【学习重点】

▲ 了解ECMAScript 6

▲ 熟悉ECMAScript 6变量、字符串和数值变化

▲ 使用数组新特性

▲ 使用对象新特性

▲ 使用函数新特性

▲ 了解数据结构、循环体和状态机

▲ 正确使用预处理、类和模块

24.1 ECMAScript 6概述

2013年3月,ECMAScript 6的草案封闭,不再接受新功能了,新的功能将被加入下一个版本。2015年6月17日正式发布ECMAScript 6,同时JavaScript的官方名称改为ECMAScript 2015。ECMAScript 6设计目标:使用JavaScript语言可以编写大型的复杂的应用程序。

24.1.1 兼容性

由于ECMAScript 6还没有定案,有些语法规则还会变动,目前支持ECMAScript 6的软件和开发环境还不多。各大浏览器的最新版本,对ECMAScript 6的支持可以查看http://kangax.github.io/compattable/es6/,如图24-1所示。

图24-1 ECMAScript 6浏览器兼容性列表

24.1.2 使用ECMAScript 6

Google的V8引擎已经部署了ECMAScript 6的部分特性。使用Node.js 0.11版,就可以体验这些特性。

Google的Traceur编译器可以将ECMAScript 6代码编译为ECMAScript 5代码。Traceur提供一个在线编译器(http://google.github.io/traceur-compiler/demo/repl.html),可以在线将ECMAScript 6代码转换为ECMAScript 5代码。转换后的代码,可以直接作为ECMAScript 5代码插入网页运行。

24.2 变量

ECMAScript 6新增了块作用域,以及let和const命令,完善变量定义规范,为数组和对象增加了解构赋值。

24.2.1 let命令

ECMAScript 6新增了let命令,用来声明变量。用法与var类似,但是所声明的变量,只能在let命令所在的代码块内有效。

【示例1】下面代码在代码块中分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。

let不会发生“变量提升”现象,仅能够在声明之后使用。例如:

上面代码在声明foo之前,就使用这个变量,结果会抛出一个错误。

提示:let不允许在相同作用域内,重复声明同一个变量。下面写法是错误的:

【拓展】let实际上为JavaScript新增了块级作用域。例如,在下面函数中包含两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

块级作用域的出现,使得立即执行匿名函数(IIFE)不再必要了。

ECMAScript 6规定函数本身的作用域,在其所在的块级作用域之内。

24.2.2 const命令

const也用来声明变量,但声明的是常量。一旦声明,常量的值就不能改变。

【示例】下面代码表明改变常量的值是不起作用的。但对常量重新赋值不会报错,只会失效。

     const PI=3.1415;
     PI //3.1415
     PI=3;
     PI //3.1415
     const PI=3.1;
     PI //3.1415

const的作用域与let命令相同:只在声明所在的块级作用域内有效,不可重复声明。

24.2.3 数组解构赋值

ECMAScript 6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

【示例1】ECMAScript 5为变量赋值,只能直接指定值。

     var a=1;
     var b=2;
     var c=3;

ECMAScript 6允许按如下方式进行赋值。

     var [a, b, c]=[1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

【示例2】下面代码演示使用嵌套数组进行解构。

     var [foo, [[bar], baz]]=[1, [[2], 3]];
     foo //1
     bar //2
     baz //3
     var [,,third]=["foo","bar","baz"];
     third //"baz"
     var [head, ...tail]=[1, 2, 3, 4];
     head //1
     tail //[2, 3, 4]

如果解构不成功,变量的值就等于undefined。

     var [foo]=[];
     var [foo]=1;
     var [foo]='Hello';
     var [foo]=false;
     var [foo]=NaN;
     var [bar, foo]=[1];

以上几种情况都属于解构不成功,foo的值都会等于undefined。另一种情况是不完全解构。

     var [x, y]=[1, 2, 3];

上面代码中,x和y可以顺利取到值。

如果对undefined或null进行解构,会报错。例如:

     var [foo]=undefined;
     var [foo]=null;

这是因为解构只能用于数组或对象。其他原始类型的值都可以转为相应的对象,但是,undefined和null不能转为对象,因此报错。

【示例3】解构赋值允许指定默认值。

     var [foo=true]=[];
     foo //true
     [x, y='b']=['a'] //x='a', y='b'
     [x, y='b']=['a', undefined] //x='a', y='b'

解构赋值不仅适用于var命令,也适用于let和const命令。

     var [v1, v2, ..., vN ]=array;
     let [v1, v2, ..., vN ]=array;
     const [v1, v2, ..., vN ]=array;
24.2.4 对象解构赋值

解构不仅可以用于数组,还可以用于对象。对象的解构与数组的不同之处:数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

【示例1】在下面代码中,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。如果变量没有对应的同名属性,导致取不到值,最后等于undefined。

     var { bar, foo }={ foo:"aaa", bar:"bbb" };
     foo //返回值"aaa"
     bar //返回值"bbb"
     var { baz }={ foo:"aaa", bar:"bbb" };
     baz //返回值undefined

如果变量名与属性名不一致,必须写成下面这样。

     var { foo: baz }={ foo:"aaa", bar:"bbb" };
     baz //"aaa"

【示例2】与数组一样,解构也可以用于嵌套结构的对象。

【示例3】对象的解构也可以指定默认值。

     var { x=3 }={};
     x //返回值3
     var {x, y=5}={x: 1};
     console.log(x, y) //返回值1, 5

提示:将一个已经声明的变量用于解构赋值,会提示语法错误。

        var x;
        {x}={x:1};                    //抛出语法错误

上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。例如:

     ({x})={x:1};
     //或者
     ({x}={x:1});
24.2.5 案例:解构应用

【示例1】交换变量的值。

     [x, y]=[y, x];

【示例2】从函数返回多个值。

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象中返回。有了解构赋值,取出这些值就非常方便。

【示例3】定义函数的参数。

     function f([x]) { ... }
     f(['a'])
     function f({x, y, z}) { ... }
     f({x:1, y:2, z:3})

这种写法对提取JSON对象中的数据尤其有用。

【示例4】定义函数参数的默认值。

指定参数的默认值,就避免了在函数体内部再写var foo=config.foo || 'default foo';这样的语句。

【示例5】遍历Map结构。任何部署了Iterator接口的对象,都可以用for of循环遍历。Map结构原生支持Iterator接口,配合变量的结构赋值,获取键名和键值就非常方便。

如果只想获取键名,或者只想获取键值,可以写成下面这样。

【示例6】为输入模块指定方法。加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

     const { SourceMapConsumer, SourceNode }=require("source-map");

24.3 字符和字符串

ECMAScript 6加强了对Unicode的支持,并且扩展了字符串对象。

24.3.1 字符
1.codePointAt()

JavaScript字符以UTF-16格式存储,每个字符为两个字节。对于那些需要4个字节存储的字符(Unicode编号大于0xFFFF的字符),JavaScript会认为它们是两个字符。

【示例1】在下面代码中,汉字“ஷ”的Unicode编号是0x20BB7,UTF-16编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节存储。对于这种4个字节的字符,JavaScript不能正确处理,字符串长度会误判为2,而且charAt()方法无法读取字符,charCodeAt()方法只能分别返回前两个字节和后两个字节的值。

     var s ="?";
     s.length //2
     s.charAt(0) //''
     s.charAt(1)    //''
     s.charCodeAt(0) //55362
     s.charCodeAt(1) //57271

【示例2】ECMAScript 6提供了codePointAt()方法,能够正确处理4个字节存储的字符,返回一个字符的Unicode编号。

     var s ="?a";
     s.codePointAt(0) //134071
     s.codePointAt(1) //57271
     s.charCodeAt(2) //97

codePointAt()方法的参数,是字符在字符串中的位置(从0开始)。上面代码中,JavaScript将“ஷa”视为3个字符,codePointAt()方法在第一个字符上,正确地识别了“ஷ”,返回了它的十进制Unicode编号134071(即十六进制的20BB7)。在第二个字符(即“ஷ”的后两个字节)和第三个字符“a”上,codePointAt()方法的结果与charCodeAt()方法相同。

codePointAt()方法会正确返回4字节的UTF-16字符的Unicode编号。对于那些两个字节存储的常规字符,它的返回结果与charCodeAt()方法相同。

【示例3】codePointAt()方法是测试一个字符由两个字节还是由4个字节组成的最简单方法。

2.String.fromCodePoint()

该方法用于从Unicode编号返回对应的字符串,作用与codePointAt()正好相反。例如:

     String.fromCodePoint(134071) //"?"

提示:fromCodePoint()方法定义在String对象上,而codePointAt()方法定义在字符串的实例对象上。

3.字符表示

JavaScript允许采用“\uxxxx”形式表示一个字符,其中“xxxx”表示字符的Unicode编号。但是,这种表示法只限于\u0000~\uFFFF之间的字符,超出这个范围的字符,必须用两个双字节的形式表达。

【示例4】下面代码表示,如果直接在“\u”后面跟上超过0xFFFF的数值,如\u20BB7,JavaScript会理解成“\u20BB+7”。因为\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。

【示例5】ECMAScript 6对这一点做出了改进,只要将超过0xFFFF的编号放入大括号,就能正确解读该字符。

     "\u{20BB7}"                   //"?"
24.3.2 字符串
1.u修饰符

ECMAScript 6为正则表达式添加了u修饰符,用来正确处理大于\uFFFF的Unicode字符。

【示例1】使用u修饰符。

     var s ="?";
     /^.$/.test(s) //false
     /^.$/u.test(s) //true

上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

利用这一点,可以写出一个正确返回字符串长度的函数。

2.包含检测

传统上,JavaScript只有indexOf()方法,可以用来确定一个字符串是否包含在另一个字符串中。ECMAScript 6又提供了3种新方法。

☑ contains():表示是否找到了参数字符串。

☑ startsWith():表示参数字符串是否在源字符串的头部。

☑ endsWith():表示参数字符串是否在源字符串的尾部。

【示例2】使用包含检测方法。

     var s ="Hello world!";
     s.startsWith("Hello") //true
     s.endsWith("!") //true
     s.contains("o") //true

【示例3】上面3个方法都支持第二个参数,表示开始搜索的位置。

     var s ="Hello world!";
     s.startsWith("o", 4) //true
     s.endsWith("o", 8) //true
     s.contains("o", 8) //false

上面代码表示,使用第二个参数n时,endsWith()的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

3.定义重复字符串

repeat()返回一个新字符串,表示将原字符串重复n次。

     "x".repeat(3) //"xxx"
     "hello".repeat(2) //"hellohello"
4.y修饰符

除了u修饰符,ECMAScript 6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。它的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,不同之处在于,g修饰符只确保剩余位置中存在匹配,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的含义。

【示例4】使用y修饰符。

     var s ="aaa_aa_a";
     var r1=/a+/g;
     var r2=/a+/y;
     r1.exec(s) //["aaa"]
     r2.exec(s) //["aaa"]
     r1.exec(s) //["aa"]
     r2.exec(s) //null

上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行时,两者行为相同,剩余字符串都是“_aa_a”。因为g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。

如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。上面代码每次匹配,都是从剩余字符串的头部开始。

进一步说,y修饰符号隐含了头部匹配的标志ˆ。

     /b/y.exec("aba")                    //null

上面代码由于不能保证头部匹配,所以返回null。y修饰符的设计本意,就是让头部匹配的标志ˆ在全局匹配中都有效。

与y修饰符相匹配,ECMAScript 6的正则对象多了sticky属性,表示是否设置了y修饰符。

     var r=/hello\d/y;
     r.sticky                           //true
24.3.3 模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

【示例1】使用模板字符串。

上面代码表示,在模板字符串中嵌入变量,需要将变量名写在${}中。

【示例2】模板字符串使得字符串与变量的结合更容易。

24.4 数值

ECMAScript 6对数值的进制表示进行修订,新增了Number和Math方法,扩展数学计算能力。

24.4.1 进制表示

ECMAScript 6提供了二进制和八进制数值的新的写法,分别用前缀0b和0o表示。

     0b111110111===503 //true
     0o767===503 //true

八进制用0o前缀表示的方法,将要取代已经在ECMAScript 5中被逐步淘汰的加前缀0的写法。

24.4.2 Number方法
1.Number.isFinite()、Number.isNaN()

ECMAScript 6在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法,用来检查Infinite和NaN这两个特殊值。

与传统的isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回false。

     isFinite(25) //true
     isFinite("25") //true
     Number.isFinite(25) //true
     Number.isFinite("25") //false
     isNaN(NaN) //true
     isNaN("NaN") //true
     Number.isNaN(NaN) //true
     Number.isNaN("NaN") //false
2.Number.parseInt()、Number.parseFloat()

ECMAScript 6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

3.Number.isInteger()和安全整数

Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的存储方法,所以3和3.0被视为同一个值。

     Number.isInteger(25) //true
     Number.isInteger(25.0) //true
     Number.isInteger(25.1) //false

JavaScript能够准确表示的整数范围在-2ˆ53~2ˆ53之间。ECMAScript 6引入了Number.MAX_ SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围内。

     var inside=Number.MAX_SAFE_INTEGER;
     var outside=inside+1;
     Number.isInteger(inside) //true
     Number.isSafeInteger(inside) //true
     Number.isInteger(outside) //true
     Number.isSafeInteger(outside) //false
24.4.3 Math方法
1.Math.trunc()

Math.trunc方法用于去除一个数的小数部分,返回整数部分。

     Math.trunc(4.1) //4
     Math.trunc(4.9) //4
     Math.trunc(-4.1) //-4
     Math.trunc(-4.9) //-4
2.新数学方法

ECMAScript 6在Math对象上新增了很多新的数学方法。简单说明如下。

☑ Math.acosh(x):返回x的反双曲余弦。

☑ Math.asinh(x):返回x的反双曲正弦。

☑ Math.atanh(x):返回x的反双曲正切。

☑ Math.cbrt(x):返回x的立方根。

☑ Math.clz32(x):返回x的32位二进制整数表示形式的前导0的个数。

☑ Math.cosh(x):返回x的双曲余弦。

☑ Math.expm1(x):返回eˆx–1。

☑ Math.fround(x):返回x的单精度浮点数形式。

☑ Math.hypot(...values):返回所有参数的平方和的平方根。

☑ Math.imul(x, y):返回两个参数以32位整数形式相乘的结果。

☑ Math.log1p(x):返回1+x的自然对数。

☑ Math.log10(x):返回以10为底的x的对数。

☑ Math.log2(x):返回以2为底的x的对数。

☑ Math.sign(x):如果x为负返回-1,x为0返回0,x为正返回1。

☑ Math.tanh(x):返回x的双曲正切。

24.5 数组

ECMAScript 6在ECMAScript 5基础上扩展了JavaScript数组功能,新增转换方法,引入数组推导和监听概念。

24.5.1 转换
1.Array.from()

Array.from()用于将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象,其中包括ECMAScript 6新增的Set和Map结构。

【示例】在下面代码中,querySelectorAll()方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用forEach()方法。

Array.from()还可以接收第二个参数,作用类似于数组的map()方法,用来对每个元素进行处理。

     Array.from(arrayLike, x => x * x);

等同于:

     Array.from(arrayLike).map(x => x * x);
2.Array.of()

Array.of()方法用于将一组值转换为数组。这个函数的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。例如:

     Array(3) //[undefined, undefined, undefined]
     Array.of(3).length //1
     Array(3,11,8) //[3, 11, 8]
     Array.of(3, 11, 8) //[3,11,8]

上面代码说明,只有当参数个数不少于两个,Array()才会返回由参数组成的新数组。

24.5.2 实例
1.find()和findIndex()

数组实例的find()用于找出第一个符合条件的数组元素。它的参数是一个回调函数,所有数组元素依次遍历该回调函数,直到找出第一个返回值为true的元素,然后返回该元素,否则返回undefined。

【示例1】使用find()方法。

从上面代码可以看到,回调函数接收3个参数,依次为当前的值、当前的位置和原数组。

数组实例的findIndex()的用法与find()非常类似,返回第一个符合条件的数组元素的位置,如果所有元素都不符合条件,则返回-1。

这两个方法都可以接收第二个参数,用来绑定回调函数的this对象。另外,这两个方法都可以发现NaN,弥补了IndexOf()的不足。

2.fill()

fill()使用给定值,填充一个数组。

【示例2】使用fill()方法。

上面代码表明,fill()方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。fill()还可以接收第二个和第三个参数,用于指定填充的起始位置和结束位置。

     ['a', 'b', 'c'].fill(7, 1, 2)                //['a', 7, 'c']
3.entries()、keys()和values()

ECMAScript 6提供3个新的方法:entries()、keys()和values(),用于遍历数组。它们都返回一个遍历器,可以用for of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。例如:

24.5.3 推导

ECMAScript 6提供简洁写法,允许直接通过现有数组生成新数组,这被称为数组推导。

【示例1】下面代码通过for of结构,数组a2直接在a1的基础上生成。

    var a1=[1, 2, 3, 4];
    var a2=[for (i of a1) i * 2];
    a2 //[2, 4, 6, 8]

在数组推导中,for of结构总是写在最前面,返回的表达式写在最后面。

【示例2】for of后面还可以附加if语句,用来设定循环的限制条件。

     var years=[ 1954, 1974, 1990, 2006, 2010, 2014 ];
     [for (year of years) if (year > 2000) year];//[ 2006, 2010, 2014 ]
     [for (year of years) if (year > 2000) if(year < 2010) year];//[ 2006]
     [for (year of years) if (year > 2000 && year < 2010) year];//[ 2006]

上面代码表明,if语句写在for of与返回的表达式之间,可以使用多个if语句。

数组推导可以替代map()和filter()方法。例如:

     [for (i of [1, 2, 3]) i * i];

等价于:

     [1, 2, 3].map(function (i) { return i * i });

     [for (i of [1,4,2,3,-8]) if (i < 3) i];

等价于:

     [1,4,2,3,-8].filter(function(i) { return i < 3 });

上面代码说明,模拟map功能只要单纯的for of循环就行了,模拟filter功能除了for of循环,还必须加上if语句。

【示例3】在一个数组推导中,还可以使用多个for of结构,构成多重循环。

     var a1=["x1","y1"];
     var a2=["x2","y2"];
     var a3=["x3","y3"];
     [for (s of a1) for (w of a2) for (r of a3) console.log(s+w+r)];
     // x1x2x3
     // x1x2y3
     // x1y2x3
     // x1y2y3
     // y1x2x3
     // y1x2y3
     // y1y2x3
     // y1y2y3

上面代码在一个数组推导中,使用了3个for of结构。

提示:数组推导的方括号构成了一个单独的作用域,在这个方括号中声明的变量类似于使用let语句声明的变量。

由于字符串可以视为数组,因此字符串也可以直接用于数组推导。

     [for (c of 'abcde') if (/[aeiou]/.test(c)) c].join('') // 'ae'
     [for (c of 'abcde') c+'0'].join('') // 'a0b0c0d0e0'

上面代码使用了数组推导,对字符串进行处理。

数组推导需要注意的地方是,新数组会立即在内存中生成。这时,如果原数组是一个很大的数组,将会非常耗费内存。

24.5.4 监听

Array.observe()和Array.unobserve()方法用于监听或取消监听数组的变化,指定回调函数。它们的用法与Object.observe和Object.unobserve()方法完全一致。唯一的区别是,对象可监听的变化一共有6种,而数组只有4种:add、update、delete、splice(数组的length属性发生变化)。

24.6 对象

ECMAScript 6在ECMAScript 5基础上继续完善JavaScript对象系统,新增了多个静态方法和原型方法,完善了对象直接量的语法格式和用法灵活性,增强对象代理保护和监听控制。

24.6.1 新增方法
1.Object.is()

Object.is()用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处:+0不等于-0,NaN等于自身。

【示例1】使用Object.is()工具函数比较两个值。

     +0===-0 //true
     NaN===NaN //false
     Object.is(+0, -0) //false
     Object.is(NaN, NaN) //true
2.Object.assign()

Object.assign()方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误。

【示例2】使用Object.assign()方法复制属性。

     var target={ a: 1 };
     var source1={ b: 2 };
     var source2={ c: 3 };
     Object.assign(target, source1, source2);
     target //{a:1, b:2, c:3}

提示:如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

        var target={ a: 1, b: 1 };
        var source1={ b: 2, c: 2 };
        var source2={ c: 3 };
        Object.assign(target, source1, source2);
        target //{a:1, b:2, c:3}
24.6.2 原型方法
1.proto属性

proto属性用来读取或设置当前对象的prototype对象。该属性一度被正式写入ECMAScript 6草案,但后来又被移除。目前,所有浏览器(包括IE 11)都部署了这个属性。

有了这个属性,实际上已经不需要通过Object.create()来生成新对象了。

2.Object.setPrototypeOf()

Object.setPrototypeOf()方法的作用与proto相同,用来设置一个对象的prototype对象。基本用法如下:

     Object.setPrototypeOf(object, prototype)

该方法等同于下面的函数:

3.Object.getPrototypeOf()

该方法与setPrototypeOf()方法配套,用于读取一个对象的prototype对象。用法如下:

     Object.getPrototypeOf(obj)
24.6.3 增强语法

ECMAScript 6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

【示例】快速定义函数。

这种写法用于函数的返回值,将会非常方便。

24.6.4 属性名表达式

ECMAScript 6允许定义对象时,用表达式作为对象的属性名。在写法上,要把表达式放在方括号内。

【示例1】下面代码中,对象a的属性名lastWord是一个变量。

【示例2】下面是一个将字符串的加法表达式作为属性名的例子。

24.6.5 符号数据

ECMAScript 6引入了一种新的原始数据类型Symbol。它通过Symbol()函数生成。

【示例1】下面代码中,Symbol()函数接收一个字符串作为参数,用来指定生成的Symbol的名称,可以通过name属性读取。typeof运算符的结果,表明Symbol是一种单独的数据类型。

注意:Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。

【示例2】symbol数据的最大特点就是每一个Symbol都是不相等的,保证产生一个独一无二的值。在下面代码中,w1、w2、w3 3个变量都等于Symbol(),但它们的值是不相等的。

     let w1=Symbol();
     let w2=Symbol();
     let w3=Symbol();

由于这种特点,Symbol类型适合作为标识符,用于对象的属性名,保证了属性名之间不会发生冲突。如果一个对象由多个模块构成,这样就不会出现同名的属性。

【示例3】使用Symbol可以防止属性值被不小心修改。

     var a={};
     var mySymbol=Symbol();
     a[mySymbol]='Hello!';

上面代码通过方括号结构和Object.defineProperty()两种方法,将对象的属性名指定为一个Symbol值。

提示:不能使用点结构,将Symbol值作为对象的属性名。

        var a={};
        var mySymbol=Symbol();
        a.mySymbol='Hello!';
        a[mySymbol]                  //返回undefined

上面代码中,mySymbol属性的值为未定义,原因在于a.mySymbol这样的写法,并不是把一个Symbol值当作属性名,而是把mySymbol这个字符串当作属性名进行赋值,这是因为点结构中的属性名永远都是字符串。

下面的写法为Map结构添加了一个成员,但是该成员永远无法被引用。

     let a=Map();
     a.set(Symbol(), 'Noise');
     a.size //1

如果要在对象内部使用Symbol属性名,必须采用属性名表达式。

采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。

【示例4】Symbol类型作为属性名,不会出现在for...in循环中,也不会被Object.getOwnProperty Names()方法返回,但是有一个对应的Object.getOwnPropertySymbols()方法,以及Object.getOwn PropertyKeys()方法都可以获取Symbol属性名。

上面代码中,使用Object.getOwnPropertyNames()方法得不到Symbol属性名,需要使用Object. getOwnPropertySymbols()方法。

24.6.6 代理防护层

ECMAScript 6引入对象代理层Proxy,Proxy可以理解成在目标对象之前,架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

【示例1】ECMAScript 6原生提供Proxy构造函数,用来生成proxy实例对象。

上面代码就是Proxy构造函数使用实例,它接收两个参数,第一个是所要代理的目标对象(上例是一个空对象),第二个是拦截函数,它有一个get方法,用来拦截对目标对象的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。

【示例2】下面是另一个拦截函数的例子。

上面代码表示,如果访问目标对象不存在的属性,则会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,则只会返回undefined。

【示例3】除了取值函数get,Proxy还可以设置存值函数set,用来拦截某个属性的赋值行为。本示例定义Person对象有一个age属性,该属性应该是一个不大于200的整数,那么可以使用Proxy对象保证age的属性值符合要求。

上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误。

24.6.7 监听

Object.observe()方法用来监听对象(以及数组)的变化。一旦监听对象发生变化,就会触发回调函数。

【示例1】在下面代码中,Object.observe()方法监听一个空对象o,一旦o发生变化(如新增或删除一个属性),就会触发回调函数。

Object.observe()方法指定的回调函数,接收一个数组(changes)作为参数。该数组的成员与对象的变化一一对应,也就是说,对象发生多少个变化,该数组就有多少个成员。每个成员是一个对象(change),它的name属性表示发生变化源对象的属性名,oldValue属性表示发生变化前的值,object属性指向变动后的源对象,type属性表示变化的种类。基本上,change对象是下面的样子。

Object.observe()方法目前共支持监听6种变化。

☑ add:添加属性。

☑ update:属性值的变化。

☑ delete:删除属性。

☑ setPrototype:设置原型。

☑ reconfigure:属性的attributes对象发生变化。

☑ preventExtensions:对象被禁止扩展。

Object.observe()方法还可以接收第三个参数,用来指定监听的事件种类。用法如下:

     Object.observe(o, observer, ['delete']);

上面代码表示,只在发生delete事件时,才会调用回调函数。

Object.unobserve()方法用来取消监听。

     Object.unobserve(o, observer);

提示:Object.observe()和Object.unobserve()这两个方法不属于ECMAScript 6,而是属于ES7的一部分,Chrome 36已经开始支持了。

24.7 函数

ECMAScript 6增强了JavaScript函数功能,改进了很多函数定义的方式,使函数使用更方便、更高效。

24.7.1 默认值

在ECMAScript 6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。例如:

上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。例如:

ECMAScript 6允许为函数的参数设置默认值,即直接写在参数定义的后面。

可以看到,ECMAScript 6的写法比ECMAScript 5简洁许多,而且非常自然。

【示例1】下面代码为函数的参数设置默认值。

【示例2】利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

上面代码的foo()函数,如果调用时没有参数,就会调用默认值throwIfMissing()函数,从而抛出一个错误。

从上面代码可以看到,参数mustBeProvided的默认值等于throwIfMissing()函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。

【示例3】参数默认值的作用域不是全局作用域,而是函数作用域。

上面代码中,参数y的默认值等于x,由于处在函数作用域,所以x等于参数x,而不是全局变量x。

【示例4】参数默认值可以与解构赋值联合使用。

上面代码中,foo()函数的参数是一个对象,变量x和y用于解构赋值,y有默认值5。

24.7.2 rest参数

ECMAScript 6引入rest参数(...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

【示例1】下面代码中的add()函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。

rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。

【示例2】下面代码利用rest参数改写数组push()方法。

提示:rest参数之后不能再有其他参数,否则会报错。

24.7.3 扩展运算符

扩展运算符(spread)是3个点(...)。它能够将一个数组转换为用逗号分隔的参数序列。该运算符主要用于函数调用。

【示例1】在下面代码中,array.push(...items)和add(...numbers)都是函数调用,都使用了扩展运算符。该运算符能够将一个数组转换为参数序列。

【示例2】在下面代码中,使用扩展运算符简化求数组最大元素。

     //ECMAScript 6
     Math.max(...[14, 3, 77])
     //等同于
     Math.max(14, 3, 77);
     Math.max.apply(null, [14, 3, 77]) ;

上面代码表示,由于JavaScript不提供求数组最大元素的函数,所以只能套用Math.max()函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max()了。

【示例3】在下面代码中,使用扩展运算符为数组赋值。

     var a=[1];
     var b=[2, 3, 4];
     var c=[6, 7];
     var d=[0, ...a, ...b, 5, ...c];
     d                                  //[0, 1, 2, 3, 4, 5, 6, 7]
24.7.4 箭头函数

ES6允许使用箭头(=>)定义函数。例如:

     var f=v => v;

等同于:

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。例如:

     var sum=(num1, num2) => num1+num2;

等同于:

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。例如:

     var sum=(num1, num2) => { return num1+num2; }

提示:由于大括号被解释为代码块,如果箭头函数直接返回一个对象,必须在对象外面加上括号。例如:

        var getTempItem=id => ({ id: id, name:"Temp" });

【示例1】使用箭头函数可以简化回调函数。

提示:使用箭头函数应该注意下面几点问题:

☑ 函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。

☑ 箭头函数不可以当作构造函数,即不可以使用new命令,否则会抛出一个错误。

☑ 箭头函数不可以使用arguments对象,该对象在函数体内不存在。

【示例2】在函数体内,this对象的指向是可变的,但是在箭头函数中,它是固定的。

上面代码的init方法中,使用了箭头函数,这导致this绑定handler对象,否则回调函数运行时,this.doSomething这一行会报错,因为此时this指向全局对象。

由于this在箭头函数中被绑定,所以不能用call()、apply()、bind()这些方法去改变this的指向。

长期以来,JavaScript语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数绑定this,很大程度上解决了这个困扰。

24.8 数据结构

ECMAScript 6完善了JavaScript数据结构,新增了两个数据类型:Set和Map。下面分别进行说明。

24.8.1 Set

ECMAScript 6提供了新的数据结构Set。它类似于数组,但成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,调用该函数可以生成Set数据结构。

【示例1】下面代码使用add()方法向Set结构添加成员,结果显示Set结构不会包含重复的值。

     var s=new Set();
     [2,3,5,4,5,2,2].map(x => s.add(x));
     for (i of s) {console.log(i)}                //2 3 4 5

【示例2】Set()函数也可以接收一个数组作为参数,用来初始化Set结构。

     var items=new Set([1,2,3,4,5,5,5,5]);
     items.size;  //5

向Set加入值时,不会发生类型转换。因此在Set中,5和“5”是两个不同的值。

Set结构的原型(Set.prototype)定义两个属性,说明如下。

☑ constructor:构造函数,默认就是Set函数。

☑ size:返回Set结构的成员数。

Set数据结构的原型(Set.prototype)定义了4个方法,说明如下。

☑ add(value):添加某个值。

☑ delete(value):删除某个值。

☑ has(value):返回一个布尔值,表示该值是否为set的成员。

☑ clear():清除所有成员。

【示例3】下面代码演示了如何使用Set的属性和方法。

【示例4】下面代码对比对象结构和Set结构的写法和用法异同。

【示例5】使用Array.from()方法可以将Set结构转为数组结构。

     var items=new Set([1, 2, 3, 4, 5]);
     var array=Array.from(items);

因此可以使用下面代码去除数组中的重复元素:

     Array.from(new Set(array));

借助for of循环,可以遍历Set结构。

24.8.2 WeakSet

WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。

☑ WeakSet的成员只能是对象,而不能是其他类型的值。

☑ WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。

WeakSet是一个构造函数,可以使用new命令创建WeakSet数据结构。

     var ws=new WeakSet();

作为构造函数,WeakSet可以接收一个数组或类似数组的对象作为参数。该数组的所有成员都会自动成为WeakSet实例对象的成员。

【示例】在下面代码中,a是一个数组,它有两个成员,也都是数组。将a作为WeakSet构造函数的参数,a的成员会自动成为WeakSet的成员。

     var a=[[1,2], [3,4]];
     var ws=new WeakSet(a);

WeakSet结构的原型(WeakSet.prototype)定义了4个方法:

☑ add(value):向WeakSet实例添加一个新成员。

☑ clear():清除WeakSet实例的所有成员。

☑ delete(value):清除WeakSet实例的指定成员。

☑ has(value):返回一个布尔值,表示某个值是否在WeakSet实例中。

24.8.3 Map

JavaScript对象都是键值对的集合,只能使用字符串作为键。使用起来不是很方便。

【示例1】下面代码计划将一个DOM节点作为对象data的键,但是由于对象只接收字符串作为键名,所以element被自动转为字符串[Object HTMLDivElement]。

     var data={};
     var element=document.getElementById("myDiv");
     data[element]=metadata;

为了解决这个问题,ECMAScript 6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,对象也可以当作键。

【示例2】下面代码使用set()方法,将对象o当作m的一个键,然后又使用get()方法读取这个键。

     var m=new Map();
     o={p:"Hello World"};
     m.set(o,"content")
     console.log(m.get(o))                   //"content"

【示例3】map函数也可以接收一个数组进行初始化,该数组的成员是一个个表示键值对的数组。

只有对同一个对象的引用,Map结构才将其视为同一个键。

【示例4】下面代码的set()和get()方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get()方法无法读取该键,返回undefined。

     var map=new Map();
     map.set(['a'], 555);
     map.get(['a']) //undefined

同理,同样的值的两个实例,在Map结构中被视为两个键。

上面代码中,变量k1和k2的值是一样的,但是它们在Map结构中被视为两个键。

由上可知,map的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,扩展别人库时,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

Map数据结构定义了下面这些属性和方法。

☑ size:返回成员总数。

☑ set(key, value):设置一个键值对。

☑ get(key):读取一个键。

☑ has(key):返回一个布尔值,表示某个键是否在Map数据结构中。

☑ delete(key):删除某个键。

☑ clear():清除所有成员。

Map提供3个原生遍历器。具体说明如下。

☑ keys():返回键名的遍历器。

☑ values():返回键值的遍历器。

☑ entries():返回所有成员的遍历器。

【示例5】本示例分别使用上述3个遍历器访问Map数据中的键、值和所有成员。

【拓展】Map定义了forEach()方法,该方法与数组的forEach()方法类似,也可以实现遍历。

forEach()方法还可以接收第二个参数,用来绑定this。

上面代码中,forEach()方法的回调函数的this,就指向reporter。

24.8.4 WeakMap

WeakMap结构与Map结构基本类似,唯一的区别是它只接收对象作为键名(null除外),不接收原始类型的值作为键名。

设计WeakMap结构的目的:键名是对象的弱引用,当对象被回收后,WeakMap自动移除对应的键值对。

【示例】WeakMap常用于DOM元素,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除,这样可以防止内存泄漏。

     var map=new WeakMap();
     var element=document.querySelector(".element");
     map.set(element,"Original");
     var value=map.get(element);
     console.log(value); //"Original"
     element.parentNode.removeChild(element);
     element=null;
     value=map.get(element);
     console.log(value); //undefined

WeakMap支持has和delete方法,但是没有size属性,也无法遍历它的值,这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。

24.9 循环遍历

本节介绍ECMAScript 6新增的遍历器和for of循环语句。它们都是针对for循环的局限进行功能改善和增强。

24.9.1 遍历器

遍历器(Iterator)是一种协议,任何对象只要部署该协议,就可以完成遍历操作。在ECMAScript 6中,遍历操作特指for of循环。遍历器的作用如下:

☑ 为遍历对象的属性提供统一的接口。

☑ 使得对象的属性能够按次序排列。

ECMAScript 6的遍历器协议规定,部署了next()方法的对象,就具备了遍历器功能。next()方法必须返回一个包含value和done两个属性的对象。其中,value属性是当前遍历位置的值,done属性是一个布尔值,表示遍历是否结束。

【示例1】下面代码定义了一个makeIterator()函数,该函数能够返回一个遍历器对象,用来遍历参数数组。

【示例2】下面代码定义了一个无限运行的遍历器。

在ECMAScript 6中,数组、类似数组的对象、Set和Map结构,都原生具备iterator接口,可以被for of循环遍历。

24.9.2 for/of循环

JavaScript原有的for in循环,只能获得对象的键名,不能直接获取键值。ECMAScript 6新增了for of循环,允许遍历获得键值。

【示例1】本示例使用for in遍历数组arr,则获取的值为下标索引:0、1、2、3。

【示例2】本示例使用for of遍历数组arr,则获取值为a、b、c、d,而不是索引值。

上面代码表明,for in循环读取键名(下标索引),for of循环读取键值(元素值)。

使用for of循环还可以遍历对象。

【示例3】下面定义一个对象es6,包含3个属性:edition、committee、standard,使用for in循环可以读取属性名:edition、committee、standard。

如果把上面对象转换为Map数据结构,然后使用for of循环遍历,则可以读取3个数组类型的值,同时遍历对象的键名和键值:

在ECMAScript 6中,一个对象只要定义了next()方法,就被视为具有iterator接口,就可以用for of循环遍历它的值。

【示例4】本示例使用24.9.1节定义的idMaker()函数生成it遍历器。可以看到for of默认从0开始循环。

数组原生具备iterator接口。因此,for of循环完全可以取代数组实例的forEach()方法。

【示例5】对于Set和Map结构的数据,可以直接使用for of循环。

上面代码演示了如何遍历Set结构,在示例3中演示了如何遍历Map结构,Map结构可以被同时遍历键名和键值。

对于普通的对象,for of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,for in循环依然可以用来遍历键名。

【小结】for of循环可以使用的范围包括:数组、类似数组的对象,如arguments对象、DOM NodeList对象、Set和Map结构、Generator对象,以及字符串。

【示例6】本示例演示如何使用for of循环遍历字符串和DOM NodeList对象。

24.10 状态机

Generator就是一个内部状态的遍历器,即每调用一次遍历器,内部状态发生一次改变(可以理解成发生某些事件)。ECMAScript 6引入Generator函数,作用就是可以完全控制内部状态的变化,依次遍历这些状态。

24.10.1 使用Generator函数

Generator函数就是普通函数,但是有以下两个特征:

☑ function关键字后面有一个星号。

☑ 函数体内部使用yield语句,定义遍历器的每个成员,即不同的内部状态。

【示例1】下面代码定义了一个Generator函数helloWorldGenerator(),它的遍历器有两个成员“hello”和“world”。调用这个函数,就会得到遍历器。

当调用Generator函数时,该函数并不执行,而是返回一个遍历器(可以理解成暂停执行)。以后,每次调用这个遍历器的next()方法,就从函数体的头部或者上一次停下来的地方开始执行(可以理解成恢复执行),直到遇到下一个yield语句为止。也就是说,next()方法就是在遍历yield语句定义的内部状态。

上面代码一共调用了4次next()方法。

第一次调用,函数开始执行,直到遇到第一句yield语句为止。next()方法返回一个对象,它的value属性就是当前yield语句的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,函数从上次yield语句停下的地方,一直执行到下一个yield语句。next()方法返回的对象的value属性就是当前yield语句的值world,done属性的值false,表示遍历还没有结束。

第三次调用,函数从上次yield语句停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next()方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时函数已经运行完毕,next()方法返回对象的value属性为undefined,done属性为true。以后再调用next()方法,返回的都是这个值。

总之,Generator函数使用iterator接口,每次调用next()方法的返回值,就是一个标准的iterator返回值:有着value和done两个属性的对象。其中,value是yield语句后面那个表达式的值,done是一个布尔值,表示是否遍历结束。

Generator函数的本质,其实是提供一种可以暂停执行的函数。yield语句就是暂停标志,next()方法遇到yield,就会暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value属性的值。当下一次调用next()方法时,再继续往下执行,直到遇到下一个yield语句。如果没有再遇到新的yield语句,就一直运行到函数结束,将return语句后面的表达式的值,作为value属性的值,如果该函数没有return语句,则value属性的值为undefined。

由于yield后面的表达式,直到调用next()方法时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield语句与return语句有点像,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

【示例2】Generator函数可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。

上面代码中,只有调用next()方法时,函数f才会执行。

【示例3】利用Generator函数,可以在任意对象上部署iterator接口。

上述代码中,由于Generator函数返回一个具有iterator接口的对象,所以只要让yield语句每次返回一个参数对象的成员,就可以在任意对象上部署next()方法。

24.10.2 next()方法

yield语句本身没有返回值,或者说总是返回undefined。next()方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

【示例1】下面代码先定义了一个可以无限运行的Generator函数f(),如果next()方法没有参数,每次运行到yield语句,变量reset的值总是undefined。当next方法带一个参数true时,当前的变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

【示例2】下面代码第一次调用next()方法时,返回x+1的值6;第二次调用next()方法,将上一次yield语句的值设为12,因此y等于24,返回y / 3的值8;第三次调用next()方法,将上一次yield语句的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。

提示:由于next()方法的参数表示上一个yield语句的返回值,所以第一次使用next()方法时,不能带有参数。V8引擎直接忽略第一次使用next()方法时的参数,只有从第二次使用next()方法开始,参数才是有效的。

24.10.3 异步操作

Generator函数的这种暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next()方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next()方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

【示例1】下面代码表示,第一次调用loadUI()函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next()方法,则会显示Loading界面,并且异步加载数据。等到数据加载完成,再一次使用next()方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑都被封装在一个函数,按部就班非常清晰。

【示例2】Ajax是典型的异步操作,通过Generator函数部署Ajax操作,可以用同步的方式表达。

上面代码的main()函数,就是通过Ajax操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall()函数中的next()方法,必须加上response参数,因为yield语句构成的表达式,本身是没有值的,总是等于undefined。

【示例3】下面是另一个例子,通过Generator函数逐行读取文本文件。

上面代码打开文本文件,使用yield语句可以手动逐行读取文件。

提示:如果某个操作非常耗时,可以把它拆成N步。

然后,使用一个函数,按次序自动执行所有步骤。

【拓展】yield语句是同步运行,不是异步运行,否则就失去了取代回调函数的设计目的。实际操作中,一般让yield语句返回Promise对象。

上面代码使用Promise的函数库Q,yield语句返回的就是一个Promise对象。

24.10.4 for of循环

for of循环可以自动遍历Generator函数,且此时不再需要调用next()方法。

【示例1】下面代码使用for of循环,依次显示5个yield语句的值。这里需要注意,一旦next()方法的返回对象的done属性为true,for of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for of循环中。

【示例2】下面是一个利用generator函数和for of循环,实现斐波那契数列的例子。

从上面代码可见,使用for of语句时不需要使用next()方法。

24.10.5 throw方法

Generator函数还有一个特点,它可以在函数体外抛出错误,然后在函数体内捕获。

【示例1】下面代码中,遍历器i连续抛出两个错误。第一个错误被Generator函数体内的catch捕获,然后Generator函数执行完成,于是第二个错误被函数体外的catch捕获。

【示例2】这种函数体内捕获错误的机制,大大方便了对错误的处理。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数写一个错误处理语句。

【示例3】使用Generator函数可以大大简化上面的代码。

24.10.6 yield*语句

如果yield命令后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器。这被称为yield*语句。

【示例1】在下面代码中,delegatingIterator是代理者,delegatedIterator是被代理者。由于yield* delegatedIterator语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个Genertor函数,有递归的效果。

【示例2】下面是一个稍微复杂的例子,使用yield*语句遍历完全二叉树。

24.11 预处理

ECMAScript 6原生提供了Promise对象。所谓Promise对象,就是代表了未来某个将要发生的事件(通常是一个异步操作)。它的好处在于,有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象还提供了一整套完整的接口,使得可以更加容易地控制异步操作。

24.11.1 基本用法

ECMAScript 6的Promise对象是一个构造函数,用来生成Promise实例。下面是Promise对象的基本用法。

【示例1】下面代码表示,Promise构造函数接收一个函数作为参数,该函数的两个参数分别是resolve()方法和reject()方法。如果异步操作成功,则用resolve()方法将Promise对象的状态变为“成功”(即从pending变为resolved);如果异步操作失败,则用reject()方法将状态变为“失败”(即从pending变为rejected)。

【示例2】Promise实例生成以后,可以用then()方法分别指定resolve()方法和reject()方法的回调函数。下面是一个使用Promise对象的简单例子。

上面代码的timeout()方法返回一个Promise实例对象,表示一段时间以后改变自身状态,从而触发then()方法绑定的回调函数。

【示例3】下面是一个用Promise对象实现的Ajax操作的例子。

上面代码中,resolve()方法和reject()方法调用时,都带有参数。它们的参数会被传递给回调函数。

【示例4】reject()方法的参数通常是Error对象的实例,而resolve()方法的参数除了正常的值以外,还可能是另一个Promise实例,例如像下面这样。

上面代码中,p1和p2都是Promise的实例,但是p2的resolve()方法将p1作为参数,这时p1的状态就会传递给p2。如果调用时,p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是fulfilled或者rejected,那么p2的回调函数将会立刻执行。

24.11.2 then()方法

Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。

上面的代码使用then()方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

这种设计使得嵌套的异步操作,可以被很容易地改写,从回调函数的“横向发展”改为“向下发展”。

24.11.3 catch()方法

Promise.prototype.catch()方法是Promise.prototype.then(null,rejection)的别名,用于指定发生错误时的回调函数。

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

24.11.4 all()和race()方法

Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。

     var p=Promise.all([p1,p2,p3]);

上面代码中,Promise.all()方法接收一个数组作为参数,p1、p2、p3都是Promise对象的实例。(Promise.all()方法的参数不一定是数组,但是必须具有iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

☑ 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

☑ 只要p1、p2、p3中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

【示例】下面是一个具体的例子。

Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例。

     var p=Promise.race([p1,p2,p3]);

上面代码中,只要p1、p2、p3中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

如果Promise.all()方法和Promise.race()方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为Promise实例,再进一步处理。

24.11.5 resolve()和reject()方法

有时需要将现有对象转为Promise对象,Promise.resolve()方法就起到这个作用。

     var jsPromise=Promise.resolve($.ajax('/whatever.json'));

上面代码将jQuery生成deferred对象,转为一个新的ECMAScript 6的Promise对象。

如果Promise.resolve()方法的参数,不是具有then()方法的对象(又称thenable对象),则返回一个新的Promise对象,且它的状态为fulfilled。

上面代码生成一个新的Promise对象的实例p,它的状态为fulfilled,所以回调函数会立即执行,Promise.resolve()方法的参数就是回调函数的参数。

如果Promise.resolve()方法的参数是一个Promise对象的实例,则会被原封不动地返回。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise. reject()方法的参数reason,会被传递给实例的回调函数。

上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。

24.11.6 async函数

async函数是用来取代回调函数的另一种方法。只要函数名之前加上async关键字,就表明该函数内部有异步操作。该异步操作应该返回一个Promise对象,前面用await关键字注明。当函数执行时,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

【示例1】下面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数将返回一个Promise对象。调用该函数时,当遇到await关键字,立即返回它后面的表达式(getStockPrice()函数)产生的Promise对象,不再执行函数体内后面的语句。等到getStockPrice完成,再自动回到函数体内,执行剩下的语句。

【示例2】下面是一个更一般性的例子。

上面代码中,asyncValue()函数前面有async关键字,表明函数体内有异步操作。执行时,遇到await语句就会先返回,等到timeout()函数执行完毕,再返回value。

async函数并不属于ECMAScript 6,而是被列入了ES7,但是traceur编译器已经实现了这个功能。

24.12 类和模块

类和模块都是代码封装的基本方法,它们的主要差异在于:类可以实例化为对象,而标准模块则不能。由于标准模块的数据只有一个副本,因此当程序的一部分更改标准模块中的公共变量时,如果程序的其他任何部分随后读取该变量,都会获取同样的值。与之相反,每个实例化对象的对象数据则单独存在。另一个不同在于:不像标准模块,类可以实现接口。

24.12.1 类

ECMAScript 5通过构造函数,定义类。ECMAScript 6开始引入Class(类)概念。通过class关键字,可以定义类。

【示例1】本示例定义了一个类,类中包含一个constructor构造函数,而this关键字表示实例对象。除了构造方法,还可以自定义方法。定义方法时,前面不需要加上function保留字。

【示例2】Class之间可以通过extends关键字实现继承。

     class ColorPoint extends Point {}

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

【示例3】本示例在示例2的基础上,在ColorPoint内部加上代码。

上面代码中,constructor()方法和toString()方法中,都出现了super关键字,它指代父类的同名方法。在constructor()方法内,super指代父类的constructor()方法;在toString()方法内,super指代父类的toString()方法。

24.12.2 模块

ECMAScript 6支持模块功能,解决JavaScript代码的依赖和部署问题,取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。

1.基本用法

模块功能有两个关键字:export和import。export用于用户自定义模块,规定对外接口;import用于输入其他模块提供的功能,同时创造命名空间(namespace),防止函数名冲突。

ECMAScript 6允许将独立的JavaScript文件作为模块,也就是说,允许一个JavaScript脚本文件调用另一个脚本文件。该文件内部的所有变量,外部无法获取,必须使用export关键字输出变量。

【示例1】下面是一个JavaScript文件,里面使用export关键字输出变量。

     //profile.js
     export var firstName='David';
     export var lastName='Belle';
     export var year=1973;

上面是profile.js文件,ECMAScript 6将其视为一个模块,里面用export关键字输出了3个变量。

【示例2】export的写法,除了像上面这样,还有另外一种。

     //profile.js
     var firstName='David';
     var lastName='Belle';
     var year=1973;
     export {firstName, lastName, year};

上面代码在export关键字后,使用大括号输出一组变量,它与前一种写法是等价的。

【示例3】使用export定义模块后,其他JavaScript文件就可以通过import关键字加载这个模块(文件)。

在上面代码中,import关键字接收一个对象(用大括号表示),里面指定要从其他模块导入的变量。大括号里面的变量名,必须与被导入模块对外接口的名称相同。

如果为输入的属性或方法重新取一个名字,则可以使用as关键字,代码如下:

     import { someMethod, another as newName } from './exporter';
2.模块的整体加载

export关键字除了输出变量,还可以输出方法或类(class)。

【示例4】本示例是一个circle.js文件,它输出两个方法。

然后,可以在main.js文件中引用这个模块。

上面写法是逐一指定要导入的方法。

【示例5】下面示例以整体方式导入。

     import * as circle from 'circle';
     console.log("圆面积:"+circle.area(4));
     console.log("圆周长:"+circle.circumference(14));

【示例6】module关键字可以取代import语句,达到整体输入模块的作用。

     //main.js
     module circle from 'circle';
     console.log("圆面积:"+circle.area(4));
     console.log("圆周长:"+circle.circumference(14));

module关键字后面跟一个变量,表示导入的模块定义在该变量上。

3.export default语句

【示例7】如果不想为某个属性或方法指定输入的名称,可以使用export default语句。

在上面代码中,foo()方法被称为该模块的默认方法。

在其他模块导入该模块时,import语句可以为默认方法指定任意名字。

     //import-default.js
     import customName from './export-default';
     customName(); //'foo'

注意:一个模块只能有一个默认方法。

如果想在一条import语句中,同时输入默认方法和指定名称的变量,可以写成下面这样。

        import customName, { otherMethod } from './export-default';

如果要输出默认属性,只需将值跟在export default之后即可。

        export default 42;

提示:export default也可以用来输出类。

        //MyClass.js
        export default class { ... }
        //main.js
        import MyClass from 'MyClass'
4.模块继承

模块之间也可以继承。

【示例8】下面示例设计一个circleplus模块,继承了circle模块。

在上面代码中,“export *”表示输出circle模块的所有属性和方法,export default命令定义模块的默认方法。

这时可以为circle中的属性或方法,改名后再输出。

     export { area as circleArea } from 'circle';

加载上面模块的写法如下:

     //main.js
     module math from"circleplus";
     import exp from"circleplus";
     console.log(exp(math.pi));

上面代码中的“import exp”表示将circleplus模块的默认方法加载为exp方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值