# 实训课程-ECMAScript6 **Repository Path**: sailerwen/es6-webpack-master ## Basic Information - **Project Name**: 实训课程-ECMAScript6 - **Description**: ECMAScript6入门例子 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 6 - **Created**: 2019-09-22 - **Last Updated**: 2021-08-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ES6快速入门 ## 简述 [官网](http://es6-features.org/#Constants) ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。本课程主要简介ES6在实际开发当中的使用,及完成简单案例制作。 **学习资料:** [ECMAScript 6 入门](http://es6.ruanyifeng.com/) [ECMAScript 6 权威例子](http://es6-features.org/#Constants) [Babel - Learn ES2015](https://babeljs.io/docs/en/learn) [ECMAscript-2015-功能](https://babeljs.io/docs/en/learn#ecmascript-2015-features) **视频学习:** [ES6快速入门 视频](https://www.imooc.com/learn/955) [深入解读ES6系列视频(全18讲)](https://www.bilibili.com/video/av20327829?from=search&seid=4980819792315870628) **其他学习资料:** [整理最有趣的前端技术教程及文档](https://github.com/cucygh/fe-material) ----- ## 环境准备 ### 工具下载 | 名称 | 版本 | 下载 | 其他 | | ----------- | ------------ | ------------------------------------------------------------ | -------------------------------------------- | | **Node.js** | 10.16.3 | [64位](https://nodejs.org/dist/v10.16.3/node-v10.16.3-x64.msi) | 基于 Chrome V8 引擎的 JavaScript 运行环境。 | | **VS Code** | Version 1.37 | [64位](https://vscode.cdn.azure.cn/stable/f06011ac164ae4dc8e753a3fe7f9549844d15e35/VSCodeUserSetup-x64-1.37.1.exe) | Free. Built on open source. Runs everywhere. | ### 安装及设置 **Nodejs使用淘宝 NPM 镜像** 大家都知道国内直接使用 npm 的官方镜像是非常慢的,这里推荐使用淘宝 NPM 镜像。 淘宝 NPM 镜像是一个完整 npmjs.org 镜像,你可以用此代替官方版本(只读),同步频率目前为 10分钟 一次以保证尽量与官方服务同步。 你可以使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm: ``` $ npm install -g cnpm --registry=https://registry.npm.taobao.org ``` 这样就可以使用 **cnpm** 命令来安装模块了: ``` $ cnpm install [name] ``` 更多信息可以查阅:http://npm.taobao.org/。 ### 兼容性 各大浏览器的最新版本,对 ES6 的支持可以查看[kangax.github.io/compat-table/es6/](https://kangax.github.io/compat-table/es6/)。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。 **兼容性:**IE10+、Chrome、firefox、移动端、Node.js 如果不直接支持可以把ES6**编译**和**转换**为兼容的。 1. 在线转换 2. 提前编译 3. 引入browser.js:引入它的作用是使浏览器支持babel,你可以使用ES2015进行编码。 Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。 ```bash // Linux & Mac $ node --v8-options | grep harmony // Windows $ node --v8-options | findstr harmony ``` 我写了一个工具 [ES-Checker](https://github.com/ruanyf/es-checker),用来检查各种运行环境对 ES6 的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker),可以看到您的浏览器支持 ES6 的程度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。 ```bash $ cnpm install -g es-checker $ es-checkerc ========================================= Passes 39 feature Detections Your runtime supports 92% of ECMAScript 6 ========================================= ``` ## 使用webpack进行ES6开发 - 安装Node之后,让我们尝试构建我们的第一个ES6项目。 1. **下载源码** ```shell git clone https://gitee.com/sailerwen/es6-webpack-master.git ``` 2. **安装** ```shell cnpm install cnpm install webpack -g cnpm install webpack-cli --save-dev cnpm install webpack-dev-server -g ``` 3. **运行** ```shell npm start ``` ### ES6新特性在Babel下的兼容性列表 [Babel 转码器](http://es6.ruanyifeng.com/#docs/intro) [Babel](https://babeljs.io/) 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。 [ECMAscript-2015-功能](https://babeljs.io/docs/en/learn#ecmascript-2015-features) | ES6特性 | 兼容性 | | ---------------------------------------------- | -------------------- | | 箭头函数 | 支持 | | 类的声明和继承 | 部分支持,IE8不支持 | | 增强的对象文字 | 支持 | | 字符串模板 | 支持 | | 解构 | 支持,但注意使用方式 | | 参数默认值,不定参数,拓展参数 | 支持 | | let与const | 支持 | | for of | IE不支持 | | iterator, generator | 不支持 | | 模块 module、Proxies、Symbol | 不支持 | | Map,Set 和 WeakMap,WeakSet | 不支持 | | Promises、Math,Number,String,Object 的新API | 不支持 | | export & import | 支持 | | 生成器函数 | 不支持 | | 数组拷贝 | 支持 | 总结:要知道目前浏览器和转换工具并没有完全支持ES6的全部新特性,但并不影响大家对ES6的学习热情,因为这是未来的行业标准,前端开发者必须掌握的技能;而今天介绍的babel就是目前对 ES6 的支持程度较高,使用广泛的ES6转码器。 ## [ECMAScript 6简介](http://es6.ruanyifeng.com/#docs/intro) ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准(2015 年 6 月正式发布,ES6 制定的起点其实是 2000 年从开始制定到最后发布,整整用了 15 年)。 **ES6的目标**是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外实现ECMAScript标准的方言还有 JScript 和 ActionScript)。 编程语言 JavaScript 是 ECMAScript 的实现和扩展,由 ECMA(一个类似 W3C 的标准组织)参与进行标准化。ECMAScript 定义了: - [语言语法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar) – 语法解析规则、关键字、语句、声明、运算符等。 - [类型](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures) – 布尔型、数字、字符串、对象等。 - [原型和继承](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) - 内建对象和函数的[标准库](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) – [JSON ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON)、[ Math ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math)、[数组方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)、[对象自省方法](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)等。 ECMAScript 标准不定义 HTML 或 CSS 的相关功能,也不定义类似 DOM(文档对象模型)的[ Web API ](https://developer.mozilla.org/en-US/docs/Web/API),这些都在独立的标准中进行定义。ECMAScript 涵盖了各种环境中 JS 的使用场景,无论是浏览器环境还是类似[ node.js ](http://nodejs.org/)的非浏览器环境。 **ES6 将彻底改变你编写 JS 代码的方式!** ## 编程风格 [编程风格](http://es6.ruanyifeng.com/#docs/style) ## var 、let 和 const 的作用域 > 为什么需要块级作用域? | 关键字 | 声明 | 修改 | 作用域 | | --------- | ------------ | ------------------------ | -------------- | | **var** | 可以重复声明 | 无法限制修改(可以修改) | 没有块级作用域 | | **let** | 不能重复声明 | 变量--可以修改 | 块级作用域 | | **const** | 不能重复声明 | 常量--不可以修改 | 块级作用域 | 块级作用域绑定结构。`let`是新的`var`。`const`是单人作业。静态限制会阻止分配前使用。 var太自由(不严谨),尤其是在团队协同开发中编写的JS代码容易发生作用域和作用域链的问题! 故es6定制新标准:`let`和`const`关键字,解决作用域问题! ### ES6 声明变量的六种方法 ES5 只有两种声明变量的方法:`var`命令和`function`命令。ES6 除了添加`let`和`const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以,ES6 一共有 6 种声明变量的方法。 ### var变量的作用域 var关键字声明变量,无论声明在何处,都会被视为声明在函数的最顶部(不在函数内即在全局作用域的最顶部)。 ```javascript //ECMAScript 5 var test = 100;// 全局变量test Window Scoped Reference //等效于 window.test = 100; console.log(test); ``` 顶层对象,在浏览器环境指的是`window`对象,在 Node 指的是`global`对象。ES5 之中,**顶层对象的属性与全局变量是等价的。** 使用`function`声明的**全局函数**也是属于顶层对象的。 在函数中**使用** `var`声明的变量:变量的作用域是局部变量,这就意味着它们只能在它们 所定义的函数内部访问。 在函数中**不使用** `var`声明的变量:变量的作用域是全局变量(window),也就是变量可以被web 页面中任何地方的所有 JavaScript 代码(或者在本页面所包含的任何外部 JS 库中)访问。 ```javascript //ECMAScript 5 function myfun(){ var a = 100; } console.log(a);// ReferenceError: a is not defined. function myfun2(){ b = 100; } console.log(b);// 100 Window Scoped Reference ``` java的局部变量和全局变量非常严谨的,所以编写的代码非常安全!而Javascript中的var太过自由了,容易发生作用域的错误! [Javascript之闭包以及闭包实例和常见面试题](https://www.cnblogs.com/heyushuo/p/9975911.html) [闭包详细教程](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures) [深入理解JavaScript中的作用域、作用域链和闭包](https://blog.csdn.net/qappleh/article/details/80311443) ### Constants常量 常量(也称为“不变变量”),即无法重新分配新内容的变量。注意:这只会使变量本身不可变,而不是为其分配的内容(例如,如果内容是对象,则意味着对象本身仍然可以更改)。 `const`声明一个只读的常量。一旦声明,常量的值就不能改变。 ```javascript const PI =3.141593 PI > 3.0 PI = 3; // TypeError: Assignment to constant variable. ``` 上面代码表明改变常量的值会报错。 `const`声明的变量不得改变值,这意味着,`const`一旦声明变量,就必须立即初始化,不能留到以后赋值。 ```javascript const foo; // SyntaxError: Missing initializer in const declaration ``` 上面代码表示,对于`const`来说,只声明不赋值,就会报错。 `const`的作用域与`let`命令相同:只在声明所在的块级作用域内有效。 `const`的作用域与`let`命令相同:只在声明所在的块级作用域内有效。 ```javascript if (true) { const MAX = 5; } MAX // Uncaught ReferenceError: MAX is not defined ``` `const`命令声明的常量也是**不提升**,同样存在暂时性死区,只能在声明的位置后面使用。 ```javascript if (true) { console.log(MAX); // ReferenceError const MAX = 5; } ``` 上面代码在常量`MAX`声明之前就调用,结果报错。 `const`声明的常量,也与`let`一样不可重复声明。 ```javascript var message = "Hello!"; let age = 25; // 以下两行都会报错 const message = "Goodbye!"; const age = 30; ``` ### let变量-块级作用域 1.ES6 新增了`let`命令,用来声明变量。 它的用法类似于`var`,但是所声明的变量,只在`let`命令所在的代码块内有效。 ```javascript { let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 1 ``` 2.`for`循环的计数器,就很合适使用`let`命令。 ```javascript //1.计数器i只在for循环体内有效,在循环体外引用就会报错。 for (let i = 0; i < a.length; i++) { let x = a[i] … } console.log(i);// ReferenceError: i is not defined ``` ```javascript //ECMAScript 5 //2.使用var声明循环的计数器i,最后输出的是10。 var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 ``` 变量i是var命令声明的,作用域为全局变量,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。 ```javascript //3.如果使用let声明循环的计数器i,声明的变量i仅在块级作用域内有效,最后输出的是6。 var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 ``` 上面代码中,变量`i`是`let`声明的,当前的`i`只在本轮循环有效,所以每一次循环的`i`其实都是一个新的变量,所以最后输出的是`6`。你可能会问,如果每一轮循环的变量`i`都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量`i`时,就在上一轮循环的基础上进行计算。 ```javascript //4.let callbacks为全局变量==window let callbacks = [];//全局变量 for (let i = 0; i <= 2; i++) { callbacks[i] = function () { return i * 2 } } callbacks[0]() === 0 callbacks[1]() === 2 callbacks[2]() === 4 ``` 3.不存在变量(和常量)**提升**:`for`循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。 ```javascript for (let i = 0; i < 3; i++) //父作用域 { //子作用域 let i = 'abc'; console.log(i); } // abc // abc // abc ``` ### 函数-块作用域 块作用域的函数定义。下面两个**同名函数**的作用域由所在的**代码块{}**决定! ```javascript { function foo () { return 1 } foo() === 1 { function foo () { return 2 } foo() === 2 } foo() === 1 } ``` ## 箭头函数【Arrow Functions】 ### Expression Bodies 表达式语法 更富有表现力的**闭包语法**。 ES6 允许使用“箭头”(`=>`)定义函数。 ``` () => {} ``` - =>的左边:为函数的参数列表。只有一个参数可以省略(),没有参数或多个参数则不可省略()。 - =>的右边:为函数的返回值。 - 如果箭头函数直接返回一个对象,必须在对象外面加上括号()。 - 如果箭头函数体只有一行语句,且不需要返回值void,就不用写**大括号**【花括号】{}了。 ```javascript //ECMAScript 5 odds = evens.map(function (v) { return v + 1; }); //ECMAScript 6 odds = evens.map(v => v + 1) ``` 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分,函数只有一个参数可以省略圆括号。 ```javascript //ECMAScript 5 nums = evens.map(function (v, i) { return v + i; }); //ECMAScript 6 nums = evens.map((v, i) => v + i) ``` 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。 ```javascript //ECMAScript 5 pairs = evens.map(function (v) { return { even: v, odd: v + 1 }; }); //ECMAScript 6 pairs = evens.map(v => ({ even: v, odd: v + 1 })) ``` 例子中:{ even: v, odd: v + 1 }为一个对象。 ```javascript // 报错:引擎认为大括号是代码块,所以执行了一行语句id: id, name: "Temp" let getTempItem = id => { id: id, name: "Temp" }; // 不报错:所以需要圆括号括起来作为一个完整的对象返回。 let getTempItem = id => ({ id: id, name: "Temp" }); ``` ### Statement Bodies 代码块语法 更富有表现力的**闭包语法**。 箭头函数代码块声明:如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来。 ```javascript nums.forEach(v => { if (v % 5 === 0) fives.push(v) }) ``` 如果有返回则使用`return`语句返回。 ```javascript let sum = (num1, num2) => { console.log("多行代码..."); return num1 + num2; } ``` 如果箭头函数只有一行语句,且不需要返回值void,可以采用下面的写法,就不用写**大括号**{}了。 ```javascript let fn = () => void doesNotReturn(); ``` **箭头函数有几个使用注意点:** 1. 函数体内的`this`对象,就是定义时所在的对象,而不是使用时所在的对象。 2. 不可以当作构造函数,也就是说,不可以使用`new`命令,否则会抛出一个错误。 3. 不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。 4. 不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 ​ ### `this`关键字 当前对象上下文的更直观处理。 ```javascript this.nums.forEach(v => { if (v % 5 === 0) this.fives.push(v) }) ``` 函数体内的`this`对象,就是定义时所在的对象,而不是使用时所在的对象。 上面这点值得注意。`this`对象的指向是可变的(作用域传递),但是在箭头函数中,它是固定的。 ```javascript function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42 ``` 上面代码中,`setTimeout`的参数是一个箭头函数,这个箭头函数的定义生效是在`foo`函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时`this`应该指向全局对象`window`,这时应该输出`21`。但是,箭头函数导致`this`总是指向函数定义生效时所在的对象(本例是`{id: 42}`),所以输出的是`42`。 箭头函数表达式:**没有自己的 this arguments super new.target, 不能用作构造函数,没有prototype属性,不能用作生成器** 箭头函数不会创建自己的 this, 它只会**从自己作用域链的上一层继承this** 这点很重要。如下面示例,箭头函数内的this的作用域上一层即 `Person` 函数内的 `this` ``` function Person(){ this.age = 0; setInterval(() => { this.age++; // |this| 正确地指向person 对象 }, 1000); } var p = new Person(); ``` 箭头函数没有自己的 `this` 指针, `call() apply() bind()` 调用的时候, **第一个参数会被忽略**。 我们来看例子:**使用call()进行作用域传递**: ``` var adder = { base : 1, add : function(a) { var f = v => v + this.base; return f(a); }, addThruCall: function(a) { var f = v => v + this.base; var b = { base : 2 }; return f.call(b, a);// 第一个参数b被忽略 }, addTest: function(a) { var f = v => v + this.base; var b = { base : 2 }; return f.call(b, 23, a);// 第一个参数b被忽略 } }; console.log(adder.add(1)); // 输出 2 console.log(adder.addThruCall(1)); // 仍然输出 2 console.log(adder.addTest('a')); // 输出 24 (23+1) 而不是“a1” ('a'+1) ``` 很明显了,**第一个参数会被忽略** 是忽略 `call` 方法里的第一个参数。 **this不适用场合** 由于箭头函数使得`this`从“动态”变成“静态”,下面两个场合不应该使用箭头函数。 第一个场合是定义对象的方法,且该方法内部包括`this`。 ```javascript const cat = { lives: 9, jumps: () => { this.lives--; } } ``` 上面代码中,`cat.jumps()`方法是一个箭头函数,这是错误的。调用`cat.jumps()`时,如果是普通函数,该方法内部的`this`指向`cat`;如果写成上面那样的箭头函数,使得`this`指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致`jumps`箭头函数定义时的作用域就是全局作用域。 第二个场合是需要动态`this`的时候,也不应使用箭头函数。 ```javascript var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); }); ``` 上面代码运行时,点击按钮会报错,因为`button`的监听函数是一个箭头函数,导致里面的`this`就是全局对象。如果改成普通函数,`this`就会动态指向被点击的按钮对象。 ## 函数的参数扩展 ### 默认参数值[Default Parameter Values] 功能参数的简单直观的默认值。 ```javascript function f (x, y = 7, z = 42) { return x + y + z } f(1) === 50 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() // TypeError: Cannot read property 'x' of undefined ``` ### Rest 参数[Rest Parameter] rest 参数.可以向该函数传入任意数目的参数。**Rest Parameter必须是参数列表最后一个** 语法: ```javascript // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort(); ``` 下面是一个 rest 参数代替`arguments`变量的例子。 ```javascript // arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort(); ``` 将其余参数聚合为可变参数的单个参数。 ```javascript function f (x, y, ...a) { console.log('a.length:',a.length); return (x + y) * a.length } f(1, 2, "hello", true, 7) === 9 ``` ### 展开(数组)操作符[Spread Operator] 将可迭代集合的元素(如数组或什至字符串)传播到文字元素和单个函数参数中。 ```javascript var params = [ "hello", true, 7 ] var other = [ 1, 2, ...params ] // [ 1, 2, "hello", true, 7 ] function f (x, y, ...a) { return (x + y) * a.length } f(1, 2, ...params) === 9 var str = "foo" var chars = [ ...str ] // [ "f", "o", "o" ] ``` ## 模板文字[Template Literals] 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。 ### 字符串插值[String Interpolation] 语法:模板字符串中嵌入变量,需要将变量名写在`${}`之中。 ```javascript `你好 ${VariablesName}` ``` 单行和多行字符串的直观表达式插值。 ```javascript var customer = { name: "Foo" } var card = { amount: 7, product: "Bar", unitprice: 20 } var message = `Hello ${customer.name}, want to buy ${card.amount} ${card.product} for a total of ${card.amount * card.unitprice} bucks?` console.log(message) ``` 注意:请不要混淆,模板文字在ECMAScript 6语言规范的草案中最初被称为“模板字符串”. 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。 ```javascript let greeting = `\`Yo\` World!`; //`Yo` World! ``` ### 自定义插值[Custom Interpolation] 灵活的表达式插值可用于任意方法: ```javascript get`http://example.com/foo?bar=${bar + baz}&quux=${quux}` ``` ### 原始字符串访问[Raw String Access] 访问原始模板字符串的内容(不解释反斜杠)。 ```javascript function quux (strings, ...values) { console.log(strings[0]) === "foo\n" console.log(strings[1]) === "bar" console.log(values[0]) === 42 console.log(strings.raw[0]) === "foo\\n" console.log(strings.raw[1]) === "bar" } quux`foo\n${ 42 }bar` //使用String.raw不解释反斜杠 console.log(String.raw`foo\n${ 42 }bar`) === "foo\\n42bar" ``` ## 扩展文字 ### 二进制和八进制文字 直接支持安全的二进制和八进制文字。 ```javascript 0b111110111 === 503 0o767 === 503 ``` ### Unicode字符串和RegExp正则表达式文字 在字符串和正则表达式中使用Unicode的扩展支持。 ```javascript "𠮷".length === 2 "𠮷".match(/./u)[0].length === 2 "𠮷" === "\uD842\uDFB7" "𠮷" === "\u{20BB7}" "𠮷".codePointAt(0) == 0x20BB7 for (let codepoint of "𠮷") console.log(codepoint) ``` ### 正则表达式粘性匹配 保持匹配在匹配之间的位置,这样就可以有效地解析任意长输入字符串,即使使用任意数量的不同正则表达式也是如此。 ```javascript let parser = (input, match) => { for (let pos = 0, lastPos = input.length; pos < lastPos; ) { for (let i = 0; i < match.length; i++) { match[i].pattern.lastIndex = pos let found if ((found = match[i].pattern.exec(input)) !== null) { match[i].action(found) pos = match[i].pattern.lastIndex break } } } } let report = (match) => { console.log(JSON.stringify(match)) } parser("Foo 1 Bar 7 Baz 42", [ { pattern: /Foo\s+(\d+)/y, action: (match) => report(match) }, { pattern: /Bar\s+(\d+)/y, action: (match) => report(match) }, { pattern: /Baz\s+(\d+)/y, action: (match) => report(match) }, { pattern: /\s*/y, action: (match) => {} } ]) ["Foo 1","1"] ["Bar 7","7"] ["Baz 42","42"] ``` [正则表达式 – 教程](https://www.w3cschool.cn/zhengzebiaodashi/regexp-tutorial.html) [常用正则表达式汇总](https://www.cnblogs.com/cxsabc/p/10627631.html) [在线正则表达式测试](http://tool.oschina.net/regex/) ## 增强的对象属性 ### 属性简写[Property Shorthand] 通用对象属性定义典型风格的简短语法。 ```javascript var x = 0, y = 0 obj = { x, y } ``` ### 计算的属性名称[Computed Property Names] 支持对象属性定义中的计算名称。 ```javascript let obj = { foo: "bar", [ "baz" + quux() ]: 42 } ``` ### 方法属性 支持对象属性定义中声明方法,包括**正则函数**和**生成器函数**。 ```javascript obj = { foo (a, b) { … }, bar (x, y) { … }, *quux (x, y) { … } } ``` ## 解构赋值[Destructuring Assignment] 解构赋值主要分为**对象的解构**和**数组的解构**,在没有解构赋值的时候,我们赋值是这样的: ```bash let arr = [0,1,2] let a = arr[0] let b = arr[1] let c = arr[2] ``` 这样写很繁琐,那么我们有没办法既声明,又赋值,更优雅的写法呢?肯定是有的,那就是**解构赋值**,解构赋值,简单理解就是等号的左边和右边相等。 **解构赋值约定**: 1. 两边结构相同,左边就可以顺序赋予等号右边相应值。 2. 声明和赋值不可以分开。 3. 等号右边必须是**对象**和**数组对象**其一。 ### 数组的解构赋值匹配 在分配期间将**数组**直观且灵活地分解为单个变量。 ```javascript var list = [ 1, 2, 3 ] var [ a, , b ] = list [ b, a ] = [ a, b ] ``` ### 对象的解构赋值匹配,简化符号 在分配期间将**对象**直观且灵活地分解为单个变量。 ```javascript var { op, lhs,rhs } = getASTNode() ``` ### 对象的解构赋值匹配,深度匹配 在分配期间将**对象**直观且灵活地分解为单个变量。 ```javascript var { op: a, lhs: { op: b }, rhs: c } = getASTNode() ``` ### 对象和数组匹配,默认值 用于解构对象和数组的简单直观的默认值。 ```javascript var obj = { a: 1 } var list = [ 1 ] var { a, b = 2 } = obj //b 的默认值为 2 var [ x, y = 2 ] = list //y 的默认值为 2 ``` ### 参数上下文匹配 在函数调用期间将数组和对象直观且灵活地分解为单个参数。 ```javascript function f ([ name, val ]) { console.log(name, val) } function g ({ name: n, val: v }) { console.log(n, v) } function h ({ name, val }) { console.log(name, val) } f([ "bar", 42 ]) //以 数组 的方式传参数自动分解匹配 g({ name: "foo", val: 7 }) //以 复杂对象 的方式传参数自动分解匹配 h({ name: "bar", val: 42 }) //以 简单对象 的方式传参数自动分解匹配 ``` ### 自动解构的故障 自动解构的故障(数据并非一一对应时),可以选择使用默认设置。 ```javascript var list = [ 7, 42 ] var [ a = 1, b = 2, c = 3, d ] = list //输出 a === 7 b === 42 c === 3 d === undefined ``` 很多时候,数据并非一一对应的,并且往往我们希望得到一个默认值。 在解构赋值的过程中,c =undefined时,会使用默认值。 那么当c=null时呢?当c=null时,那么c就不会使用默认值,而是使用null。 ## 模块[Modules] ### 导出和导入值[Value Export/Import] 支持从模块导出exporting和导入importing值,而不会造成全局**命名空间**namespace 污染。 ```javascript // lib/math.js export function sum (x, y) { return x + y } export function sqrt (x) { return x * x } export var pi = 3.141593 // someApp.js import * as math from "lib/math" console.log("2π = " + math.sum(math.pi, math.pi)) // otherApp.js import { sum, pi } from "lib/math" console.log("2π = " + sum(pi, pi))get`http://example.com/foo?bar=${bar + baz}&quux=${quux}` ``` 模块功能主要由两个命令构成:`export`和`import`。 使用`export`命令定义了模块的对外接口以后,其他 JS 文件就可以通过`import`命令加载这个模块。 `export`命令用于规定(声明)模块的对外接口。 `import`命令用于输入其他模块提供的功能。配合`from`指定JS模块文件的位置。(模块文件`.js`后缀可以省略) 注意,`import`命令具有提升效果,会提升到整个模块的头部,首先执行。 | 语法糖 | 解析 | 性能 | | ---------------------------------- | ------------------------------------------------------------ | ------------------------------ | | import * as math from "lib/math" | 从路径lib/math.js文件中导入所有的接口,并使用(* as math)引用名。 | 整体加载 | | import { sum, pi } from "lib/math" | 从路径lib/math.js文件中导入{ sum, pi }两个接口直接使用。 | 逐一指定加载 | 注意:从性能、打包代码等方面考虑更加推荐第二种方式导入接口。 最后,`import`语句会执行所加载的模块,因此可以有下面的写法。 ```javascript import 'lodash'; ``` 上面代码仅仅执行`lodash`模块,但是不输入任何值。 如果多次重复执行同一句`import`语句,那么只会执行一次,而不会执行多次。 ```javascript import 'lodash'; import 'lodash'; ``` 上面代码加载了两次`lodash`,但是只会执行一次。 ```javascript import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; ``` 上面代码中,虽然`foo`和`bar`在两个语句中加载,但是它们对应的是同一个`my_module`实例。也就是说,`import`语句是 Singleton 模式。 目前阶段,通过 Babel 转码,CommonJS 模块的`require`命令和 ES6 模块的`import`命令,可以写在同一个模块里面,但是最好不要这样做。因为`import`在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。 ```javascript require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React'; ``` 注意,模块整体加载所在的那个对象(`circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。 ```javascript import * as circle from './circle'; // 下面两行都是不允许的 circle.foo = 'hello'; circle.area = function () {}; ``` ## export default 命令通配符 将值标记为默认导出值和值的质量混合 ```javascript // lib/mathplusplus.js export * from "lib/math" //pi export var e = 2.71828182846 //e export default (x) => Math.exp(x) //exp // someApp.js import exp, { pi, e } from "lib/mathplusplus" console.log("e^{π} = " + exp(pi)) ``` 从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到`export default`命令,为模块指定默认输出。 ## 类[Classes] ### 类定义 JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。 ```javascript function Shape(id, x, y) { this.id = id; this.x = x; this.y = y; } Shape.prototype.toString = function () { return '(' + this.id + ', '+ this.x + ', ' + this.y + ')'; }; var shape = new Shape(999, 1, 2); ``` 上面这种写法跟传统let的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过`class`关键字,可以定义类。 基本上,ES6 的`class`可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的`class`写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的`class`改写,就是下面这样。 更直观,OOP风格和无模板的类: ```javascript //形状类 class Shape { constructor (id, x, y) { this.id = id this.move(x, y) } move (x, y) { this.x = x this.y = y } } let p = new Shape(1, 2); ``` `Shape`类除了构造方法,还定义了一个`move`方法。注意,定义“类”的方法的时候,前面不需要加上`function`这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。 ### 类继承 更直观,OOP风格和无模板继承。 ```javascript //矩形类:宽和高 class Rectangle extends Shape { constructor (id, x, y, width, height) { super(id, x, y) this.width = width this.height = height } } //圆形类:半径 class Circle extends Shape { constructor (id, x, y, radius) { super(id, x, y) this.radius = radius } } ``` ### Class 表达式 与函数一样,类也可以使用表达式的形式定义。 ```javascript const MyClass = class Me { getClassName() { return Me.name; } }; ``` 上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是`Me`,但是`Me`只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用`MyClass`引用。 ```javascript let inst = new MyClass(); inst.getClassName() // Me Me.name // ReferenceError: Me is not defined ``` 上面代码表示,`Me`只在 Class 内部有定义。 如果类的内部没用到的话,可以省略`Me`,也就是可以写成下面的形式。 ```javascript const MyClass = class { /* ... */ }; ``` 采用 Class 表达式,可以写出立即执行的 Class。 ```javascript let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('张三'); person.sayName(); // "张三" ``` 上面代码中,`person`是一个立即执行的类的实例。 ### 从表达式继承类 类的属性名,可以采用表达式。 通过从产生函数对象的表达式扩展来支持混合类型继承。 [注意:当然,一般的**聚合函数**功能通常是由像[这样的库](https://github.com/rse/aggregation)提供的. ```javascript var aggregation = (baseClass, ...mixins) => { let base = class _Combined extends baseClass { constructor (...args) { super(...args) mixins.forEach((mixin) => { mixin.prototype.initializer.call(this) }) } } let copyProps = (target, source) => { Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .forEach((prop) => { if (prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/)) return Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop)) }) } mixins.forEach((mixin) => { copyProps(base.prototype, mixin.prototype) copyProps(base, mixin) }) return base } class Colored { initializer () { this._color = "white" } get color () { return this._color } set color (v) { this._color = v } } class ZCoord { initializer () { this._z = 0 } get z () { return this._z } set z (v) { this._z = v } } class Shape { constructor (x, y) { this._x = x; this._y = y } get x () { return this._x } set x (v) { this._x = v } get y () { return this._y } set y (v) { this._y = v } } class Rectangle extends aggregation(Shape, Colored, ZCoord) {} var rect = new Rectangle(7, 42) rect.z = 1000 rect.color = "red" console.log(rect.x, rect.y, rect.z, rect.color) ``` ### 访问基类[Base Class Access] 通过super关键字访问父类(基类)的方法和构造函数。 ```javascript class Shape { … toString () { return `Shape(${this.id})` } } class Rectangle extends Shape { constructor (id, x, y, width, height) { super(id, x, y) … } toString () { return "Rectangle > " + super.toString() } } class Circle extends Shape { constructor (id, x, y, radius) { super(id, x, y) … } toString () { return "Circle > " + super.toString() } } ``` ### 静态成员 对静态类成员的简单支持。static成员,l所有的实例共享,类名直接打点访问。(代码重用,编写工具类方法) ```javascript class Rectangle extends Shape { … static defaultRectangle () { return new Rectangle("default", 0, 0, 100, 100) } } class Circle extends Shape { … static defaultCircle () { return new Circle("default", 0, 0, 100) } } var defRectangle = Rectangle.defaultRectangle() var defCircle = Circle.defaultCircle() ``` 静态属性指的是 Class 本身的属性,即`Class.propName`,而不是定义在实例对象(`this`)上的属性。 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个[提案](https://github.com/tc39/proposal-class-fields)提供了类的静态属性,写法是在实例属性的前面,加上`static`关键字。 ```javascript class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 } } ``` ### Getter /Setter Getter / Setter也直接在类中(而不仅仅是对象初始化器中,因为自ECMAScript 5.1起,这是可能的)。 ```javascript class Rectangle { constructor (width, height) { this._width = width this._height = height } set width (width) { this._width = width } get width () { return this._width } set height (height) { this._height = height } get height () { return this._height } get area () { return this._width * this._height } } var r = new Rectangle(50, 20) r.area === 1000 r.width = 100 // setter width: 100 r.width // getter width: 100 ``` 与 ES5 一样,在“类”的内部可以使用`get`和`set`关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 ## Symbol 数据类型 ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入`Symbol`的原因。 ES6 引入了一种新的原始数据类型`Symbol`,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:`undefined`、`null`、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。 Symbol 值通过`Symbol()`函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 ----- ### Symbol 类型 唯一且不可变的数据类型用作对象属性的标识符。符号可以具有可选的描述,但仅用于调试目的。 ```javascript let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 == s2 // false Symbol("foo") !== Symbol("foo") //Symbol("foo")是独一无二的值,所以不会相等 const foo = Symbol() const bar = Symbol() typeof foo === "symbol" typeof bar === "symbol" let obj = {} obj[foo] = "foo" obj[bar] = "bar" //属性名的遍历 JSON.stringify(obj) // {} Object.keys(obj) // [] Object.getOwnPropertyNames(obj) // [] Object.getOwnPropertySymbols(obj) // [ foo, bar ] ``` 常量量foo就是一个独一无二的值。`Symbol()`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,**Symbol 它是一种类似于字符串的数据类型。** typeof运算符用于查看变量的数据类型,表明变量foo是 Symbol 数据类型,而不是字符串之类的其他类型。 **属性名的遍历** Symbol 作为属性名,该属性不会出现在`for...in`、`for...of`循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`、`JSON.stringify()`返回。但是,它也不是私有属性,有一个`Object.getOwnPropertySymbols`方法,可以获取指定对象的所有 Symbol 属性名。 ---- ### 全局Symbol 类型 Symbol.for('key')通过唯一键索引的全局符号。 ```javascript let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); s1 === s2 // true Symbol.for("app.foo") === Symbol.for("app.foo") const foo = Symbol.for("app.foo") const bar = Symbol.for("app.bar") Symbol.keyFor(foo) === "app.foo" Symbol.keyFor(bar) === "app.bar" typeof foo === "symbol" typeof bar === "symbol" let obj = {} obj[foo] = "foo" obj[bar] = "bar" JSON.stringify(obj) // {} Object.keys(obj) // [] Object.getOwnPropertyNames(obj) // [] Object.getOwnPropertySymbols(obj) // [ foo, bar ] ``` 有时,我们希望重新使用同一个 Symbol 值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。 ```csharp let a = Symbol.for('my'); let obj = { [a]:'qiang', age:18 } ``` - 使用let of遍历该对象是拿不到Symbol属性的。 ```jsx for(let [key,value] of Object.entries(obj)){ console.log(key,value); } //age 18 ``` - 使用Object.getOwnPropertySymbol(obj)可得到一个包含该对象所有Symbol类型属性的数组 ```jsx Object.getOwnPropertySymbols(obj).forEach(function(item){ console.log(item,obj[item]); }) //Symbol('my') qiang ``` - 使用Reflect.ownKeys(obj)可得到一个包含该对象所有属性的数组 ```jsx Reflect.ownKeys(obj).forEach(function(item){ console.log(item,obj[item]); }) //age 18 //Symbol('my') qiang ``` ## 迭代器[Iterators]和For-Of运算符 ​ 支持“可迭代”协议,以允许对象自定义其迭代行为。此外,支持“迭代器”协议以生成值序列(有限或无限)。最后,提供方便的`of`运算符来迭代可迭代对象的所有值。 JavaScript 原有的表示“集合”的数据结构,主要是数组(`Array`)和对象(`Object`),ES6 又添加了`Map`和`Set`。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是`Map`,`Map`的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator 的作用有三个: 1. 为各种数据结构,提供一个统一的、简便的访问接口; 2. 使得数据结构的成员能够按某种次序排列; 3. ES6 创造了一种新的遍历命令`for...of`循环,Iterator 接口主要供`for...of`消费。 Iterator 的遍历过程是这样的。 (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 (2)第一次调用指针对象的`next`方法,可以将指针指向数据结构的第一个成员。 (3)第二次调用指针对象的`next`方法,指针就指向数据结构的第二个成员。 (4)不断调用指针对象的`next`方法,直到它指向数据结构的结束位置。 每一次调用`next`方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含`value`和`done`两个属性的对象。其中,`value`属性是当前成员的值,`done`属性是一个布尔值,表示遍历是否结束。 ```javascript let fibonacci = { [Symbol.iterator]() { let pre = 0, cur = 1 return { next () { [ pre, cur ] = [ cur, pre + cur ] return { done: false, value: cur } } } } } for (let n of fibonacci) { if (n > 1000) break console.log(n) } 1 2 3 5 8 ... 987 ``` 上面代码中,对象`fibonacci`是可遍历的(iterable),因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value`和`done`两个属性。 ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。 原生具备 Iterator 接口的数据结构如下。 - Array - Map - Set - String - TypedArray - 函数的 arguments 对象 - NodeList 对象 下面的例子是数组的`Symbol.iterator`属性。 ```javascript let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true } ``` 上面代码中,变量`arr`是一个数组,原生就具有遍历器接口,部署在`arr`的`Symbol.iterator`属性上面。所以,调用这个属性,就得到遍历器对象。 对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。 ## 生成器[Generators] ### 基本概念 [生成器[Generators]](http://es6.ruanyifeng.com/#docs/generator) Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。 Generator 函数有多种理解角度: 1. 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 2. 形式上,Generator 函数是一个普通函数,但是有两个特征。 一是,`function`关键字与函数名之间有一个星号; 二是,函数体内部使用`yield`表达式,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。 ```javascript function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); ``` 上面代码定义了一个 Generator 函数`helloWorldGenerator`,它内部有两个`yield`表达式(`hello`和`world`),即该函数有三个状态:hello,world 和 return 语句(结束执行)。 然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。 下一步,必须调用遍历器对象的`next`方法,使得指针移向下一个状态。也就是说,每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`表达式(或`return`语句)为止。换言之,Generator 函数是分段执行的,`yield`表达式是暂停执行的标记,而`next`方法可以恢复执行。 ```javascript 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`方法。 第一次调用,Generator 函数开始执行,直到遇到第一个`yield`表达式为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`表达式的值`hello`,`done`属性的值`false`,表示遍历还没有结束。 第二次调用,Generator 函数从上次`yield`表达式停下的地方,一直执行到下一个`yield`表达式。`next`方法返回的对象的`value`属性就是当前`yield`表达式的值`world`,`done`属性的值`false`,表示遍历还没有结束。 第三次调用,Generator 函数从上次`yield`表达式停下的地方,一直执行到`return`语句(如果没有`return`语句,就执行到函数结束)。`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为`undefined`),`done`属性的值`true`,表示遍历已经结束。 第四次调用,此时 Generator 函数已经运行完毕,`next`方法返回对象的`value`属性为`undefined`,`done`属性为`true`。以后再调用`next`方法,返回的都是这个值。 总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的`next`方法,就会返回一个有着`value`和`done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`表达式后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。 ES6 没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。 ```javascript function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· } ``` 由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在`function`关键字后面。本书也采用这种写法。 ### yield 表达式 由于 Generator 函数返回的遍历器对象,只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`表达式就是暂停标志。 遍历器对象的`next`方法的运行逻辑如下。 (1)遇到`yield`表达式,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。 (2)下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`表达式。 (3)如果没有再遇到新的`yield`表达式,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。 (4)如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined`。 需要注意的是,`yield`表达式后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。 ```javascript function* gen() { yield 123 + 456; } ``` 上面代码中,`yield`后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。 `yield`表达式与`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`表达式。正常函数只能返回一个值,因为只能执行一次`return`;Generator 函数可以返回一系列的值,因为可以有任意多个`yield`。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。 Generator 函数可以不用`yield`表达式,这时就变成了一个单纯的暂缓执行函数。 ```javascript function* f() { console.log('执行了!') } var generator = f(); setTimeout(function () { generator.next() }, 2000); ``` 上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。 另外需要注意,`yield`表达式只能用在 Generator 函数里面,用在其他地方都会报错。 ```javascript (function (){ yield 1; })() // SyntaxError: Unexpected number ``` 上面代码在一个普通函数中使用`yield`表达式,结果产生一个句法错误。 下面是另外一个例子。 ```javascript var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { a.forEach(function (item) { if (typeof item !== 'number') { yield* flat(item); } else { yield item; } }); }; for (var f of flat(arr)){ console.log(f); } ``` 上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`表达式(这个函数里面还使用了`yield*`表达式,详细介绍见后文)。一种修改方法是改用`for`循环。 ```javascript var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a) { var length = a.length; for (var i = 0; i < length; i++) { var item = a[i]; if (typeof item !== 'number') { yield* flat(item); } else { yield item; } } }; for (var f of flat(arr)) { console.log(f); } // 1, 2, 3, 4, 5, 6 ``` 另外,`yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。 ```javascript function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK } ``` `yield`表达式用作函数参数或放在赋值表达式的右边,可以不加括号。 ```javascript function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK } ``` ### 生成器函数和迭代协议[Generator Function, Iterator Protocol] 支持生成器,这是迭代器的一种特殊情况,它包含生成器功能,可以在其中暂停和恢复控制流,以生成值序列(有限或无限)。 1. `function`关键字与函数名之间有一个星号; 2. 函数体内部使用`yield`表达式,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。 ```javascript let fibonacci = { *[Symbol.iterator]() { let pre = 0, cur = 1 for (;;) { [ pre, cur ] = [ cur, pre + cur ] yield cur } } } for (let n of fibonacci) { if (n > 1000) break console.log(n) } ``` ### 直接使用生成器函数 Generator Function, Direct Use 支持生成器功能,这是函数的一种特殊变体,可以在其中暂停和恢复控制流,以生成值序列(有限或无限)。 ```javascript function* range (start, end, step) { while (start < end) { yield start start += step } } for (let i of range(0, 10, 2)) { console.log(i) // 0, 2, 4, 6, 8 } ``` ### 生成器匹配 **Generator Matching** 支持生成器功能,即可以暂停和恢复控制流的功能,以便生成和扩展值序列(有限或无限)。 ```javascript let fibonacci = function* (numbers) { let pre = 0, cur = 1 while (numbers-- > 0) { [ pre, cur ] = [ cur, pre + cur ] yield cur } } for (let n of fibonacci(1000)) console.log(n) let numbers = [ ...fibonacci(1000) ] let [ n1, n2, n3, ...others ] = fibonacci(1000) ``` ### 生成器控制流 **Generator Control-Flow** 支持生成器,这是Iterator的一种特殊情况,可以暂停和恢复控制流,以支持“协程序”风格与Promises结合使用的异步编程(请参见下文)。[注意:一般`async`功能通常由可重用的库提供,此处提供此功能只是为了更好地理解。请参阅 [co](https://github.com/tj/co)或[Bluebird](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisecoroutinegeneratorfunction-generatorfunction---function)的`coroutine`实践。] ```javascript // generic asynchronous control-flow driver 通用异步控制流驱动程序 function async (proc, ...params) { let iterator = proc(...params) return new Promise((resolve, reject) => { let loop = (value) => { let result try { result = iterator.next(value) } catch (err) { reject(err) } if (result.done) resolve(result.value) else if ( typeof result.value === "object" && typeof result.value.then === "function") result.value.then((value) => { loop(value) }, (err) => { reject(err) }) else loop(result.value) } loop() }) } // application-specific asynchronous builder 特殊应用程序:异步生成器 function makeAsync (text, after) { return new Promise((resolve, reject) => { setTimeout(() => resolve(text), after) }) } // application-specific asynchronous procedure 特殊应用程序:异步过程 async(function* (greeting) { let foo = yield makeAsync("foo", 300) let bar = yield makeAsync("bar", 200) let baz = yield makeAsync("baz", 100) return `${greeting} ${foo} ${bar} ${baz}` }, "Hello").then((msg) => { console.log("RESULT:", msg) // "Hello foo bar baz" }) ``` 需要课外扩展:[Generator 函数的异步应用](http://es6.ruanyifeng.com/#docs/generator-async) ## Promises ### [Promise 对象](http://es6.ruanyifeng.com/#docs/promise) Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了`Promise`对象。 所谓`Promise`,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 `Promise`对象有以下两个特点。 (1)对象的状态不受外界影响。`Promise`对象代表一个异步操作,有三种状态:`pending`(进行中)、`fulfilled`(已成功)和`rejected`(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。`Promise`对象的状态改变,只有两种可能:从`pending`变为`fulfilled`和从`pending`变为`rejected`。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对`Promise`对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 注意,为了行文方便,本章后面的`resolved`统一只指`fulfilled`状态,不包含`rejected`状态。 有了`Promise`对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,`Promise`对象提供统一的接口,使得控制异步操作更加容易。 `Promise`也有一些缺点。首先,无法取消`Promise`,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,`Promise`内部抛出的错误,不会反应到外部。第三,当处于`pending`状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 如果某些事件不断地反复发生,一般来说,使用 [Stream](https://nodejs.org/api/stream.html) 模式是比部署`Promise`更好的选择。 ### Promises承诺用法 可以异步生成并在将来可用的值的一流表示。 ```javascript function msgAfterTimeout (msg, who, timeout) { return new Promise((resolve, reject) => { setTimeout(() => resolve(`${msg} Hello ${who}!`), timeout) }) } msgAfterTimeout("", "Foo", 100).then((msg) => msgAfterTimeout(msg, "Bar", 200) ).then((msg) => { console.log(`done after 300ms:${msg}`) }) ``` ### Promises承诺组合 将一个或多个promise合并为新promise,而不必自己照顾基础异步操作的顺序。 ```javascript function fetchAsync (url, timeout, onData, onError) { … } let fetchPromised = (url, timeout) => { return new Promise((resolve, reject) => { fetchAsync(url, timeout, resolve, reject) }) } Promise.all([ fetchPromised("http://backend/foo.txt", 500), fetchPromised("http://backend/bar.txt", 500), fetchPromised("http://backend/baz.txt", 500) ]).then((data) => { let [ foo, bar, baz ] = data console.log(`success: foo=${foo} bar=${bar} baz=${baz}`) }, (err) => { console.log(`error: ${err}`) }) ``` ## Meta编程 ### Proxying代理 涉及运行时级别的对象元操作。 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 ```javascript var obj = new Proxy({}, { get: function (target, key, receiver) { console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function (target, key, value, receiver) { console.log(`setting ${key}!`); return Reflect.set(target, key, value, receiver); } }); ``` 上面代码对一个空对象架设了一层拦截,重定义了属性的读取(`get`)和设置(`set`)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象`obj`,去读写它的属性,就会得到下面的结果。 ```javascript obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2 ``` 上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。 ```javascript var proxy = new Proxy(target, handler); ``` Proxy 对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个`Proxy`实例,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。 下面是另一个拦截读取属性行为的例子。 ```javascript 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`对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。 如果`handler`没有设置任何拦截,那就等同于直接通向原对象。 ```javascript var target = {}; var handler = {}; var proxy = new Proxy(target, handler); proxy.a = 'b'; target.a // "b" ``` 上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`。 一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。 ```javascript var object = { proxy: new Proxy(target, handler) }; ``` Proxy 实例也可以作为其他对象的原型对象。 ```javascript var proxy = new Proxy({}, { get: function(target, property) { return 35; } }); let obj = Object.create(proxy); obj.time // 35 ``` 上面代码中,`proxy`对象是`obj`对象的原型,`obj`对象本身并没有`time`属性,所以根据原型链,会在`proxy`对象上读取该属性,导致被拦截。 同一个拦截器函数,可以设置拦截多个操作。 ```javascript var handler = { get: function(target, name) { if (name === 'prototype') { return Object.prototype; } return 'Hello, ' + name; }, apply: function(target, thisBinding, args) { return args[0]; }, construct: function(target, args) { return {value: args[1]}; } }; var fproxy = new Proxy(function(x, y) { return x + y; }, handler); fproxy(1, 2) // 1 new fproxy(1, 2) // {value: 2} fproxy.prototype === Object.prototype // true fproxy.foo === "Hello, foo" // true ``` 对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。 下面是 Proxy 支持的拦截操作一览,一共 13 种。 - **get(target, propKey, receiver)**:拦截对象属性的读取,比如`proxy.foo`和`proxy['foo']`。 - **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。 - **has(target, propKey)**:拦截`propKey in proxy`的操作,返回一个布尔值。 - **deleteProperty(target, propKey)**:拦截`delete proxy[propKey]`的操作,返回一个布尔值。 - **ownKeys(target)**:拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`、`for...in`循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而`Object.keys()`的返回结果仅包括目标对象自身的可遍历属性。 - **getOwnPropertyDescriptor(target, propKey)**:拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。 - **defineProperty(target, propKey, propDesc)**:拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。 - **preventExtensions(target)**:拦截`Object.preventExtensions(proxy)`,返回一个布尔值。 - **getPrototypeOf(target)**:拦截`Object.getPrototypeOf(proxy)`,返回一个对象。 - **isExtensible(target)**:拦截`Object.isExtensible(proxy)`,返回一个布尔值。 - **setPrototypeOf(target, proto)**:拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。 - **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。 #### [代理更多详讲](http://es6.ruanyifeng.com/#docs/proxy) ### Reflection反射 `Reflect`对象与`Proxy`对象一样,也是 ES6 为了操作对象而提供的新 API。 `Reflect`对象的设计目的: (1) 将`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。现阶段,某些方法同时在`Object`和`Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。也就是说,从`Reflect`对象上可以拿到语言内部的方法。 (2) 修改某些`Object`方法的返回结果,让其变得更合理。比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`。 ```javascript // 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure } ``` (3) 让`Object`操作都变成函数行为。某些`Object`操作是命令式,比如`name in obj`和`delete obj[name]`,而`Reflect.has(obj, name)`和`Reflect.deleteProperty(obj, name)`让它们变成了函数行为。 ```javascript // 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true ``` (4)`Reflect`对象的方法与`Proxy`对象的方法一一对应,只要是`Proxy`对象的方法,就能在`Reflect`对象上找到对应的方法。这就让`Proxy`对象可以方便地调用对应的`Reflect`方法,完成默认行为,作为修改行为的基础。也就是说,不管`Proxy`怎么修改默认行为,你总可以在`Reflect`上获取默认行为。 ```javascript Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } }); ``` 上面代码中,`Proxy`方法拦截`target`对象的属性赋值行为。它采用`Reflect.set`方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。 下面是另一个例子。 ```javascript var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); } }); ``` 上面代码中,每一个`Proxy`对象的拦截操作(`get`、`delete`、`has`),内部都调用对应的`Reflect`方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。 有了`Reflect`对象以后,很多操作会更易读。 ```javascript // 老写法 Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 新写法 Reflect.apply(Math.floor, undefined, [1.75]) // 1 ``` ## Map/Set & WeakMap/WeakSet [Set 和 Map 数据结构概述](http://es6.ruanyifeng.com/#docs/set-map) 参考代码: [**Set Data-Structure**](http://es6-features.org/#SetDataStructure) [**Map Data-Structure**](http://es6-features.org/#MapDataStructure) [**Weak-Link Data-Structures**](http://es6-features.org/#WeakLinkDataStructures) ## Arrays类型 [数组的扩展](http://es6.ruanyifeng.com/#docs/array) [**数组类型 Typed Arrays**](http://es6-features.org/#TypedArrays) [ArrayBuffer对象](http://es6.ruanyifeng.com/#docs/arraybuffer) ## 新的内置方法 [**Object 对象属性分配**](http://es6-features.org/#ObjectPropertyAssignment) [**Array 数组元素查找**](http://es6-features.org/#ArrayElementFinding) [**string字符串重复**](http://es6-features.org/#StringRepeating) [**string字符串搜索**](http://es6-features.org/#StringSearching) [**Number类型检查**](http://es6-features.org/#NumberTypeChecking) [**Number安全检查**](http://es6-features.org/#NumberSafetyChecking) [**Number比较**](http://es6-features.org/#NumberComparison) [**Number数字截断**](http://es6-features.org/#NumberTruncation) [**Number 符号确定**](http://es6-features.org/#NumberSignDetermination) [**Math-对象的扩展**](http://es6.ruanyifeng.com/#docs/number#Math-对象的扩展) ## 国际化与本地化 [**排序方式Collation**](http://es6-features.org/#Collation) [**数字格式化 Number Formatting**](http://es6-features.org/#NumberFormatting) [**货币格式化Currency Formatting**](http://es6-features.org/#CurrencyFormatting) [**日期时间格式化 Date/Time Formatting**](http://es6-features.org/#DateTimeFormatting)