ECMAScript 6 入门_ecmascript 6 接口-程序员宅基地

技术标签: javascript  

ECMAScript 6简介

ECMAScript 6是JavaScript语言的下一代标准。

目标是使得JavaScript语言可以用来编写大型的复杂的应用程序。

ECMAScript和JavaScript的关系

ECMAScript是JavaScript语言的国际标准,JavaScript是ECMAScript的实现。

ECMAScript的历史
  • 1996年11月,ECMAScript 1.0版。
  • 1998年6月,ECMAScript 2.0版发布。
  • 1999年12月,ECMAScript 3.0版发布,成为JavaScript的通行标准。
  • 2007年10月,ECMAScript 4.0版草案发布,对3.0版做了大幅升级,发生了严重分歧。
  • 2008年7月,中止ECMAScript 4.0的开发,将其中涉及现有功能改善的一小部分,发布为ECMAScript 3.1,会后不久,ECMAScript 3.1就改名为ECMAScript 5。
  • 2009年12月,ECMAScript 5.0版正式发布。
  • 2011年6月,ECMAscript 5.1版发布。
  • 2015年6月,ECMAScript 6发布正式版本。
部署进度

各大浏览器的最新版本对ES6的支持

Traceur编译器

Google公司的Traceur编译器,可以将ES6代码编译为ES5代码。

(1)直接插入网页

首先,必须在网页头部加载Traceur库文件。

<!-- 加载Traceur编译器 -->
<script src="http://google.github.io/traceur-compiler/bin/traceur.js"
        type="text/javascript"></script>
<!-- 将Traceur编译器用于网页 -->
<script src="http://google.github.io/traceur-compiler/src/bootstrap.js"
        type="text/javascript"></script>
<!-- 打开实验选项,否则有些特性可能编译不成功 -->
<script>
        traceur.options.experimental = true;
</script>

接下来,就可以把ES6代码放入上面这些代码的下方。

<script type="module">
    class Calc {
        constructor(){
            console.log('Calc constructor');
        }
        add(a, b){
            return a + b;
        }
    }

    var c = new Calc();
    console.log(c.add(4,5));
</script>

如果ES6代码是一个外部文件,也可以用script标签插入网页。

<script type="module" src="calc.js" >
</script>
(2)在线转换

Traceur提供一个在线编译器,可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。

上面的例子转为ES5代码运行,就是下面这个样子。

<script src="http://google.github.io/traceur-compiler/bin/traceur.js"
        type="text/javascript"></script>
<script src="http://google.github.io/traceur-compiler/src/bootstrap.js"
        type="text/javascript"></script> 
<script>
        traceur.options.experimental = true;
</script>
<script>
$traceurRuntime.ModuleStore.getAnonymousModule(function() {
     
    "use strict";

    var Calc = function Calc() {
     
        console.log('Calc constructor');
    };

    ($traceurRuntime.createClass)(Calc, {add: function(a, b) {
     
        return a + b;
    }}, {});

    var c = new Calc();
    console.log(c.add(4, 5));
    return {};
});
</script>
(3)命令行转换

作为命令行工具使用时,Traceur是一个node.js的模块,首先需要用npm安装。

$ npm install -g traceur

traceur直接运行es6脚本文件,会在标准输出显示运行结果,以前面的calc.js为例。

$ traceur calc.js
Calc constructor
9

如果要将ES6脚本转为ES5保存,要采用下面的写法

$ traceur --script calc.es6.js --out calc.es5.js

为了防止有些特性编译不成功,最好加上–experimental选项。

$ traceur --script calc.es6.js --out calc.es5.js --experimental
(4)Node.js环境的用法
var traceur = require('traceur');
var fs = require('fs');

// 将ES6脚本转为字符串
var contents = fs.readFileSync('es6-file.js').toString();

var result = traceur.compile(contents, {
  filename: 'es6-file.js',
  sourceMap: true,
  // 其他设置
  modules: 'commonjs'
});

if (result.error)
  throw result.error;

// result对象的js属性就是转换后的ES5代码  
fs.writeFileSync('out.js', result.js);
// sourceMap属性对应map文件
fs.writeFileSync('out.js.map', result.sourceMap);
ECMAScript 7

ES7可能包括的功能有:

(1)Object.observe:用来监听对象(以及数组)的变化。一旦监听对象发生变化,就会触发回调函数。
(2)Multi-Threading:多线程支持。目前,Intel和Mozilla有一个共同的研究项目RiverTrail,致力于让JavaScript多线程运行。预计这个项目的研究成果会被纳入ECMAScript标准。
(3)Traits:它将是“类”功能(class)的一个替代。通过它,不同的对象可以分享同样的特性。

其他可能包括的功能还有:更精确的数值计算、改善的内存回收、增强的跨站点安全、类型化的更贴近硬件的低级别操作、国际化支持(Internationalization Support)、更多的数据结构等等。

let和const命令

let命令

它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
    let a = 10;
    var b = 1;
}

a // ReferenceError: a is not defined.
b //1

for循环的计数器,就很合适使用let命令。

for(let i = 0; i < arr.length; i++){}

console.log(i)
//ReferenceError: i is not defined

下面的代码如果使用var,最后输出的是9。

var a = [];
for (var i = 0; i < 10; i++) {
  var c = i;
  a[i] = function () {
    
    console.log(c);
  };
}
a[6](); // 9

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

var a = [];
for (var i = 0; i < 10; i++) {
  let c = i;
  a[i] = function () {
    
    console.log(c);
  };
}
a[6](); // 6

let不像var那样,会发生“变量提升”现象。

function do_something() {
    
  console.log(foo); // ReferenceError
  let foo = 2;
}

注意,let不允许在相同作用域内,重复声明同一个变量。

// 报错
{
    let a = 10;
    var a = 1;
}

// 报错
{
    let a = 10;
    let a = 1;
}
块级作用域

let实际上为JavaScript新增了块级作用域。

function f1() {
    
  let n = 5;
  if (true) {
      let n = 10;
  }
  console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

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

// IIFE写法
(function () {
    
    var tmp = ...;
    ...
}());

// 块级作用域写法
{
    let tmp = ...;
    ...
}

另外,ES6也规定,函数本身的作用域,在其所在的块级作用域之内。

function f() {
     console.log('I am outside!'); }
(function () {
    
  if(false) {
    // 重复声明一次函数f
    function f() {
     console.log('I am inside!'); }
  }

  f();
}());

上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块,其内部声明的函数皆不会影响到作用域的外部。

需要注意的是,如果在严格模式下,函数只能在顶层作用域和函数内声明,其他情况(比如if代码块、循环代码块)的声明都会报错。

const命令

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

const PI = 3.1415;
PI // 3.1415

PI = 3;
PI // 3.1415

const PI = 3.1;
PI // 3.1415

上面代码表明改变常量的值是不起作用的。需要注意的是,对常量重新赋值不会报错,只会默默地失败。

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

if (condition) {
    const MAX = 5;
}

// 常量MAX在此处不可得

const声明的常量,也与let一样不可重复声明。

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

变量的解构赋值

数组的解构赋值

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

以前,为变量赋值,只能直接指定值。

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

ES6允许写成下面这样。

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

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

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

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不能转为对象,因此报错。

解构赋值允许指定默认值。

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;

对于Set结构,也可以使用数组的解构赋值。

[a, b, c] = new Set(["a", "b", "c"])
a // "a"

事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的结构赋值。

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

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

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

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

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

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

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

和数组一样,解构也可以用于嵌套结构的对象。

var o = {
  p: [
    "Hello",
    { y: "World" }
  ]
};

var { p: [x, { y }] } = o;
x // "Hello"
y // "World"

对象的解构也可以指定默认值。

var { x = 3 } = {};
x // 3

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

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法

var x;
{x} = {x:1};
// SyntaxError: syntax error

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

// 正确的写法

({x}) = {x:1};
// 或者
({x} = {x:1});
用途
(1)交换变量的值
[x, y] = [y, x];
(2)从函数返回多个值

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

// 返回一个数组

function example() {
    
    return [1, 2, 3];
}
var [a, b, c] = example();

// 返回一个对象

function example() {
    
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = example();
(3)函数参数的定义
function f([x]) {
     ... }

f(['a'])

function f({x, y, z}) {
     ... }

f({x:1, y:2, z:3})

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

(4)函数参数的默认值
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {
    },
  cache = true,
  complete = function () {
    },
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};

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

(5)遍历Map结构

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

var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

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

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}
(6)输入模块的指定方法

