参考链接

最后更新于:2022-04-01 23:30:40

## 官方文件 * [ECMAScript 6 Language Specification](http://people.mozilla.org/~jorendorff/es6-draft.html): 语言规格草案 * [harmony:proposals](http://wiki.ecmascript.org/doku.php?id=harmony:proposals): ES6的各种提案 * [Draft Specification for ES.next (Ecma-262 Edition 6)](http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts): ES6草案各版本之间的变动 ## 综合介绍 * Sayanee Basu, [Use ECMAScript 6 Today](http://net.tutsplus.com/articles/news/ecmascript-6-today/) * Ariya Hidayat, [Toward Modern Web Apps with ECMAScript 6](http://www.sencha.com/blog/toward-modern-web-apps-with-ecmascript-6/) * Dale Schouten, [10 Ecmascript-6 tricks you can perform right now](http://html5hub.com/10-ecmascript-6-tricks-you-can-perform-right-now/) * Colin Toh, [Lightweight ES6 Features That Pack A Punch](http://colintoh.com/blog/lightweight-es6-features): ES6的一些“轻量级”的特性介绍 * Domenic Denicola, [ES6: The Awesome Parts](http://www.slideshare.net/domenicdenicola/es6-the-awesome-parts) * Nicholas C. Zakas, [Understanding ECMAScript 6](https://github.com/nzakas/understandinges6) * Justin Drake, [ECMAScript 6 in Node.JS](https://github.com/JustinDrake/node-es6-examples) * Ryan Dao, [Summary of ECMAScript 6 major features](http://ryandao.net/portal/content/summary-ecmascript-6-major-features) * Luke Hoban, [ES6 features](https://github.com/lukehoban/es6features): ES6新语法点的罗列 * Traceur-compiler, [Language Features](https://github.com/google/traceur-compiler/wiki/LanguageFeatures): Traceur文档列出的一些ES6例子 * Axel Rauschmayer, [ECMAScript 6: what’s next for JavaScript?](https://speakerdeck.com/rauschma/ecmascript-6-whats-next-for-javascript-august-2014): 关于ES6新增语法的综合介绍,有很多例子 * Toby Ho, [ES6 in io.js](http://davidwalsh.name/es6-io) * Guillermo Rauch, [ECMAScript 6](http://rauchg.com/2015/ecmascript-6/) * Charles King, [The power of ECMAScript 6](http://charlesbking.com/power_of_es6/#/) * Benjamin De Cock, [Frontend Guidelines](https://github.com/bendc/frontend-guidelines): ES6最佳实践 * Jani Hartikainen, [ES6: What are the benefits of the new features in practice?](http://codeutopia.net/blog/2015/01/06/es6-what-are-the-benefits-of-the-new-features-in-practice/) ## 语法点 * Kyle Simpson, [For and against `let`](http://davidwalsh.name/for-and-against-let): 讨论let命令的作用域 * kangax, [Why `typeof` is no longer “safe”](http://es-discourse.com/t/why-typeof-is-no-longer-safe/15): 讨论在块级作用域内,let命令的变量声明和赋值的行为 * Axel Rauschmayer, [Variables and scoping in ECMAScript 6](http://www.2ality.com/2015/02/es6-scoping.html): 讨论块级作用域与let和const的行为 * Nick Fitzgerald, [Destructuring Assignment in ECMAScript 6](http://fitzgeraldnick.com/weblog/50/): 详细介绍解构赋值的用法 * Nicholas C. Zakas, [Understanding ECMAScript 6 arrow functions](http://www.nczonline.net/blog/2013/09/10/understanding-ecmascript-6-arrow-functions/) * Jack Franklin, [Real Life ES6 - Arrow Functions](http://javascriptplayground.com/blog/2014/04/real-life-es6-arrow-fn/) * Axel Rauschmayer, [Handling required parameters in ECMAScript 6](http://www.2ality.com/2014/04/required-parameters-es6.html) * Axel Rauschmayer, [ECMAScript 6’s new array methods](http://www.2ality.com/2014/05/es6-array-methods.html): 对ES6新增的数组方法的全面介绍 * Dmitry Soshnikov, [ES6 Notes: Default values of parameters](http://dmitrysoshnikov.com/ecmascript/es6-notes-default-values-of-parameters/): 介绍参数的默认值 * Ragan Wald, [Destructuring and Recursion in ES6](http://raganwald.com/2015/02/02/destructuring.html): rest参数和扩展运算符的详细介绍 ## Collections * Mozilla Developer Network, [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet):介绍WeakSet数据结构 * Dwayne Charrington, [What Are Weakmaps In ES6?](http://ilikekillnerds.com/2015/02/what-are-weakmaps-in-es6/): WeakMap数据结构介绍 * Axel Rauschmayer, [ECMAScript 6: maps and sets](http://www.2ality.com/2015/01/es6-maps-sets.html): Set和Map结构的详细介绍 * Jason Orendorff, [ES6 In Depth: Collections](https://hacks.mozilla.org/2015/06/es6-in-depth-collections/):Set和Map结构的设计思想 ## 字符串 * Mathias Bynens, [Unicode-aware regular expressions in ES6](https://mathiasbynens.be/notes/es6-unicode-regex): 详细介绍正则表达式的u修饰符 * Nicholas C. Zakas, [A critical review of ECMAScript 6 quasi-literals](http://www.nczonline.net/blog/2012/08/01/a-critical-review-of-ecmascript-6-quasi-literals/) * Mozilla Developer Network, [Template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings) * Addy Osmani, [Getting Literal With ES6 Template Strings](http://updates.html5rocks.com/2015/01/ES6-Template-Strings): 模板字符串的介绍 * Blake Winton, [ES6 Templates](https://weblog.latte.ca/blake/tech/firefox/templates.html): 模板字符串的介绍 ## Object * Nicholas C. Zakas, [Creating defensive objects with ES6 proxies](http://www.nczonline.net/blog/2014/04/22/creating-defensive-objects-with-es6-proxies/) * Addy Osmani, [Data-binding Revolutions with Object.observe()](http://www.html5rocks.com/en/tutorials/es7/observe/): 介绍Object.observe()的概念 * Sella Rafaeli, [Native JavaScript Data-Binding](http://www.sellarafaeli.com/blog/native_javascript_data_binding): 如何使用Object.observe方法,实现数据对象与DOM对象的双向绑定 * Axel Rauschmayer, [Meta programming with ECMAScript 6 proxies](http://www.2ality.com/2014/12/es6-proxies.html): Proxy详解 * Daniel Zautner, [Meta-programming JavaScript Using Proxies](http://dzautner.com/meta-programming-javascript-using-proxies/): 使用Proxy实现元编程 * Tom Van Cutsem, [Harmony-reflect](https://github.com/tvcutsem/harmony-reflect/wiki): Reflect对象的设计目的 * Tom Van Cutsem, [Proxy Traps](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/traps.md):Proxy拦截操作一览 * Tom Van Cutsem, [Reflect API](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/api.md) * Tom Van Cutsem, [Proxy Handler API](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/handler_api.md) ## Symbol * Axel Rauschmayer, [Symbols in ECMAScript 6](http://www.2ality.com/2014/12/es6-symbols.html): Symbol简介 * MDN, [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol): Symbol类型的详细介绍 * Jason Orendorff, [ES6 In Depth: Symbols](https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/) * Keith Cirkel, [Metaprogramming in ES6: Symbols and why they're awesome](http://blog.keithcirkel.co.uk/metaprogramming-in-es6-symbols/): Symbol的深入介绍 ## Iterator * Mozilla Developer Network, [Iterators and generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) * Mozilla Developer Network, [The Iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/The_Iterator_protocol) * Jason Orendorff, [ES6 In Depth: Iterators and the for-of loop](https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/): 遍历器与for...of循环的介绍 * Axel Rauschmayer, [Iterators and generators in ECMAScript 6](http://www.2ality.com/2013/06/iterators-generators.html): 探讨Iterator和Generator的设计目的 * Axel Rauschmayer, [Iterables and iterators in ECMAScript 6](http://www.2ality.com/2015/02/es6-iteration.html): Iterator的详细介绍 * Kyle Simpson, [Iterating ES6 Numbers](http://blog.getify.com/iterating-es6-numbers/): 在数值对象上部署遍历器 * Mahdi Dibaiee, [ES7 Array and Generator comprehensions](http://dibaiee.ir/es7-array-generator-comprehensions/):ES7的Generator推导 ## Generator * Matt Baker, [Replacing callbacks with ES6 Generators](http://flippinawesome.org/2014/02/10/replacing-callbacks-with-es6-generators/) * Steven Sanderson, [Experiments with Koa and JavaScript Generators](http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/) * jmar777, [What's the Big Deal with Generators?](http://devsmash.com/blog/whats-the-big-deal-with-generators) * Marc Harter, [Generators in Node.js: Common Misconceptions and Three Good Use Cases](http://strongloop.com/strongblog/how-to-generators-node-js-yield-use-cases/): 讨论Generator函数的作用 * StackOverflow, [ES6 yield : what happens to the arguments of the first call next()?](http://stackoverflow.com/questions/20977379/es6-yield-what-happens-to-the-arguments-of-the-first-call-next): 第一次使用next方法时不能带有参数 * Kyle Simpson, [ES6 Generators: Complete Series](http://davidwalsh.name/es6-generators): 由浅入深探讨Generator的系列文章,共四篇 * Gajus Kuizinas, [The Definitive Guide to the JavaScript Generators](http://gajus.com/blog/2/the-definetive-guide-to-the-javascript-generators): 对Generator的综合介绍 * Jan Krems, [Generators Are Like Arrays](https://gist.github.com/jkrems/04a2b34fb9893e4c2b5c): 讨论Generator可以被当作数据结构看待 * Harold Cooper, [Coroutine Event Loops in Javascript](http://syzygy.st/javascript-coroutines/): Generator用于实现状态机 * Ruslan Ismagilov, [learn-generators](https://github.com/isRuslan/learn-generators): 编程练习,共6道题 * Steven Sanderson, [Experiments with Koa and JavaScript Generators](http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/): Generator入门介绍,以Koa框架为例 ## Promise对象 * Jake Archibald, [JavaScript Promises: There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/) * Tilde, [rsvp.js](https://github.com/tildeio/rsvp.js) * Sandeep Panda, [An Overview of JavaScript Promises](http://www.sitepoint.com/overview-javascript-promises/): ES6 Promise入门介绍 * Dave Atchley, [ES6 Promises](http://www.datchley.name/es6-promises/): Promise的语法介绍 * Jafar Husain, [Async Generators](https://docs.google.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU/view?sle=true): 对async与Generator混合使用的一些讨论 * Axel Rauschmayer, [ECMAScript 6 promises (2/2): the API](http://www.2ality.com/2014/10/es6-promises-api.html): 对ES6 Promise规格和用法的详细介绍 * Jack Franklin, [Embracing Promises in JavaScript](http://javascriptplayground.com/blog/2015/02/promises/): catch方法的例子 * Luke Hoban, [Async Functions for ECMAScript](https://github.com/lukehoban/ecmascript-asyncawait): Async函数的设计思想,与Promise、Gernerator函数的关系 * Jafar Husain, [Asynchronous Generators for ES7](https://github.com/jhusain/asyncgenerator): Async函数的深入讨论 * Nolan Lawson, [Taming the asynchronous beast with ES7](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html): async函数通俗的实例讲解 ## Class与模块 * Sebastian Porto, [ES6 classes and JavaScript prototypes](https://reinteractive.net/posts/235-es6-classes-and-javascript-prototypes): ES6 Class的写法与ES5 Prototype的写法对比 * Jack Franklin, [An introduction to ES6 classes](http://javascriptplayground.com/blog/2014/07/introduction-to-es6-classes-tutorial/): ES6 class的入门介绍 * Axel Rauschmayer, [ECMAScript 6: new OOP features besides classes](http://www.2ality.com/2014/12/es6-oop.html) * Axel Rauschmayer, [Classes in ECMAScript 6 (final semantics)](http://www.2ality.com/2015/02/es6-classes-final.html): Class语法的详细介绍和设计思想分析 * Maximiliano Fierro, [Declarative vs Imperative](http://elmasse.github.io/js/decorators-bindings-es7.html): Decorators和Mixin介绍 * Addy Osmani, [Exploring ES2016 Decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841): Decorator的深入介绍 * Maximiliano Fierro, [Traits with ES7 Decorators](http://cocktailjs.github.io/blog/traits-with-es7-decorators.html): Trait的用法介绍 ## 模块 * Jack Franklin, [JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/): ES6模块入门 * Axel Rauschmayer, [ECMAScript 6 modules: the final syntax](http://www.2ality.com/2014/09/es6-modules-final.html): ES6模块的介绍,以及与CommonJS规格的详细比较 * Dave Herman, [Static module resolution](http://calculist.org/blog/2012/06/29/static-module-resolution/): ES6模块的静态化设计思想 ## 工具 * Google, [traceur-compiler](https://github.com/google/traceur-compiler): Traceur编译器 * Casper Beyer, [ECMAScript 6 Features and Tools](http://caspervonb.github.io/2014/03/05/ecmascript6-features-and-tools.html) * Stoyan Stefanov, [Writing ES6 today with jstransform](http://www.phpied.com/writing-es6-today-with-jstransform/) * ES6 Module Loader, [ES6 Module Loader Polyfill](https://github.com/ModuleLoader/es6-module-loader): 在浏览器和node.js加载ES6模块的一个库,文档里对ES6模块有详细解释 * Paul Miller, [es6-shim](https://github.com/paulmillr/es6-shim): 一个针对老式浏览器,模拟ES6部分功能的垫片库(shim) * army8735, [Javascript Downcast](https://github.com/army8735/jsdc): 国产的ES6到ES5的转码器 * esnext, [ES6 Module Transpiler](https://github.com/esnext/es6-module-transpiler):基于node.js的将ES6模块转为ES5代码的命令行工具 * Sebastian McKenzie, [BabelJS](http://babeljs.io/): ES6转译器 * SystemJS, [SystemJS](https://github.com/systemjs/systemjs): 在浏览器中加载AMD、CJS、ES6模块的一个垫片库 * Modernizr, [HTML5 Cross Browser Polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#ecmascript-6-harmony): ES6垫片库清单 * Facebook, [regenerator](https://github.com/facebook/regenerator): 将Generator函数转为ES5的转码器
';

编程风格

最后更新于:2022-04-01 23:30:38

