导言:ECMAScript的演化不会中止,可是咱们彻底不必惧怕。除了ES6这个前所未有的版本带来了海量的信息和知识点之外,以后每一年一发的版本都仅仅带有少许的增量更新,一年更新的东西花半个小时就能搞懂了,彻底不必畏惧。本文将带您花大约一个小时左右的时间,迅速过一遍ES7,ES8,ES9的新特性。javascript
想追求更好的阅读体验,请移步原文地址java
题记:本文提供了一个在线PPT版本,方便您浏览 细解JAVASCRIPT ES7 ES8 ES9 新特性 在线PPT vergit
本文的大部份内容译自做者Axel Rauschmayer博士的网站,想了解更多关于做者的信息,能够浏览Exploring JS: JavaScript books for programmersgithub
TC39 (Technical Committee 39)。正则表达式
TC39 是推动 JavaScript 发展的委员会。其会员都是公司(其中主要是浏览器厂商)。TC39 按期召开会议,会议由会员公司的表明与特邀专家出席。会议纪录均可在网上查看,可让你对 TC39 如何工做有一个清晰的概念。express
颇有意思的是,TC39 实行的是协商一致的原则:经过一项决议必须获得每一位会员(公司表明)的同意。json
在2015年发布的 ECMAScript(ES6)新增内容不少,在 ES5 发布近 6 年(2009-11 至 2015-6)以后才将其标准化。两个发布版本之间时间跨度如此之大主要有两大缘由:api
所以,从 ECMAScript 2016(ES7)开始,版本发布变得更加频繁,每一年发布一个新版本,这么一来新增内容也会更小。新版本将会包含每一年截止时间以前完成的全部特性。数组
每一个 ECMAScript 特性的建议将会从阶段 0 开始, 而后通过下列几个成熟阶段。其中从一个阶段到下一个阶段必须通过 TC39 的批准。promise
当前的stage 0列表能够查看这里 --> Stage 0 Proposals
什么是 Proposal?一份新特性的正式建议文档。提案必须指明此建议的潜在问题,例如与其余特性之间的关联,实现难点等。
什么是 Draft?草案是规范的第一个版本。其与最终标准中包含的特性不会有太大差异。
草案以后,原则上只接受增量修改。这个阶段开始实验如何实现,实现形式包括polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如babel)
候选阶段,得到具体实现和用户的反馈。此后,只有在实现和使用过程当中出现了重大问题才会修改。至少要在一个浏览器中实现,提供polyfill或者babel插件。
已经准备就绪,该特性会出如今下个版本的ECMAScript规范之中。
当前的stage 1-3列表能够查看这里 --> ECMAScript proposals
Proposal | Author | Champion(s) | TC39 meeting notes | Expected Publication Year |
---|---|---|---|---|
Array.prototype.includes |
Domenic Denicola | Domenic Denicola Rick Waldron |
November 2015 | 2016 |
Exponentiation operator | Rick Waldron | Rick Waldron | January 2016 | 2016 |
Object.values /Object.entries |
Jordan Harband | Jordan Harband | March 2016 | 2017 |
String padding | Jordan Harband | Jordan Harband Rick Waldron |
May 2016 | 2017 |
Object.getOwnPropertyDescriptors |
Jordan Harband Andrea Giammarchi |
Jordan Harband Andrea Giammarchi |
May 2016 | 2017 |
Trailing commas in function parameter lists and calls | Jeff Morrison | Jeff Morrison | July 2016 | 2017 |
Async functions | Brian Terlson | Brian Terlson | July 2016 | 2017 |
Shared memory and atomics | Lars T Hansen | Lars T Hansen | January 2017 | 2017 |
Lifting template literal restriction | Tim Disney | Tim Disney | March 2017 | 2018 |
s (dotAll ) flag for regular expressions |
Mathias Bynens | Brian Terlson Mathias Bynens |
November 2017 | 2018 |
RegExp named capture groups | Gorkem Yakin Daniel Ehrenberg |
Daniel Ehrenberg Brian Terlson Mathias Bynens |
November 2017 | 2018 |
Rest/Spread Properties | Sebastian Markbåge | Sebastian Markbåge | January 2018 | 2018 |
RegExp Lookbehind Assertions | Gorkem Yakin Nozomu Katō Daniel Ehrenberg |
Daniel Ehrenberg Mathias Bynens |
January 2018 | 2018 |
RegExp Unicode Property Escapes | Mathias Bynens | Brian Terlson Daniel Ehrenberg Mathias Bynens |
January 2018 | 2018 |
Promise.prototype.finally |
Jordan Harband | Jordan Harband | January 2018 | 2018 |
Asynchronous Iteration | Domenic Denicola | Domenic Denicola | January 2018 | 2018 |
Optional catch binding |
Michael Ficarra | Michael Ficarra | May 2018 | 2019 |
JSON superset | Richard Gibson | Mark Miller Mathias Bynens |
May 2018 | 2019 |
ES7在ES6的基础上主要添加了两项内容:
includes() 方法用来判断一个数组是否包含一个指定的值,根据状况,若是包含则返回 true,不然返回false。
var array = [1, 2, 3]; console.log(array.includes(2)); // expected output: true var pets = ['cat', 'dog', 'bat']; console.log(pets.includes('cat')); // expected output: true console.log(pets.includes('at')); // expected output: false
Array.prototype.includes()方法接收两个参数:
当第二个参数被传入时,该方法会从索引处开始日后搜索(默认索引值为0)。若搜索值在数组中存在则返回true,不然返回false。 且看下面示例:
['a', 'b', 'c', 'd'].includes('b') // true ['a', 'b', 'c', 'd'].includes('b', 1) // true ['a', 'b', 'c', 'd'].includes('b', 2) // false
乍一看,includes的做用跟数组的indexOf重叠,为何要特地增长这么一个api呢?主要区别有如下几点:
var ary = [1]; if (ary.indexOf(1) !== -1) { console.log("数组存在1") } if (ary.includes(1)) { console.log("数组存在1") }
var ary1 = [NaN]; console.log(ary1.indexOf(NaN))//-1 console.log(ary1.includes(NaN))//true
var ary1 = new Array(3); console.log(ary1.indexOf(undefined));//-1 console.log(ary1.includes(undefined))//true
加/减法咱们一般都是用其中缀形式,直观易懂。在ECMAScript2016中,咱们可使用**
来替代Math.pow。
4 ** 3 // 64
效果等同于
Math.pow(4,3)
值得一提的是,做为中缀运算符,**还支持如下操做
let n = 4; n **= 3; // 64
在2017年1月的TC39会议上,ECMAScript 2017的最后一个功能“Shared memory and atomics”推动到第4阶段。这意味着它的功能集现已完成。
ECMAScript 2017特性一览
主要新功能:
次要新功能:
Async Functions也就是咱们常说的Async/Await,相信你们对于这个概念都已经不陌生了。Async/Await是一种用于处理JS异步操做的语法糖,能够帮助咱们摆脱回调地狱,编写更加优雅的代码。
通俗的理解,async关键字的做用是告诉编译器对于标定的函数要区别对待。当编译器遇到标定的函数中的await关键字时,要暂时中止运行,带到await标定的函数处理完毕后,再进行相应操做。若是该函数fulfiled了,则返回值是fulfillment value,不然获得的就是reject value。
下面经过拿普通的promise写法来对比,就很好理解了:
async function asyncFunc() { const result = await otherAsyncFunc(); console.log(result); } // Equivalent to: function asyncFunc() { return otherAsyncFunc() .then(result => { console.log(result); }); }
按顺序处理多个异步函数的时候优点更为明显:
async function asyncFunc() { const result1 = await otherAsyncFunc1(); console.log(result1); const result2 = await otherAsyncFunc2(); console.log(result2); } // Equivalent to: function asyncFunc() { return otherAsyncFunc1() .then(result1 => { console.log(result1); return otherAsyncFunc2(); }) .then(result2 => { console.log(result2); }); }
并行处理多个异步函数:
async function asyncFunc() { const [result1, result2] = await Promise.all([ otherAsyncFunc1(), otherAsyncFunc2(), ]); console.log(result1, result2); } // Equivalent to: function asyncFunc() { return Promise.all([ otherAsyncFunc1(), otherAsyncFunc2(), ]) .then([result1, result2] => { console.log(result1, result2); }); }
处理错误:
async function asyncFunc() { try { await otherAsyncFunc(); } catch (err) { console.error(err); } } // Equivalent to: function asyncFunc() { return otherAsyncFunc() .catch(err => { console.error(err); }); }
Async Functions如果要展开去讲,能够占用很大段的篇幅。鉴于本文是一篇介绍性文章,再次再也不进行深刻。
注,若是以前您没有接触过ArrayBuffer相关知识的话,建议您从内存管理速成教程系列漫画解说入门,强推:
A crash course in memory management
[A cartoon intro to ArrayBuffers and SharedArrayBuffers
](https://hacks.mozilla.org/201...
[Avoiding race conditions in SharedArrayBuffers with Atomics
](https://hacks.mozilla.org/201...
ECMAScript 2017 特性 SharedArrayBuffer 和 atomics”,由Lars T. Hansen设计。它引入了一个新的构造函数 SharedArrayBuffer 和 具备辅助函数的命名空间对象 Atomics。
在咱们开始以前,让咱们澄清两个类似但大相径庭的术语:并行(Parallelism) 和 并发(Concurrency) 。他们存在许多定义,我使用的定义以下
Web workers 将任务并行引入了 JavaScript :这些是相对重量级的进程。每一个 workers 都有本身的全局环境。默认状况下,不共享任何内容。 workers 之间的通讯(或在 workers 和主线程之间的通讯)发展:
共享阵列缓冲区是更高并发抽象的基本构建块。它们容许您在多个 workers 和主线程之间共享 SharedArrayBuffer 对象的字节(该缓冲区是共享的,用于访问字节,将其封装在一个 TypedArray 中)这种共享有两个好处:
你能够更快地在 workers 之间共享数据。
workers 之间的协调变得更简单和更快(与 postMessage() 相比)。
// main.js const worker = new Worker('worker.js'); // 要分享的buffer const sharedBuffer = new SharedArrayBuffer( // (A) 10 * Int32Array.BYTES_PER_ELEMENT); // 10 elements // 使用Worker共用sharedBuffer worker.postMessage({sharedBuffer}); // clone // 仅限本地使用 const sharedArray = new Int32Array(sharedBuffer); // (B)
建立一个共享数组缓冲区(Shared Array Buffers)的方法与建立普通的数组缓冲区(Array Buffer)相似:经过调用构造函数,并以字节的形式指定缓冲区的大小(行A)。你与 workers 共享的是 缓冲区(buffer) 。对于你本身的本地使用,你一般将共享数组缓冲区封装在 TypedArray 中(行B)。
workers的实现以下所列。
// worker.js self.addEventListener('message', function (event) { const {sharedBuffer} = event.data; const sharedArray = new Int32Array(sharedBuffer); // (A) // ··· });
构造函数:
建立一个 length 字节的 buffer(缓冲区)。
静态属性:
默认状况下返回 this。 覆盖以控制 slice() 的返回。
实例属性:
返回 buffer(缓冲区) 的字节长度。
建立一个新的 this.constructor[Symbol.species] 实例,并用字节填充从(包括)开始到(不包括)结束的索引。
举一个例子
// main.js sharedArray[1] = 11; sharedArray[2] = 22;
在单线程中,您能够从新排列这些写入操做,由于在中间没有读到任何内容。 对于多线程,当你指望以特定顺序执行写入操做时,就会遇到麻烦:
// worker.js while (sharedArray[2] !== 22) ; console.log(sharedArray[1]); // 0 or 11
Atomics 方法能够用来与其余 workers 进行同步。例如,如下两个操做可让你读取和写入数据,而且不会被编译器从新排列:
这个想法是使用常规操做读取和写入大多数数据,而 Atomics 操做(load ,store 和其余操做)可确保读取和写入安全。一般,您将使用自定义同步机制,例如锁,其实现基于Atomics。
这是一个很是简单的例子,它老是有效的:
// main.js console.log('notifying...'); Atomics.store(sharedArray, 0, 123); // worker.js while (Atomics.load(sharedArray, 0) !== 123) ; console.log('notified');
Atomic 函数的主要操做数必须是 Int8Array ,Uint8Array ,Int16Array ,Uint16Array ,Int32Array 或 Uint32Array 的一个实例。它必须包裹一个 SharedArrayBuffer 。
全部函数都以 atomically 方式进行操做。存储操做的顺序是固定的而且不能由编译器或 CPU 从新排序。
加载和存储
读取和返回 ta[index] 上的元素,返回数组指定位置上的值。
在 ta[index] 上写入 value,而且返回 value。
将 ta[index] 上的元素设置为 value ,而且返回索引 index 原先的值。
若是 ta[index] 上的当前元素为 expectedValue , 那么使用 replacementValue 替换。而且返回索引 index 原先(或者未改变)的值。
简单修改 TypeArray 元素
如下每一个函数都会在给定索引处更改 TypeArray 元素:它将一个操做符应用于元素和参数,并将结果写回元素。它返回元素的原始值。
执行 ta[index] += value 并返回 ta[index] 的原始值。
执行 ta[index] -= value 并返回 ta[index] 的原始值。
执行 ta[index] &= value 并返回 ta[index] 的原始值。
执行 ta[index] |= value 并返回 ta[index] 的原始值。
执行 ta[index] ^= value 并返回 ta[index] 的原始值。
等待和唤醒
若是 ta[index] 的当前值不是 value ,则返回 'not-equal'。不然继续等待,直到咱们经过 Atomics.wake() 唤醒或直到等待超时。 在前一种状况下,返回 'ok'。在后一种状况下,返回'timed-out'。timeout 以毫秒为单位。记住此函数执行的操做:“若是 ta[index] 为 value,那么继续等待” 。
唤醒等待在 ta[index] 上的 count workers。
Object.values() 方法返回一个给定对象本身的全部可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于for-in循环枚举原型链中的属性 )。
obj参数是须要待操做的对象。能够是一个对象,或者一个数组(是一个带有数字下标的对象,[10,20,30] -> {0: 10,1: 20,2: 30})。
const obj = { x: 'xxx', y: 1 }; Object.values(obj); // ['xxx', 1] const obj = ['e', 's', '8']; // 至关于 { 0: 'e', 1: 's', 2: '8' }; Object.values(obj); // ['e', 's', '8'] // 当咱们使用数字键值时,返回的是数字排序 // 根据键值排序 const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' }; Object.values(obj); // ['yyy', 'zzz', 'xxx'] Object.values('es8'); // ['e', 's', '8']
Object.entries 方法返回一个给定对象自身可遍历属性 [key, value] 的数组, 排序规则和 Object.values 同样。这个方法的声明比较琐碎:
const obj = { x: 'xxx', y: 1 }; Object.entries(obj); // [['x', 'xxx'], ['y', 1]] const obj = ['e', 's', '8']; Object.entries(obj); // [['0', 'e'], ['1', 's'], ['2', '8']] const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' }; Object.entries(obj); // [['1', 'yyy'], ['3', 'zzz'], ['10': 'xxx']] Object.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]
为 String 对象增长了 2 个函数:padStart 和 padEnd。
像它们名字那样,这几个函数的主要目的就是填补字符串的首部和尾部,为了使获得的结果字符串的长度能达到给定的长度。你能够经过特定的字符,或者字符串,或者默认的空格填充它。下面是函数的声明:
str.padStart(targetLength [, padString]) str.padEnd(targetLength [, padString])
这些函数的第一个参数是 targetLength(目标长度),这个是结果字符串的长度。第二个参数是可选的 padString(填充字符),一个用于填充到源字符串的字符串。默认值是空格。
'es8'.padStart(2); // 'es8' 'es8'.padStart(5); // ' es8' 'es8'.padStart(6, 'woof'); // 'wooes8' 'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8' 'es8'.padStart(7, '0'); // '0000es8' 'es8'.padEnd(2); // 'es8' 'es8'.padEnd(5); // 'es8 ' 'es8'.padEnd(6, 'woof'); // 'es8woo' 'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo' 'es8'.padEnd(7, '6'); // 'es86666'
getOwnPropertyDescriptors 方法返回指定对象全部自身属性的描述对象。属性描述对象是直接在对象上定义的,而不是继承于对象的原型。ES2017加入这个函数的主要动机在于方便将一个对象深度拷贝给另外一个对象,同时能够将getter/setter拷贝。声明以下:
Object.getOwnPropertyDescriptors(obj)
obj 是待操做对象。返回的描述对象键值有:configurable, enumerable, writable, get, set and value。
const obj = { get es7() { return 777; }, get es8() { return 888; } }; Object.getOwnPropertyDescriptor(obj); // { // es7: { // configurable: true, // enumerable: true, // get: function es7(){}, //the getter function // set: undefined // }, // es8: { // configurable: true, // enumerable: true, // get: function es8(){}, //the getter function // set: undefined // } // }
结尾逗号用代码展现很是明了:
// 参数定义时 function foo( param1, param2, ) {} // 函数调用时 foo( 'abc', 'def', ); // 对象中 let obj = { first: 'Jane', last: 'Doe', }; // 数组中 let arr = [ 'red', 'green', 'blue', ];
这个改动有什么好处呢?
[ 'foo' ]
修改成
[ 'foo', 'bar' ]
致使线条'foo'和线条'bar'被标记为已更改,即便惟一真正的变化是后一条线被添加。
ES9的新特性索引以下:
主要新功能:
新的正则表达式功能:
其余新功能:
ES6引入了同步迭代器,其工做原理以下:
示例:
const iterable = ['a', 'b']; const iterator = iterable[Symbol.iterator](); iterator.next() // { value: 'a', done: false } iterator.next() // { value: 'b', done: false } iterator.next() // { value: undefined, done: true }
先前的迭代方式是同步的,并不适用于异步数据源。例如,在如下代码中,readLinesFromFile()没法经过同步迭代传递其异步数据:
for (const line of readLinesFromFile(fileName)) { console.log(line); }
异步迭代器和常规迭代器的工做方式很是类似,可是异步迭代器涉及promise:
async function example() { // 普通迭代器: const iterator = createNumberIterator(); iterator.next(); // Object {value: 1, done: false} iterator.next(); // Object {value: 2, done: false} iterator.next(); // Object {value: 3, done: false} iterator.next(); // Object {value: undefined, done: true} // 异步迭代器: const asyncIterator = createAsyncNumberIterator(); const p = asyncIterator.next(); // Promise await p;// Object {value: 1, done: false} await asyncIterator.next(); // Object {value: 2, done: false} await asyncIterator.next(); // Object {value: 3, done: false} await asyncIterator.next(); // Object {value: undefined, done: true} }
异步迭代器对象的next()方法返回了一个Promise,解析后的值跟普通的迭代器相似。
用法:iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}}
const promises = [ new Promise(resolve => resolve(1)), new Promise(resolve => resolve(2)), new Promise(resolve => resolve(3)), ]; async function test() { for await (const p of promises) { console.log(p); } } test(); //1 ,2 3
这个就是咱们一般所说的rest参数和扩展运算符,这项特性在ES6中已经引入,可是ES6中的做用对象仅限于数组:
restParam(1, 2, 3, 4, 5); function restParam(p1, p2, ...p3) { // p1 = 1 // p2 = 2 // p3 = [3, 4, 5] } const values = [99, 100, -1, 48, 16]; console.log( Math.max(...values) ); // 100
在ES9中,为对象提供了像数组同样的rest参数和扩展运算符:
const obj = { a: 1, b: 2, c: 3 } const { a, ...param } = obj; console.log(a) //1 console.log(param) //{b: 2, c: 3} function foo({a, ...param}) { console.log(a); //1 console.log(param) //{b: 2, c: 3} }
//正则表达式命名捕获组 const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj[1]; // 1999 const month = matchObj[2]; // 12 const day = matchObj[3]; // 31
经过数字引用捕获组有几个缺点:
ES9中能够经过名称来识别捕获组:(?<year>[0-9]{4})
在这里,咱们用名称标记了前一个捕获组year。该名称必须是合法的JavaScript标识符(认为变量名称或属性名称)。匹配后,您能够经过访问捕获的字符串matchObj.groups.year来访问。
让咱们重写前面的代码:
const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj.groups.year; // 1999 const month = matchObj.groups.month; // 12 const day = matchObj.groups.day; // 31 // 使用解构语法更为简便 const {groups: {day, year}} = RE_DATE.exec('1999-12-31'); console.log(year); // 1999 console.log(day); // 31
能够发现,命名捕获组有如下优势:
该特性容许您使用\p{}
经过说起大括号内的Unicode字符属性来匹配字符,在正则表达式中使用标记 u
(unicode) 设置。
/^\p{White_Space}+$/u.test('\t \n\r') // true /^\p{Script=Greek}+$/u.test('μετά') // true
因为在Unicode里面,中文字符对应的Unicode Script是Han,因而咱们就能够用这个reg来匹配中文:
/\p{Script=Han}/u
这样咱们就能够不用记忆繁琐又很差记的/[\u4e00-\u9fa5]/
了,何况这个表达式已经有些年头了,说实话,后来又新增的属性为Han的字符并不在这个范围内,所以这个有年头reg并不必定好使。
我随便从网上找了一个Unicode8.0添加的中文字符“𬬭”,我测了一下两种reg的兼容性:
oldReg=/[\u4e00-\u9fa5]/ newReg=/\p{Script=Han}/u oldReg.test('abc') // false newReg.test('abc') // false oldReg.test('地平线') // true newReg.test('地平线') // true oldReg.test('𬬭') // false newReg.test('𬬭') // true
http://www.unicode.org/charts...
能够参考一下这个PDF,是Unicode的汉字全集,从524页9FA6至526页(最后一页)用旧匹配方式都没法生效。
Name:惟一名称,由大写字母,数字,连字符和空格组成。例如:
General_Category:对字符进行分类。例如:
White_Space:用于标记不可见的间距字符,例如空格,制表符和换行符。例如:
Age:引入字符的Unicode标准版本。例如:欧元符号€在Unicode标准的2.1版中添加。
Script:是一个或多个书写系统使用的字符集合。
例子:
\p{prop=value}
\P{prop=value}
\p{bin_prop}
\P{bin_prop}
/^\p{White_Space}+$/u.test('\t \n\r') //true
匹配字母:
/^\p{Letter}+$/u.test('πüé') //true
匹配希腊字母:
/^\p{Script=Greek}+$/u.test('μετά') //true
匹配拉丁字母:
/^\p{Script=Latin}+$/u.test('Grüße') //true
先来看下正则表达式先行断言是什么:
如获取货币的符号
const noReLookahead = /\D(\d+)/, reLookahead = /\D(?=\d+)/, match1 = noReLookahead.exec('$123.45'), match2 = reLookahead.exec('$123.45'); console.log(match1[0]); // $123 console.log(match2[0]); // $
在ES9中能够容许反向断言:
const reLookahead = /(?<=\D)[\d\.]+/; match = reLookahead.exec('$123.45'); console.log(match[0]); // 123.45
使用?<=进行反向断言,可使用反向断言获取货币的价格,而忽略货币符号。
正则表达式中点.匹配除回车外的任何单字符,标记s改变这种行为,容许行终止符的出现,例如:
/hello.world/.test('hello\nworld'); // false /hello.world/s.test('hello\nworld'); // true
这个基本没什么好讲的,看名字就能看懂了。其用法以下:
promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
finally的回调总会被执行。
ES2018 移除对 ECMAScript 在带标签的模版字符串中转义序列的语法限制。
以前,u开始一个 unicode 转义,x开始一个十六进制转义,后跟一个数字开始一个八进制转义。这使得建立特定的字符串变得不可能,例如Windows文件路径 C:uuuxxx111。
要取消转义序列的语法限制,可在模板字符串以前使用标记函数String.raw:
`\u{54}` // "T" String.raw`\u{54}` // "\u{54}"
ECMAScript的演化不会中止,可是咱们彻底不必惧怕。除了ES6这个前所未有的版本带来了海量的信息和知识点之外,以后每一年一发的版本都仅仅带有少许的增量更新,一年更新的东西花半个小时就能搞懂了,彻底不必畏惧。
Stay hungry. Stay foolish.