加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。

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

字符串的扩展

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

codePointAt()

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

var s = "��";

s.length // 2
s.charAt(0) // ''
s.charAt(1)    // ''
s.charCodeAt(0) // 55362        
s.charCodeAt(1) // 57271

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

ES6提供了codePointAt方法,能够正确处理4个字节储存的字符,返回一个字符的码点。

var s = "��a";

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.charCodeAt(2) // 97

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

总之,codePointAt方法会正确返回四字节的UTF-16字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。

codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

function is32Bit(c) {
    
    return c.codePointAt(0) > 0xFFFF;
}

is32Bit("��") // true
is32Bit("a") // false
String.fromCodePoint()

ES5提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别辅助平面的字符(编号大于0xFFFF)。

String.fromCharCode(0x20BB7)
// "ஷ"

上面代码中,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。

ES6提供了String.fromCodePoint方法,可以识别0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

String.fromCodePoint(0x20BB7) 
// "��"

注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

at()

ES5提供String.prototype.charAt方法,返回字符串给定位置的字符。该方法不能识别码点大于0xFFFF的字符。

'��'.charAt(0)
// '\uD842'

上面代码中,charAt方法返回的是UTF-16编码的第一个字节,实际上是无法显示的。

ES7提供了at方法,可以识别Unicode编号大于0xFFFF的字符,返回正确的字符。

'��'.at(0)
// '��'
字符的Unicode表示法

JavaScript允许采用“\uxxxx”形式表示一个字符,其中“xxxx”表示字符的码点。

"\u0061"
// "a"

但是,这种表示法只限于\u0000——\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表达。

"\uD842\uDFB7"
// "��"

"\u20BB7"
// " 7"

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

ES6对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

"\u{20BB7}"
// "��"

"\u{41}\u{42}\u{43}"
// "ABC"
正则表达式的u修饰符

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

(1)点字符

点(.)字符在正则表达式中,解释为除了换行以外的任意单个字符。对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符。

var s = "��";

/^.$/.test(s) // false
/^.$/u.test(s) // true

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

(2)Unicode字符表示法

ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。

/\u{
   61}/.test('a') // false
/\u{
   61}/u.test('a') // true
/\u{
   20BB7}/u.test('��') // true

上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配61个连续的u。

(3)量词

使用u修饰符后,所有量词都会正确识别大于码点大于0xFFFF的Unicode字符。

/a{
   2}/.test('aa') // true
/a{
   2}/u.test('aa') // true
/��{
   2}/.test('����') // false
/��{
   2}/u.test('����') // true
(4)预定义模式

u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的Unicode字符。

/^\S$/.test('��') // false
/^\S$/u.test('��')

上面代码的\S是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的Unicode字符。

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

function codePointLength(text) {
    
    var result = text.match(/[\s\S]/gu);
    return result ? result.length : 0;
}

var s = "����";

s.length // 4
codePointLength(s) // 2
(5)i修饰符

有些Unicode字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。

/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

上面代码中,不加u修饰符,就无法识别非规范的K字符。

normalize()

为了表示语调和重音符号,Unicode提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别。

'\u01D1'==='\u004F\u030C' //false

'\u01D1'.length // 1
'\u004F\u030C'.length // 2

上面代码表示,JavaScript将合成字符视为两个字符,导致两种表示方法不相等。

ES6提供String.prototype.normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。

'\u01D1'.normalize() === '\u004F\u030C'.normalize() 
// true

normalize方法可以接受四个参数。

  • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。
  • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
'\u004F\u030C'.normalize(NFC).length // 1
'\u004F\u030C'.normalize(NFD).length // 2

上面代码表示,NFC参数返回字符的合成形式,NFD参数返回字符的分解形式。

不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过Unicode编号区间判断。

contains(), startsWith(), endsWith()

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

  • contains():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = "Hello world!";

s.startsWith("Hello") // true
s.endsWith("!") // true
s.contains("o") // true

这三个方法都支持第二个参数,表示开始搜索的位置。

var s = "Hello world!";

s.startsWith("o", 4) // true
s.endsWith("o", 8) // true
s.contains("o", 8) // false

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

repeat()

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

"x".repeat(3) // "xxx"
"hello".repeat(2) // "hellohello"
正则表达式的y修饰符

除了u修饰符,ES6还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。它的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,不同之处在于,g修饰符只确保剩余位置中存在匹配,而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修饰符就会返回结果了。

var s = "aaa_aa_a";
var r = /a+_/y;

r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]

上面代码每次匹配,都是从剩余字符串的头部开始。

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

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

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

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

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

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

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面代码中的字符串,都是用反引号表示。如果在模板字符串中嵌入变量,需要将变量名写在${}之中。

大括号内部可以进行运算,以及引用对象属性。

var x = 1;
var y = 2;

console.log(`${x} + ${y} = ${x+y}`) 
// "1 + 2 = 3"

console.log(`${x} + ${y*2} = ${x+y*2}`) 
// "1 + 4 = 5"

var obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`)
// 3

模板字符串使得字符串与变量的结合,变得容易。下面是一个例子。

if (x > MAX) {
    throw new Error(`Most ${MAX} allowed: ${x}!`);
    // 传统写法为'Most '+MAX+' allowed: '+x+'!'
}

数值的扩展

二进制和八进制表示法

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

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

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

Number.isFinite(), Number.isNaN()

ES6在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
Number.parseInt(), Number.parseFloat()

ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。

// ES5的写法
parseInt("12.34") // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt("12.34") // 12
Number.parseFloat('123.45#') // 123.45

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

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 and 2ˆ53之间。ES6引入了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
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)数学方法

ES6在Math对象上还提供了许多新的数学方法。

  • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
  • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
  • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
  • Math.cbrt(x) 返回x的立方根
  • Math.clz32(x) 返回x的32位二进制整数表示形式的前导0的个数
  • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
  • 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的双曲正切(hyperbolic tangent)

数组的扩展

Array.from()

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

let ps = document.querySelectorAll('p');

Array.from(ps).forEach(function (p) {
    
  console.log(p);
});

上面代码中,querySelectorAll方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用forEach方法。

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

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from()的一个应用是,将字符串转为数组,然后返回字符串的长度。这样可以避免JavaScript将大于\uFFFF的Unicode字符,算作两个字符的bug。

function countSymbols(string) {
    
  return Array.from(string).length;
}
Array.of()

Array.of()方法用于将一组值,转换为数组。

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

这个函数的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

Array() // []
Array(3) // [undefined, undefined, undefined]
Array(3,11,8) // [3, 11, 8]

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

数组实例的find()和findIndex()

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

[1, 5, 10, 15].find(function(value, index, arr) {
    
    return value > 9;
}) // 10

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

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

[1, 5, 10, 15].findIndex(function(value, index, arr) {
    
    return value > 9;
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

另外,这两个方法都可以发现NaN,弥补了IndexOf()的不足。

[NaN].indexOf(NaN) 
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0
数组实例的fill()

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

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

fill()还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
数组实例的entries(),keys()和values()

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

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
数组推导

ES6提供简洁写法,允许直接通过现有数组生成新数组,这被称为数组推导(array comprehension)。

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

a2 // [2, 4, 6, 8]

上面代码表示,通过for…of结构,数组a2直接在a1的基础上生成。

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

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语句。

在一个数组推导中,还可以使用多个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

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

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

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

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

[for (c of 'abcde') c+'0'].join('') // 'a0b0c0d0e0'

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

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

Array.observe(),Array.unobserve()

这两个方法用于监听(取消监听)数组的变化,指定回调函数。

它们的用法与Object.observe和Object.unobserve方法完全一致,也属于ES7的一部分,请参阅《对象的扩展》一章。唯一的区别是,对象可监听的变化一共有六种,而数组只有四种:add、update、delete、splice(数组的length属性发生变化)。

对象的扩展

属性的简洁表示法

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

var Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

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

function getPoint() {
    
  var x = 1;
  var y = 10;

  return {x, y};
}

getPoint()
// {x:1, y:10}

下面是一个类似的例子。

let x = 4;
let y = 1;

// 下行等同于 let obj = { x: x, y: y };
let obj = { x, y };
属性名表达式

JavaScript语言定义对象的属性,有两种方法。

// 方法一
obj.foo = true;

// 方法二
obj['a'+'bc'] = 123;

上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

但是,如果使用字面量方式定义对象(使用大括号),在ES5中只能使用方法一(标识符)定义属性。

var obj = {
  foo: true,
  abc: 123
};

ES6允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';

let obj = {
   [propKey]: true,
   ['a'+'bc']: 123
};

下面是另一个例子。

var lastWord = "last word";

var a = {
    "first word": "hello",
    [lastWord]: "world"
};

a["first word"] // "hello"
a[lastWord] // "world"
a["last word"] // "world"

表达式还可以用于定义方法名。

let obj = {
  ['h'+'ello']() {
    return 'hi';
  }
};

console.log(obj.hello()); // hi
Object.is()

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

+0 === -0 //true
NaN === NaN // false

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

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

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}

assign方法有很多用处。

(1)为对象添加属性
class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

上面方法通过assign方法,将x属性和y属性添加到Point类的对象实例。

(2)为对象添加方法
Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
    
  ···
};
SomeClass.prototype.anotherMethod = function () {
    
  ···
};

上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign方法添加到SomeClass.prototype之中。

(3)克隆对象
function clone(origin) {
    
  return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

function clone(origin) {
    
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}
(4)为属性指定默认值
const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};

function processContent(options) {
    
  let options = Object.assign({}, DEFAULTS, options);
}

上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。assign方法将DEFAULTS和options合并成一个新对象,如果两者有同名属性,则option的属性值会覆盖DEFAULTS的属性值。

proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()
(1)proto属性

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

// es6的写法

var obj = {
  __proto__: someOtherObj,
  method: function() {
     ... }
}

// es5的写法

var obj = Object.create(someOtherObj);
obj.method = function() {
     ... }

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

(2)Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与proto相同,用来设置一个对象的prototype对象。它是ES6正式推荐的设置原型对象的方法。

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
var o = Object.setPrototypeOf({}, null);

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

function (obj, proto) {
    
  obj.__proto__ = proto;
  return obj;
}
(3)Object.getPrototypeOf()

该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。

Object.getPrototypeOf(obj)
Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的ID。它通过Symbol函数生成。

let symbol1 = Symbol();

typeof symbol
// "symbol"

上面代码中,变量symbol1就是一个独一无二的ID。typeof运算符的结果,表明变量symbol1是Symbol数据类型,而不是字符串之类的其他类型。

Symbol函数可以接受一个字符串作为参数,表示Symbol实例的名称。

var mySymbol = Symbol('Test');

mySymbol.name
// Test

上面代码表示,Symbol函数的字符串参数,用来指定生成的Symbol的名称,可以通过name属性读取。之所以要新增name属性,是因为键名是Symbol类型,而有些场合需要一个字符串类型的值来指代这个键。

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

Symbol类型的值不能与其他类型的值进行运算,会报错。

var sym = Symbol('My symbol');
'' + sym
// TypeError: Cannot convert a Symbol value to a string

但是,Symbol类型的值可以转为字符串。

String(sym)
// 'Symbol(My symbol)'

sym.toString()
// 'Symbol(My symbol)'

symbol的最大特点,就是每一个Symbol都是不相等的,保证产生一个独一无二的值。

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

w1 === w2 // false
w1 === w3 // false
w2 === w3 // false

function f(w) {
    
  switch (w) {
    case w1:
      ...
    case w2:
      ...
    case w3:
      ...
  }
}

上面代码中,w1、w2、w3三个变量都等于Symbol(),但是它们的值是不相等的。

由于这种特点,Symbol类型适合作为标识符,用于对象的属性名,保证了属性名之间不会发生冲突。如果一个对象由多个模块构成,这样就不会出现同名的属性,也就防止了键值被不小心改写或覆盖。Symbol类型还可以用于定义一组常量,防止它们的值发生冲突。

var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
   [mySymbol]: 123
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

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函数添加一个参数,就可以引用了。

let a = Map();
a.set(Symbol('my_key'), 'Noise');

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

let specialMethod = Symbol();

let obj = {
  [specialMethod]: function (arg) {
     ... }
};

obj[specialMethod](123);

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

let obj = {
  [specialMethod](arg) { ... }
};

Symbol类型作为属性名,不会出现在for…in循环中,也不会被Object.keys()、Object.getOwnPropertyNames()返回,但是有一个对应的Object.getOwnPropertySymbols方法,以及Object.getOwnPropertyKeys方法都可以获取Symbol属性名。

var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
    value: "foobar",
});

Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

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

Reflect.ownKeys方法返回所有类型的键名。

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
// [Symbol(my_key), 'enum', 'nonEnum']
Proxy

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy可以理解成在目标对象之前,架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作。

ES6原生提供Proxy构造函数,用来生成Proxy实例。

var proxy = new Proxy({}, {
  get: function(target, property) {
    
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个设置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,设置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。

注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

Proxy实例也可以作为其他对象的原型对象。

var proxy = new Proxy({}, {
  get: function(target, property) {
    
    return 35;
  }
});

let obj = Object.create(proxy);

obj.time // 35

上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所有根据原型链,会在proxy对象上读取该属性,导致被拦截。

对于没有设置拦截的操作,则直接落在目标函数上,按照原先的方式产生结果。

下面是另一个拦截读取操作的例子。

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, property) {
    
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("Property \"" + property + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

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

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

let validator = {
  set: function(obj, prop, value) {
    
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于age以外的属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误。利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新DOM。

ownKeys方法用来拦截Object.keys()操作。

let target = {};

let handler = {
  ownKeys(target) {
    return ['hello', 'world'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'hello', 'world' ]

上面代码拦截了对于target对象的Object.keys()操作,返回预先设定的数组。

Proxy支持的拦截操作一览。

  • defineProperty(target, propKey, propDesc):返回一个布尔值,拦截Object.defineProperty(proxy, propKey, propDesc)
  • deleteProperty(target, propKey) :返回一个布尔值,拦截delete proxy[propKey]
  • enumerate(target):返回一个遍历器,拦截for (x in proxy)
  • get(target, propKey, receiver):返回类型不限,拦截对象属性的读取
  • getOwnPropertyDescriptor(target, propKey) :返回属性的描述对象,拦截Object.getOwnPropertyDescriptor(proxy, propKey)
  • getPrototypeOf(target) :返回一个对象,拦截Object.getPrototypeOf(proxy)
  • has(target, propKey):返回一个布尔值,拦截propKey in proxy
  • isExtensible(target):返回一个布尔值,拦截Object.isExtensible(proxy)
  • ownKeys(target):返回一个数组,拦截Object.getOwnPropertyPropertyNames(proxy)、Object.getOwnPropertyPropertySymbols(proxy)、Object.keys(proxy)
  • preventExtensions(target):返回一个布尔值,拦截Object.preventExtensions(proxy)
  • set(target, propKey, value, receiver):返回一个布尔值,拦截对象属性的设置
  • setPrototypeOf(target, proto):返回一个布尔值,拦截Object.setPrototypeOf(proxy, proto)

如果目标对象是函数,那么还有两种额外操作可以拦截。

  • apply方法:拦截Proxy实例作为函数调用的操作,比如proxy(···)、proxy.call(···)、proxy.apply(···)。
  • construct方法:拦截Proxy实例作为构造函数调用的操作,比如new proxy(···)。

Proxy.revocable方法返回一个可取消的Proxy实例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked
Object.observe(),Object.unobserve()

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

var user = {};
Object.observe(user, function(changes){
        
  changes.forEach(function(change) {
    
    user.fullName = user.firstName+" "+user.lastName;         
  });
});

user.firstName = 'Michael';
user.lastName = 'Jackson';
user.fullName // 'Michael Jackson'

上面代码中,Object.observer方法监听user对象。一旦该对象发生变化,就自动生成fullName属性。

一般情况下,Object.observe方法接受两个参数,第一个参数是监听的对象,第二个函数是一个回调函数。一旦监听对象发生变化(比如新增或删除一个属性),就会触发这个回调函数。很明显,利用这个方法可以做很多事情,比如自动更新DOM。

var div = $("#foo");

Object.observe(user, function(changes){
        
  changes.forEach(function(change) {
    
    var fullName = user.firstName+" "+user.lastName;         
    div.text(fullName);
  });
});

上面代码中,只要user对象发生变化,就会自动更新DOM。如果配合jQuery的change方法,就可以实现数据对象与DOM对象的双向自动绑定。

回调函数的changes参数是一个数组,代表对象发生的变化。下面是一个更完整的例子。

var o = {};

function observer(changes){
    
    changes.forEach(function(change) {
    
        console.log('发生变动的属性:' + change.name);
        console.log('变动前的值:' + change.oldValue);
        console.log('变动后的值:' + change.object[change.name]);
        console.log('变动类型:' + change.type);
    });
}

Object.observe(o, observer);

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

var change = {
  object: {...}, 
  type: 'update', 
  name: 'p2', 
  oldValue: 'Property 2'
}

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

  • 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这两个方法不属于ES6,而是属于ES7的一部分。不过,Chrome浏览器从33版起就已经支持。

函数的扩展

函数参数的默认值

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

function log(x, y) {
    
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

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

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

// 写法一
if (typeof y === 'undefined') {
  y = 'World';
}


// 写法二
if (arguments.length === 1) {
  y = 'World';
}

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

function log(x, y = 'World') {
    
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

可以看到,ES6的写法比ES5简洁许多,而且非常自然。下面是另一个例子。

function Point(x = 0, y = 0) {
    
   this.x = x;
   this.y = y;
}

var p = new Point(); 
// p = { x:0, y:0 }

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

function throwIfMissing() {
    
    throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
    
    return mustBeProvided;
}

foo()
// Error: Missing parameter

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

从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与python语言不一样。

另一个需要注意的地方是,参数默认值所处的作用域,不是全局作用域,而是函数作用域。

var x = 1;

function foo(x, y = x) {
    
  console.log(y);
}

foo(2) // 2

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

参数默认值可以与解构赋值,联合起来使用。

function foo({x, y = 5}) {
    
  console.log(x, y);
}

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2

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

rest参数

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

function add(...values) {
    
   let sum = 0;

   for (var val of values) {
      sum += val;
   }

   return sum;
}

add(2, 5, 3) // 10

上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。

前面说过,rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。

function push(array, ...items) {
     
  items.forEach(function(item) {
    
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

注意,rest参数之后不能再有其他参数,否则会报错。

// 报错
function f(a, ...b, c) {
     
  // ...
}
扩展运算符

扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。

function push(array, ...items) {
    
  array.push(...items);
}

function add(x, y) {
    
  return x + y;
}

var numbers = [4, 38];
add(...numbers) // 42

上面代码中,array.push(…items)和add(…numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符可以简化求出一个数组最大元素的写法。

// ES5
Math.max.apply(null, [14, 3, 77])

// ES6
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

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

扩展运算符还可以用于数组的赋值。

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]

扩展运算符还可以将字符串转为真正的数组。

[..."hello"] 
// [ "h", "e", "l", "l", "o" ]

扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);


let arr = [...map.keys()]; // [1, 2, 3]

Generator函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

var go = function*(){
    
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]

上面代码中,变量go是一个Generator函数,执行后返回的是一个遍历器,对这个遍历器执行扩展运算符,就会将内部遍历得到的值,转为一个数组。

箭头函数

ES6允许使用“箭头”(=>)定义函数。

var f = v => v;

上面的箭头函数等同于:

var f = function(v) {
    
    return v;
};

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

var f = () => 5; 
// 等同于
var f = function (){
     return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
    
    return num1 + num2;
};

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

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

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

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

箭头函数的一个用处是简化回调函数。

// 正常函数写法
[1,2,3].map(function (x) {
    
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

另一个例子是

// 正常函数写法
var result = values.sort(function(a, b) {
    
    return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

箭头函数有几个使用注意点。

  • 函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。

上面三点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。下面的代码是一个例子,将this对象绑定定义时所在的对象。

var handler = {

    id: "123456",

    init: function() {
    
        document.addEventListener("click",
            event => this.doSomething(event.type), false);
    },

    doSomething: function(type) {
    
        console.log("Handling " + type  + " for " + this.id);
    }
};

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

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

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

Set和Map数据结构

Set
基本用法

ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

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

上面代码通过add方法向Set结构加入成员,结果表明Set结构不会添加重复的值。

Set函数可以接受一个数组作为参数,用来初始化

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

items.size 
// 5

向Set加入值的时候,不会发生类型转换,5和“5”是两个不同的值。Set内部判断两个值是否精确相等,使用的是精确相等运算符(===)。这意味着,两个对象总是不相等的。

let set = new Set();

set.add({})
set.size // 1

set.add({})
set.size // 2

上面代码表示,由于两个空对象不是精确相等,所以它们被视为两个值。

属性和方法

Set结构有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set的成员总数。

Set数据结构有以下方法。

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

下面是这些属性和方法的使用演示。

s.add(1).add(2).add(2); 
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

下面是一个对比,看看在判断是否包括一个键上面,对象结构和Set结构的写法不同。

// 对象的写法

var properties = {
    "width": 1,
    "height": 1
};

if (properties[someName]) {
    // do something
}

// Set的写法

var properties = new Set();

properties.add("width");
properties.add("height");

if (properties.has(someName)) {
    // do something
}

Array.from方法可以将Set结构转为数组。

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

这就提供了一种去除数组中重复元素的方法。

function dedupe(array) {
    
    return Array.from(new Set(array));
}
遍历操作

Set结构有一个values方法,返回一个遍历器。

let set = new Set(['red', 'green', 'blue']);
for ( let item of set.values() ){
  console.log(item);
}
// red
// green
// blue

Set结构的默认遍历器就是它的values方法。

Set.prototype[Symbol.iterator] === Set.prototype.values
// true

这意味着,可以省略values方法,直接用for…of循环遍历Set。

let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

Set结构的foreach方法,用于对每个成员执行某种操作,返回修改后的Set结构。

let set = new Set([1, 2, 3]);

set.foreach((value, key) => value*2 )
// 返回Set结构{2, 4, 6}

上面代码说明,foreach方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身(上例省略了该参数)。另外,foreach方法还可以有第二个参数,表示绑定的this对象。

为了与Map结构保持一致,Set结构也有keys和entries方法,这时每个值的键名就是键值。

let set = new Set(['red', 'green', 'blue']);
for ( let item of set.keys() ){
  console.log(item);
}
// red
// green
// blue

for ( let [key, value] of set.entries() ){
  console.log(key, value);
}
// red, red
// green, green
// blue, blue

由于扩展运算符(…)内部使用for…of循环,所以也可以用于Set结构。

let set = new Set(['red', 'green', 'blue']);
let arr = [...set]; 
// ['red', 'green', 'blue']

这就提供了另一种便捷的去除数组重复元素的方法。

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; 
// [3, 5, 2]

而且,数组的map和filter方法也可以用于Set了。

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}
WeakSet

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

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

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

var ws = new WeakSet();

作为构造函数,WeakSet可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有iterable接口的对象,都可以作为WeakSet的对象。)该数组的所有成员,都会自动成为WeakSet实例对象的成员。

var a = [[1,2], [3,4]];

var ws = new WeakSet(a);

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

WeakSet结构有以下三个方法。

  • WeakSet.prototype.add(value):向WeakSet实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除WeakSet实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。

下面是一个例子。

var ws = new WeakSet();
var obj = {};
var foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo);    // false

ws.delete(window); 
ws.has(window);    // false

ws.clear();
Map
(1)基本用法

JavaScript的对象,本质上是键值对的集合,但是只能用字符串当作键。这给它的使用带来了很大的限制。

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

data[element] = metadata;

上面代码原意是将一个DOM节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[Object HTMLDivElement]

为了解决这个问题,ES6提供了map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应。

var m = new Map();
var o = {p: "Hello World"};

m.set(o, "content")
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

上面代码使用set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。

作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

var map = new Map([ ["name", "张三"], ["title", "Author"]]);

map.size // 2
map.has("name") // true
map.get("name") // "张三"
map.has("title") // true
map.get("title") // "Author"

注意,只有对同一个对象的引用,Map结构才将其视为同一个键。这一点要非常小心。

var map = new Map();

map.set(['a'], 555); 
map.get(['a']) // undefined

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

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

var map = new Map();

var k1 = ['a'];
var k2 = ['a'];

map.set(k1, 111);
map.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

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

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

如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键。这对于NaN,以及+0和-0都成立。

let map = new Map();

map.set(NaN, 123);
map.get(NaN) // 123

map.set(-0, 123);
map.get(+0) // 123

如果读取一个未知的键,则返回undefined。

new Map().get('asfddfsasadf')
// undefined
(2)属性和方法

Map数据结构有以下属性和方法。

  • size:返回成员总数。
  • set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  • get(key):读取key对应的键值,如果找不到key,返回undefined。
  • has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
  • delete(key):删除某个键,返回true。如果删除失败,返回false。
  • clear():清除所有成员,没有返回值。

set()方法返回的是Map本身,因此可以采用链式写法。

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

下面是has()和delete()的例子。

var m = new Map(); 

m.set("edition", 6)        // 键是字符串
m.set(262, "standard")     // 键是数值
m.set(undefined, "nah")    // 键是undefined

var hello = function() {
    console.log("hello");}
m.set(hello, "Hello ES6!") // 键是函数

m.has("edition")     // true
m.has("years")       // false
m.has(262)           // true
m.has(undefined)     // true
m.has(hello)         // true

m.delete(undefined)
m.has(undefined)       // false

m.get(hello)  // Hello ES6!
m.get("edition")  // 6

下面是size属性和clear方法的例子。

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0
(3)遍历

Map原生提供三个遍历器。

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。

下面是使用实例。

let map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}

上面代码最后的那个例子,表示Map结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

map[Symbol.iterator] === map.entries
// true

Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(…)。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身没有map和filter方法)。

let map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

let map1 = new Map(
  [...map0].filter(([k, v]) => k < 3) 
);
// 产生Map结构 {1 => 'a', 2 => 'b'}

let map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生Map结构 {2 => '_a', 4 => '_b', 6 => '_c'}

此外,Map还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

map.forEach(function(value, key, map)) {
    
  console.log("Key: %s, Value: %s", key, value);
};

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

var reporter = {
  report: function(key, value) {
    
    console.log("Key: %s, Value: %s", key, value);
  }
};

map.forEach(function(value, key, map) {
    
  this.report(key, value);
}, reporter);

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

WeakMap

WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受原始类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。

WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

下面是WeakMap结构的一个例子,可以看到用法上与Map几乎一样。

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与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。

WeakMap的一个用处是部署私有属性。

let _counter = new WeakMap();
let _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

let c = new Countdown(2, () => console.log('DONE'));

c.dec()
c.dec()
// DONE

上面代码中,Countdown类的两个内部属性_counter和_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

Iterator和for…of循环

Iterator(遍历器)

遍历器(Iterator)是一种接口规格,任何对象只要部署这个接口,就可以完成遍历操作。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得对象的属性能够按某种次序排列。在ES6中,遍历操作特指for…of循环,即Iterator接口主要供for…of循环使用。

遍历器提供了一个指针,指向当前对象的某个属性,使用next方法,就可以将指针移动到下一个属性。next方法返回一个包含value和done两个属性的对象。其中,value属性是当前遍历位置的值,done属性是一个布尔值,表示遍历是否结束。下面是一个模拟next方法返回值的例子。

function makeIterator(array){
    
  var nextIndex = 0;
  return {
    next: function(){
    
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  }
}

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

上面代码定义了一个makeIterator函数,它的作用是返回一个遍历器对象,用来遍历参数数组。next方法依次遍历数组的每个成员,请特别注意,next返回值的构造。

下面是一个无限运行的遍历器例子。

function idMaker(){
    
  var index = 0;

  return {
    next: function(){
    
      return {value: index++, done: false};
    }
  }
}

var it = idMaker();

it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...

上面的例子,只是为了说明next方法返回值的结构。Iterator接口返回的遍历器,原生具备next方法,不用自己部署。所以,真正需要部署的是Iterator接口,让其返回一个遍历器。在ES6中,有三类数据结构原生具备Iterator接口:数组、类似数组的对象、Set和Map结构。除此之外,其他数据结构(主要是对象)的Iterator接口都需要自己部署。

下面就是如何部署Iterator接口。一个对象如果要有Iterator接口,必须部署一个@@iterator方法(原型链上的对象具有该方法也可),该方法部署在一个键名为Symbol.iterator的属性上,对应的键值是一个函数,该函数返回一个遍历器对象。

class MySpecialTree {
  // ...
  [Symbol.iterator]() { 
    // ...
    return theIterator;
  }
}

上面代码是一个类部署Iterator接口的写法。Symbol.iterator是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内(请参考Symbol一节)。这里要注意,@@iterator的键名是Symbol.iterator,键值是一个方法(函数),该方法执行后,返回一个当前对象的遍历器。

下面是为对象添加Iterator接口的例子。

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

《数组的扩展》一章中提到,ES6对数组提供entries()、keys()和values()三个方法,就是返回三个遍历器。

var arr = [1, 5, 7];
var arrEntries = arr.entries();

arrEntries.toString() 
// "[object Array Iterator]"

arrEntries === arrEntries[Symbol.iterator]()
// true

上面代码中,entries方法返回的是一个遍历器(iterator),本质上就是调用了Symbol.iterator方法。

字符串是一个类似数组的对象,因此也原生具有Iterator接口。

var someString = "hi";
typeof someString[Symbol.iterator] 
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

上面代码中,调用Symbol.iterator方法返回一个遍历器,在这个遍历器上可以调用next方法,实现对于字符串的遍历。

可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。

var str = new String("hi");

[...str] // ["h", "i"]

str[Symbol.iterator] = function() {
    
  return { 
    next: function() {
    
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

[...str] // ["bye"]  
str // "hi"

上面代码中,字符串str的Symbol.iterator方法被修改了,所以扩展运算符(…)返回的值变成了bye,而字符串本身还是hi。

部署Symbol.iterator方法的最简单实现,还是使用下一节要提到的Generator函数。

var myIterable = {};

myIterable[Symbol.iterator] = function* () {
    
    yield 1;
    yield 2;
    yield 3;
};
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// hello
// world

如果Symbol.iterator方法返回的不是遍历器,解释引擎将会报错。

var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

上面代码中,变量obj的@@iterator方法返回的不是遍历器,因此报错。

for…of循环

ES6中,一个对象只要部署了@@iterator方法,就被视为具有iterator接口,就可以用for…of循环遍历它的值。也就是说,for…of循环内部调用是原对象的Symbol.iterator方法。

数组原生具备iterator接口。

const arr = ['red', 'green', 'blue'];

for(let v of arr) {
    console.log(v); // red green blue
}

上面代码说明,for…of循环可以代替数组实例的forEach方法。

const arr = ['red', 'green', 'blue'];

arr.forEach(function (element, index) {
    
    console.log(element); // red green blue
    console.log(index);   // 0 1 2
});

JavaScript原有的for…in循环,只能获得对象的键名,不能直接获取键值。ES6提供for…of循环,允许遍历获得键值。

var arr = ["a", "b", "c", "d"];
for (a in arr) {
  console.log(a); // 0 1 2 3
}

for (a of arr) {
  console.log(a); // a b c d
}

上面代码表明,for…in循环读取键名,for…of循环读取键值。如果要通过for…of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法,参见《数组的扩展》章节。

Set和Map结构也原生具有Iterator接口,可以直接使用for…of循环。

var engines = Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
    console.log(e);
}
// Gecko
// Trident
// Webkit

var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

上面代码演示了如何遍历Set结构和Map结构,后者是同时遍历键名和键值。

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

var es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (e of es6) {
  console.log(e);
}
// TypeError: es6 is not iterable

总结一下,for…of循环可以使用的范围包括数组、类似数组的对象(比如arguments对象、DOM NodeList对象)、Set和Map结构、后文的Generator对象,以及字符串。下面是for…of循环用于字符串和DOM NodeList对象的例子。

// 字符串的例子

let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

// DOM NodeList对象的例子

let paras = document.querySelectorAll("p");

for (let p of paras) {
  p.classList.add("test");
}

Generator 函数

语法
简介

所谓Generator,有多种理解角度。首先,可以把它理解成一个函数的内部状态的遍历器,每调用一次,函数的内部状态发生一次改变(可以理解成发生某些事件)。ES6引入Generator函数,作用就是可以完全控制函数的内部状态的变化,依次遍历这些状态。

在形式上,Generator是一个普通函数,但是有两个特征。一是,function命令与函数名之间有一个星号;二是,函数体内部使用yield语句,定义遍历器的每个成员,即不同的内部状态(yield语句在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
    
  yield 'hello';
  yield 'world';
    return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个Generator函数helloWorldGenerator,它的遍历器有两个成员“hello”和“world”。调用这个函数,就会得到遍历器。

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

hw.next() 
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代码一共调用了四次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函数返回的遍历器,只有调用next方法才会遍历下一个成员,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志,next方法遇到yield,就会暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value属性的值。当下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。如果没有再遇到新的yield语句,就一直运行到函数结束,将return语句后面的表达式的值,作为value属性的值,如果该函数没有return语句,则value属性的值为undefined。另一方面,由于yield后面的表达式,直到调用next方法时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield语句与return语句有点像,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说Generator生成了一系列的值,这也就是它的名称的来历(在英语中,generator这个词是“生成器”的意思)。

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

function* f() {
    
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
    
  generator.next() 
}, 2000);

上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个Generator函数,就变成只有调用next方法时,函数f才会执行。

next方法的参数

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

function* f() {
    
  for(var i=0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

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

这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

再看一个例子。

function* foo(x) {
    
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var it = foo(5);

it.next()
// { value:6, done:false }
it.next(12)
// { value:8, done:false }
it.next(13)
// { value:42, done:true }

上面代码第一次调用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方法开始,参数才是有效的。

for…of循环

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

function *foo() {
    
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

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

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

function* fibonacci() {
    
    let [prev, curr] = [0, 1];
    for (;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}

for (let n of fibonacci()) {
    if (n > 1000) break;
    console.log(n);
}

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

throw方法

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

var g = function* () {
    
    while (true) {
        try {
            yield;
        } catch (e) {
            if (e != 'a') {
                throw e;
            }
            console.log('内部捕获', e);
        }
    }
};

var i = g();
i.next();

try {
    i.throw('a');
    i.throw('b');
} catch (e) {
    console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

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

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

foo('a', function (a) {
    
    if (a.error) {
        throw new Error(a.error);
    }

    foo('b', function (b) {
    
        if (b.error) {
            throw new Error(b.error);
        }

        foo('c', function (c) {
    
            if (c.error) {
                throw new Error(c.error);
            }

            console.log(a, b, c);
        });
    });
});

使用Generator函数可以大大简化上面的代码。

function* g(){
    
  try {
        var a = yield foo('a');
        var b = yield foo('b');
        var c = yield foo('c');
    } catch (e) {
        console.log(e);
    }

  console.log(a, b, c);

}
yield*语句

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

let delegatedIterator = (function* () {
    
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
    
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

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

如果yield*后面跟着一个数组,就表示该数组会返回一个遍历器,因此就会遍历数组成员。

function* gen(){
    
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器。

yield*命令可以很方便地取出嵌套数组的所有成员。

function* iterTree(tree) {
    
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

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

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
    
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
    
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉树
function make(array) {
    
  // 判断是否为叶节点
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
  result.push(node); 
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以简写成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

它的完整形式如下,两者是等价的。

let obj = {
  myGeneratorMethod: function* () {
    
    ···
  }
};
含义
Generator与状态机

Generator是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。

var ticking = true;
var clock = function() {
    
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

上面代码的clock函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用Generator实现,就是下面这样。

var clock = function*(_) {
    
  while (true) {
    yield _;
    console.log('Tick!');
    yield _;
    console.log('Tock!');
  }
};

上面的Generator实现与ES5实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

Generator与协程

协程(coroutine)是一种程序运行的方式。传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个函数可以并行执行,但是只有一个函数处于正在运行的状态,其他函数都处于暂停态(suspended),函数之间可以交换执行权。也就是说,一个函数执行到一半,可以暂停执行,将执行权交给另一个函数,等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的函数,就称为协程。

不难看出,协程适合用于多任务运行的环境。在这个意义上,它与线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他都处于暂停状态。此外,线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

Generator函数是ECMAScript 6对协程的实现,但属于不完全实现,只做到了暂停执行和转移执行权,有一些特性没有实现,比如不支持所调用的函数之中的yield语句(即递归执行yield语句)。

如果将Generator函数看作多任务运行的方式,存在多个进入点和退出点。那么,一方面,并发的多任务可以写成多个Generator函数;另一方面,继发的任务则可以按照发生顺序,写在一个Generator函数之中,然后用一个任务管理函数执行(参考本节的“控制流管理”部分)。

应用

Generator可以暂停函数执行,返回任意表达式的值。这种特点使得Generator有多种应用场景。

(1)异步操作的同步化表达

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

function* loadUI() {
     
    showLoadingScreen(); 
    yield loadUIDataAsynchronously(); 
    hideLoadingScreen(); 
} 
var loader = loadUI();
// 加载UI
loader.next() 

// 卸载UI
loader.next()

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

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

function* main() {
    
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
    
  makeAjaxCall(url, function(response){
    
    it.next(response);
  });
}

var it = main();
it.next();

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

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

function* numbers() {
    
    let file = new FileReader("numbers.txt");
    try {
        while(!file.eof) {
            yield parseInt(file.readLine(), 10);
        }
    } finally {
        file.close();
    }
}

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

(2)控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

step1(function (value1) {
    
  step2(value1, function(value2) {
    
    step3(value2, function(value3) {
    
      step4(value3, function(value4) {
    
        // Do something with value4
      });
    });
  });
});

采用Promise改写上面的代码。

Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
    
    // Do something with value4
}, function (error) {
    
    // Handle any error from step1 through step4
})
.done();

上面代码已经把回调函数,改成了直线执行的形式。Generator函数可以进一步改善代码运行流程。

function* longRunningTask() {
    
  try {    
    var value1 = yield step1();
    var value2 = yield step2(value1);
    var value3 = yield step3(value2);
    var value4 = yield step4(value3);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

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

scheduler(longRunningTask());

function scheduler(task) {
    
    setTimeout(function () {
    
        if (!task.next(task.value).done) {
            scheduler(task);
    }
  }, 0);
}

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

var Q = require('q');

function delay(milliseconds) {
    
  var deferred = Q.defer();
  setTimeout(deferred.resolve, milliseconds);
  return deferred.promise;
}

function* f(){
    
  yield delay(100);
};

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

(3)部署iterator接口

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

function* iterEntries(obj) {
    
    let keys = Object.keys(obj);
    for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
    }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
    console.log(key, value);
}

// foo 3
// bar 7

上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了iterator接口。也就是说,可以在任意对象上部署next方法。

下面是一个对数组部署Iterator接口的例子,尽管数组原生具有这个接口。

function* makeSimpleGenerator(array){
    
  var nextIndex = 0;

  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true
(4)作为数据结构

Generator可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

function *doStuff() {
    
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

上面代码就是依次返回三个函数,但是由于使用了Generator函数,导致可以像处理数组那样,处理这三个返回的函数。

for (task of doStuff()) {
  // task是一个函数,可以像回调函数那样使用它
}

实际上,如果用ES5表达,完全可以用数组模拟Generator的这种用法。

function doStuff() {
    
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

上面的函数,可以用一模一样的for…of循环处理!两相一比较,就不难看出Generator使得数据或者操作,具备了类似数组的接口。

Promise对象

基本用法

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

var promise = new Promise(function(resolve, reject) {
    
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
    
  // success
}, function(value) {
    
  // failure
});

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

promise实例生成以后,可以用then方法分别指定resolve方法和reject方法的回调函数。

下面是一个使用Promise对象的简单例子。

function timeout(ms) {
    
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

timeout(100).then(() => {
  console.log('done');
});

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

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

var getJSON = function(url) {
    
  var promise = new Promise(function(resolve, reject){
    
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    function handler() {
    
      if (this.status === 200) { 
              resolve(this.response); 
          } else { 
              reject(new Error(this.statusText)); 
          }
    };
  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
    
  console.log('Contents: ' + json);
}, function(error) {
    
  console.error('出错了', error);
});

上面代码中,resolve方法和reject方法调用时,都带有参数。它们的参数会被传递给回调函数。reject方法的参数通常是Error对象的实例,而resolve方法的参数除了正常的值以外,还可能是另一个Promise实例,比如像下面这样。

var p1 = new Promise(function(resolve, reject){
    
  // ... some code
});

var p2 = new Promise(function(resolve, reject){
    
  // ... some code
  resolve(p1);
})

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

Promise.prototype.then方法:链式操作

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

getJSON("/posts.json").then(function(json) {
    
  return json.post;
}).then(function(post) {
    
  // proceed
});

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

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

getJSON("/post/1.json").then(function(post) {
    
  return getJSON(post.commentURL);
}).then(function(comments) {
    
  // 对comments进行处理
});

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

Promise.prototype.catch方法:捕捉错误

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

getJSON("/posts.json").then(function(posts) {
    
  // some code
}).catch(function(error) {
    
  // 处理前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

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

getJSON("/post/1.json").then(function(post) {
    
  return getJSON(post.commentURL);
}).then(function(comments) {
    
  // some code
}).catch(function(error) {
    
  // 处理前两个回调函数的错误
});
Promise.all方法,Promise.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决定,分成两种情况。

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

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

下面是一个具体的例子。

// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
    
  return getJSON("/post/" + id + ".json");
});

Promise.all(promises).then(function(posts) {
    
  // ...  
}).catch(function(reason){
    
  // ...
});

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实例,再进一步处理。

Promise.resolve方法,Promise.reject方法

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

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

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

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

var p = Promise.resolve('Hello');

p.then(function (s){
    
  console.log(s)
});
// Hello

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

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

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

var p = Promise.reject('出错了');

p.then(null, function (s){
    
  console.log(s)
});
// 出错了

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

async函数

async函数是用来取代回调函数的另一种方法。

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

async function getStockPrice(symbol, currency) {
    
    let price = await getStockPrice(symbol);
    return convert(price, currency);
}

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

下面是一个更一般性的例子。

function timeout(ms) {
    
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncValue(value) {
    
  await timeout(50);
  return value;
}

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

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

Class和Module

Class

ES5通过构造函数,定义并生成新对象。下面是一个例子。

function Point(x,y){
    
    this.x = x;
    this.y = y;
}

Point.prototype.toString = function () {
    
    return '('+this.x+', '+this.y+')';
}

ES6引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。上面的代码用“类”改写,就是下面这样。

//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '('+this.x+', '+this.y+')';
  }

}

var point = new Point(2,3);
point.toString() // (2, 3)

上面代码定义了一个“类”,可以看到里面有一个constructor函数,这就是构造函数,而this关键字则代表实例对象。这个类除了构造方法,还定义了一个toString方法。注意,定义方法的时候,前面不需要加上function这个保留字,直接把函数定义放进去了就可以了。

Class之间可以通过extends关键字,实现继承。

class ColorPoint extends Point {}

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

vclass ColorPoint extends Point {

  constructor(x, y, color) {
    super(x, y); // 等同于super.constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color+' '+super();
  }

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

有一个地方,需要注意。类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。

Module的基本用法

JavaScript没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如Ruby的require、Python的import,甚至就连CSS都有@import,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。

var { stat, exists, readFile } = require('fs');

ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式。

import { stat, exists, readFile } from 'fs';

所以,ES6可以在编译时就完成模块编译,效率要比CommonJS模块高。

(1)export命令,import命令

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

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

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。

// main.js

import {firstName, lastName, year} from './profile';

function sfirsetHeader(element) {
    
  element.textContent = firstName + ' ' + lastName;
}

上面代码属于另一个文件main.js,import命令就用于加载profile.js文件,并从中输入变量。import命令接受一个对象(用大括号表示),里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import语句中要使用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile';
(2)模块的整体输入,module命令

export命令除了输出变量,还可以输出方法或类(class)。下面是一个circle.js文件,它输出两个方法area和circumference。

// circle.js

export function area(radius) {
    
  return Math.PI * radius * radius;
}

export function circumference(radius) {
    
  return 2 * Math.PI * radius;
}

然后,main.js输入circlek.js模块。

// main.js

import { area, circumference } from 'circle';

console.log("圆面积:" + area(4));
console.log("圆周长:" + circumference(14));

上面写法是逐一指定要输入的方法。另一种写法是整体输入。

import * as circle from 'circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

module命令可以取代import语句,达到整体输入模块的作用。

// main.js

module circle from 'circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

module命令后面跟一个变量,表示输入的模块定义在该变量上。

(3)export default命令

如果想要输出匿名函数,可以使用export default命令。

// export-default.js

export default function () {
    
    console.log('foo');
}

其他模块输入该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js

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

上面代码的import命令,可以用任意名称指向输出的匿名函数。需要注意的是,这时import命令后面,不使用大括号。

export default命令用在非匿名函数前,也是可以的。

// export-default.js

export default function foo() {
    
  console.log('foo');
}

// 或者写成

function foo() {
    
  console.log('foo');
}

export default foo;

上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

export default命令用于指定模块的默认输出。如果模块加载时,只能输出一个值或方法,那就是export default所指定的那个值或方法。所以,import命令后面才不用加大括号。显然,一个模块只能有一个默认输出,因此export deault命令只能使用一次。

有了export default命令,输入模块时就非常直观了,以输入jQuery模块为例。

import $ from 'jquery';

如果想在一条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'
let o = new MyClass();
模块的继承

模块之间也可以继承。

假设有一个circleplus模块,继承了circle模块。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
    
    return Math.exp(x);
}

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

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

// circleplus.js

export { area as circleArea } from 'circle';

上面代码表示,只输出circle模块的area方法,且将其改名为circleArea。

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

// main.js

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

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

ES6模块的转码

浏览器目前还不支持ES6模块,为了现在就能使用,可以将转为ES5的写法。

(1)ES6 module transpiler

ES6 module transpiler是square公司开源的一个转码器,可以将ES6模块转为CommonJS模块或AMD模块的写法,从而在浏览器中使用。

首先,安装这个转玛器。

$ npm install -g es6-module-transpiler

然后,使用compile-modules convert命令,将ES6模块文件转码。

$ compile-modules convert file1.js file2.js

o参数可以指定转码后的文件名。

$ compile-modules convert -o out.js file1.js
(2)SystemJS

另一种解决方法是使用SystemJS。它是一个垫片库(polyfill),可以在浏览器内加载ES6模块、AMD模块和CommonJS模块,将其转为ES5格式。它在后台调用的是Google的Traceur转码器。

使用时,先在网页内载入system.js文件。

<script src="system.js"></script>

然后,使用System.import方法加载模块文件。

<script>
  System.import('./app');
</script>

上面代码中的./app,指的是当前目录下的app.js文件。它可以是ES6模块文件,System.import会自动将其转码。

需要注意的是,System.import使用异步加载,返回一个Promise对象,可以针对这个对象编程。下面是一个模块文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

然后,在网页内加载这个模块文件。

<script>

System.import('app/es6-file').then(function(m) {
     
  console.log(new m.q().es6); // hello
});

</script>

上面代码中,System.import方法返回的是一个Promise对象,所以可以用then方法指定回调函数。

参考链接

官方文件
综合介绍
语法点
Object
Iterator和Generator
Promise对象
Class与模块
工具

《ECMAScript 6入门》

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/helloPurple/article/details/80997230

智能推荐

7-10 集合划分 (10 分)_7个元素的集合有多少种划分-程序员宅基地

文章浏览阅读973次,点赞3次,收藏3次。当n=4 时,集合{1,2,3,4}可以划分为15个不同的非空子集如下:{{1},{2},{3},{4}},{{1,2},{3},{4}},{{1,3},{2},{4}},{{1,4},{2},{3}},{{2,3},{1},{4}},{{2,4},{1},{3}},{{3,4},{1},{2}},{{1,2},{3,4}},{{1,3},{2,4}},{{1,4},{2,3}},{{1,2,3},{4}},{{1,2,4},{3}},{{1,3,4},{2}},{{2,3,4}_7个元素的集合有多少种划分

全网唯一的echarts树状图——单个节点样式修改_echarts 怎么在修改节点文本使用dom模板-程序员宅基地

文章浏览阅读6.2k次,点赞3次,收藏11次。前段时间一直在用Echarts做一个树状图,本来是挺简单的,Echarts官网也有,不过官网上的节点数据是封装在一个Json文件中去,这里就不再赘述,有兴趣自行去官网查看链接:https://echarts.apache.org/examples/zh/editor.html?c=tree-basicJson数据链接:https://echarts.apache.org/examples/da..._echarts 怎么在修改节点文本使用dom模板

php include传参数,解决PHP的include带参数的有关问题-程序员宅基地

文章浏览阅读1k次。php的include可以载入一个外部php文件,其参数是一个文件名,既然是文件名,就不能带参数了。php中,如果include载入的文件一定要带参数,就只有一种办法,即这个参数是一个全路径的URL地址。例如:include "http://localhost/aaa.php?id=1"; //正确include "aaa.php?id=1"; //错误其实include本义就只是用来载入一个外部..._php include 传参

iOS越狱开发 常用检测Hook 代码 闪退检测断点_ios hook exit-程序员宅基地

文章浏览阅读1.8k次。__attribute__((constructor)) static void entry(){ rebind_symbols((struct rebinding[1]){{"abort", abort_hook, (void *)&abort_old}}, 1); rebind_symbols((struct rebinding[2]){{"exit", ..._ios hook exit

python转dll_python调用dll方法-程序员宅基地

文章浏览阅读376次。【转载】python调用dll方法python调用dll方法来自http://blog.csdn.net/lf8289/article/details/2322550分类:python2008-04-2412:276833人阅读评论(6)收藏举报在python中调用dll文件中的接口比较简单,实例代码如下:如我们有一个test.dll文件,内部定义如下:extern"C"{int__st..._python 转dll 第三方库

hihocoder 1388 Periodic Signal FFT-程序员宅基地

文章浏览阅读204次。最近做了几个FFT的题,实在是如果不记录一下就转头忘了 T T传送门题意:求思路:将上式拆开,那么就变成了,前两项都是固定的值那么我们求的最大值就可以了,n的数据范围为6*1e4,所以要用FFT优化下面是我现在的理解,如果有错还恰巧被大佬看到了,求指出T T如果有两个数组:A:1到n-1;B:1到m-1那么得到的卷积C:1到m+n-2,其中,注意这里

随便推点

JAVA-扫描局域网、自定义网段IP加端口在线设备_局域网设备扫描 java-程序员宅基地

文章浏览阅读4.6k次。JAVA-扫描局域网、自定义网段IP加端口在线设备_局域网设备扫描 java

《C语言及程序设计》实践参考——M$pszi$y是嘛意思-程序员宅基地

文章浏览阅读159次。返回:贺老师课程教学链接实践要求【项目1-M$pszi$y是嘛意思?】背景:小明让同学传纸条给小丽。小丽接到会心一笑,大家却不知所云。纸条上写着M$pszi$y,两人暗中约定是,真实字符为实际字符前面的第4个!M$pszi$y是神马意思?推算一下,或从ASCII码表中查一下,自然是I love u。(1)小明请你写一个程序,在给小丽写情书时,再不..._m$是哪编程语言的语法

MBUS应用笔记/主站-程序员宅基地

文章浏览阅读422次。MBUS主站,TSS721,SSP721_mbus

基于springboot旅游网站_基于springboot的旅游网站-程序员宅基地

文章浏览阅读849次。功能模块:旅游路线、旅游景点、旅游酒店、旅游车票、旅游保险、旅游策略、订单管理、留言管理、数据分析等等。实现了在线预订、统计数据分析等功能。技术路线:springboot、springmvc、maven、layui、mybatis数据库:MySQL系统录屏:链接: https://pan.baidu.com/s/1C519vCBP66ZaoDuEHSYxfw 提取码: gxui首页:后端系统:..._基于springboot的旅游网站

qiankun(乾坤)微前端框架简介_乾坤框架-程序员宅基地

文章浏览阅读4k次,点赞10次,收藏14次。qiankun(乾坤)微前端框架简介_乾坤框架

【图神经网络】 - GNN的几个模型及论文解析(NN4G、GAT、GCN)-程序员宅基地

文章浏览阅读1.3k次,点赞19次,收藏31次。图神经网络(Graph Neural Network,GNN)是指使用神经网络来学习图结构数据,提取和发掘图结构数据中的特征和模式,满足聚类、分类、预测、分割、生成等图学习任务需求的算法总称。