本章探讨如何将ES6的新语法,运用到编码实践之中,与传统的JavaScript语法结合在一起,写出合理的、易于阅读和维护的代码。多家公司和组织已经公开了它们的风格规范,具体可参阅[jscs.info](http://jscs.info/),下面的内容主要参考了[Airbnb](http://jscs.info/)的JavaScript风格规范。 ## 块级作用域 **(1)let取代var** ES6提出了两个新的声明变量的命令:let和const。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。 ~~~ "use strict"; if(true) { let x = 'hello'; } for (let i = 0; i < 10; i++) { console.log(i); } ~~~ 上面代码如果用var替代let,实际上就声明了一个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,var命令做不到这一点。 var命令存在变量提升效用,let命令没有这个问题。 ~~~ "use strict"; if(true) { console.log(x); // ReferenceError let x = 'hello'; } ~~~ 上面代码如果使用var替代let,console.log那一行就不会报错,而是会输出undefined,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。 所以,建议不再使用var命令,而是使用let命令取代。 **(2)全局常量和线程安全** 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。这符合函数式编程思想,有利于将来的分布式运算。 ~~~ // bad var a = 1, b = 2, c = 3; // good const a = 1; const b = 2; const c = 3; // best const [a, b, c] = [1, 2, 3]; ~~~ const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。 所有的函数都应该设置为常量。 let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。 **(3)严格模式** V8引擎只在严格模式之下,支持let和const。结合前两点,这实际上意味着,将来所有的编程都是针对严格模式的。 ## 字符串 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。 ~~~ // bad const a = "foobar"; const b = 'foo' + a + 'bar'; // acceptable const c = `foobar`; // good const a = 'foobar'; const b = `foo${a}bar`; const c = 'foobar'; ~~~ ## 解构赋值 使用数组成员对变量赋值,优先使用解构赋值。 ~~~ const arr = [1, 2, 3, 4]; // bad const first = arr[0]; const second = arr[1]; // good const [first, second] = arr; ~~~ 函数的参数如果是对象的成员,优先使用解构赋值。 ~~~ // bad function getFullName(user) { const firstName = user.firstName; const lastName = user.lastName; } // good function getFullName(obj) { const { firstName, lastName } = obj; } // best function getFullName({ firstName, lastName }) { } ~~~ 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。 ~~~ // bad function processInput(input) { return [left, right, top, bottom]; } // good function processInput(input) { return { left, right, top, bottom }; } const { left, right } = processInput(input); ~~~ ## 对象 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。 ~~~ // bad const a = { k1: v1, k2: v2, }; const b = { k1: v1, k2: v2 }; // good const a = { k1: v1, k2: v2 }; const b = { k1: v1, k2: v2, }; ~~~ 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。 ~~~ // bad const a = {}; a.x = 3; // if reshape unavoidable const a = {}; Object.assign(a, { x: 3 }); // good const a = { x: null }; a.x = 3; ~~~ 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。 ~~~ // bad const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true; // good const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, }; ~~~ 上面代码中,对象obj的最后一个属性名,需要计算得到。这时最好采用属性表达式,在新建obj的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。 另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。 ~~~ var ref = 'some value'; // bad const atom = { ref: ref, value: 1, addValue: function (value) { return atom.value + value; }, }; // good const atom = { ref, value: 1, addValue(value) { return atom.value + value; }, }; ~~~ ## 数组 使用扩展运算符(...)拷贝数组。 ~~~ // bad const len = items.length; const itemsCopy = []; let i; for (i = 0; i < len; i++) { itemsCopy[i] = items[i]; } // good const itemsCopy = [...items]; ~~~ 使用Array.from方法,将类似数组的对象转为数组。 ~~~ const foo = document.querySelectorAll('.foo'); const nodes = Array.from(foo); ~~~ ## 函数 立即执行函数可以写成箭头函数的形式。 ~~~ (() => { console.log('Welcome to the Internet.'); })(); ~~~ 那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this。 ~~~ // bad [1, 2, 3].map(function (x) { return x * x; }); // good [1, 2, 3].map((x) => { return x * x; }); ~~~ 箭头函数取代Function.prototype.bind,不应再用self/_this/that绑定 this。 ~~~ // bad const self = this; const boundMethod = function(...params) { return method.apply(self, params); } // acceptable const boundMethod = method.bind(this); // best const boundMethod = (...params) => method.apply(this, params); ~~~ 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。 ~~~ // bad function divide(a, b, option = false ) { } // good function divide(a, b, { option = false } = {}) { } ~~~ 不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。 ~~~ // bad function concatenateAll() { const args = Array.prototype.slice.call(arguments); return args.join(''); } // good function concatenateAll(...args) { return args.join(''); } ~~~ 使用默认值语法设置函数参数的默认值。 ~~~ // bad function handleThings(opts) { opts = opts || {}; } // good function handleThings(opts = {}) { // ... } ~~~ ## Map结构 注意区分Object和Map,只有模拟实体对象时,才使用Object。如果只是需要key:value的数据结构,使用Map。因为Map有内建的遍历机制。 ~~~ let map = new Map(arr); for (let key of map.keys()) { console.log(key); } for (let value of map.values()) { console.log(value); } for (let item of map.entries()) { console.log(item[0], item[1]); } ~~~ ## Class 总是用class,取代需要prototype操作。因为class的写法更简洁,更易于理解。 ~~~ // bad function Queue(contents = []) { this._queue = [...contents]; } Queue.prototype.pop = function() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } // good class Queue { constructor(contents = []) { this._queue = [...contents]; } pop() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } } ~~~ 使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。 ~~~ // bad const inherits = require('inherits'); function PeekableQueue(contents) { Queue.apply(this, contents); } inherits(PeekableQueue, Queue); PeekableQueue.prototype.peek = function() { return this._queue[0]; } // good class PeekableQueue extends Queue { peek() { return this._queue[0]; } } ~~~ ## 模块 首先,Module语法是JavaScript模块的标准写法,坚持使用这种写法。使用import取代require。 ~~~ // bad const moduleA = require('moduleA'); const func1 = moduleA.func1; const func2 = moduleA.func2; // good import { func1, func2 } from 'moduleA'; ~~~ 使用export取代module.exports。 ~~~ // commonJS的写法 var React = require('react'); var Breadcrumbs = React.createClass({ render() { return
';

Module

最后更新于:2022-04-01 23:30:35

ES6的Class只是面向对象编程的语法糖,升级了ES5的构造函数的原型链继承的写法,并没有解决模块化问题。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模块高。 ## export命令 模块功能主要由两个命令构成: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命令除了输出变量,还可以输出函数或类(class)。 ~~~ export function multiply (x, y) { return x * y; }; ~~~ 上面代码对外输出一个函数multiply。 ## import命令 使用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'; ~~~ ES6支持多重加载,即所加载的模块中又加载其他模块。 ~~~ import { Vehicle } from './Vehicle'; class Car extends Vehicle { move () { console.log(this.name + ' is spinning wheels...') } } export { Car } ~~~ 上面的模块先加载Vehicle模块,然后在其基础上添加了move方法,再作为一个新模块输出。 如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。 ~~~ export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6; ~~~ 上面代码中,export和import语句可以结合在一起,写成一行。但是从可读性考虑,不建议采用这种写法,h应该采用标准写法。 ## 模块的整体输入 下面是一个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命令 module命令可以取代import语句,达到整体输入模块的作用。 ~~~ // main.js module circle from 'circle'; console.log("圆面积:" + circle.area(4)); console.log("圆周长:" + circle.circumference(14)); ~~~ module命令后面跟一个变量,表示输入的模块定义在该变量上。 ## export default命令 从前面的例子可以看出,使用import的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到`export default`命令,为模块指定默认输出。 ~~~ // export-default.js export default function () { console.log('foo'); } ~~~ 上面代码是一个模块文件`export-default.js`,它的默认输出是一个函数。 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。 ~~~ // import-default.js import customName from './export-default'; customName(); // 'foo' ~~~ 上面代码的import命令,可以用任意名称指向`export-default.js`输出的方法。需要注意的是,这时import命令后面,不使用大括号。 export default命令用在非匿名函数前,也是可以的。 ~~~ // export-default.js export default function foo() { console.log('foo'); } // 或者写成 function foo() { console.log('foo'); } export default foo; ~~~ 上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。 下面比较一下默认输出和正常输出。 ~~~ import crc32 from 'crc32'; // 对应的输出 export default function crc32(){} import { crc32 } from 'crc32'; // 对应的输出 export function crc32(){}; ~~~ 上面代码的两组写法,第一组是使用`export default`时,对应的import语句不需要使用大括号;第二组是不使用`export default`时,对应的import语句需要使用大括号。 `export default`命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此`export deault`命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。 本质上,`export default`就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。 ~~~ // modules.js export default function (x, y) { return x * y; }; // app.js import { default } from 'modules'; ~~~ 有了`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的写法。 ### ES6 module transpiler [ES6 module transpiler](https://github.com/esnext/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 ~~~ ### SystemJS 另一种解决方法是使用[SystemJS](https://github.com/systemjs/systemjs)。它是一个垫片库(polyfill),可以在浏览器内加载ES6模块、AMD模块和CommonJS模块,将其转为ES5格式。它在后台调用的是Google的Traceur转码器。 使用时,先在网页内载入system.js文件。 ~~~ ~~~ 然后,使用`System.import`方法加载模块文件。 ~~~ ~~~ 上面代码中的`./app`,指的是当前目录下的app.js文件。它可以是ES6模块文件,`System.import`会自动将其转码。 需要注意的是,`System.import`使用异步加载,返回一个Promise对象,可以针对这个对象编程。下面是一个模块文件。 ~~~ // app/es6-file.js: export class q { constructor() { this.es6 = 'hello'; } } ~~~ 然后,在网页内加载这个模块文件。 ~~~ ~~~ 上面代码中,`System.import`方法返回的是一个Promise对象,所以可以用then方法指定回调函数。
';

Class

最后更新于:2022-04-01 23:30:33

## Class基本语法 ### (1)概述 JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。 ~~~ function Point(x,y){ this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; } ~~~ 上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。 ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样。 ~~~ //定义类 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '('+this.x+', '+this.y+')'; } } ~~~ 上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。 Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个保留字,直接把函数定义放进去了就可以了。 ES6的类,完全可以看作构造函数的另一种写法。 ~~~ class Point{ // ... } typeof Point // "function" ~~~ 上面代码表明,类的数据类型就是函数。 构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,除了constructor方法以外,类的方法都定义在类的prototype属性上面。 ~~~ class Point { constructor(){ // ... } toString(){ // ... } toValue(){ // ... } } // 等同于 Point.prototype = { toString(){}, toValue(){} } ~~~ 由于类的方法(除constructor以外)都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。`Object.assign`方法可以很方便地一次向类添加多个方法。 ~~~ class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} }) ~~~ prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。 ~~~ Point.prototype.constructor === Point // true ~~~ 另外,类的内部所有定义的方法,都是不可枚举的(enumerable)。 ~~~ class Point { constructor(x, y) { // ... } toString() { // ... } } Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] ~~~ 上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致。 ~~~ var Point = function (x, y){ // ... } Point.prototype.toString = function() { // ... } Object.keys(Point.prototype) // ["toString"] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] ~~~ 上面代码采用ES5的写法,toString方法就是可枚举的。 类的属性名,可以采用表达式。 ~~~ let methodName = "getArea"; class Square{ constructor(length) { // ... } [methodName]() { // ... } } ~~~ 上面代码中,Square类的方法名getArea,是从表达式得到的。 ### (2)constructor方法 constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。 ~~~ constructor() {} ~~~ constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。 ~~~ class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false ~~~ 上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。 ### (3)实例对象 生成实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。 ~~~ // 报错 var point = Point(2, 3); // 正确 var point = new Point(2, 3); ~~~ 与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在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) point.hasOwnProperty('x') // true point.hasOwnProperty('y') // true point.hasOwnProperty('toString') // false point.__proto__.hasOwnProperty('toString') // true ~~~ 上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。 与ES5一样,类的所有实例共享一个原型对象。 ~~~ var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__ === p2.__proto__ //true ~~~ 上面代码中,p1和p2都是Point的实例,它们的原型都是Point,所以__proto__属性是相等的。 这也意味着,可以通过实例的__proto__属性为Class添加方法。 ~~~ var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__.printName = function () { return 'Oops' }; p1.printName() // "Oops" p2.printName() // "Oops" var p3 = new Point(4,2); p3.printName() // "Oops" ~~~ 上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变Class的原始定义,影响到所有实例。 ### (4)name属性 由于本质上,ES6的Class只是ES5的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。 ~~~ class Point {} Point.name // "Point" ~~~ name属性总是返回紧跟在class关键字后面的类名。 ### (5)Class表达式 与函数一样,Class也可以使用表达式的形式定义。 ~~~ const MyClass = class Me { getClassName() { return Me.name; } }; ~~~ 上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。 ~~~ let inst = new MyClass(); inst.getClassName() // Me Me.name // ReferenceError: Me is not defined ~~~ 上面代码表示,Me只在Class内部有定义。 如果Class内部没用到的话,可以省略Me,也就是可以写成下面的形式。 ~~~ const MyClass = class { /* ... */ }; ~~~ 采用Class表达式,可以写出立即执行的Class。 ~~~ let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }("张三"); person.sayName(); // "张三" ~~~ 上面代码中,person是一个立即执行的Class的实例。 ### (6)不存在变量提升 Class不存在变量提升(hoist),这一点与ES5完全不同。 ~~~ new Foo(); // ReferenceError class Foo {} ~~~ 上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把变量声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。 ~~~ { let Foo = class {}; class Bar extends Foo { } } ~~~ 如果存在Class的提升,上面代码将报错,因为let命令也是不提升的。 ### (7)严格模式 类和模块的内部,默认就是严格模式,所以不需要使用`use strict`指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。 考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。 ## Class的继承 ### 基本用法 Class之间可以通过extends关键字,实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。 ~~~ class ColorPoint extends Point {} ~~~ 上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。 ~~~ class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } } ~~~ 上面代码中,constructor方法和toString方法之中,都出现了super关键字,它指代父类的实例(即父类的this对象)。 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。 ~~~ class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError ~~~ 上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。 ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(`Parent.apply(this)`)。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。 ~~~ constructor(...args) { super(...args); } ~~~ 另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。 ~~~ class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正确 } } ~~~ 上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。 下面是生成子类实例的代码。 ~~~ let cp = new ColorPoint(25, 8, 'green'); cp instanceof ColorPoint // true cp instanceof Point // true ~~~ 上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。 ### 类的prototype属性和__proto__属性 在ES5中,每一个对象都有`__proto__`属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和`__proto__`属性,因此同时存在两条继承链。 (1)子类的`__proto__`属性,表示构造函数的继承,总是指向父类。 (2)子类prototype属性的`__proto__`属性,表示方法的继承,总是指向父类的prototype属性。 ~~~ class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true ~~~ 上面代码中,子类A的`__proto__`属性指向父类B,子类A的prototype属性的**proto**属性指向父类B的prototype属性。 这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(`__proto__属性`)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。 ~~~ B.prototype = new A(); // 等同于 B.prototype.__proto__ = A.prototype; ~~~ 此外,考虑三种特殊情况。第一种特殊情况,子类继承Object类。 ~~~ class A extends Object { } A.__proto__ === Object // true A.prototype.__proto__ === Object.prototype // true ~~~ 这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。 第二种特性情况,不存在任何继承。 ~~~ class A { } A.__proto__ === Function.prototype // true A.prototype.__proto__ === Object.prototype // true ~~~ 这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承`Funciton.prototype`。但是,A调用后返回一个空对象(即Object实例),所以`A.prototype.__proto__`指向构造函数(Object)的prototype属性。 第三种特殊情况,子类继承null。 ~~~ class A extends null { } A.__proto__ === Function.prototype // true A.prototype.__proto__ === null // true ~~~ 这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承`Funciton.prototype`。但是,A调用后返回的对象不继承任何方法,所以它的`__proto__`指向`Function.prototype`,即实质上执行了下面的代码。 ~~~ class C extends null { constructor() { return Object.create(null); } } ~~~ ### Object.getPrototypeOf() Object.getPrototypeOf方法可以用来从子类上获取父类。 ~~~ Object.getPrototypeOf(ColorPoint) === Point // true ~~~ ### 实例的__proto__属性 父类实例和子类实例的__proto__属性,指向是不一样的。 ~~~ var p1 = new Point(2, 3); var p2 = new ColorPoint(2, 3, 'red'); p2.__proto__ === p1.__proto // false p2.__proto__.__proto__ === p1.__proto__ // true ~~~ 通过子类实例的__proto__属性,可以修改父类实例的行为。 ~~~ p2.__proto__.__proto__.printName = function () { console.log('Ha'); }; p1.printName() // "Ha" ~~~ 上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1。 ### 原生构造函数的继承 原生构造函数是指语言内置的构造函数,通常用来生成数据结构,比如`Array()`。以前,这些原生构造函数是无法继承的,即不能自己定义一个Array的子类。 ~~~ function MyArray() { Array.apply(this, arguments); } MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true, enumerable: true } }); ~~~ 上面代码定义了一个继承Array的MyArray类。但是,这个类的行为与Array完全不一致。 ~~~ var colors = new MyArray(); colors[0] = "red"; colors.length // 0 colors.length = 0; colors[0] // "red" ~~~ 之所以会发生这种情况,是因为原生构造函数无法外部获取,通过`Array.apply()`或者分配给原型对象都不行。ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的属性无法获取,导致无法继承原生的构造函数。 ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。 ~~~ class MyArray extends Array { constructor(...args) { super(...args); } } var arr = new MyArray(); arr[0] = 12; arr.length // 1 arr.length = 0; arr[0] // undefined ~~~ 上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6可以自定义原生数据结构(比如Array、String等)的子类,这是ES5无法做到的。 上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。下面是一个自定义Error子类的例子。 ~~~ class MyError extends Error { } throw new MyError('Something happened!'); ~~~ ## class的取值函数(getter)和存值函数(setter) 与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 ~~~ class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } } let inst = new MyClass(); inst.prop = 123; // setter: 123 inst.prop // 'getter' ~~~ 上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。 存值函数和取值函数是设置在属性的descriptor对象上的。 ~~~ class CustomHTMLElement { constructor(element) { this.element = element; } get html() { return this.element.innerHTML; } set html(value) { this.element.innerHTML = value; } } var descriptor = Object.getOwnPropertyDescriptor( CustomHTMLElement.prototype, "html"); "get" in descriptor // true "set" in descriptor // true ~~~ 上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与ES5完全一致。 下面的例子针对所有属性,设置存值函数和取值函数。 ~~~ class Jedi { constructor(options = {}) { // ... } set(key, val) { this[key] = val; } get(key) { return this[key]; } } ~~~ 上面代码中,Jedi实例所有属性的存取,都会通过存值函数和取值函数。 ## Class的Generator方法 如果某个方法之前加上星号(*),就表示该方法是一个Generator函数。 ~~~ class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } } } for (let x of new Foo('hello', 'world')) { console.log(x); } // hello // world ~~~ 上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个Generator函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。 ## Class的静态方法 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 ~~~ class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: undefined is not a function ~~~ 上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(`Foo.classMethod()`),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。 父类的静态方法,可以被子类继承。 ~~~ class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod(); // 'hello' ~~~ 上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。 静态方法也是可以从super对象上调用的。 ~~~ class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod(); ~~~ ## new.target属性 new是从构造函数生成实例的命令。ES6为new命令引入了一个`new.target`属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,`new.target`会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。 ~~~ function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('必须使用new生成实例'); } } // 另一种写法 function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('必须使用new生成实例'); } } var person = new Person('张三'); // 正确 var notAPerson = Person.call(person, '张三'); // 报错 ~~~ 上面代码确保构造函数只能通过new命令调用。 Class内部调用`new.target`,返回当前Class。 ~~~ class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); this.length = length; this.width = width; } } var obj = new Rectangle(3, 4); // 输出 true ~~~ 需要注意的是,子类继承父类时,`new.target`会返回子类。 ~~~ class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); // ... } } class Square extends Rectangle { constructor(length) { super(length, length); } } var obj = new Square(3); // 输出 false ~~~ 上面代码中,`new.target`会返回子类。 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。 ~~~ class Shape { constructor() { if (new.target === Shape) { throw new Error('本类不能实例化'); } } } class Rectangle extends Shape { constructor(length, width) { super(); // ... } } var x = new Shape(); // 报错 var y = new Rectangle(3, 4); // 正确 ~~~ 上面代码中,Shape类不能被实例化,只能用于继承。 注意,在函数外部,使用`new.target`会报错。 ## 修饰器 ### 类的修饰 修饰器(Decorator)是一个表达式,用来修改类的行为。这是ES7的一个[提案](https://github.com/wycats/javascript-decorators),目前Babel转码器已经支持。 修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。 ~~~ function testable(target) { target.isTestable = true; } @testable class MyTestableClass () {} console.log(MyTestableClass.isTestable) // true ~~~ 上面代码中,`@testable`就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。 修饰器函数可以接受三个参数,依次是目标函数、属性名和该属性的描述对象。后两个参数可省略。上面代码中,testable函数的参数target,就是所要修饰的对象。如果希望修饰器的行为,能够根据目标对象的不同而不同,就要在外面再封装一层函数。 ~~~ function testable(isTestable) { return function(target) { target.isTestable = isTestable; } } @testable(true) class MyTestableClass () {} console.log(MyTestableClass.isTestable) // true @testable(false) class MyClass () {} console.log(MyClass.isTestable) // false ~~~ 上面代码中,修饰器testable可以接受参数,这就等于可以修改修饰器的行为。 如果想要为类的实例添加方法,可以在修饰器函数中,为目标类的prototype属性添加方法。 ~~~ function testable(target) { target.prototype.isTestable = true; } @testable class MyTestableClass () {} let obj = new MyClass(); console.log(obj.isTestable) // true ~~~ 上面代码中,修饰器函数testable是在目标类的prototype属性添加属性,因此就可以在类的实例上调用添加的属性。 下面是另外一个例子。 ~~~ // mixins.js export function mixins(...list) { return function (target) { Object.assign(target.prototype, ...list) } } // main.js import { mixins } from './mixins' const Foo = { foo() { console.log('foo') } } @mixins(Foo) class MyClass {} let obj = new MyClass() obj.foo() // 'foo' ~~~ 上面代码通过修饰器mixins,可以为类添加指定的方法。 修饰器可以用`Object.assign()`模拟。 ~~~ const Foo = { foo() { console.log('foo') } } class MyClass {} Object.assign(MyClass.prototype, Foo); let obj = new MyClass(); obj.foo() // 'foo' ~~~ ### 方法的修饰 修饰器不仅可以修饰类,还可以修饰类的属性。 ~~~ class Person { @readonly name() { return `${this.first} ${this.last}` } } ~~~ 上面代码中,修饰器readonly用来修饰”类“的name方法。 此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。 ~~~ readonly(Person.prototype, 'name', descriptor); function readonly(target, name, descriptor){ // descriptor对象原来的值如下 // { // value: specifiedFunction, // enumerable: false, // configurable: true, // writable: true // }; descriptor.writable = false; return descriptor; } Object.defineProperty(Person.prototype, 'name', descriptor); ~~~ 上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。下面是另一个例子。 ~~~ class Person { @nonenumerable get kidCount() { return this.children.length; } } function nonenumerable(target, name, descriptor) { descriptor.enumerable = false; return descriptor; } ~~~ 修饰器有注释的作用。 ~~~ @testable class Person { @readonly @nonenumerable name() { return `${this.first} ${this.last}` } } ~~~ 从上面代码中,我们一眼就能看出,MyTestableClass类是可测试的,而name方法是只读和不可枚举的。 除了注释,修饰器还能用来类型检查。所以,对于Class来说,这项功能相当有用。从长期来看,它将是JavaScript代码静态分析的重要工具。 ### core-decorators.js [core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。 **(1)@autobind** autobind修饰器使得方法中的this对象,绑定原始对象。 ~~~ import { autobind } from 'core-decorators'; class Person { @autobind getPerson() { return this; } } let person = new Person(); let getPerson = person.getPerson; getPerson() === person; // true ~~~ **(2)@readonly** readonly修饰器是的属性或方法不可写。 ~~~ import { readonly } from 'core-decorators'; class Meal { @readonly entree = 'steak'; } var dinner = new Meal(); dinner.entree = 'salmon'; // Cannot assign to read only property 'entree' of [object Object] ~~~ **(3)@override** override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。 ~~~ import { override } from 'core-decorators'; class Parent { speak(first, second) {} } class Child extends Parent { @override speak() {} // SyntaxError: Child#speak() does not properly override Parent#speak(first, second) } // or class Child extends Parent { @override speaks() {} // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain. // // Did you mean "speak"? } ~~~ **(4)@deprecate (别名@deprecated)** deprecate或deprecated修饰器在控制台显示一条警告,表示该方法将废除。 ~~~ import { deprecate } from 'core-decorators'; class Person { @deprecate facepalm() {} @deprecate('We stopped facepalming') facepalmHard() {} @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' }) facepalmHarder() {} } let person = new Person(); person.facepalm(); // DEPRECATION Person#facepalm: This function will be removed in future versions. person.facepalmHard(); // DEPRECATION Person#facepalmHard: We stopped facepalming person.facepalmHarder(); // DEPRECATION Person#facepalmHarder: We stopped facepalming // // See http://knowyourmeme.com/memes/facepalm for more details. // ~~~ **(5)@suppressWarnings** suppressWarnings修饰器抑制decorated修饰器导致的`console.warn()`调用。但是,异步代码出发的调用除外。 ~~~ import { suppressWarnings } from 'core-decorators'; class Person { @deprecated facepalm() {} @suppressWarnings facepalmWithoutWarning() { this.facepalm(); } } let person = new Person(); person.facepalmWithoutWarning(); // no warning is logged ~~~ ### Mixin 在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。 请看下面的例子。 ~~~ const Foo = { foo() { console.log('foo') } }; class MyClass {} Object.assign(MyClass.prototype, Foo); let obj = new MyClass(); obj.foo() // 'foo' ~~~ 上面代码之中,对象Foo有一个foo方法,通过`Object.assign`方法,可以将foo方法“混入”MyClass类,导致MyClass的实例obj对象都具有foo方法。这就是“混入”模式的一个简单实现。 下面,我们部署一个通用脚本`mixins.js`,将mixin写成一个修饰器。 ~~~ export function mixins(...list) { return function (target) { Object.assign(target.prototype, ...list); }; } ~~~ 然后,就可以使用上面这个修饰器,为类“混入”各种方法。 ~~~ import { mixins } from './mixins' const Foo = { foo() { console.log('foo') } }; @mixins(Foo) class MyClass {} let obj = new MyClass(); obj.foo() // "foo" ~~~ 通过mixins这个修饰器,实现了在MyClass类上面“混入”Foo对象的foo方法。 ### Trait Trait也是一种修饰器,功能与Mixin类型,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。 下面采用[traits-decorator](https://github.com/CocktailJS/traits-decorator)这个第三方模块作为例子。这个模块提供的traits修饰器,不仅可以接受对象,还可以接受ES6类作为参数。 ~~~ import {traits } from 'traits-decorator' class TFoo { foo() { console.log('foo') } } const TBar = { bar() { console.log('bar') } } @traits(TFoo, TBar) class MyClass { } let obj = new MyClass() obj.foo() // foo obj.bar() // bar ~~~ 上面代码中,通过traits修饰器,在MyClass类上面“混入”了TFoo类的foo方法和TBar对象的bar方法。 Trait不允许“混入”同名方法。 ~~~ import {traits } from 'traits-decorator' class TFoo { foo() { console.log('foo') } } const TBar = { bar() { console.log('bar') }, foo() { console.log('foo') } } @traits(TFoo, TBar) class MyClass { } // 报错 // throw new Error('Method named: ' + methodName + ' is defined twice.'); // ^ // Error: Method named: foo is defined twice. ~~~ 上面代码中,TFoo和TBar都有foo方法,结果traits修饰器报错。 一种解决方法是排除TBar的foo方法。 ~~~ import { traits, excludes } from 'traits-decorator' class TFoo { foo() { console.log('foo') } } const TBar = { bar() { console.log('bar') }, foo() { console.log('foo') } } @traits(TFoo, TBar::excludes('foo')) class MyClass { } let obj = new MyClass() obj.foo() // foo obj.bar() // bar ~~~ 上面代码使用绑定运算符(::)在TBar上排除foo方法,混入时就不会报错了。 另一种方法是为TBar的foo方法起一个别名。 ~~~ import { traits, alias } from 'traits-decorator' class TFoo { foo() { console.log('foo') } } const TBar = { bar() { console.log('bar') }, foo() { console.log('foo') } } @traits(TFoo, TBar::alias({foo: 'aliasFoo'})) class MyClass { } let obj = new MyClass() obj.foo() // foo obj.aliasFoo() // foo obj.bar() // bar ~~~ 上面代码为TBar的foo方法起了别名aliasFoo,于是MyClass也可以混入TBar的foo方法了。 alias和excludes方法,可以结合起来使用。 ~~~ @traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'})) class MyClass {} ~~~ 上面代码排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz。 as方法则为上面的代码提供了另一种写法。 ~~~ @traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}})) class MyClass {} ~~~ ### Babel转码器的支持 目前,Babel转码器已经支持Decorator,命令行的用法如下。 ~~~ $ babel --optional es7.decorators ~~~ 脚本中打开的命令如下。 ~~~ babel.transfrom("code", {optional: ["es7.decorators"]}) ~~~ Babel的官方网站提供一个[在线转码器](https://babeljs.io/repl/),只要勾选Experimental,就能支持Decorator的在线转码。
';

异步操作

最后更新于:2022-04-01 23:30:31

异步编程对JavaScript语言太重要。JavaScript只有一根线程,如果没有异步编程,根本没法用,非卡死不可。 ES6诞生以前,异步编程的方法,大概有下面四种。 * 回调函数 * 事件监听 * 发布/订阅 * Promise 对象 ES6将JavaScript异步编程带入了一个全新的阶段。 ## 基本概念 ### 异步 所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。 ### 回调函数 JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字callback,直译过来就是"重新调用"。 读取文件进行处理,是这样写的。 ~~~ fs.readFile('/etc/passwd', function (err, data) { if (err) throw err; console.log(data); }); ~~~ 上面代码中,readFile函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了`/etc/passwd`这个文件以后,回调函数才会执行。 一个有趣的问题是,为什么Node.js约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。 ### Promise 回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。 ~~~ fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... }); }); ~~~ 不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为“回调函数噩梦”(callback hell)。 Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。采用Promise,连续读取多个文件,写法如下。 ~~~ var readFile = require('fs-readfile-promise'); readFile(fileA) .then(function(data){ console.log(data.toString()); }) .then(function(){ return readFile(fileB); }) .then(function(data){ console.log(data.toString()); }) .catch(function(err) { console.log(err); }); ~~~ 上面代码中,我使用了fs-readfile-promise模块,它的作用就是返回一个Promise版本的readFile函数。Promise提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。 可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。 Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。 那么,有没有更好的写法呢? ## Generator函数 ### 协程 传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。 协程有点像函数,又有点像线程。它的运行流程大致如下。 * 第一步,协程A开始执行。 * 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。 * 第三步,(一段时间后)协程B交还执行权。 * 第四步,协程A恢复执行。 上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。 举例来说,读取文件的协程写法如下。 ~~~ function asnycJob() { // ...其他代码 var f = yield readFile(fileA); // ...其他代码 } ~~~ 上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。 协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。 ### Generator函数的概念 Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。 整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator函数的执行方法如下。 ~~~ function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true } ~~~ 上面代码中,调用Generator函数,会返回一个内部指针(即遍历器)g 。这是Generator函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到`x + 2`为止。 换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段。 ### Generator函数的数据交换和错误处理 Generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。 next方法返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,这是向Generator函数体内输入数据。 ~~~ function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true } ~~~ 上面代码中,第一个next方法的value属性,返回表达式`x + 2`的值(3)。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的 value 属性,返回的就是2(变量y的值)。 Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。 ~~~ function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了'); // 出错了 ~~~ 上面代码的最后一行,Generator函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try ...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。 ### 异步任务的封装 下面看看如何使用 Generator 函数,执行一个真实的异步任务。 ~~~ var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } ~~~ 上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。 执行这段代码的方法如下。 ~~~ var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); }); ~~~ 上面代码中,首先执行Generator函数,获取遍历器对象,然后使用next 方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next 方法。 可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。 ## Thunk函数 ### 参数的求值策略 Thunk函数早在上个世纪60年代就诞生了。 那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。 ~~~ var x = 1; function f(m){ return m * 2; } f(x + 5) ~~~ 上面代码先定义函数f,然后向它传入表达式`x + 5`。请问,这个表达式应该何时求值? 一种意见是"传值调用"(call by value),即在进入函数体之前,就计算`x + 5`的值(等于6),再将这个值传入函数f 。C语言就采用这种策略。 ~~~ f(x + 5) // 传值调用时,等同于 f(6) ~~~ 另一种意见是"传名调用"(call by name),即直接将表达式`x + 5`传入函数体,只在用到它的时候求值。Hskell语言采用这种策略。 ~~~ f(x + 5) // 传名调用时,等同于 (x + 5) * 2 ~~~ 传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。 ~~~ function f(a, b){ return b; } f(3 * x * x - 2 * x - 1, x); ~~~ 上面代码中,函数f的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。 ### Thunk函数的含义 编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。 ~~~ function f(m){ return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk){ return thunk() * 2; } ~~~ 上面代码中,函数f的参数`x + 5`被一个函数替换了。凡是用到原参数的地方,对`Thunk`函数求值即可。 这就是Thunk函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式。 ### JavaScript语言的Thunk函数 JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。 ~~~ // 正常版本的readFile(多参数版本) fs.readFile(fileName, callback); // Thunk版本的readFile(单参数版本) var readFileThunk = Thunk(fileName); readFileThunk(callback); var Thunk = function (fileName){ return function (callback){ return fs.readFile(fileName, callback); }; }; ~~~ 上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做Thunk函数。 任何函数,只要参数有回调函数,就能写成Thunk函数的形式。下面是一个简单的Thunk函数转换器。 ~~~ var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; }; ~~~ 使用上面的转换器,生成`fs.readFile`的Thunk函数。 ~~~ var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback); ~~~ ### Thunkify模块 生产环境的转换器,建议使用Thunkify模块。 首先是安装。 ~~~ $ npm install thunkify ~~~ 使用方式如下。 ~~~ var thunkify = require('thunkify'); var fs = require('fs'); var read = thunkify(fs.readFile); read('package.json')(function(err, str){ // ... }); ~~~ Thunkify的源码与上一节那个简单的转换器非常像。 ~~~ function thunkify(fn){ return function(){ var args = new Array(arguments.length); var ctx = this; for(var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } return function(done){ var called; args.push(function(){ if (called) return; called = true; done.apply(null, arguments); }); try { fn.apply(ctx, args); } catch (err) { done(err); } } } }; ~~~ 它的源码主要多了一个检查机制,变量called确保回调函数只运行一次。这样的设计与下文的Generator函数相关。请看下面的例子。 ~~~ function f(a, b, callback){ var sum = a + b; callback(sum); callback(sum); } var ft = thunkify(f); ft(1, 2)(console.log); // 3 ~~~ 上面代码中,由于thunkify只允许回调函数执行一次,所以只输出一行结果。 ### Generator 函数的流程管理 你可能会问, Thunk函数有什么用?回答是以前确实没什么用,但是ES6有了Generator函数,Thunk函数现在可以用于Generator函数的自动流程管理。 以读取文件为例。下面的Generator函数封装了两个异步操作。 ~~~ var fs = require('fs'); var thunkify = require('thunkify'); var readFile = thunkify(fs.readFile); var gen = function* (){ var r1 = yield readFile('/etc/fstab'); console.log(r1.toString()); var r2 = yield readFile('/etc/shells'); console.log(r2.toString()); }; ~~~ 上面代码中,yield命令用于将程序的执行权移出Generator函数,那么就需要一种方法,将执行权再交还给Generator函数。 这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给Generator函数。为了便于理解,我们先看如何手动执行上面这个Generator函数。 ~~~ var g = gen(); var r1 = g.next(); r1.value(function(err, data){ if (err) throw err; var r2 = g.next(data); r2.value(function(err, data){ if (err) throw err; g.next(data); }); }); ~~~ 上面代码中,变量g是Generator函数的内部指针,表示目前执行到哪一步。next方法负责将指针移动到下一步,并返回该步的信息(value属性和done属性)。 仔细查看上面的代码,可以发现Generator函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。 ### Thunk函数的自动流程管理 Thunk函数真正的威力,在于可以自动执行Generator函数。下面就是一个基于Thunk函数的Generator执行器。 ~~~ function run(fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); } next(); } run(gen); ~~~ 上面代码的run函数,就是一个Generator函数的自动执行器。内部的next函数就是Thunk的回调函数。next函数先将指针移到Generator函数的下一步(gen.next方法),然后判断Generator函数是否结束(result.done 属性),如果没结束,就将next函数再传入Thunk函数(result.value属性),否则就直接退出。 有了这个执行器,执行Generator函数方便多了。不管有多少个异步操作,直接传入run函数即可。当然,前提是每一个异步操作,都要是Thunk函数,也就是说,跟在yield命令后面的必须是Thunk函数。 ~~~ var gen = function* (){ var f1 = yield readFile('fileA'); var f2 = yield readFile('fileB'); // ... var fn = yield readFile('fileN'); }; run(gen); ~~~ 上面代码中,函数gen封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。 Thunk函数并不是Generator函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。 ## co模块 ### 基本用法 [co模块](https://github.com/tj/co)是著名程序员TJ Holowaychuk于2013年6月发布的一个小工具,用于Generator函数的自动执行。 比如,有一个Generator函数,用于依次读取两个文件。 ~~~ var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ~~~ co模块可以让你不用编写Generator函数的执行器。 ~~~ var co = require('co'); co(gen); ~~~ 上面代码中,Generator函数只要传入co函数,就会自动执行。 co函数返回一个Promise对象,因此可以用then方法添加回调函数。 ~~~ co(gen).then(function (){ console.log('Generator 函数执行完成'); }) ~~~ 上面代码中,等到Generator函数执行结束,就会输出一行提示。 ### co模块的原理 为什么co可以自动执行Generator函数? 前面说过,Generator就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。 两种方法可以做到这一点。 (1)回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权。 (2)Promise 对象。将异步操作包装成Promise对象,用then方法交回执行权。 co模块其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。 上一节已经介绍了基于Thunk函数的自动执行器。下面来看,基于Promise对象的自动执行器。这是理解co模块必须的。 ### 基于Promise对象的自动执行 还是沿用上面的例子。首先,把fs模块的readFile方法包装成一个Promise对象。 ~~~ var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ~~~ 然后,手动执行上面的Generator函数。 ~~~ var g = gen(); g.next().value.then(function(data){ g.next(data).value.then(function(data){ g.next(data); }); }) ~~~ 手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。 ~~~ function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next(); } run(gen); ~~~ 上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。 ### co模块的源码 co就是上面那个自动执行器的扩展,它的源码只有几十行,非常简单。 首先,co函数接受Generator函数作为参数,返回一个 Promise 对象。 ~~~ function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { }); } ~~~ 在返回的Promise对象里面,co先检查参数gen是否为Generator函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将Promise对象的状态改为resolved。 ~~~ function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); }); } ~~~ 接着,co将Generator函数的内部指针对象的next方法,包装成onFulefilled函数。这主要是为了能够捕捉抛出的错误。 ~~~ function co(gen) { var ctx = this; return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } }); } ~~~ 最后,就是关键的next函数,它会反复调用自身。 ~~~ function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } ~~~ 上面代码中,next 函数的内部代码,一共只有四行命令。 * 第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。 * 第二行,确保每一步的返回值,是 Promise 对象。 * 第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。 * 第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。 ### 处理并发的异步操作 co支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。 这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。 ~~~ // 数组的写法 co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2) ]; console.log(res); }).catch(onerror); // 对象的写法 co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); }).catch(onerror); ~~~ 下面是另一个例子。 ~~~ co(function* () { var values = [n1, n2, n3]; yield values.map(somethingAsync); }); function* somethingAsync(x) { // do something async return y } ~~~ 上面的代码允许并发三个somethingAsync异步操作,等到它们全部完成,才会进行下一步。 ## async函数 ### 含义 async 函数是什么?一句话,async函数就是Generator函数的语法糖。 前文有一个Generator函数,依次读取两个文件。 ~~~ var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ~~~ 写成 async 函数,就是下面这样。 ~~~ var asyncReadFile = async function (){ var f1 = await readFile('/etc/fstab'); var f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ~~~ 一比较就会发现,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已。 async 函数对 Generator 函数的改进,体现在以下三点。 (1)内置执行器。Generator函数的执行必须靠执行器,所以才有了co模块,而async 函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。 ~~~ var result = asyncReadFile(); ~~~ (2)更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。 (3)更广的适用性。 co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 ### async函数的实现 async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。 ~~~ async function fn(args){ // ... } // 等同于 function fn(args){ return spawn(function*() { // ... }); } ~~~ 所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。 下面给出 spawn 函数的实现,基本就是前文自动执行器的翻版。 ~~~ function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); } ~~~ async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。 ### async 函数的用法 同Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。 下面是一个例子。 ~~~ async function getStockPriceByName(name) { var symbol = await getStockSymbol(name); var stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName('goog').then(function (result){ console.log(result); }); ~~~ 上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。 下面的例子,指定多少毫秒后输出一个值。 ~~~ function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function asyncPrint(value, ms) { await timeout(ms); console.log(value) } asyncPrint('hello world', 50); ~~~ 上面代码指定50毫秒以后,输出"hello world"。 ### 注意点 await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。 ~~~ async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一种写法 async function myFunction() { await somethingThatReturnsAPromise().catch(function (err){ console.log(err); }; } ~~~ await命令只能用在async函数之中,如果用在普通函数,就会报错。 ~~~ async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(function (doc) { await db.post(doc); }); } ~~~ 上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。 ~~~ async function dbFuc(db) { let docs = [{}, {}, {}]; // 可能得到错误结果 docs.forEach(async function (doc) { await db.post(doc); }); } ~~~ 上面代码可能不会正常工作,原因是这时三个`db.post`操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。 ~~~ async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } } ~~~ 如果确实希望多个请求并发执行,可以使用 Promise.all 方法。 ~~~ async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); } ~~~ ES6将await增加为保留字。使用这个词作为标识符,在ES5是合法的,在ES6将抛出SyntaxError。 ### 与Promise、Generator的比较 我们通过一个例子,来看Async函数与Promise、Generator函数的区别。 假定某个DOM元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。 首先是Promise的写法。 ~~~ function chainAnimationsPromise(elem, animations) { // 变量ret用来保存上一个动画的返回值 var ret = null; // 新建一个空的Promise var p = Promise.resolve(); // 使用then方法,添加所有动画 for(var anim in animations) { p = p.then(function(val) { ret = val; return anim(elem); }) } // 返回一个部署了错误捕捉机制的Promise return p.catch(function(e) { /* 忽略错误,继续执行 */ }).then(function() { return ret; }); } ~~~ 虽然Promise的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是Promise的API(then、catch等等),操作本身的语义反而不容易看出来。 接着是Generator函数的写法。 ~~~ function chainAnimationsGenerator(elem, animations) { return spawn(function*() { var ret = null; try { for(var anim of animations) { ret = yield anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }); } ~~~ 上面代码使用Generator函数遍历了每个动画,语义比Promise写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行Generator函数,上面代码的spawn函数就是自动执行器,它返回一个Promise对象,而且必须保证yield语句后面的表达式,必须返回一个Promise。 最后是Async函数的写法。 ~~~ async function chainAnimationsAsync(elem, animations) { var ret = null; try { for(var anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; } ~~~ 可以看到Async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用Generator写法,自动执行器需要用户自己提供。
';

Promise对象

最后更新于:2022-04-01 23:30:28

## Promise的含义 Promise在JavaScript语言早有实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。 所谓Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的API,可供进一步处理。 Promise对象有以下两个特点。 (1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 如果某些事件不断地反复发生,一般来说,使用stream模式是比部署Promise更好的选择。 ## 基本用法 ES6规定,Promise对象是一个构造函数,用来生成Promise实例。 下面代码创造了一个Promise实例。 ~~~ var promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); ~~~ Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。 resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。 ~~~ promise.then(function(value) { // success }, function(value) { // failure }); ~~~ then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。 下面是一个Promise对象的简单例子。 ~~~ function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms, 'done'); }); } timeout(100).then((value) => { console.log(value); }); ~~~ 上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为Resolved,就会触发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); }); ~~~ 上面代码中,getJSON是对XMLHttpRequest对象的封装,用于发出一个针对JSON数据的HTTP请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。 如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。 ~~~ var p1 = new Promise(function(resolve, reject){ // ... }); var p2 = new Promise(function(resolve, reject){ // ... resolve(p1); }) ~~~ 上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。 注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。 ## Promise.prototype.then() Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。 then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。 ~~~ getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... }); ~~~ 上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。 采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。 ~~~ getJSON("/post/1.json").then(function(post) { return getJSON(post.commentURL); }).then(function funcA(comments) { console.log("Resolved: ", comments); }, function funcB(err){ console.log("Rejected: ", err); }); ~~~ 上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB。 如果采用箭头函数,上面的代码可以写得更简洁。 ~~~ getJSON("/post/1.json").then( post => getJSON(post.commentURL) ).then( comments => console.log("Resolved: ", comments), err => console.log("Rejected: ", err) ); ~~~ ## Promise.prototype.catch() Promise.prototype.catch方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。 ~~~ getJSON("/posts.json").then(function(posts) { // ... }).catch(function(error) { // 处理前一个回调函数运行时发生的错误 console.log('发生错误!', error); }); ~~~ 上面代码中,getJSON方法返回一个Promise对象,如果该对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。 ~~~ p.then((val) => console.log("fulfilled:", val)) .catch((err) => console.log("rejected:", err)); // 等同于 p.then((val) => console.log(fulfilled:", val)) .then(null, (err) => console.log("rejected:", err)); ~~~ 下面是一个例子。 ~~~ var promise = new Promise(function(resolve, reject) { throw new Error('test') }); promise.catch(function(error) { console.log(error) }); // Error: test ~~~ 上面代码中,Promise抛出一个错误,就被catch方法指定的回调函数捕获。 如果Promise状态已经变成resolved,再抛出错误是无效的。 ~~~ var promise = new Promise(function(resolve, reject) { resolve("ok"); throw new Error('test'); }); promise .then(function(value) { console.log(value) }) .catch(function(error) { console.log(error) }); // ok ~~~ 上面代码中,Promise在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。 Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。 ~~~ getJSON("/post/1.json").then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 处理前面三个Promise产生的错误 }); ~~~ 上面代码中,一共有三个Promise对象:一个由getJSON产生,两个由then产生。它们之中任何一个抛出的错误,都会被最后一个catch捕获。 跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。 ~~~ var someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { console.log('everything is great'); }); ~~~ 上面代码中,someAsyncThing函数产生的Promise对象会报错,但是由于没有调用catch方法,这个错误不会被捕获,也不会传递到外层代码,导致运行后没有任何输出。 ~~~ var promise = new Promise(function(resolve, reject) { resolve("ok"); setTimeout(function() { throw new Error('test') }, 0) }); promise.then(function(value) { console.log(value) }); // ok // Uncaught Error: test ~~~ 上面代码中,Promise指定在下一轮“事件循环”再抛出错误,结果由于没有指定catch语句,就冒泡到最外层,成了未捕获的错误。 Node.js有一个unhandledRejection事件,专门监听未捕获的reject错误。 ~~~ process.on('unhandledRejection', function (err, p) { console.error(err.stack) }); ~~~ 上面代码中,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是报错的Promise实例,它可以用来了解发生错误的环境信息。。 需要注意的是,catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。 ~~~ var someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { return someOtherAsyncThing(); }).catch(function(error) { console.log('oh no', error); }).then(function() { console.log('carry on'); }); // oh no [ReferenceError: x is not defined] // carry on ~~~ 上面代码运行完catch方法指定的回调函数,会接着运行后面那个then方法指定的回调函数。 catch方法之中,还能再抛出错误。 ~~~ var someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { return someOtherAsyncThing(); }).catch(function(error) { console.log('oh no', error); // 下面一行会报错,因为y没有声明 y + 2; }).then(function() { console.log('carry on'); }); // oh no [ReferenceError: x is not defined] ~~~ 上面代码中,catch方法抛出一个错误,因为后面没有别的catch方法了,导致这个错误不会被捕获,也不会传递到外层。如果改写一下,结果就不一样了。 ~~~ someAsyncThing().then(function() { return someOtherAsyncThing(); }).catch(function(error) { console.log('oh no', error); // 下面一行会报错,因为y没有声明 y + 2; }).catch(function(error) { console.log('carry on', error); }); // oh no [ReferenceError: x is not defined] // carry on [ReferenceError: y is not defined] ~~~ 上面代码中,第二个catch方法用来捕获,前一个catch方法抛出的错误。 ## Promise.all() 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.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对象,Promise.resolve方法就起到这个作用。 ~~~ var jsPromise = Promise.resolve($.ajax('/whatever.json')); ~~~ 上面代码将jQuery生成deferred对象,转为一个新的Promise对象。 如果Promise.resolve方法的参数,不是具有then方法的对象(又称thenable对象),则返回一个新的Promise对象,且它的状态为Resolved。 ~~~ var p = Promise.resolve('Hello'); p.then(function (s){ console.log(s) }); // Hello ~~~ 上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。 Promise.resolve方法允许调用时不带参数。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。 ~~~ var p = Promise.resolve(); p.then(function () { // ... }); ~~~ 上面代码的变量p就是一个Promise对象。 如果Promise.resolve方法的参数是一个Promise实例,则会被原封不动地返回。 ## Promise.reject() Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。 ~~~ var p = Promise.reject('出错了'); p.then(null, function (s){ console.log(s) }); // 出错了 ~~~ 上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。 ## Generator函数与Promise的结合 使用Generator函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。 ~~~ function getFoo () { return new Promise(function (resolve, reject){ resolve('foo'); }); } var g = function* () { try { var foo = yield getFoo(); console.log(foo); } catch (e) { console.log(e); } }; function run (generator) { var it = generator(); function go(result) { if (result.done) return result.value; return result.value.then(function (value) { return go(it.next(value)); }, function (error) { return go(it.throw(value)); }); } go(it.next()); } run(g); ~~~ 上面代码的Generator函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。 ## async函数 async函数与Promise、Generator函数一样,是用来取代回调函数、解决异步操作的一种方法。它本质上是Generator函数的语法糖。async函数并不属于ES6,而是被列入了ES7,但是traceur、Babel.js、regenerator等转码器已经支持这个功能,转码后立刻就能使用。 async函数的详细介绍,请看《异步操作》一章。
';

Generator 函数

最后更新于:2022-04-01 23:30:26

## 简介 ### 基本概念 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍Generator函数的语法和API,它的异步编程应用请看《异步操作》一章。 Generator函数有多种理解角度。从语法上,首先可以把它理解成一个函数的内部状态的遍历器(也就是说,Generator函数是一个状态机)。它每调用一次,就进入下一个内部状态。Generator函数可以控制内部状态的变化,依次遍历这些状态。 形式上,Generator函数是一个普通函数,但是有两个特征。一是,function命令与函数名之间有一个星号;二是,函数体内部使用yield语句,定义遍历器的每个成员,即不同的内部状态(yield语句在英语里的意思就是“产出”)。 ~~~ 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方法可以恢复执行。 ~~~ 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函数,返回一个部署了Iterator接口的遍历器对象,用来操作内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。 ### 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)的语法功能。 ~~~ function* gen{ yield 123 + 456; } ~~~ 上面代码中,yield后面的表达式`123 + 456`,不会立即求值,只会在next方法将指针移到这一句时,才会求值。 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才会执行。 另外需要注意,yield语句不能用在普通函数中,否则会报错。 ~~~ (function (){ yield 1; })() // SyntaxError: Unexpected number ~~~ 上面代码在一个普通函数中使用yield语句,结果产生一个句法错误。 下面是另外一个例子。 ~~~ 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语句。一种修改方法是改用for循环。 ~~~ var arr = [1, [[2, 3], 4], [5, 6]]; var flat = function* (a){ var length = a.length; for(var i =0;i 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捕获。 注意,上面代码的错误,是用遍历器的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。 ~~~ var g = function* () { while (true) { try { yield; } catch (e) { if (e != 'a') throw e; console.log('内部捕获', e); } } }; var i = g(); i.next(); try { throw new Error('a'); throw new Error('b'); } catch (e) { console.log('外部捕获', e); } // 外部捕获 [Error: a] ~~~ 上面代码之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续执行try语句块了。 如果遍历器函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。 ~~~ var g = function* () { while (true) { yield; console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕获', e); } // 外部捕获 a ~~~ 上面代码中,遍历器函数g内部,没有部署try...catch代码块,所以抛出的错误直接被外部catch代码块捕获。 如果遍历器函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历,否则遍历直接终止。 ~~~ var gen = function* gen(){ yield console.log('hello'); yield console.log('world'); } var g = gen(); g.next(); try { g.throw(); } catch (e) { g.next(); } // hello ~~~ 上面代码只输出hello就结束了,因为第二次调用next方法时,遍历器状态已经变成终止了。但是,如果使用throw方法抛出错误,不会影响遍历器状态。 ~~~ var gen = function* gen(){ yield console.log('hello'); yield console.log('world'); } var g = gen(); g.next(); try { throw new Error(); } catch (e) { g.next(); } // hello // world ~~~ 上面代码中,throw命令抛出的错误不会影响到遍历器的状态,所以两次执行next方法,都取到了正确的操作。 这种函数体内捕获错误的机制,大大方便了对错误的处理。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数写一个错误处理语句。 ~~~ 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); } ~~~ 反过来,Generator函数内抛出的错误,也可以被函数体外的catch捕获。 ~~~ function *foo() { var x = yield 3; var y = x.toUpperCase(); yield y; } var it = foo(); it.next(); // { value:3, done:false } try { it.next(42); } catch (err) { console.log(err); } ~~~ 上面代码中,第二个next方法向函数体内传入一个参数42,数值是没有toUpperCase方法的,所以会抛出一个TypeError错误,被函数体外的catch捕获。 一旦Generator执行过程中抛出错误,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即JavaScript引擎认为这个Generator已经运行结束了。 ~~~ function* g() { yield 1; console.log('throwing an exception'); throw new Error('generator broke!'); yield 2; yield 3; } function log(generator) { var v; console.log('starting generator'); try { v = generator.next(); console.log('第一次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } try { v = generator.next(); console.log('第二次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } try { v = generator.next(); console.log('第三次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } console.log('caller done'); } log(g()); // starting generator // 第一次运行next方法 { value: 1, done: false } // throwing an exception // 捕捉错误 { value: 1, done: false } // 第三次运行next方法 { value: undefined, done: true } // caller done ~~~ 上面代码一共三次运行next方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator函数就已经结束了,不再执行下去了。 ## 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*语句等同于在Generator函数内部,部署一个for...of循环。 ~~~ function* concat(iter1, iter2) { yield* iter1; yield* iter2; } // 等同于 function* concat(iter1, iter2) { for (var value of iter1) { yield value; } for (var value of iter2) { yield value; } } ~~~ 上面代码说明,yield*不过是for...of的一种简写形式,完全可以用后者替代前者。 再来看一个对比的例子。 ~~~ function* inner() { yield 'hello!' } function* outer1() { yield 'open' yield inner() yield 'close' } var gen = outer1() gen.next() // -> 'open' gen.next() // -> a generator gen.next() // -> 'close' function* outer2() { yield 'open' yield* inner() yield 'close' } var gen = outer2() gen.next() // -> 'open' gen.next() // -> 'hello!' gen.next() // -> 'close' ~~~ 上面例子中,outer2使用了`yield*`,outer1没使用。结果就是,outer1返回一个遍历器,outer2返回该遍历器的内部值。 如果`yield*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。 ~~~ function* gen(){ yield* ["a", "b", "c"]; } gen().next() // { value:"a", done:false } ~~~ 上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器。 如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。 ~~~ function *foo() { yield 2; yield 3; return "foo"; } function *bar() { yield 1; var v = yield *foo(); console.log( "v: " + v ); yield 4; } var it = bar(); it.next(); // it.next(); // it.next(); // it.next(); // "v: foo" it.next(); // ~~~ 上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数foo的return语句,向函数bar提供了返回值。 `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() { ··· } }; ~~~ 上面代码中,myGeneratorMethod属性前面有一个星号,表示这个属性是一个Generator函数。 它的完整形式如下,与上面的写法是等价的。 ~~~ let obj = { myGeneratorMethod: function* () { // ··· } }; ~~~ ## Generator函数推导 ES7在数组推导的基础上,提出了Generator函数推导(Generator comprehension)。 ~~~ let generator = function* () { for (let i = 0; i < 6; i++) { yield i; } } let squared = ( for (n of generator()) n * n ); // 等同于 // let squared = Array.from(generator()).map(n => n * n); console.log(...squared); // 0 1 4 9 16 25 ~~~ “推导”这种语法结构,不仅可以用于数组,ES7将其推广到了Generator函数。for...of循环会自动调用遍历器的next方法,将返回值的value属性作为数组的一个成员。 Generator函数推导是对数组结构的一种模拟,它的最大优点是惰性求值,即直到真正用到时才会求值,这样可以保证效率。请看下面的例子。 ~~~ let bigArray = new Array(100000); for (let i = 0; i < 100000; i++) { bigArray[i] = i; } let first = bigArray.map(n => n * n)[0]; console.log(first); ~~~ 上面例子遍历一个大数组,但是在真正遍历之前,这个数组已经生成了,占用了系统资源。如果改用Generator函数推导,就能避免这一点。下面代码只在用到时,才会生成一个大数组。 ~~~ let bigGenerator = function* () { for (let i = 0; i < 100000; i++) { yield i; } } let squared = ( for (n of bigGenerator()) n * n ); console.log(squared.next()); ~~~ ## 含义 ### 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)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。 **(1)协程与子例程的差异** 传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。 从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。 **(2)协程与普通线程的差异** 不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。 由于ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。 Generator函数是ECMAScript 6对协程的实现,但属于不完全实现。Generator函数被称为“半协程”(semi-coroutine),意思是只有Generator函数的调用者,才能将程序的执行权还给Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。 如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。 ## 应用 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(); ~~~ 上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量Promise的语法。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() { var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value 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对象。 多个任务按顺序一个接一个执行时,yield语句可以按顺序排列。多个任务需要并列执行时(比如只有A任务和B任务都执行完,才能执行C任务),可以采用数组的写法。 ~~~ function* parallelDownloads() { let [text1,text2] = yield [ taskA(), taskB() ]; console.log(text1, text2); } ~~~ 上面代码中,yield语句的参数是一个数组,成员就是两个任务taskA和taskB,只有等这两个任务都完成了,才会接着执行下面的语句。 ### (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使得数据或者操作,具备了类似数组的接口。
';

Iterator和for…of循环

最后更新于:2022-04-01 23:30:24

## Iterator(遍历器)的概念 JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。 Iterator的遍历过程是这样的。 (1)创建一个指针,指向当前数据结构的起始位置。也就是说,遍历器的返回值是一个指针对象。 (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。 (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。 (4)调用指针对象的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函数,它的作用就是返回数组的指针对象。对数组`['a', 'b']`执行这个函数,就会返回该数组的指针对象it。 指针对象的next方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b。 next方法返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。 总之,指针对象具有next方法。调用next方法,就可以遍历事先给定的数据结构。 由于Iterator只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器,或者说用遍历器模拟出数据结构。下面是一个无限运行的遍历器例子。 ~~~ 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' // ... ~~~ 上面的例子中,遍历器idMaker函数返回的指针对象,并没有对应的数据结构,或者说遍历器自己描述了一个数据结构出来。 在ES6中,有些数据结构原生提供遍历器(比如数组),即不用任何处理,就可以被for...of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了System.iterator属性(详见下文),有些没有。凡是部署了System.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个指针对象。 如果使用TypeScript的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。 ~~~ interface Iterable { [System.iterator]() : Iterator, } interface Iterator { next(value?: any) : IterationResult, } interface IterationResult { value: any, done: boolean, } ~~~ ## 数据结构的默认Iterator接口 Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。 ES6规定,默认的Iterator接口部署在数据结构的`Symbol.iterator`属性,或者一个数据结构只要具有`Symbol.iterator`属性,就可以认为是“可遍历的”(iterable)。也就是说,调用`Symbol.iterator`方法,就会得到当前数据结构的默认遍历器。`Symbol.iterator`本身是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内(请参考Symbol一节)。 在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。 ~~~ 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循环遍历。 对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。 一个对象如果要有可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器方法(原型链上的对象具有该方法也可)。 ~~~ class RangeIterator { constructor(start, stop) { this.value = start; this.stop = stop; } [Symbol.iterator]() { return this; } next() { var value = this.value; if (value < this.stop) { this.value++; return {done: false, value: value}; } else { return {done: true, value: undefined}; } } } function range(start, stop) { return new RangeIterator(start, stop); } for (var value of range(0, 3)) { console.log(value); } ~~~ 上面代码是一个类部署Iterator接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器。 下面是通过遍历器实现指针结构的例子。 ~~~ function Obj(value){ this.value = value; this.next = null; } Obj.prototype[Symbol.iterator] = function(){ var iterator = { next: next }; var current = this; function next(){ if (current){ var value = current.value; var done = current == null; current = current.next; return { done: done, value: value } } else { return { done: true } } } return iterator; } var one = new Obj(1); var two = new Obj(2); var three = new Obj(3); one.next = two; two.next = three; for (var i of one){ console.log(i) } // 1 // 2 // 3 ~~~ 上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。 下面是另一个为对象添加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 }; } } }; } }; ~~~ 对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数值的Iterator接口。 ~~~ NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; ~~~ 如果Symbol.iterator方法返回的不是遍历器,解释引擎将会报错。 ~~~ var obj = {}; obj[Symbol.iterator] = () => 1; [...obj] // TypeError: [] is not a function ~~~ 上面代码中,变量obj的Symbol.iterator方法返回的不是遍历器,因此报错。 有了遍历器接口,数据结构就可以用for...of循环遍历(详见下文),也可以使用while循环遍历。 ~~~ var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (!$result.done) { var x = $result.value; // ... $result = $iterator.next(); } ~~~ 上面代码中,ITERABLE代表某种可遍历的数据结构,$iterator是它的遍历器。遍历器每次移动指针(next方法),都检查一下返回值的done属性,如果遍历还没结束,就移动遍历器的指针到下一步(next方法),不断循环。 ## 调用默认Iterator接口的场合 有一些场合会默认调用iterator接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。 **(1)解构赋值** 对数组和Set结构进行解构赋值时,会默认调用iterator接口。 ~~~ let set = new Set().add('a').add('b').add('c'); let [x,y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c']; ~~~ **(2)扩展运算符** 扩展运算符(...)也会调用默认的iterator接口。 ~~~ // 例一 var str = 'hello'; [...str] // ['h','e','l','l','o'] // 例二 let arr = ['b', 'c']; ['a', ...arr, 'd'] // ['a', 'b', 'c', 'd'] ~~~ 上面代码的扩展运算符内部就调用iterator接口。 实际上,这提供了一种简便机制,可以将任何部署了iterator接口的数据结构,转为数组。也就是说,只要某个数据结构部署了iterator接口,就可以对它使用扩展运算符,将其转为数组。 ~~~ let arr = [...iterable]; ~~~ **(3)其他场合** 以下场合也会用到默认的iterator接口,可以查阅相关章节。 * yield* * Array.from() * Map(), Set(), WeakMap(), WeakSet() * Promise.all(), Promise.race() ## 原生具备Iterator接口的数据结构 《数组的扩展》一章中提到,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。 ## Iterator接口与Generator函数 `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`方法几乎不用部署任何代码,只要用yield命令给出每一步的返回值即可。 ## 遍历器的return(),throw() 遍历器返回的指针对象除了具有next方法,还可以具有return方法和throw方法。其中,next方法是必须部署的,return方法和throw方法是否部署是可选的。 return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。 throw方法主要是配合Generator函数使用,一般的遍历器用不到这个方法。请参阅《Generator函数》一章。 ## for...of循环 ES6借鉴C++、Java、C#和Python语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。一个数据结构只要部署了`Symbol.iterator`方法,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的`Symbol.iterator`方法。 for...of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(比如arguments对象、DOM NodeList对象)、后文的Generator对象,以及字符串。 ### 数组 数组原生具备iterator接口,for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。 ~~~ const arr = ['red', 'green', 'blue']; let iterator = arr[Symbol.iterator](); for(let v of arr) { console.log(v); // red green blue } for(let v of iterator) { console.log(v); // red green blue } ~~~ 上面代码的for...of循环的两种写法是等价的。 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结构 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结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set结构遍历时,返回的是一个值,而Map结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map成员的键名和键值。 ~~~ let map = new Map().set('a', 1).set('b', 2); for (let pair of map) { console.log(pair); } // ['a', 1] // ['b', 2] for (let [key, value] of map) { console.log(key + ' : ' + value); } // a : 1 // b : 2 ~~~ ### 计算生成的数据结构 有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器。 * entries() 返回一个遍历器,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。 * keys() 返回一个遍历器,用来遍历所有的键名。 * values() 返回一个遍历器,用来遍历所有的键值。 这三个方法调用后生成的遍历器,所遍历的都是计算生成的数据结构。 ~~~ let arr = ['a', 'b', 'c']; for (let pair of arr.entries()) { console.log(pair); } // [0, 'a'] // [1, 'b'] // [2, 'c'] ~~~ ### 类似数组的对象 类似数组的对象包括好几类。下面是for...of循环用于字符串、DOM NodeList对象、arguments对象的例子。 ~~~ // 字符串 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"); } // arguments对象 function printArgs() { for (let x of arguments) { console.log(x); } } printArgs('a', 'b'); // 'a' // 'b' ~~~ 对于字符串来说,for...of循环还有一个特点,就是会正确识别32位UTF-16字符。 ~~~ for (let x of 'a\uD83D\uDC0A') { console.log(x); } // 'a' // '\uD83D\uDC0A' ~~~ 并不是所有类似数组的对象都具有iterator接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。 ~~~ let arrayLike = { length: 2, 0: 'a', 1: 'b' }; // 报错 for (let x of arrayLike) { console.log(x); } // 正确 for (let x of Array.from(arrayLike)) { console.log(x); } ~~~ ### 对象 对于普通的对象,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...in循环可以遍历键名,for...of循环会报错。 一种解决方法是,使用`Object.keys`方法将对象的键名生成一个数组,然后遍历这个数组。 ~~~ for (var key of Object.keys(someObject)) { console.log(key + ": " + someObject[key]); } ~~~ 在对象上部署iterator接口的代码,参见本章前面部分。一个方便的方法是将数组的`Symbol.iterator`属性,直接赋值给其他对象的`Symbol.iterator`属性。比如,想要让for...of循环遍历jQuery对象,只要加上下面这一行就可以了。 ~~~ jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; ~~~ 另一个方法是使用Generator函数将对象重新包装一下。 ~~~ function* entries(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]]; } } for (let [key, value] of entries(obj)) { console.log(key, "->", value); } // a -> 1 // b -> 2 // c -> 3 ~~~ ### 与其他遍历语法的比较 以数组为例,JavaScript提供多种遍历语法。最原始的写法就是for循环。 ~~~ for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } ~~~ 这种写法比较麻烦,因此数组提供内置的forEach方法。 ~~~ myArray.forEach(function (value) { console.log(value); }); ~~~ 这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。 for...in循环可以遍历数组的键名。 ~~~ for (var index in myArray) { console.log(myArray[index]); } ~~~ for...in循环有几个缺点。 1)数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。 2)for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。 3)某些情况下,for...in循环会以任意顺序遍历键名。 总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。 for...of循环相比上面几种做法,有一些显著的优点。 ~~~ for (let value of myArray) { console.log(value); } ~~~ * 有着同for...in一样的简洁语法,但是没有for...in那些缺点。 * 不同用于forEach方法,它可以与break、continue和return配合使用。 * 提供了遍历所有数据结构的统一操作接口。 下面是一个使用break语句,跳出for...of循环的例子。 ~~~ for (var n of fibonacci) { if (n > 1000) break; console.log(n); } ~~~ 上面的例子,会输出斐波纳契数列小于等于1000的项。如果当前项大于1000,就会使用break语句跳出for...of循环。
';

Set和Map数据结构

最后更新于:2022-04-01 23:30:21

## 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 5 4 ~~~ 上面代码通过add方法向Set结构加入成员,结果表明Set结构不会添加重复的值。 Set函数可以接受一个数组作为参数,用来初始化。 ~~~ var items = new Set([1,2,3,4,5,5,5,5]); items.size // 5 ~~~ 向Set加入值的时候,不会发生类型转换,所以5和“5”是两个不同的值。Set内部判断两个值是否不同,使用的算法类似于精确相等运算符(===),这意味着,两个对象总是不相等的。唯一的例外是NaN等于自身(精确相等运算符认为NaN不等于自身)。 ~~~ let set = new Set(); set.add({}) set.size // 1 set.add({}) set.size // 2 ~~~ 上面代码表示,由于两个空对象不是精确相等,所以它们被视为两个值。 ### Set实例的属性和方法 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 ~~~ 下面是一个对比,看看在判断是否包括一个键上面,Object结构和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)); } dedupe([1,1,2,3]) // [1, 2, 3] ~~~ ### 遍历操作 Set结构的实例有四个遍历方法,可以用于遍历成员。 * keys():返回一个键名的遍历器 * values():返回一个键值的遍历器 * entries():返回一个键值对的遍历器 * forEach():使用回调函数遍历每个成员 key方法、value方法、entries方法返回的都是遍历器(详见《Iterator对象》一章)。由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以key方法和value方法的行为完全一致。 ~~~ let set = new Set(['red', 'green', 'blue']); for ( let item of set.keys() ){ console.log(item); } // red // green // blue for ( let item of set.values() ){ console.log(item); } // red // green // blue for ( let item of set.entries() ){ console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"] ~~~ 上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。 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 ~~~ 由于扩展运算符(...)内部使用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} ~~~ 因此使用Set,可以很容易地实现并集(Union)和交集(Intersect)。 ~~~ let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); let union = new Set([...a, ...b]); // [1, 2, 3, 4] let intersect = new Set([...a].filter(x => b.has(x))); // [2, 3] ~~~ Set结构的实例的forEach方法,用于对每个成员执行某种操作,没有返回值。 ~~~ let set = new Set([1, 2, 3]); set.forEach((value, key) => console.log(value * 2) ) // 2 // 4 // 6 ~~~ 上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身(上例省略了该参数)。另外,forEach方法还可以有第二个参数,表示绑定的this对象。 如果想在遍历操作中,同步改变原来的Set结构,目前没有直接的方法,但有两种变通方法。一种是利用原Set结构映射出一个新的结构,然后赋值给原来的Set结构;另一种是利用Array.from方法。 ~~~ // 方法一 let set = new Set([1, 2, 3]); set = new Set([...set].map(val => val * 2)); // set的值是2, 4, 6 // 方法二 let set = new Set([1, 2, 3]); set = new Set(Array.from(set, val => val * 2)); // set的值是2, 4, 6 ~~~ 上面代码提供了两种方法,直接在遍历操作中改变原来的Set结构。 ## WeakSet WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。 首先,WeakSet的成员只能是对象,而不能是其他类型的值。 其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。 ~~~ var ws = new WeakSet(); ws.add(1) // TypeError: Invalid value used in weak set ~~~ 上面代码试图向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 ~~~ WeakSet没有size属性,没有办法遍历它的成员。 ~~~ ws.size // undefined ws.forEach // undefined ws.forEach(function(item){ console.log('WeakSet has ' + item)}) // TypeError: undefined is not a function ~~~ 上面代码试图获取size和forEach属性,结果都不能成功。 WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保存成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。 ## Map ### Map结构的目的和基本用法 JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是只能用字符串当作键。这给它的使用带来了很大的限制。 ~~~ var data = {}; var element = document.getElementById("myDiv"); data[element] = metadata; data["[Object HTMLDivElement]"] // metadata ~~~ 上面代码原意是将一个DOM节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串`[Object HTMLDivElement]`。 为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。 ~~~ 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实例时,就指定了两个键name和title。 如果对同一个键多次赋值,后面的值将覆盖前面的值。 ~~~ let map = new Map(); map.set(1, 'aaa'); map.set(1, 'bbb'); map.get(1) // "bbb" ~~~ 上面代码对键1连续赋值两次,后一次的值覆盖前一次的值。 如果读取一个未知的键,则返回undefined。 ~~~ new Map().get('asfddfsasadf') // undefined ~~~ 注意,只有对同一个对象的引用,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将其视为一个键,包括0和-0。另外,虽然NaN不严格相等于自身,但Map将其视为同一个键。 ~~~ let map = new Map(); map.set(NaN, 123); map.get(NaN) // 123 map.set(-0, 123); map.get(+0) // 123 ~~~ ### 实例的属性和操作方法 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 ~~~ ### 遍历方法 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 wm = new WeakMap(); var element = document.querySelector(".element"); wm.set(element, "Original"); wm.get(element) // "Original" element.parentNode.removeChild(element); element = null; wm.get(element) // undefined ~~~ 上面代码中,变量wm是一个WeakMap实例,我们将一个DOM节点element作为键名,然后销毁这个节点,element对应的键就自动消失了,再引用这个键名就返回undefined。 WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。 ~~~ var wm = new WeakMap(); wm.size // undefined wm.forEach // undefined ~~~ 前文说过,WeakMap应用的典型场合就是DOM节点作为键名。下面是一个例子。 ~~~ let myElement = document.getElementById('logo'); let myWeakmap = new WeakMap(); myWeakmap.set(myElement, {timesClicked: 0}); myElement.addEventListener('click', function() { let logoData = myWeakmap.get(myElement); logoData.timesClicked++; myWeakmap.set(myElement, logoData); }, false); ~~~ 上面代码中,myElement是一个DOM节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在WeakMap里,对应的键名就是myElement。一旦这个DOM节点删除,该状态就会自动消失,不存在内存泄漏风险。 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,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。
';

函数的扩展

最后更新于:2022-04-01 23:30:19

## 函数参数的默认值 在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 } ~~~ 除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本彻底拿到这个参数,也不会导致以前的代码无法运行。 默认值的写法非常灵活,下面是一个为对象属性设置默认值的例子。 ~~~ fetch(url, { body = '', method = 'GET', headers = {} }){ console.log(method); } ~~~ 上面代码中,传入函数fetch的第二个参数是一个对象,调用的时候可以为它的三个属性设置默认值。 甚至还可以设置双重默认值。 ~~~ fetch(url, { method = 'GET' } = {}){ console.log(method); } ~~~ 上面代码中,调用函数fetch时,如果不含第二个参数,则默认值为一个空对象;如果包含第二个参数,则它的method属性默认值为GET。 定义了默认值的参数,必须是函数的尾部参数,其后不能再有其他无默认值的参数。这是因为有了默认值以后,该参数可以省略,只有位于尾部,才可能判断出到底省略了哪些参数。 ~~~ // 以下两种写法都是错的 function f(x = 5, y) { } function f(x, y = 5, z) { } ~~~ 如果传入undefined,将触发该参数等于默认值,null则没有这个效果。 ~~~ function foo(x = 5, y = 6){ console.log(x,y); } foo(undefined, null) // 5 null ~~~ 上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。 ~~~ (function(a){}).length // 1 (function(a = 5){}).length // 0 (function(a, b, c = 5){}).length // 2 ~~~ 上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。 ~~~ 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。 参数变量是默认声明的,所以不能用let或const再次声明。 ~~~ function foo(x = 5) { let x = 1; // error const x = 2; // error } ~~~ 上面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。 参数默认值可以与解构赋值,联合起来使用。 ~~~ 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参数代替arguments变量的例子。 ~~~ // arguments变量的写法 const sortNumbers = () => Array.prototype.slice.call(arguments).sort(); // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort(); ~~~ 上面代码的两种写法,比较后可以发现,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) { // ... } ~~~ 函数的length属性,不包括rest参数。 ~~~ (function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1 ~~~ ## 扩展运算符 扩展运算符(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)`这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。 下面是Date函数的参数使用扩展运算符的例子。 ~~~ const date = new Date(...[2015, 1, 1]); ~~~ 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。 ~~~ // ES5的写法 function f (x, y, z){} var args = [0, 1, 2]; f.apply(null, args); // ES6的写法 function f (x, y, z){} var args = [0, 1, 2]; f(...args); ~~~ 扩展运算符与正常的函数参数可以结合使用,非常灵活。 ~~~ function f(v, w, x, y, z) { } var args = [0, 1]; f(-1, ...args, 2, ...[3]); ~~~ 下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。 ~~~ // ES5的写法 Math.max.apply(null, [14, 3, 77]) // ES6的写法 Math.max(...[14, 3, 77]) // 等同于 Math.max(14, 3, 77); ~~~ 上面代码表示,由于JavaScript不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。 另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。 ~~~ // ES5的写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); // ES6的写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; arr1.push(...arr2); ~~~ 上面代码的ES5写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。 扩展运算符还可以用于数组的赋值。 ~~~ 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] ~~~ 上面代码其实也提供了,将一个数组拷贝进另一个数组的便捷方法。 ~~~ const arr2 = [...arr1]; ~~~ 扩展运算符也可以与解构赋值结合起来,用于生成数组。 ~~~ const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // []: const [first, ...rest] = ["foo"]; first // "foo" rest // [] const [first, ...rest] = ["foo", "bar"]; first // "foo" rest // ["bar"] const [first, ...rest] = ["foo", "bar", "baz"]; first // "foo" rest // ["bar","baz"] ~~~ 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。 ~~~ const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错 const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错 ~~~ JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。 ~~~ var dateFields = readDateFields(database); var d = new Date(...dateFields); ~~~ 上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数Date。 扩展运算符还可以将字符串转为真正的数组。 ~~~ [..."hello"] // [ "h", "e", "l", "l", "o" ] ~~~ 任何类似数组的对象,都可以用扩展运算符转为真正的数组。 ~~~ var nodeList = document.querySelectorAll('div'); var array = [...nodeList]; ~~~ 上面代码中,querySelectorAll方法返回的是一个nodeList对象,扩展运算符可以将其转为真正的数组。 扩展运算符内部调用的是数据结构的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" }); ~~~ 箭头函数可以与变量解构结合使用。 ~~~ const full = ({ first, last }) => first + ' ' + last; // 等同于 function full( person ){ return person.first + ‘ ‘ + person.name; } ~~~ 箭头函数使得表达更加简洁。 ~~~ const isEven = n => n % 2 == 0; const square = n => n * n; ~~~ 上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。 箭头函数的一个用处是简化回调函数。 ~~~ // 正常函数写法 [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); ~~~ 下面是rest参数与箭头函数结合的例子。 ~~~ const numbers = (...nums) => nums; numbers(1, 2, 3, 4, 5) // [1,2,3,4,5] const headAndTail = (head, ...tail) => [head, tail]; headAndTail(1, 2, 3, 4, 5) // [1,[2,3,4,5]] ~~~ 箭头函数有几个使用注意点。 * 函数体内的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,很大程度上解决了这个困扰。 箭头函数内部,还可以再使用箭头函数。下面是一个ES5语法的多重嵌套函数。 ~~~ function insert(value) { return {into: function (array) { return {after: function (afterValue) { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; }}; }}; } insert(2).into([1, 3]).after(1); //[1, 2, 3] ~~~ 上面这个函数,可以使用箭头函数改写。 ~~~ let insert = (value) => ({into: (array) => ({after: (afterValue) => { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; }})}); insert(2).into([1, 3]).after(1); //[1, 2, 3] ~~~ 下面是一个部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入。 ~~~ const pipeline = (...funcs) => val => funcs.reduce((a, b) => b(a), val); const plus1 = a => a + 1; const mult2 = a => a * 2; const addThenMult = pipeline(plus1, mult2); addThenMult(5) // 12 ~~~ 如果觉得上面的写法可读性比较差,也可以采用下面的写法。 ~~~ const plus1 = a => a + 1; const mult2 = a => a * 2; mult2(plus1(5)) // 12 ~~~ 箭头函数还有一个功能,就是可以很方便地改写λ演算。 ~~~ // λ演算的写法 fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v))) // ES6的写法 var fix = f => (x => f(v => x(x)(v))) (x => f(v => x(x)(v))); ~~~ 上面两种写法,几乎是一一对应的。由于λ演算对于计算机科学非常重要,这使得我们可以用ES6作为替代工具,探索计算机科学。 ## 函数绑定 箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。 函数绑定运算符是并排的两个双引号(::),双引号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。 ~~~ let log = ::console.log; // 等同于 var log = console.log.bind(console); foo::bar; // 等同于 bar.call(foo); foo::bar(...arguments); i// 等同于 bar.apply(foo, arguments); ~~~ ## 尾调用优化 ### 什么是尾调用? 尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。 ~~~ function f(x){ return g(x); } ~~~ 上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。 以下三种情况,都不属于尾调用。 ~~~ // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; } // 情况三 function f(x){ g(x); } ~~~ 上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。 ~~~ function f(x){ g(x); return undefined; } ~~~ 尾调用不一定出现在函数尾部,只要是最后一步操作即可。 ~~~ function f(x) { if (x > 0) { return m(x) } return n(x); } ~~~ 上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。 ### 尾调用优化 尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 ~~~ function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3); ~~~ 上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。 这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。 ### 尾递归 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 ~~~ function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 ~~~ 上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 ~~~ function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 ~~~ 由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。 ### 递归函数的改写 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1? 两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。 ~~~ function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120 ~~~ 上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。 函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 ~~~ function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } const factorial = currying(tailFactorial, 1); factorial(5) // 120 ~~~ 上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。 第二种方法就简单多了,就是采用ES6的函数默认值。 ~~~ function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120 ~~~ 上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。 总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
';

对象的扩展

最后更新于:2022-04-01 23:30:17

## 属性的简洁表示法 ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。 ~~~ function f( x, y ) { return { x, y }; } // 等同于 function f( x, y ) { return { x: x, y: y }; } ~~~ 上面是属性简写的例子,方法也可以简写。 ~~~ var o = { method() { return "Hello!"; } }; // 等同于 var o = { method: function() { return "Hello!"; } }; ~~~ 下面是一个更实际的例子。 ~~~ 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} ~~~ ## 属性名表达式 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 ~~~ ## 方法的name属性 函数的name属性,返回函数名。ES6为对象方法也添加了name属性。 ~~~ var person = { sayName: function() { console.log(this.name); }, get firstName() { return "Nicholas" } } person.sayName.name // "sayName" person.firstName.name // "get firstName" ~~~ 上面代码中,方法的name属性返回函数名(即方法名)。如果使用了存值函数,则会在方法名前加上get。如果是存值函数,方法名的前面会加上set。 ~~~ var doSomething = function() { // ... }; console.log(doSomething.bind().name); // "bound doSomething" console.log((new Function()).name); // "anonymous" ~~~ 有两种特殊情况:bind方法创造的函数,name属性返回“bound”加上原函数的名字;Function构造函数创造的函数,name属性返回“anonymous”。 ~~~ (new Function()).name // "anonymous" var doSomething = function() { // ... }; doSomething.bind().name // "bound doSomething" ~~~ ## Object.is() Object.is()用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。 ~~~ +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true ~~~ ES5可以通过下面的代码,部署Object.is()。 ~~~ Object.defineProperty(Object, 'is', { value: function(x, y) { if (x === y) { // 针对+0 不等于 -0的情况 return x !== 0 || 1 / x === 1 / y; } // 针对NaN的情况 return x !== x && y !== y; }, configurable: true, enumerable: false, writable: 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 merge = (target, ...sources) => Object.assign(target, ...sources); ~~~ 如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。 ~~~ const merge = (...sources) => Object.assign({}, ...sources); ~~~ **(5)为属性指定默认值** ~~~ 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 ### 概述 在ES5中,对象的属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,新方法的名字有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。 ES6引入了一种新的原始数据类型Symbol,表示独一无二的ID。它通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 ~~~ let s = Symbol(); typeof s // "symbol" ~~~ 上面代码中,变量s就是一个独一无二的ID。typeof运算符的结果,表明变量s是Symbol数据类型,而不是字符串之类的其他类型。 注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 ~~~ var s1 = Symbol('foo'); var s2 = Symbol('bar'); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // "Symbol(foo)" s2.toString() // "Symbol(bar)" ~~~ 上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是`Symbol()`,不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。 注意,Symbol函数的参数只是表示对当前Symbol类型的值的描述,因此相同参数的Symbol函数的返回值是不相等的。 ~~~ // 没有参数的情况 var s1 = Symbol(); var s2 = Symbol(); s1 === s2 // false // 有参数的情况 var s1 = Symbol("foo"); var s2 = Symbol("foo"); s1 === s2 // false ~~~ 上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。 Symbol类型的值不能与其他类型的值进行运算,会报错。 ~~~ var sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}` // TypeError: can't convert symbol to string ~~~ 但是,Symbol类型的值可以转为字符串。 ~~~ var sym = Symbol('My symbol'); String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)' ~~~ ### 作为属性名的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 mySymbol = Symbol(); var a = {}; a.mySymbol = 'Hello!'; a[mySymbol] // undefined a['mySymbol'] // "Hello!" ~~~ 上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个Symbol值。 同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。 ~~~ let s = Symbol(); let obj = { [s]: function (arg) { ... } }; obj[s](123); ~~~ 上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个Symbol值。 采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。 ~~~ let obj = { [s](arg) { ... } }; ~~~ Symbol类型还可以用于定义一组常量,保证这组常量的值都是不相等的。 ~~~ log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn'), }; log(log.levels.DEBUG, 'debug message'); log(log.levels.INFO, 'info message'); ~~~ 还有一点需要注意,Symbol值作为属性名时,该属性还是公开属性,不是私有属性。 ### 属性名的遍历 Symbol作为属性名,该属性不会出现在for...in、for...of循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名。 Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。 ~~~ var obj = {}; var a = Symbol('a'); var b = Symbol.for('b'); obj[a] = 'Hello'; obj[b] = 'World'; var objectSymbols = Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)] ~~~ 下面是另一个例子,Object.getOwnPropertySymbols方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。 ~~~ var obj = {}; var foo = Symbol("foo"); Object.defineProperty(obj, foo, { value: "foobar", }); for (var i in obj) { console.log(i); // 无输出 } Object.getOwnPropertyNames(obj) // [] Object.getOwnPropertySymbols(obj) // [Symbol(foo)] ~~~ 上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。 另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和Symbol键名。 ~~~ let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3 }; Reflect.ownKeys(obj) // [Symbol(my_key), 'enum', 'nonEnum'] ~~~ 由于以Symbol值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。 ~~~ var size = Symbol('size'); class Collection { constructor() { this[size] = 0; } add(item) { this[this[size]] = item; this[size]++; } static sizeOf(instance) { return instance[size]; } } var x = new Collection(); Collection.sizeOf(x) // 0 x.add('foo'); Collection.sizeOf(x) // 1 Object.keys(x) // ['0'] Object.getOwnPropertyNames(x) // ['0'] Object.getOwnPropertySymbols(x) // [Symbol(size)] ~~~ 上面代码中,对象x的size属性是一个Symbol值,所以`Object.keys(x)`、`Object.getOwnPropertyNames(x)`都无法获取它。这就造成了一种非私有的内部方法的效果。 ### Symbol.for(),Symbol.keyFor() 有时,我们希望重新使用同一个Symbol值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。 ~~~ var s1 = Symbol.for('foo'); var s2 = Symbol.for('foo'); s1 === s2 // true ~~~ 上面代码中,s1和s2都是Symbol值,但是它们都是同样参数的`Symbol.for`方法生成的,所以实际上是同一个值。 `Symbol.for()`与`Symbol()`这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。`Symbol.for()`不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用`Symbol.for("cat")`30次,每次都会返回同一个Symbol值,但是调用`Symbol("cat")`30次,会返回30个不同的Symbol值。 ~~~ Symbol.for("bar") === Symbol.for("bar") // true Symbol("bar") === Symbol("bar") // false ~~~ 上面代码中,由于`Symbol()`写法没有登记机制,所以每次调用都会返回一个不同的值。 Symbol.keyFor方法返回一个已登记的Symbol类型值的key。 ~~~ var s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" var s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined ~~~ 上面代码中,变量s2属于未登记的Symbol值,所以返回undefined。 需要注意的是,`Symbol.for`为Symbol值登记的名字,是全局环境的,可以在不同的iframe或service worker中取到同一个值。 ~~~ iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') // true ~~~ 上面代码中,iframe窗口生成的Symbol值,可以在主页面得到。 ### 内置的Symbol值 除了定义自己使用的Symbol值以外,ES6还提供一些内置的Symbol值,指向语言内部使用的方法。 **(1)Symbol.hasInstance** 对象的Symbol.hasInstance属性,指向一个内部方法。该对象使用instanceof运算符时,会调用这个方法,判断该对象是否为某个构造函数的实例。比如,`foo instanceof Foo`在语言内部,实际调用的是`Foo[Symbol.hasInstance](foo)`。 **(2)Symbol.isConcatSpreadable** 对象的Symbol.isConcatSpreadable属性,指向一个方法。该对象使用Array.prototype.concat()时,会调用这个方法,返回一个布尔值,表示该对象是否可以扩展成数组。 **(3)Symbol.isRegExp** 对象的Symbol.isRegExp属性,指向一个方法。该对象被用作正则表达式时,会调用这个方法,返回一个布尔值,表示该对象是否为一个正则对象。 **(4)Symbol.match** 对象的Symbol.match属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。 **(5)Symbol.iterator** 对象的Symbol.iterator属性,指向该对象的默认遍历器方法,即该对象进行for...of循环时,会调用这个方法,返回该对象的默认遍历器,详细介绍参见《Iterator和for...of循环》一章。 ~~~ class Collection { *[Symbol.iterator]() { let i = 0; while(this[i] !== undefined) { yield this[i]; ++i; } } } let myCollection = new Collection(); myCollection[0] = 1; myCollection[1] = 2; for(let value of myCollection) { console.log(value); } // 1 // 2 ~~~ **(6)Symbol.toPrimitive** 对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。 **(7)Symbol.toStringTag** 对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用`Object.prototype.toString`方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制`[object Object]`或`[object Array]`中object后面的那个字符串。 ~~~ class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } var x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]" ~~~ **(8)Symbol.unscopables** 对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,那些属性会被with环境排除。 ~~~ Array.prototype[Symbol.unscopables] // { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // keys: true // } Object.keys(Array.prototype[Symbol.unscopables]) // ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys'] ~~~ 上面代码说明,数组有6个属性,会被with命令排除。 ~~~ // 没有unscopables时 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // 有unscopables时 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 } ~~~ ## Proxy ### 概述 Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 ~~~ 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,去读写它的属性,就会得到下面的结果。 ~~~ obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2 ~~~ 上面代码说明,Proxy实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 ES6原生提供Proxy构造函数,用来生成Proxy实例。 ~~~ var proxy = new Proxy(target, handler) ~~~ Proxy对象的所用用法,都是上面这种形式,不同的只是handler参数的写法。其中,`new Proxy()`表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。 下面是另一个拦截读取属性行为的例子。 ~~~ 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对象,设置到`object.proxy`属性,从而可以在object对象上调用。 ~~~ var object = { proxy: new Proxy(target, handler) } ~~~ 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 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 args[1]; } }; var fproxy = new Proxy(function(x,y) { return x+y; }, handler); fproxy(1,2); // 1 new fproxy(1,2); // 2 fproxy.prototype; // Object.prototype fproxy.foo; // 'Hello, foo' ~~~ 下面是Proxy支持的拦截操作一览。 对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。 **(1)get(target, propKey, receiver)** 拦截对象属性的读取,比如`proxy.foo`和`proxy['foo']`,返回类型不限。最后一个参数receiver可选,当target对象设置了propKey属性的get函数时,receiver对象会绑定get函数的this对象。 **(2)set(target, propKey, value, receiver)** 拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。 **(3)has(target, propKey)** 拦截`propKey in proxy`的操作,返回一个布尔值。 **(4)deleteProperty(target, propKey)** 拦截`delete proxy[propKey]`的操作,返回一个布尔值。 **(5)enumerate(target)** 拦截`for (var x in proxy)`,返回一个遍历器。 **6)hasOwn(target, propKey)** 拦截`proxy.hasOwnProperty('foo')`,返回一个布尔值。 **(7)ownKeys(target)** 拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`,返回一个数组。该方法返回对象所有自身的属性,而`Object.keys()`仅返回对象可遍历的属性。 **(8)getOwnPropertyDescriptor(target, propKey)** 拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。 **(9)defineProperty(target, propKey, propDesc)** 拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。 **(10)preventExtensions(target)** 拦截`Object.preventExtensions(proxy)`,返回一个布尔值。 **(11)getPrototypeOf(target)** 拦截`Object.getPrototypeOf(proxy)`,返回一个对象。 **(12)isExtensible(target)** 拦截`Object.isExtensible(proxy)`,返回一个布尔值。 **(13)setPrototypeOf(target, proto)** 拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。 如果目标对象是函数,那么还有两种额外操作可以拦截。 **(14)apply(target, object, args)** 拦截Proxy实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。 **(15)construct(target, args, proxy)** 拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args)。 下面是其中几个重要拦截方法的详细介绍。 ### get() get方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。 ~~~ 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。 利用proxy,可以将读取属性的操作(get),转变为执行某个函数。 ~~~ var pipe = (function () { var pipe; return function (value) { pipe = []; return new Proxy({}, { get: function (pipeObject, fnName) { if (fnName == "get") { return pipe.reduce(function (val, fn) { return fn(val); }, value); } pipe.push(window[fnName]); return pipeObject; } }); } }()); var double = function (n) { return n*2 }; var pow = function (n) { return n*n }; var reverseInt = function (n) { return n.toString().split('').reverse().join('')|0 }; pipe(3) . double . pow . reverseInt . get // 63 ~~~ 上面代码设置Proxy以后,达到了将函数名链式使用的效果。 ### set() 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。 ### apply() apply方法拦截函数的调用、call和apply操作。 ~~~ var target = function () { return 'I am the target'; }; var handler = { apply: function (receiver, ...args) { return 'I am the proxy'; } }; var p = new Proxy(target, handler); p() === 'I am the proxy'; // true ~~~ 上面代码中,变量p是Proxy的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。 ### ownKeys() 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.revocable() 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 ~~~ Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。 ## Reflect ### 概述 Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象的设计目的有这样几个。 (1) 将Object对象的一些明显属于语言层面的方法,放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。 (2) 修改某些Object方法的返回结果,让其变得更合理。比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回false。 (3) 让Object操作都变成函数行为。某些Object操作是命令式,比如`name in obj`和`delete obj[name]`,而`Reflect.has(obj, name)`和`Reflect.deleteProperty(obj, name)`让它们变成了函数行为。 (4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。 ~~~ Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target,name, value, receiver); if (success) { log('property '+name+' on '+target+' set to '+value); } return success; } }); ~~~ 上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,然后再部署额外的功能。 下面是get方法的例子。 ~~~ var loggedObj = new Proxy(obj, { get: function(target, name) { console.log("get", target, name); return Reflect.get(target, name); } }); ~~~ ### 方法 Reflect对象的方法清单如下。 * Reflect.getOwnPropertyDescriptor(target,name) * Reflect.defineProperty(target,name,desc) * Reflect.getOwnPropertyNames(target) * Reflect.getPrototypeOf(target) * Reflect.deleteProperty(target,name) * Reflect.enumerate(target) * Reflect.freeze(target) * Reflect.seal(target) * Reflect.preventExtensions(target) * Reflect.isFrozen(target) * Reflect.isSealed(target) * Reflect.isExtensible(target) * Reflect.has(target,name) * Reflect.hasOwn(target,name) * Reflect.keys(target) * Reflect.get(target,name,receiver) * Reflect.set(target,name,value,receiver) * Reflect.apply(target,thisArg,args) * Reflect.construct(target,args) 上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的。下面是对其中几个方法的解释。 (1)Reflect.get(target,name,receiver) 查找并返回target对象的name属性,如果没有该属性,则返回undefined。 如果name属性部署了读取函数,则读取函数的this绑定receiver。 ~~~ var obj = { get foo() { return this.bar(); }, bar: function() { ... } } // 下面语句会让 this.bar() // 变成调用 wrapper.bar() Reflect.get(obj, "foo", wrapper); ~~~ (2)Reflect.set(target, name, value, receiver) 设置target对象的name属性等于value。如果name属性设置了赋值函数,则赋值函数的this绑定receiver。 (3)Reflect.has(obj, name) 等同于`name in obj`。 (4)Reflect.deleteProperty(obj, name) 等同于`delete obj[name]`。 (5)Reflect.construct(target, args) 等同于`new target(...args)`,这提供了一种不使用new,来调用构造函数的方法。 (6)Reflect.getPrototypeOf(obj) 读取对象的__proto__属性,等同于`Object.getPrototypeOf(obj)`。 (7)Reflect.setPrototypeOf(obj, newProto) 设置对象的__proto__属性。注意,Object对象没有对应这个方法的方法。 (8)Reflect.apply(fun,thisArg,args) 等同于`Function.prototype.apply.call(fun,thisArg,args)`。一般来说,如果要绑定一个函数的this对象,可以这样写`fn.apply(obj, args)`,但是如果函数定义了自己的apply方法,就只能写成`Function.prototype.apply.call(fn, obj, args)`,采用Reflect对象可以简化这种操作。 另外,需要注意的是,Reflect.set()、Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和Reflect.preventExtensions()返回一个布尔值,表示操作是否成功。它们对应的Object方法,失败时都会抛出错误。 ~~~ // 失败时抛出错误 Object.defineProperty(obj, name, desc); // 失败时返回false Reflect.defineProperty(obj, name, desc); ~~~ 上面代码中,Reflect.defineProperty方法的作用与Object.defineProperty是一样的,都是为对象定义一个属性。但是,Reflect.defineProperty方法失败时,不会抛出错误,只会返回false。 ## 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版起就已经支持。
';

数组的扩展

最后更新于:2022-04-01 23:30:14

## 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方法可以将函数的arguments对象,转为数组。 ~~~ function foo() { var args = Array.from( arguments ); } foo( "a", "b", "c" ); ~~~ 任何有length属性的对象,都可以通过Array.from方法转为数组。 ~~~ Array.from({ 0: "a", 1: "b", 2: "c", length: 3 }); // [ "a", "b" , "c" ] ~~~ 对于还没有部署该方法的浏览器,可以用Array.prototyp.slice方法替代。 ~~~ const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj) )(); ~~~ Array.from()还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理。 ~~~ Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); ~~~ 下面的例子将数组中布尔值为false的成员转为0。 ~~~ Array.from([1, , 2, , 3], (n) => n || 0) // [1, 0, 2, 0, 3] ~~~ 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) // [3] Array.of(3).length // 1 ~~~ 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。 ~~~ Array() // [] Array(3) // [undefined, undefined, undefined] Array(3,11,8) // [3, 11, 8] ~~~ 上面代码说明,只有当参数个数不少于2个,Array()才会返回由参数组成的新数组。 Array.of方法可以用下面的代码模拟实现。 ~~~ function ArrayOf(){ return [].slice.call(arguments); } ~~~ ## 数组实例的find()和findIndex() 数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。 ~~~ var found = [1, 4, -5, 10].find((n) => n < 0); console.log("found:", found); ~~~ 上面代码找出数组中第一个小于0的成员。 ~~~ [1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10 ~~~ 上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 数组实例的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 ~~~ 上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。 ## 数组实例的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" ~~~ ## 数组实例的includes() Array.protypeto.includes方法返回一个布尔值,表示某个数组是否包含给定的值。该方法属于ES7。 ~~~ [1, 2, 3].includes(2); // true [1, 2, 3].includes(4); // false [1, 2, NaN].includes(NaN); // true ~~~ 该方法的第二个参数表示搜索的起始位置,默认为0。 ~~~ [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true ~~~ 下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。 ~~~ const contains = (() => Array.prototype.includes ? (arr, value) => arr.includes(value) : (arr, value) => arr.some(el => el === value) )(); contains(["foo", "bar"], "baz"); // => false ~~~ ## 数组推导 数组推导(array comprehension)提供简洁写法,允许直接通过现有数组生成新数组。这项功能本来是要放入ES6的,但是TC39委员会想继续完善这项功能,让其支持所有数据结构(内部调用iterator对象),不像现在只支持数组,所以就把它推迟到了ES7。Babel转码器已经支持这个功能。 ~~~ 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属性发生变化)。
';

数值的扩展

最后更新于:2022-04-01 23:30:12

## 二进制和八进制表示法 ES6提供了二进制和八进制数值的新的写法,分别用前缀0b和0o表示。 ~~~ 0b111110111 === 503 // true 0o767 === 503 // true ~~~ 八进制不再允许使用前缀0表示,而改为使用前缀0o。 ~~~ 011 === 9 // 不正确 0o11 === 9 // 正确 ~~~ ## Number.isFinite(), Number.isNaN() ES6在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法,用来检查Infinite和NaN这两个特殊值。 Number.isFinite()用来检查一个数值是否非无穷(infinity)。 ~~~ Number.isFinite(15); // true Number.isFinite(0.8); // true Number.isFinite(NaN); // false Number.isFinite(Infinity); // false Number.isFinite(-Infinity); // false Number.isFinite("foo"); // false Number.isFinite("15"); // false Number.isFinite(true); // false ~~~ ES5通过下面的代码,部署Number.isFinite方法。 ~~~ (function (global) { var global_isFinite = global.isFinite; Object.defineProperty(Number, 'isFinite', { value: function isFinite(value) { return typeof value === 'number' && global_isFinite(value); }, configurable: true, enumerable: false, writable: true }); })(this); ~~~ Number.isNaN()用来检查一个值是否为NaN。 ~~~ Number.isNaN(NaN); // true Number.isNaN(15); // false Number.isNaN("15"); // false Number.isNaN(true); // false ~~~ ES5通过下面的代码,部署Number.isNaN()。 ~~~ (function (global) { var global_isNaN = global.isNaN; Object.defineProperty(Number, 'isNaN', { value: function isNaN(value) { return typeof value === 'number' && global_isNaN(value); }, configurable: true, enumerable: false, writable: true }); })(this); ~~~ 它们与传统的全局方法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 Number.isInteger("15") // false Number.isInteger(true) // false ~~~ ES5通过下面的代码,部署Number.isInteger()。 ~~~ (function (global) { var floor = Math.floor, isFinite = global.isFinite; Object.defineProperty(Number, 'isInteger', { value: function isInteger(value) { return typeof value === 'number' && isFinite(value) && value > -9007199254740992 && value < 9007199254740992 && floor(value) === value; }, configurable: true, enumerable: false, writable: true }); })(this); ~~~ 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对象的扩展 ES6在Math对象上新增了17个与数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。 ### Math.trunc() Math.trunc方法用于去除一个数的小数部分,返回整数部分。 ~~~ Math.trunc(4.1) // 4 Math.trunc(4.9) // 4 Math.trunc(-4.1) // -4 Math.trunc(-4.9) // -4 ~~~ 对于空值和无法截取整数的值,返回NaN。 ~~~ Math.trunc(NaN); // NaN Math.trunc('foo'); // NaN Math.trunc(); // NaN ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.trunc = Math.trunc || function(x) { return x < 0 ? Math.ceil(x) : Math.floor(x); } ~~~ ### Math.sign() Math.sign方法用来判断一个数到底是正数、负数、还是零。 它会返回五种值。 * 参数为正数,返回+1; * 参数为负数,返回-1; * 参数为0,返回0; * 参数为-0,返回-0; * 其他值,返回NaN。 ~~~ Math.sign(-5) // -1 Math.sign(5) // +1 Math.sign(0) // +0 Math.sign(-0) // -0 Math.sign(NaN) // NaN Math.sign('foo'); // NaN Math.sign(); // NaN ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.sign = Math.sign || function(x) { x = +x; // convert to a number if (x === 0 || isNaN(x)) { return x; } return x > 0 ? 1 : -1; } ~~~ ### Math.cbrt() Math.cbrt方法用于计算一个数的立方根。 ~~~ Math.cbrt(-1); // -1 Math.cbrt(0); // 0 Math.cbrt(1); // 1 Math.cbrt(2); // 1.2599210498948734 ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.cbrt = Math.cbrt || function(x) { var y = Math.pow(Math.abs(x), 1/3); return x < 0 ? -y : y; }; ~~~ ### Math.clz32() JavaScript的整数使用32位二进制形式表示,Math.clz32方法返回一个数的32位无符号整数形式有多少个前导0。 ~~~ Math.clz32(0) // 32 Math.clz32(1) // 31 Math.clz32(1000) // 22 ~~~ 上面代码中,0的二进制形式全为0,所以有32个前导0;1的二进制形式是0b1,只占1位,所以32位之中有31个前导0;1000的二进制形式是0b1111101000,一共有10位,所以32位之中有22个前导0。 对于小数,Math.clz32方法只考虑整数部分。 ~~~ Math.clz32(3.2) // 30 Math.clz32(3.9) // 30 ~~~ 对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。 ~~~ Math.clz32() // 32 Math.clz32(NaN) // 32 Math.clz32(Infinity) // 32 Math.clz32(null) // 32 Math.clz32('foo') // 32 Math.clz32([]) // 32 Math.clz32({}) // 32 Math.clz32(true) // 31 ~~~ ### Math.imul() Math.imul方法返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数。 ~~~ Math.imul(2, 4); // 8 Math.imul(-1, 8); // -8 Math.imul(-2, -2); // 4 ~~~ 如果只考虑最后32位(含第一个整数位),大多数情况下,`Math.imul(a, b)`与`a * b`的结果是相同的,即该方法等同于`(a * b)|0`的效果。之所以需要部署这个方法,是因为JavaScript有精度限制,超过2的53次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。 ~~~ (0x7fffffff * 0x7fffffff)|0 // 0 ~~~ 上面这个乘法算式,返回结果为0。但是由于这两个数的个位数都是1,所以这个结果肯定是不正确的。这个错误就是因为它们的乘积超过了2的53次方,JavaScript无法保存额外的精度,就把低位的值都变成了0。Math.imul方法可以返回正确的值1。 ~~~ Math.imul(0x7fffffff, 0x7fffffff) // 1 ~~~ ### Math.fround() Math.fround方法返回一个数的单精度浮点数形式。 ~~~ Math.fround(0); // 0 Math.fround(1); // 1 Math.fround(1.337); // 1.3370000123977661 Math.fround(1.5); // 1.5 Math.fround(NaN); // NaN ~~~ 对于整数来说,Math.fround方法返回结果不会有任何不同,区别主要是那些无法用64个二进制位精确表示的小数。这时,Math.fround方法会返回最接近这个小数的单精度浮点数。 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.fround = Math.fround || function(x) { return new Float32Array([x])[0]; }; ~~~ ### Math.hypot() Math.hypot方法返回所有参数的平方和的平方根。 ~~~ Math.hypot(3, 4); // 5 Math.hypot(3, 4, 5); // 7.0710678118654755 Math.hypot(); // 0 Math.hypot(NaN); // NaN Math.hypot(3, 4, 'foo'); // NaN Math.hypot(3, 4, '5'); // 7.0710678118654755 Math.hypot(-3); // 3 ~~~ 上面代码中,3的平方加上4的平方,等于5的平方。 如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回NaN。 ### 对数方法 ES6新增了4个对数相关方法。 (1) Math.expm1() `Math.expm1(x)`返回ex - 1。 ~~~ Math.expm1(-1); // -0.6321205588285577 Math.expm1(0); // 0 Math.expm1(1); // 1.718281828459045 ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.expm1 = Math.expm1 || function(x) { return Math.exp(x) - 1; }; ~~~ (2)Math.log1p() `Math.log1p(x)`方法返回1 + x的自然对数。如果x小于-1,返回NaN。 ~~~ Math.log1p(1); // 0.6931471805599453 Math.log1p(0); // 0 Math.log1p(-1); // -Infinity Math.log1p(-2); // NaN ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.log1p = Math.log1p || function(x) { return Math.log(1 + x); }; ~~~ (3)Math.log10() `Math.log10(x)`返回以10为底的x的对数。如果x小于0,则返回NaN。 ~~~ Math.log10(2); // 0.3010299956639812 Math.log10(1); // 0 Math.log10(0); // -Infinity Math.log10(-2); // NaN Math.log10(100000); // 5 ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.log10 = Math.log10 || function(x) { return Math.log(x) / Math.LN10; }; ~~~ (4)Math.log2() `Math.log2(x)`返回以2为底的x的对数。如果x小于0,则返回NaN。 ~~~ Math.log2(3); // 1.584962500721156 Math.log2(2); // 1 Math.log2(1); // 0 Math.log2(0); // -Infinity Math.log2(-2); // NaN Math.log2(1024); // 10 ~~~ 对于没有部署这个方法的环境,可以用下面的代码模拟。 ~~~ Math.log2 = Math.log2 || function(x) { return Math.log(x) / Math.LN2; }; ~~~ ### 三角函数方法 ES6新增了6个三角函数方法。 * Math.sinh(x) 返回x的双曲正弦(hyperbolic sine) * Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine) * Math.tanh(x) 返回x的双曲正切(hyperbolic tangent) * Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine) * Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine) * Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
';

字符串的扩展

最后更新于:2022-04-01 23:30:10

ES6加强了对Unicode的支持,并且扩展了字符串对象。 ## codePointAt() JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符。 ~~~ var s = "
';

变量的解构赋值

最后更新于:2022-04-01 23:30:08

## 数组的解构赋值 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 以前,为变量赋值,只能直接指定值。 ~~~ var a = 1; var b = 2; var c = 3; ~~~ ES6允许写成下面这样。 ~~~ var [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] ~~~ 如果解构不成功,变量的值就等于undefined。 ~~~ var [foo] = []; var [foo] = 1; var [foo] = false; var [foo] = NaN; var [bar, foo] = [1]; ~~~ 以上几种情况都属于解构不成功,foo的值都会等于undefined。这是因为原始类型的值,会自动转为对象,比如数值1转为`new Number(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 ~~~ 上面代码的两个例子,都属于不完全解构,但是可以成功。 如果对undefined或null进行解构,会报错。 ~~~ // 报错 let [foo] = undefined; let [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' ~~~ 注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。 ~~~ var [x = 1] = [undefined]; x // 1 var [x = 1] = [null]; x // null ~~~ 上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。 解构赋值不仅适用于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接口,都可以采用数组形式的解构赋值。 ~~~ function* fibs() { var a = 0; var b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } var [first, second, third, fourth, fifth, sixth] = fibs(); sixth // 5 ~~~ 上面代码中,fibs是一个Generator函数,原生具有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" let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; f // 'hello' l // 'world' ~~~ 和数组一样,解构也可以用于嵌套结构的对象。 ~~~ var obj = { p: [ "Hello", { y: "World" } ] }; var { p: [x, { y }] } = obj; x // "Hello" y // "World" ~~~ 对象的解构也可以指定默认值。 ~~~ var {x = 3} = {}; x // 3 var {x, y = 5} = {x: 1}; console.log(x, y) // 1, 5 var { message: msg = "Something went wrong" } = {}; console.log(msg); // "Something went wrong" ~~~ 默认值生效的条件是,对象的属性值严格等于undefined。 ~~~ var {x = 3} = {x: undefined}; x // 3 var {x = 3} = {x: null}; x // null ~~~ 上面代码中,如果x属性等于null,就不严格相等于undefined,导致默认值不会生效。 如果要将一个已经声明的变量用于解构赋值,必须非常小心。 ~~~ // 错误的写法 var x; {x} = {x:1}; // SyntaxError: syntax error ~~~ 上面代码的写法会报错,因为JavaScript引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。 ~~~ // 正确的写法 ({x} = {x:1}); ~~~ 上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。 ~~~ let { log, sin, cos } = Math; ~~~ 上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。 ## 字符串的解构赋值 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 ~~~ const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" ~~~ 类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。 ~~~ let {length : len} = 'hello'; len // 5 ~~~ ## 函数参数的解构赋值 函数的参数也可以使用解构。 ~~~ function add([x, y]){ return x + y; } add([1, 2]) // 3 ~~~ 上面代码中,函数add的参数实际上不是一个数组,而是通过解构得到的变量x和y。 函数参数的解构也可以使用默认值。 ~~~ 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指定默认值,所以会得到与前一种写法不同的结果。 ## 圆括号问题 解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。 由此带来的问题是,如果模式中出现圆括号怎么处理。ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。 但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。 ### 不能使用圆括号的情况 以下三种解构赋值不得使用圆括号。 (1)变量声明语句中,模式不能带有圆括号。 ~~~ // 全部报错 var [(a)] = [1]; var { x: (c) } = {}; var { o: ({ p: p }) } = { o: { p: 2 } }; ~~~ 上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。 (2)函数参数中,模式不能带有圆括号。 函数参数也属于变量声明,因此不能带有圆括号。 ~~~ // 报错 function f([(z)]) { return z; } ~~~ (3)不能将整个模式,或嵌套模式中的一层,放在圆括号之中。 ~~~ // 全部报错 ({ p: a }) = { p: 42 }; ([a]) = [5]; ~~~ 上面代码将整个模式放在模式之中,导致报错。 ~~~ // 报错 [({ p: a }), { x: c }] = [{}, {}]; ~~~ 上面代码将嵌套模式的一层,放在圆括号之中,导致报错。 ### 可以使用圆括号的情况 可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。 ~~~ [(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确 ~~~ 上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。 ## 用途 变量的解构赋值用途很多。 **(1)交换变量的值** ~~~ [x, y] = [y, x]; ~~~ 上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。 **(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, y, z]) { ... } f([1, 2, 3]) // 参数是一组无次序的值 function f({x, y, z}) { ... } f({x:1, y:2, z:3}) ~~~ **(4)提取JSON数据** 解构赋值对提取JSON对象中的数据,尤其有用。 ~~~ var 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接口,配合变量的解构赋值,获取键名和键值就非常方便。 ~~~ 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) { // ... } ~~~ **(7)输入模块的指定方法** 加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。 ~~~ const { SourceMapConsumer, SourceNode } = require("source-map"); ~~~
';

let和const命令

最后更新于:2022-04-01 23:30:05

## let命令 ### 基本用法 ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。 ~~~ { let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 1 ~~~ 上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。 for循环的计数器,就很合适使用let命令。 ~~~ for(let i = 0; i < arr.length; 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 ~~~ 如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。 ~~~ var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 ~~~ ### 不存在变量提升 let不像var那样,会发生“变量提升”现象。 ~~~ function do_something() { console.log(foo); // ReferenceError let foo = 2; } ~~~ 上面代码在声明foo之前,就使用这个变量,结果会抛出一个错误。 这也意味着typeof不再是一个百分之百安全的操作。 ~~~ if (1) { typeof x; // ReferenceError let x; } ~~~ 上面代码中,由于块级作用域内typeof运行时,x还没有值,所以会抛出一个ReferenceError。 只要块级作用域内存在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的“死区”。 有些“死区”比较隐蔽,不太容易发现。 ~~~ function bar(x=y, y=2) { return [x, y]; } bar(); // 报错 ~~~ 上面代码中,调用bar函数之所以报错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。 需要注意的是,函数的作用域是其声明时所在的作用域。如果函数A的参数是函数B,那么函数B的作用域不是函数A。 ~~~ let foo = 'outer'; function bar(func = x => foo) { let foo = 'inner'; console.log(func()); // outer } bar(); ~~~ 上面代码中,函数bar的参数func,默认是一个匿名函数,返回值为变量foo。这个匿名函数的作用域就不是bar。这个匿名函数声明时,是处在外层作用域,所以内部的foo指向函数体外的声明,输出outer。它实际上等同于下面的代码。 ~~~ let foo = 'outer'; let f = x => foo; function bar(func = f) { let foo = 'inner'; console.log(func()); // outer } bar(); ~~~ ### 不允许重复声明 let不允许在相同作用域内,重复声明同一个变量。 ~~~ // 报错 function () { let a = 10; var a = 1; } // 报错 function () { let a = 10; let a = 1; } ~~~ 因此,不能在函数内部重新声明参数。 ~~~ function func(arg) { let arg; // 报错 } function func(arg) { { let arg; // 不报错 } } ~~~ ## 块级作用域 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 (true) { const MAX = 5; } // 常量MAX在此处不可得 ~~~ 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 foo = {}; foo.prop = 123; foo.prop // 123 foo = {} // 不起作用 ~~~ 上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。 下面是另一个例子。 ~~~ const a = []; a.push("Hello"); // 可执行 a.length = 0; // 可执行 a = ["Dave"]; // 报错 ~~~ 上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。 如果真的想将对象冻结,应该使用Object.freeze方法。 ~~~ const foo = Object.freeze({}); foo.prop = 123; // 不起作用 ~~~ 上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用。 除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。 ~~~ var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, value) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); }; ~~~ ## 跨模块常量 上面说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量,可以采用下面的写法。 ~~~ // constants.js 模块 export const A = 1; export const B = 3; export const C = 4; // test1.js 模块 import * as constants from './constants'; console.log(constants.A); // 1 console.log(constants.B); // 3 // test2.js 模块 import {A, B} from './constants'; console.log(A); // 1 console.log(B); // 3 ~~~ ## 全局对象的属性 全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。在JavaScript语言中,所有全局变量都是全局对象的属性。 ES6规定,var命令和function命令声明的全局变量,属于全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。 ~~~ var a = 1; // 如果在node环境,可以写成global.a // 或者采用通用方法,写成this.a window.a // 1 let b = 1; window.b // undefined ~~~ 上面代码中,全局变量a由var命令声明,所以它是全局对象的属性;全局变量b由let命令声明,所以它不是全局对象的属性,返回undefined。
';

ECMAScript 6简介

最后更新于:2022-04-01 23:30:03

ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。Mozilla公司将在这个标准的基础上,推出JavaScript 2.0。 ES6的目标,是使得JavaScript语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。 标准的制定者有计划,以后每年发布一次标准,使用年份作为标准的版本。因为当前版本的ES6是在2015年发布的,所以又称ECMAScript 2015。 ## ECMAScript和JavaScript的关系 很多初学者感到困惑:ECMAScript和JavaScript到底是什么关系? 简单说,ECMAScript是JavaScript语言的国际标准,JavaScript是ECMAScript的实现。 要讲清楚这个问题,需要回顾历史。1996年11月,JavaScript的创造者Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript。这个版本就是ECMAScript 1.0版。 之所以不叫JavaScript,有两个原因。一是商标,Java是Sun公司的商标,根据授权协议,只有Netscape公司可以合法地使用JavaScript这个名字,且JavaScript本身也已经被Netscape公司注册为商标。二是想体现这门语言的制定者是ECMA,不是Netscape,这样有利于保证这门语言的开放性和中立性。因此,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现。在日常场合,这两个词是可以互换的。 ## ECMAScript的历史 1998年6月,ECMAScript 2.0版发布。 1999年12月,ECMAScript 3.0版发布,成为JavaScript的通行标准,得到了广泛支持。 2007年10月,ECMAScript 4.0版草案发布,对3.0版做了大幅升级,预计次年8月发布正式版本。草案发布后,由于4.0版的目标过于激进,各方对于是否通过这个标准,发生了严重分歧。以Yahoo、Microsoft、Google为首的大公司,反对JavaScript的大幅升级,主张小幅改动;以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。 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,在更远的将来再考虑推出。 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正式通过,成为国际标准。 ECMA的第39号技术专家委员会(Technical Committee 39,简称TC39)负责制订ECMAScript标准,成员包括Microsoft、Mozilla、Google等大公司。TC39的总体考虑是,ES5与ES3基本保持兼容,较大的语法修正和新功能加入,将由JavaScript.next完成。当时,JavaScript.next指的是ES6,第六版发布以后,就指ES7。TC39的判断是,ES5会在2013年的年中成为JavaScript开发的主流标准,并在此后五年中一直保持这个位置。 ## 部署进度 各大浏览器的最新版本,对ES6的支持可以查看[kangax.github.io/es5-compat-table/es6/](http://kangax.github.io/es5-compat-table/es6/)。随着时间的推移,支持度已经越来越高了,ES6的大部分特性都实现了。 Node.js和io.js(一个部署新功能更快的Node分支)是JavaScript语言的服务器运行环境。它们对ES6的支持度,比浏览器更高。通过它们,可以体验更多ES6的特性。 建议使用版本管理工具[nvm](https://github.com/creationix/nvm),来安装Node.js和io.js。不过,nvm不支持Windows系统,下面的操作可以改用[nvmw](https://github.com/hakobera/nvmw)或[nvm-windows](https://github.com/coreybutler/nvm-windows)代替。 安装nvm需要打开命令行窗口,运行下面的命令。 ~~~ $ curl -o- https://raw.githubusercontent.com/creationix/nvm//install.sh | bash ~~~ 上面命令的version number处,需要用版本号替换。本书写作时的版本号是v0.25.4。 该命令运行后,nvm会默认安装在用户主目录的`.nvm`子目录。然后,激活nvm。 ~~~ $ source ~/.nvm/nvm.sh ~~~ 激活以后,安装Node或io.js的最新版。 ~~~ $ nvm install node # 或 $ nvm install iojs ~~~ 安装完成后,就可以在各种版本的node之间自由切换。 ~~~ # 切换到node $ nvm use node # 切换到iojs $ nvm use iojs ~~~ 需要注意的是,Node.js对ES6的支持,需要打开harmony参数,iojs不需要。 ~~~ $ node --harmony # iojs不需要打开harmony参数 $ node ~~~ 上面命令执行后,就会进入REPL环境,该环境支持所有已经实现的ES6特性。 使用下面的命令,可以查看Node.js所有已经实现的ES6特性。 ~~~ $ node --v8-options | grep harmony --harmony_typeof --harmony_scoping --harmony_modules --harmony_symbols --harmony_proxies --harmony_collections --harmony_observation --harmony_generators --harmony_iteration --harmony_numeric_literals --harmony_strings --harmony_arrays --harmony_maths --harmony ~~~ 上面命令的输出结果,会因为版本的不同而有所不同。 我写了一个[ES-Checker](https://github.com/ruanyf/es-checker)模块,用来检查各种运行环境对ES6的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker),可以看到您的浏览器支持ES6的程度。运行下面的命令,可以查看本机支持ES6的程度。 ~~~ $ npm install -g es-checker $ es-checker ~~~ ## Babel转码器 [Babel](https://babeljs.io/)是一个广泛使用的ES6转码器,可以ES6代码转为ES5代码,从而在浏览器或其他环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。 ~~~ // 转码前 input.map(item => item + 1); // 转码后 input.map(function (item) { return item + 1; }); ~~~ 上面的原始代码用了箭头函数,这个特性还没有得到广泛支持,Babel将其转为普通函数,就能在现有的JavaScript环境执行了。 它的安装命令如下。 ~~~ $ npm install --global babel ~~~ Babel自带一个`babel-node`命令,提供支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。 ~~~ $ babel-node > > console.log([1,2,3].map(x => x * x)) [ 1, 4, 9 ] > ~~~ `babel-node`命令也可以直接运行ES6脚本。假定将上面的代码放入脚本文件`es6.js`。 ~~~ $ babel-node es6.js [1, 4, 9] ~~~ babel命令可以将ES6代码转为ES5代码。 ~~~ $ babel es6.js "use strict"; console.log([1, 2, 3].map(function (x) { return x * x; })); ~~~ `-o`参数将转换后的代码,从标准输出导入文件。 ~~~ $ babel es6.js -o es5.js # 或者 $ babel es6.js --out-file es5.js ~~~ `-d`参数用于转换整个目录。 ~~~ $ babel -d build-dir source-dir ~~~ 注意,`-d`参数后面跟的是输出目录。 如果希望生成source map文件,则要加上`-s`参数。 ~~~ $ babel -d build-dir source-dir -s ~~~ Babel也可以用于浏览器。 ~~~ ~~~ 上面代码中,`browser.js`是Babel提供的转换器脚本,可以在浏览器运行。用户的ES6脚本放在script标签之中,但是要注明`type="text/babel"`。 Babel配合Browserify一起使用,可以生成浏览器能够直接加载的脚本。 ~~~ $ browserify script.js -t babelify --outfile bundle.js ~~~ ## Traceur转码器 Google公司的[Traceur](https://github.com/google/traceur-compiler)转码器,也可以将ES6代码转为ES5代码。 ### 直接插入网页 Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加载Traceur库文件。 ~~~ ~~~ 接下来,就可以把ES6代码放入上面这些代码的下方。 ~~~ ~~~ 正常情况下,上面代码会在控制台打印出9。 注意,`script`标签的`type`属性的值是`module`,而不是`text/javascript`。这是Traceur编译器识别ES6代码的标识,编译器会自动将所有`type=module`的代码编译为ES5,然后再交给浏览器执行。 如果ES6代码是一个外部文件,也可以用`script`标签插入网页。 ~~~ ~~~ ### 在线转换 Traceur提供一个[在线编译器](http://google.github.io/traceur-compiler/demo/repl.html),可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。 上面的例子转为ES5代码运行,就是下面这个样子。 ~~~ ~~~ ### 命令行转换 作为命令行工具使用时,Traceur是一个Node.js的模块,首先需要用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.js环境的用法 Traceur的Node.js用法如下(假定已安装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); ~~~ ## ECMAScript 7 2013年3月,ES6的草案封闭,不再接受新功能了。新的功能将被加入ES7。 ES7可能包括的功能有: (1)**Object.observe**:用来监听对象(以及数组)的变化。一旦监听对象发生变化,就会触发回调函数。 (2)**Async函数**:在Promise和Generator函数基础上,提出的异步操作解决方案。 (3)**Multi-Threading**:多线程支持。目前,Intel和Mozilla有一个共同的研究项目RiverTrail,致力于让JavaScript多线程运行。预计这个项目的研究成果会被纳入ECMAScript标准。 (4)**Traits**:它将是“类”功能(class)的一个替代。通过它,不同的对象可以分享同样的特性。 其他可能包括的功能还有:更精确的数值计算、改善的内存回收、增强的跨站点安全、类型化的更贴近硬件的低级别操作、国际化支持(Internationalization Support)、更多的数据结构等等。 本书对于那些明确的、或者很有希望列入ES7的功能,尤其是那些Babel已经支持的功能,都将予以介绍。
';

前言

最后更新于:2022-04-01 23:30:01

> 来源:http://es6.ruanyifeng.com/ > 作者:阮一峰 《ECMAScript 6入门》是一本开源的JavaScript语言教程,全面介绍ECMAScript 6新引入的语法特性。 [![cover](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-23_55b08470b52a2.jpg)](http://es6.ruanyifeng.com/images/cover.jpg) 本书力争覆盖ES6与ES5的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。 本书为中级难度,适合已有一定JavaScript语言基础的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。 全书第一版已由电子工业出版社于2014年10月出版([版权页](http://es6.ruanyifeng.com/images/copyright.png),[内页1](http://es6.ruanyifeng.com/images/page1.png),[内页2](http://es6.ruanyifeng.com/images/page2.png)),铜版纸全彩印刷,附有索引。目前,网站的内容是第二版的初稿,预订2016年年初出版。感谢张春雨编辑支持我将全书开源的做法。如果您对本书感兴趣,建议考虑购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。 * [京东](http://item.jd.com/11526272.html) * [当当](http://product.dangdang.com/23546442.html) * [亚马逊](http://www.amazon.cn/%E5%9B%BE%E4%B9%A6/dp/B00MQKRLD6/) * [China-pub](http://product.china-pub.com/4284817) ## 版权许可 本书采用“保持署名—非商用”创意共享4.0许可证。 只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 详细的法律条文请参见[创意共享](http://creativecommons.org/licenses/by-nc/4.0/)网站。
';