- 快召唤伙伴们来围观吧
- 微博 QQ QQ空间 贴吧
- 文档嵌入链接
- 复制
- 微信扫一扫分享
- 已成功复制到剪贴板
es6标准入门
展开查看详情
1 .2017/11/20 ECMAScript 6 入门 - ECMAScript 6入门 本书覆盖 ES6 与上一个版本 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。 本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。 全书已由电子工业出版社出版,2017年9月推出了第三版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。 感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自 己的书籍。下面是第三版的购买地址。 - 淘宝 - 京东 - 当当 - 亚马逊 - China-pub 版权许可 本书采用“保持署名—非商用”创意共享4.0许可证。 只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 详细的法律条文请参见创意共享网站。 留言 下一章 http://es6.ruanyifeng.com/#undefined 2/2
2 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 ECMAScript 6 入门 作者:阮一峰 授权:署名-非商用许可证 目录 0.前言 1.ECMAScript 6简介 2.let 和 const 命令 3.变量的解构赋值 4.字符串的扩展 5.正则的扩展 6.数值的扩展 7.函数的扩展 8.数组的扩展 9.对象的扩展 10.Symbol 11.Set 和 Map 数据结构 12.Proxy 13.Reflect 14.Promise 对象 15.Iterator 和 for...of 循环 16.Generator 函数的语法 17.Generator 函数的异步应用 18.async 函数 19.Class 的基本语法 20.Class 的继承 21.Decorator 22.Module 的语法 23.Module 的加载实现 24.编程风格 25.读懂规格 26.ArrayBuffer 27.参考链接 其他 - 源码 - 修订历史 - 反馈意见 ECMAScript 6 简介 1.ECMAScript 和 JavaScript 的关系 2.ES6 与 ECMAScript 2015 的关系 3.语法提案的批准流程 4.ECMAScript 的历史 5.部署进度 6.Babel 转码器 7.Traceur 转码器 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来 编写复杂的大型应用程序,成为企业级开发语言。 1. ECMAScript 和 JavaScript 的关系 一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系? 要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织 ECMA,希望这 种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 上一章 下一章 ECMAScript,这个版本就是 1.0 版。 http://es6.ruanyifeng.com/#docs/intro 1/11
3 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只 有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。 因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。 日常场合,这两个词是可以互换的。 2. ES6 与 ECMAScript 2015 的关系 ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢? 2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。 但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包 括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。 但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个 月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版 本升级成为了一个不断滚动的流程,每个月都会有变动。 标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份, 草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。 ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的 《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的 includes 方法 和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。 因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。 3. 语法提案的批准流程 任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。 一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。 - Stage 0 - Strawman(展示阶段) - Stage 1 - Proposal(征求意见阶段) - Stage 2 - Draft(草案阶段) - Stage 3 - Candidate(候选人阶段) - Stage 4 - Finished(定案阶段) 一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站 Github.com/tc39/ecma262查看。 本书的写作目标之一,是跟踪 ECMAScript 语言的最新进展,介绍 5.1 版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将 予以介绍。 4. ECMAScript 的历史 ES6 从开始制定到最后发布,整整用了 15 年。 前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一 开始学习 JavaScript,其实就是在学 3.0 版的语法。 2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。 为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会 (Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。 2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、 上一章 下一章 Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持 http://es6.ruanyifeng.com/#docs/intro 2/11
4 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 当前的草案。 2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现 有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。 2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本 保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的 判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。 2011 年 6 月,ECMAscript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。 2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。 2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。 2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。 5. 部署进度 各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/es5-compat-table/es6/。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。 Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没 有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。 $ node --v8-options | grep harmony 上面命令的输出结果,会因为版本的不同而有所不同。 我写了一个工具 ES-Checker,用来检查各种运行环境对 ES6 的支持情况。访问ruanyf.github.io/es-checker,可以看到您的浏览器支持 ES6 的程 度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。 $ npm install -g es-checker $ es-checker ========================================= Passes 24 feature Dectations Your runtime supports 57% of ECMAScript 6 ========================================= 6. Babel 转码器 Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心 现有环境是否支持。下面是一个例子。 // 转码前 input.map(item => item + 1); // 转码后 input.map(function (item) { return item + 1; }); 上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。 配置文件 .babelrc Babel 的配置文件是 .babelrc ,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。 该文件用来设置转码规则和插件,基本格式如下。 { "presets": [], 上一章 下一章 http://es6.ruanyifeng.com/#docs/intro 3/11
5 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 "plugins": [] } presets 字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。 # 最新转码规则 $ npm install --save-dev babel-preset-latest # react 转码规则 $ npm install --save-dev babel-preset-react # 不同阶段语法提案的转码规则(共有4个阶段),选装一个 $ npm install --save-dev babel-preset-stage-0 $ npm install --save-dev babel-preset-stage-1 $ npm install --save-dev babel-preset-stage-2 $ npm install --save-dev babel-preset-stage-3 然后,将这些规则加入 .babelrc 。 { "presets": [ "latest", "react", "stage-2" ], "plugins": [] } 注意,以下所有 Babel 工具和模块的使用,都必须先写好 .babelrc 。 命令行转码 babel-cli Babel 提供 babel-cli 工具,用于命令行转码。 它的安装命令如下。 $ npm install --global babel-cli 基本用法如下。 # 转码结果输出到标准输出 $ babel example.js # 转码结果写入一个文件 # --out-file 或 -o 参数指定输出文件 $ babel example.js --out-file compiled.js # 或者 $ babel example.js -o compiled.js # 整个目录转码 # --out-dir 或 -d 参数指定输出目录 $ babel src --out-dir lib # 或者 $ babel src -d lib # -s 参数生成source map文件 $ babel src -d lib -s 上面代码是在全局环境下,进行 Babel 转码。这意味着,如果项目要运行,全局环境必须有 Babel,也就是说项目产生了对环境的依赖。另一方面,这样 做也无法支持不同项目使用不同版本的 Babel。 一个解决办法是将 babel-cli 安装在项目之中。 # 安装 $ npm install --save-dev babel-cli 然后,改写 package.json 。 { // ... "devDependencies": { "babel-cli": "^6.0.0" }, "scripts": { 上一章 下一章 http://es6.ruanyifeng.com/#docs/intro 4/11
6 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 "build": "babel src -d lib" }, } 转码的时候,就执行下面的命令。 $ npm run build babel-node babel-cli 工具自带一个 babel-node 命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码。 它不用单独安装,而是随 babel-cli 一起安装。然后,执行 babel-node 就进入 REPL 环境。 $ babel-node > (x => x * 2)(1) 2 babel-node 命令可以直接运行 ES6 脚本。将上面的代码放入脚本文件 es6.js ,然后直接运行。 $ babel-node es6.js 2 babel-node 也可以安装在项目中。 $ npm install --save-dev babel-cli 然后,改写 package.json 。 { "scripts": { "script-name": "babel-node script.js" } } 上面代码中,使用 babel-node 替代 node ,这样 script.js 本身就不用做任何转码处理。 babel-register babel-register 模块改写 require 命令,为它加上一个钩子。此后,每当使用 require 加载 .js 、 .jsx 、 .es 和 .es6 后缀名的文件,就会先用 Babel 进行转码。 $ npm install --save-dev babel-register 使用时,必须首先加载 babel-register 。 require("babel-register"); require("./index.js"); 然后,就不需要手动对 index.js 转码了。 需要注意的是, babel-register 只会对 require 命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用。 babel-core 如果某些代码需要调用 Babel 的 API 进行转码,就要使用 babel-core 模块。 安装命令如下。 $ npm install babel-core --save 然后,在项目中就可以调用 babel-core 。 上一章 下一章 http://es6.ruanyifeng.com/#docs/intro 5/11
7 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 var babel = require('babel-core'); // 字符串转码 babel.transform('code();', options); // => { code, map, ast } // 文件转码(异步) babel.transformFile('filename.js', options, function(err, result) { result; // => { code, map, ast } }); // 文件转码(同步) babel.transformFileSync('filename.js', options); // => { code, map, ast } // Babel AST转码 babel.transformFromAst(ast, code, options); // => { code, map, ast } 配置对象 options ,可以参看官方文档http://babeljs.io/docs/usage/options/。 下面是一个例子。 var es6Code = 'let x = n => n + 1'; var es5Code = require('babel-core') .transform(es6Code, { presets: ['latest'] }) .code; // '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};' 上面代码中, transform 方法的第一个参数是一个字符串,表示需要被转换的 ES6 代码,第二个参数是转换的配置对象。 babel-polyfill Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator 、 Generator 、 Set 、 Maps 、 Proxy 、 Reflect 、 Symbol 、 Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign )都不会转码。 举例来说,ES6 在 Array 对象上新增了 Array.from 方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill ,为当前环境 提供一个垫片。 安装命令如下。 $ npm install --save babel-polyfill 然后,在脚本头部,加入如下一行代码。 import 'babel-polyfill'; // 或者 require('babel-polyfill'); Babel 默认不转码的 API 非常多,详细清单可以查看 babel-plugin-transform-runtime 模块的definitions.js文件。 浏览器环境 Babel 也可以用于浏览器环境。但是,从 Babel 6.0 开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可 以使用babel-standalone模块提供的浏览器版本,将其插入网页。 <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script> <script type="text/babel"> // Your ES6 code </script> 注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。 下面是如何将代码打包成浏览器可以使用的脚本,以 Babel 配合 Browserify 为例。首先,安装 babelify 模块。 $ npm install --save-dev babelify babel-preset-latest 上一章 下一章 http://es6.ruanyifeng.com/#docs/intro 6/11
8 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 然后,再用命令行转换 ES6 脚本。 $ browserify script.js -o bundle.js \ -t [ babelify --presets [ latest ] ] 上面代码将 ES6 脚本 script.js ,转为 bundle.js ,浏览器直接加载后者就可以了。 在 package.json 设置下面的代码,就不用每次命令行都输入参数了。 { "browserify": { "transform": [["babelify", { "presets": ["latest"] }]] } } 在线转换 Babel 提供一个REPL 在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。 与其他工具的配合 许多工具需要 Babel 进行前置转码,这里举两个例子:ESLint 和 Mocha。 ESLint 用于静态检查代码的语法和风格,安装命令如下。 $ npm install --save-dev eslint babel-eslint 然后,在项目根目录下,新建一个配置文件 .eslintrc ,在其中加入 parser 字段。 { "parser": "babel-eslint", "rules": { ... } } 再在 package.json 之中,加入相应的 scripts 脚本。 { "name": "my-module", "scripts": { "lint": "eslint my-files.js" }, "devDependencies": { "babel-eslint": "...", "eslint": "..." } } Mocha 则是一个测试框架,如果需要执行使用 ES6 语法的测试脚本,可以修改 package.json 的 scripts.test 。 "scripts": { "test": "mocha --ui qunit --compilers js:babel-core/register" } 上面命令中, --compilers 参数指定脚本的转码器,规定后缀名为 js 的文件,都需要使用 babel-core/register 先转码。 7. Traceur 转码器 Google 公司的Traceur转码器,也可以将 ES6 代码转为 ES5 代码。 上一章 下一章 直接插入网页 http://es6.ruanyifeng.com/#docs/intro 7/11
9 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 Traceur 允许将 ES6 代码直接插入网页。首先,必须在网页头部加载 Traceur 库文件。 <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script> <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script> <script type="module"> import './Greeter.js'; </script> 上面代码中,一共有 4 个 script 标签。第一个是加载 Traceur 的库文件,第二个和第三个是将这个库文件用于浏览器环境,第四个则是加载用户脚本, 这个脚本里面可以使用 ES6 代码。 注意,第四个 script 标签的 type 属性的值是 module ,而不是 text/javascript 。这是 Traceur 编译器识别 ES6 代码的标志,编译器会自动将所有 type=module 的代码编译为 ES5,然后再交给浏览器执行。 除了引用外部 ES6 脚本,也可以直接在网页中放置 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> 正常情况下,上面代码会在控制台打印出 9 。 如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。 <script> // Create the System object window.System = new traceur.runtime.BrowserTraceurLoader(); // Set some experimental options var metadata = { traceurOptions: { experimental: true, properTailCalls: true, symbols: true, arrayComprehension: true, asyncFunctions: true, asyncGenerators: exponentiation, forOn: true, generatorComprehension: true } }; // Load your module System.import('./myModule.js', {metadata: metadata}).catch(function(ex) { console.error('Import failed', ex.stack || ex); }); </script> 上面代码中,首先生成 Traceur 的全局对象 window.System ,然后 System.import 方法可以用来加载 ES6。加载的时候,需要传入一个配置对象 metadata ,该对象的 traceurOptions 属性可以配置支持 ES6 功能。如果设为 experimental: true ,就表示除了 ES6 以外,还支持一些实验性的新功 能。 在线转换 Traceur 也提供一个在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。 上面的例子转为 ES5 代码运行,就是下面这个样子。 <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script> <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script> <script> $traceurRuntime.ModuleStore.getAnonymousModule(function() { "use strict"; var Calc = function Calc() { console.log('Calc constructor'); 上一章 下一章 }; http://es6.ruanyifeng.com/#docs/intro 8/11
10 .2017/11/20 ECMAScript 6 简介 - ECMAScript 6入门 }; ($traceurRuntime.createClass)(Calc, {add: function(a, b) { return a + b; }}, {}); var c = new Calc(); console.log(c.add(4, 5)); return {}; }); </script> 命令行转换 作为命令行工具使用时,Traceur 是一个 Node 的模块,首先需要用 npm 安装。 $ npm install -g traceur 安装成功后,就可以在命令行下使用 Traceur 了。 Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的 calc.js 为例。 $ traceur calc.js Calc constructor 9 如果要将 ES6 脚本转为 ES5 保存,要采用下面的写法。 $ traceur --script calc.es6.js --out calc.es5.js 上面代码的 --script 选项表示指定输入文件, --out 选项表示指定输出文件。 为了防止有些特性编译不成功,最好加上 --experimental 选项。 $ traceur --script calc.es6.js --out calc.es5.js --experimental 命令行下转换生成的文件,就可以直接放到浏览器中运行。 Node 环境的用法 Traceur 的 Node 用法如下(假定已安装 traceur 模块)。 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); 留言 上一章 下一章 http://es6.ruanyifeng.com/#docs/intro 9/11
11 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 ECMAScript 6 入门 作者:阮一峰 授权:署名-非商用许可证 目录 0.前言 1.ECMAScript 6简介 2.let 和 const 命令 3.变量的解构赋值 4.字符串的扩展 5.正则的扩展 6.数值的扩展 7.函数的扩展 8.数组的扩展 9.对象的扩展 10.Symbol 11.Set 和 Map 数据结构 12.Proxy 13.Reflect 14.Promise 对象 15.Iterator 和 for...of 循环 16.Generator 函数的语法 17.Generator 函数的异步应用 18.async 函数 19.Class 的基本语法 20.Class 的继承 21.Decorator 22.Module 的语法 23.Module 的加载实现 24.编程风格 25.读懂规格 26.ArrayBuffer 27.参考链接 其他 - 源码 - 修订历史 - 反馈意见 let 和 const 命令 1.let 命令 2.块级作用域 3.const 命令 4.顶层对象的属性 5.global 对象 1. let 命令 § ⇧ 基本用法 ES6 新增了 let 命令,用来声明变量。它的用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效。 { let a = 10; var b = 1; 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 1/12
12 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 } a // ReferenceError: a is not defined. b // 1 上面代码在代码块之中,分别用 let 和 var 声明了两个变量。然后在代码块之外调用这两个变量,结果 let 声明的变量报错, var 声明的变量返回了正确 的值。这表明, let 声明的变量只在它所在的代码块有效。 for 循环的计数器,就很合适使用 let 命令。 for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined 上面代码中,计数器 i 只在 for 循环体内有效,在循环体外引用就会报错。 下面的代码如果使用 var ,最后输出的是 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。 如果使用 let ,声明的变量仅在块级作用域内有效,最后输出的是 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 时,就在上一轮循环的基础上进行计算。 另外, for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。 for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc 上面代码正确运行,输出了 3 次 abc 。这表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域。 不存在变量提升 var 命令会发生”变量提升“现象,即变量可以在声明之前使用,值为 undefined 。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语 句之后才可以使用。 为了纠正这种现象, let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。 // var 的情况 console.log(foo); // 输出undefined var foo = 2; // let 的情况 console.log(bar); // 报错ReferenceError 上一章 下一章 let bar = 2; http://es6.ruanyifeng.com/#docs/let 2/12
13 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 上面代码中,变量 foo 用 var 命令声明,会发生变量提升,即脚本开始运行时,变量 foo 已经存在了,但是没有值,所以会输出 undefined 。变量 bar 用 let 命令声明,不会发生变量提升。这表示在声明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个错误。 暂时性死区 只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。 var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; } 上面代码中,存在全局变量 tmp ,但是块级作用域内 let 又声明了一个局部变量 tmp ,导致后者绑定这个块级作用域,所以在 let 声明变量前,对 tmp 赋 值会报错。 ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量, 就会报错。 总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。 if (true) { // TDZ开始 tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ结束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 } 上面代码中,在 let 命令声明变量 tmp 之前,都属于变量 tmp 的“死区”。 “暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作。 typeof x; // ReferenceError let x; 上面代码中,变量 x 使用 let 命令声明,所以在声明之前,都属于 x 的“死区”,只要用到该变量就会报错。因此, typeof 运行时就会抛出一个 ReferenceError 。 作为比较,如果一个变量根本没有被声明,使用 typeof 反而不会报错。 typeof undeclared_variable // "undefined" 上面代码中, undeclared_variable 是一个不存在的变量名,结果返回“undefined”。所以,在没有 let 之前, typeof 运算符是百分之百安全的,永远不 会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。 有些“死区”比较隐蔽,不太容易发现。 function bar(x = y, y = 2) { return [x, y]; } bar(); // 报错 上面代码中,调用 bar 函数之所以报错(某些实现可能不报错),是因为参数 x 默认值等于另一个参数 y ,而此时 y 还没有声明,属于”死区“。如果 y 的默 认值是 x ,就不会报错,因为此时 x 已经声明了。 function bar(x = 2, y = x) { return [x, y]; } bar(); // [2, 2] 另外,下面的代码也会报错,与 var 的行为不同。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 3/12
14 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 // 不报错 var x = x; // 报错 let x = x; // ReferenceError: x is not defined 上面代码报错,也是因为暂时性死区。使用 let 声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量 x 的声明 语句还没有执行完成前,就去取 x 的值,导致报错”x 未定义“。 ES6 规定暂时性死区和 let 、 const 语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。 这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。 总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获 取和使用该变量。 不允许重复声明 let 不允许在相同作用域内,重复声明同一个变量。 // 报错 function func() { let a = 10; var a = 1; } // 报错 function func() { let a = 10; let a = 1; } 因此,不能在函数内部重新声明参数。 function func(arg) { let arg; // 报错 } function func(arg) { { let arg; // 不报错 } } 2. 块级作用域 为什么需要块级作用域? ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。 第一种场景,内层变量可能会覆盖外层变量。 var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined 上面代码的原意是, if 代码块的外部使用外层的 tmp 变量,内部使用内层的 tmp 变量。但是,函数 f 执行后,输出结果为 undefined ,原因在于变量提 升,导致内层的 tmp 变量覆盖了外层的 tmp 变量。 第二种场景,用来计数的循环变量泄露为全局变量。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 4/12
15 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5 上面代码中,变量 i 只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。 ES6 的块级作用域 let 实际上为 JavaScript 新增了块级作用域。 function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 } 上面的函数有两个代码块,都声明了变量 n ,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用 var 定义变量 n ,最后输出的值 才是 10。 ES6 允许块级作用域的任意嵌套。 {{{{{let insane = 'Hello World'}}}}}; 上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。 {{{{ {let insane = 'Hello World'} console.log(insane); // 报错 }}}}; 内层作用域可以定义外层作用域的同名变量。 {{{{ let insane = 'Hello World'; {let insane = 'Hello World'} }}}}; 块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。 // IIFE 写法 (function () { var tmp = ...; ... }()); // 块级作用域写法 { let tmp = ...; ... } 块级作用域与函数声明 函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。 ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。 // 情况一 if (true) { function f() {} } // 情况二 上一章 下一章 try { http://es6.ruanyifeng.com/#docs/let 5/12
16 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 function f() {} } catch(e) { // ... } 上面两种函数声明,根据 ES5 的规定都是非法的。 但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。 ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于 let ,在块级作用域之外不可引 用。 function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); 上面代码在 ES5 中运行,会得到“I am inside!”,因为在 if 内声明的函数 f 会被提升到函数头部,实际运行的代码如下。 // ES5 环境 function f() { console.log('I am outside!'); } (function () { function f() { console.log('I am inside!'); } if (false) { } f(); }()); ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于 let ,对作用域之外没有影响。但是,如果你真的在 ES6 浏 览器中运行一下上面的代码,是会报错的,这是为什么呢? 原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览 器的实现可以不遵守上面的规定,有自己的行为方式。 - 允许在块级作用域内声明函数。 - 函数声明类似于 var ,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。 注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作 let 处理。 根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于 var 声明的变量。 // 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function 上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。 // 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { var f = undefined; if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function 考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 6/12
17 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 // 函数声明语句 { let a = 'secret'; function f() { return a; } } // 函数表达式 { let a = 'secret'; let f = function () { return a; }; } 另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。 // 不报错 'use strict'; if (true) { function f() {} } // 报错 'use strict'; if (true) function f() {} do 表达式 本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。 { let t = f(); t = t * t + 1; } 上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到 t 的值,因为块级作用域不返回值,除非 t 是全局变量。 现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上 do ,使它变为 do 表达式,然后就会返回内部 最后执行的表达式的值。 let x = do { let t = f(); t * t + 1; }; 上面代码中,变量 x 会得到整个块级作用域的返回值( t * t + 1 )。 3. const 命令 基本用法 const 声明一个只读的常量。一旦声明,常量的值就不能改变。 const PI = 3.1415; PI // 3.1415 PI = 3; // TypeError: Assignment to constant variable. 上面代码表明改变常量的值会报错。 const 声明的变量不得改变值,这意味着, const 一旦声明变量,就必须立即初始化,不能留到以后赋值。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 7/12
18 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 const foo; // SyntaxError: Missing initializer in const declaration 上面代码表示,对于 const 来说,只声明不赋值,就会报错。 const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。 if (true) { const MAX = 5; } MAX // Uncaught ReferenceError: MAX is not defined const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。 if (true) { console.log(MAX); // ReferenceError const MAX = 5; } 上面代码在常量 MAX 声明之前就调用,结果报错。 const 声明的常量,也与 let 一样不可重复声明。 var message = "Hello!"; let age = 25; // 以下两行都会报错 const message = "Goodbye!"; const age = 30; 本质 const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在 变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针, const 只能保 证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。 const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only 上面代码中,常量 foo 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 foo 指向另一个地址,但对象本身是可变的,所以 依然可以为其添加新属性。 下面是另一个例子。 const a = []; a.push('Hello'); // 可执行 a.length = 0; // 可执行 a = ['Dave']; // 报错 上面代码中,常量 a 是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给 a ,就会报错。 如果真的想将对象冻结,应该使用 Object.freeze 方法。 const foo = Object.freeze({}); // 常规模式时,下面一行不起作用; // 严格模式时,该行会报错 foo.prop = 123; 上面代码中,常量 foo 指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。 除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 8/12
19 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); }; ES6 声明变量的六种方法 ES5 只有两种声明变量的方法: var 命令和 function 命令。ES6 除了添加 let 和 const 命令,后面章节还会提到,另外两种声明变量的方法: import 命 令和 class 命令。所以,ES6 一共有 6 种声明变量的方法。 4. 顶层对象的属性 顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。 window.a = 1; a // 1 a = 2; window.a // 2 上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。 顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未 声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全 局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面, window 对象有实体含义,指的是浏览器的窗 口对象,顶层对象是一个有实体含义的对象,也是不合适的。 ES6 为了改变这一点,一方面规定,为了保持兼容性, var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定, let 命令、 const 命令、 class 命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。 var a = 1; // 如果在 Node 的 REPL 环境,可以写成 global.a // 或者采用通用方法,写成 this.a window.a // 1 let b = 1; window.b // undefined 上面代码中,全局变量 a 由 var 命令声明,所以它是顶层对象的属性;全局变量 b 由 let 命令声明,所以它不是顶层对象的属性,返回 undefined 。 5. global 对象 ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。 - 浏览器里面,顶层对象是 window ,但 Node 和 Web Worker 没有 window 。 - 浏览器和 Web Worker 里面, self 也指向顶层对象,但是 Node 没有 self 。 - Node 里面,顶层对象是 global ,但其他环境都不支持。 同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用 this 变量,但是有局限性。 - 全局环境中, this 会返回顶层对象。但是,Node 模块和 ES6 模块中, this 返回的是当前模块。 - 函数里面的 this ,如果函数不是作为对象的方法运行,而是单纯作为函数运行, this 会指向顶层对象。但是,严格模式下,这时 this 会返回 undefined 。 - 不管是严格模式,还是普通模式, new Function('return this')() ,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全政策),那么 eval 、 new Function 这些方法都可能无法使用。 综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 9/12
20 .2017/11/20 let 和 const 命令 - ECMAScript 6入门 // 方法一 (typeof window !== 'undefined' ? window : (typeof process === 'object' && typeof require === 'function' && typeof global === 'object') ? global : this); // 方法二 var getGlobal = function () { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); }; 现在有一个提案,在语言标准的层面,引入 global 作为顶层对象。也就是说,在所有环境下, global 都是存在的,都可以从它拿到顶层对象。 垫片库 system.global 模拟了这个提案,可以在所有环境拿到 global 。 // CommonJS 的写法 require('system.global/shim')(); // ES6 模块的写法 import shim from 'system.global/shim'; shim(); 上面代码可以保证各种环境里面, global 对象都是存在的。 // CommonJS 的写法 var global = require('system.global')(); // ES6 模块的写法 import getGlobal from 'system.global'; const global = getGlobal(); 上面代码将顶层对象放入变量 global 。 留言 上一章 下一章 http://es6.ruanyifeng.com/#docs/let 10/12
21 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 ECMAScript 6 入门 作者:阮一峰 授权:署名-非商用许可证 目录 0.前言 1.ECMAScript 6简介 2.let 和 const 命令 3.变量的解构赋值 4.字符串的扩展 5.正则的扩展 6.数值的扩展 7.函数的扩展 8.数组的扩展 9.对象的扩展 10.Symbol 11.Set 和 Map 数据结构 12.Proxy 13.Reflect 14.Promise 对象 15.Iterator 和 for...of 循环 16.Generator 函数的语法 17.Generator 函数的异步应用 18.async 函数 19.Class 的基本语法 20.Class 的继承 21.Decorator 22.Module 的语法 23.Module 的加载实现 24.编程风格 25.读懂规格 26.ArrayBuffer 27.参考链接 其他 - 源码 - 修订历史 - 反馈意见 变量的解构赋值 1.数组的解构赋值 2.对象的解构赋值 3.字符串的解构赋值 4.数值和布尔值的解构赋值 5.函数参数的解构赋值 6.圆括号问题 7.用途 1. 数组的解构赋值 基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 以前,为变量赋值,只能直接指定值。 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 1/11
22 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 let a = 1; let b = 2; let c = 3; ES6 允许写成下面这样。 let [a, b, c] = [1, 2, 3]; 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。 let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 let [ , , third] = ["foo", "bar", "baz"]; third // "baz" let [x, , y] = [1, 2, 3]; x // 1 y // 3 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // undefined z // [] 如果解构不成功,变量的值就等于 undefined 。 let [foo] = []; let [bar, foo] = [1]; 以上两种情况都属于解构不成功, foo 的值都会等于 undefined 。 另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。 let [x, y] = [1, 2, 3]; x // 1 y // 2 let [a, [b], d] = [1, [2, 3], 4]; a // 1 b // 2 d // 4 上面两个例子,都属于不完全解构,但是可以成功。 如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。 // 报错 let [foo] = 1; let [foo] = false; let [foo] = NaN; let [foo] = undefined; let [foo] = null; let [foo] = {}; 上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达 式)。 对于 Set 结构,也可以使用数组的解构赋值。 let [x, y, z] = new Set(['a', 'b', 'c']); x // "a" 事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。 function* fibs() { let a = 0; let b = 1; 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 2/11
23 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 while (true) { yield a; [a, b] = [b, a + b]; } } let [first, second, third, fourth, fifth, sixth] = fibs(); sixth // 5 上面代码中, fibs 是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。 默认值 解构赋值允许指定默认值。 let [foo = true] = []; foo // true let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' 注意,ES6 内部使用严格相等运算符( === ),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined ,默认值是不会生效的。 let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null 上面代码中,如果一个数组成员是 null ,默认值就不会生效,因为 null 不严格等于 undefined 。 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。 function f() { console.log('aaa'); } let [x = f()] = [1]; 上面代码中,因为 x 能取到值,所以函数 f 根本不会执行。上面的代码其实等价于下面的代码。 let x; if ([1][0] === undefined) { x = f(); } else { x = [1][0]; } 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError 上面最后一个表达式之所以会报错,是因为 x 用到默认值 y 时, y 还没有声明。 2. 对象的解构赋值 解构不仅可以用于数组,还可以用于对象。 let { foo, bar } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb" 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取 到正确的值。 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 3/11
24 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 let { bar, foo } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb" let { baz } = { foo: "aaa", bar: "bbb" }; baz // undefined 上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的 同名属性,导致取不到值,最后等于 undefined 。 如果变量名与属性名不一致,必须写成下面这样。 let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; f // 'hello' l // 'world' 这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。 let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" }; 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 let { foo: baz } = { foo: "aaa", bar: "bbb" }; baz // "aaa" foo // error: foo is not defined 上面代码中, foo 是匹配的模式, baz 才是变量。真正被赋值的是变量 baz ,而不是模式 foo 。 与数组一样,解构也可以用于嵌套结构的对象。 let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj; x // "Hello" y // "World" 注意,这时 p 是模式,不是变量,因此不会被赋值。如果 p 也要作为变量赋值,可以写成下面这样。 let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p, p: [x, { y }] } = obj; x // "Hello" y // "World" p // ["Hello", {y: "World"}] 下面是另一个例子。 const node = { loc: { start: { line: 1, column: 5 } } }; let { loc, loc: { start }, loc: { start: { line }} } = node; line // 1 loc // Object {start: Object} start // Object {line: 1, column: 5} 上面代码有三次解构赋值,分别是对 loc 、 start 、 line 三个属性的解构赋值。注意,最后一次对 line 属性的解构赋值之中,只有 line 是变量, loc 和 start 都是模式,不是变量。 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 4/11
25 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 下面是嵌套赋值的例子。 let obj = {}; let arr = []; ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true }); obj // {prop:123} arr // [true] 对象的解构也可以指定默认值。 var {x = 3} = {}; x // 3 var {x, y = 5} = {x: 1}; x // 1 y // 5 var {x: y = 3} = {}; y // 3 var {x: y = 3} = {x: 5}; y // 5 var { message: msg = 'Something went wrong' } = {}; msg // "Something went wrong" 默认值生效的条件是,对象的属性值严格等于 undefined 。 var {x = 3} = {x: undefined}; x // 3 var {x = 3} = {x: null}; x // null 上面代码中,如果 x 属性等于 null ,就不严格相等于 undefined ,导致默认值不会生效。 如果解构失败,变量的值等于 undefined 。 let {foo} = {bar: 'baz'}; foo // undefined 如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。 // 报错 let {foo: {bar}} = {baz: 'baz'}; 上面代码中,等号左边对象的 foo 属性,对应一个子对象。该子对象的 bar 属性,解构时会报错。原因很简单,因为 foo 这时等于 undefined ,再取子属 性就会报错,请看下面的代码。 let _tmp = {baz: 'baz'}; _tmp.foo.bar // 报错 如果要将一个已经声明的变量用于解构赋值,必须非常小心。 // 错误的写法 let x; {x} = {x: 1}; // SyntaxError: syntax error 上面代码的写法会报错,因为 JavaScript 引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为 代码块,才能解决这个问题。 // 正确的写法 let x; ({x} = {x: 1}); 上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。 ({} = [true, false]); ({} = 'abc'); 上一章 下一章 ({} = []); http://es6.ruanyifeng.com/#docs/destructuring 5/11
26 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 上面的表达式虽然毫无意义,但是语法是合法的,可以执行。 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。 let { log, sin, cos } = Math; 上面代码将 Math 对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。 let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3 上面代码对数组进行对象解构。数组 arr 的 0 键对应的值是 1 , [arr.length - 1] 就是 2 键,对应的值是 3 。方括号这种写法,属于“属性名表达式”,参 见《对象的扩展》一章。 3. 字符串的解构赋值 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" 类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。 let {length : len} = 'hello'; len // 5 4. 数值和布尔值的解构赋值 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。 let {toString: s} = 123; s === Number.prototype.toString // true let {toString: s} = true; s === Boolean.prototype.toString // true 上面代码中,数值和布尔值的包装对象都有 toString 属性,因此变量 s 都能取到值。 解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报 错。 let { prop: x } = undefined; // TypeError let { prop: y } = null; // TypeError 5. 函数参数的解构赋值 函数的参数也可以使用解构赋值。 function add([x, y]){ return x + y; } add([1, 2]); // 3 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 6/11
27 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 上面代码中,函数 add 的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 x 和 y 。对于函数内部的代码来说,它们能感受到的 参数就是 x 和 y 。 下面是另一个例子。 [[1, 2], [3, 4]].map(([a, b]) => a + b); // [ 3, 7 ] 函数参数的解构也可以使用默认值。 function move({x = 0, y = 0} = {}) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, 0] move({}); // [0, 0] move(); // [0, 0] 上面代码中,函数 move 的参数是一个对象,通过对这个对象进行解构,得到变量 x 和 y 的值。如果解构失败, x 和 y 等于默认值。 注意,下面的写法会得到不一样的结果。 function move({x, y} = { x: 0, y: 0 }) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0] 上面代码是为函数 move 的参数指定默认值,而不是为变量 x 和 y 指定默认值,所以会得到与前一种写法不同的结果。 undefined 就会触发函数参数的默认值。 [1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ] 6. 圆括号问题 解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不 到)等号才能知道。 由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。 但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。 不能使用圆括号的情况 以下三种解构赋值不得使用圆括号。 (1)变量声明语句 // 全部报错 let [(a)] = [1]; let {x: (c)} = {}; let ({x: c}) = {}; let {(x: c)} = {}; let {(x): c} = {}; let { o: ({ p: p }) } = { o: { p: 2 } }; 上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。 (2)函数参数 函数参数也属于变量声明,因此不能带有圆括号。 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 7/11
28 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 // 报错 function f([(z)]) { return z; } // 报错 function f([z,(x)]) { return x; } (3)赋值语句的模式 // 全部报错 ({ p: a }) = { p: 42 }; ([a]) = [5]; 上面代码将整个模式放在圆括号之中,导致报错。 // 报错 [({ p: a }), { x: c }] = [{}, {}]; 上面代码将一部分模式放在圆括号之中,导致报错。 可以使用圆括号的情况 可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。 [(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确 上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组 的第一个成员,跟圆括号无关;第二行语句中,模式是 p ,而不是 d ;第三行语句与第一行语句的性质一致。 7. 用途 变量的解构赋值用途很多。 (1)交换变量的值 let x = 1; let y = 2; [x, y] = [y, x]; 上面代码交换变量 x 和 y 的值,这样的写法不仅简洁,而且易读,语义非常清晰。 (2)从函数返回多个值 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。 // 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example(); // 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example(); (3)函数参数的定义 解构赋值可以方便地将一组参数与变量名对应起来。 // 参数是一组有次序的值 function f([x, y, z]) { ... } 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 8/11
29 .2017/11/20 变量的解构赋值 - ECMAScript 6入门 f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1}); (4)提取 JSON 数据 解构赋值对提取 JSON 对象中的数据,尤其有用。 let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309] 上面代码可以快速提取 JSON 数据的值。 (5)函数参数的默认值 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'; 这样的语句。 (6)遍历 Map 结构 任何部署了 Iterator 接口的对象,都可以用 for...of 循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方 便。 const 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) { // ... } (7)输入模块的指定方法 加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。 const { SourceMapConsumer, SourceNode } = require("source-map"); 留言 上一章 下一章 http://es6.ruanyifeng.com/#docs/destructuring 9/11