深入JavaScript(12)ES6规范详解、ESNext、babel、编译器
侧重点在第2章与第4章
1.ECMAScript发展史
对js的发展感兴趣的话,可以大概看看,主要需要具备的知识不在此章节。
1.1 JavaScript的诞⽣
JavaScript 因为互联⽹⽽⽣,紧跟着浏览器的出现⽽问世。回顾它的历史,就要从浏览器的历史讲起。
1990年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最⼤的电脑⽹络——互联⽹的基础上,发明了万维⽹(World Wide Web),从此可以在⽹上浏览⽹⻚⽂件。最早的⽹⻚只能在操作系统的终端⾥浏览,也就是说只能使⽤命令⾏操作,⽹⻚都是在字符窗⼝中显示,这当然⾮常不⽅便。
1992年底,美国国家超级电脑应⽤中⼼(NCSA)开始开发⼀个独⽴的浏览器,叫做 Mosaic。这是⼈类历史上第⼀个浏览器,从此⽹⻚可以在图形界⾯的窗⼝浏览。
1994年10⽉,NCSA 的⼀个主要程序员 Marc Andreessen 联合⻛险投资家 Jim Clark,成⽴了 Mosaic通信公司(Mosaic Communications),不久后改名为 Netscape。这家公司的⽅向,就是在 Mosaic 的基础上,开发⾯向普通⽤户的新⼀代的浏览器 Netscape Navigator。
1994年12⽉,Navigator 发布了1.0版,市场份额⼀举超过90%。
Netscape 公司很快发现,Navigator 浏览器需要⼀种可以嵌⼊⽹⻚的脚本语⾔,⽤来控制浏览器⾏为。
当时,⽹速很慢⽽且上⽹费很贵,有些操作不宜在服务器端完成。⽐如,如果⽤户忘记填写“⽤户名”,就点了“发送”按钮,到服务器再发现这⼀点就有点太晚了,最好能在⽤户发出数据之前,就告诉⽤户“请填写⽤户名”。这就需要在⽹⻚中嵌⼊⼩程序,让浏览器检查每⼀栏是否都填写了。
管理层对这种浏览器脚本语⾔的设想是:功能不需要太强,语法较为简单,容易学习和部署。那⼀年,正逢 Sun 公司的 Java 语⾔问世,市场推⼴活动⾮常成功。Netscape 公司决定与 Sun 公司合作,浏览器⽀持嵌⼊ Java ⼩程序(后来称为 Java applet)。但是,浏览器脚本语⾔是否就选⽤ Java,则存在争论。后来,还是决定不使⽤ Java,因为⽹⻚⼩程序不需要 Java 这么“重”的语法。但是,同时也决定脚本语⾔的语法要接近 Java,并且可以⽀持 Java 程序。这些设想直接排除了使⽤现存语⾔,⽐如 Perl、Python 和 TCL。
1995年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种⽹⻚脚本语⾔。Brendan Eich 有很强的函数式编程背景,希望以 Scheme 语⾔(函数式语⾔⿐祖 LISP 语⾔的⼀种⽅⾔)为蓝本,实现这种新语⾔。
1995年5⽉,Brendan Eich 只⽤了10天,就设计完成了这种语⾔的第⼀版。它是⼀个⼤杂烩,语法有多个来源。
-
基本语法:借鉴 C 语⾔和 Java 语⾔。
-
数据结构:借鉴 Java 语⾔,包括将值分成原始值和对象两⼤类。
-
函数的⽤法:借鉴 Scheme 语⾔和 Awk 语⾔,将函数当作第⼀等公⺠,并引⼊闭包。
-
原型继承模型:借鉴 Self 语⾔(Smalltalk 的⼀种变种)。
-
正则表达式:借鉴 Perl 语⾔。
-
字符串和数组处理:借鉴 Python 语⾔。
为了保持简单,这种脚本语⾔缺少⼀些关键的功能,⽐如块级作⽤域、模块、⼦类型(subtyping)等等,但是可以利⽤现有功能找出解决办法。这种功能的不⾜,直接导致了后来 JavaScript 的⼀个显著特点:对于其他语⾔,你需要学习语⾔的各种功能,⽽对于 JavaScript,你常常需要学习各种解决问题的模式。⽽且由于来源多样,从⼀开始就注定,JavaScript 的编程⻛格是函数式编程和⾯向对象编程的⼀种混合体。
Netscape 公司的这种浏览器脚本语⾔,最初名字叫做 Mocha,1995年9⽉改为 LiveScript。12⽉,Netscape 公司与 Sun 公司(Java 语⾔的发明者和所有者)达成协议,后者允许将这种语⾔叫做JavaScript。这样⼀来,Netscape 公司可以借助 Java 语⾔的声势,⽽ Sun 公司则将⾃⼰的影响⼒扩展到了浏览器。
之所以起这个名字,并不是因为 JavaScript 本身与 Java 语⾔有多么深的关系(事实上,两者关系并不深,详⻅下节),⽽是因为 Netscape 公司已经决定,使⽤ Java 语⾔开发⽹络应⽤程序,JavaScript 可以像胶⽔⼀样,将各个部分连接起来。当然,后来的历史是 Java 语⾔的浏览器插件失败了,JavaScript反⽽发扬光⼤。
1995年12⽉4⽇,Netscape 公司与 Sun 公司联合发布了 JavaScript 语⾔,对外宣传 JavaScript 是Java 的补充,属于轻量级的 Java,专⻔⽤来操作⽹⻚。
1996年3⽉,Navigator 2.0 浏览器正式内置了 JavaScript 脚本语⾔。
1.2 JavaScript 与 ECMAScript 的关系
1996年11⽉,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织ECMA,希望这种语⾔能够成为国际标准。次年,ECMA 发布 262 号标准⽂件(ECMA-262)的第⼀版,规定了浏览器脚本语⾔的标准,并将这种语⾔称为 ECMAScript ,这个版本就是1.0版。
该标准从⼀开始就是针对 JavaScript 语⾔制定的,但是之所以不叫 JavaScript ,有两个原因。⼀是商标,Java是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使⽤ JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。⼆是想体现这⻔语⾔的制定者是 ECMA ,不是 Netscape ,这样有利于保证这⻔语⾔的开放性和中⽴性。
因此,ECMAScript 和 JavaScript 的关系是,ECMAScript 是⼀个简单的 JavaScript 标准规范,JavaScript 是 ECMAScript 的⼀种实现(另外的 ECMAScript ⽅⾔还有 JScript 和 ActionScript )。并且,ECMAScript 持续不断的为 JavaScript 添加新功能。
到⽬前为⽌,像我们常问的ES6(ECMAScript2015)是在15年发布的第6版,以下是发布节奏
所以到⽬前位置,我们⼀般是ESX⽤来指代ES201(X+1)的形式来指代。但其实两者没有什么必然关系,可能会随着TC39的发版节奏改变⽽改变,针对当前位置,还未⽣效但在Stage1后的阶段,我们⼀般统称为ESNext。
ECMAScript,新功能的演进是由⼀个叫 TC39 这么个组织在统筹协调和推进的。
⼀般新特性会由社区先提案,被采纳后开始进⼊下⼀流程。⼀个提案到最终落地到成为标准,需要经过⼏个阶段(stage)。
⽬前采纳进⼊正式流程中的提案可在 tc39/proposals 查看到。
以下是各阶段及含义的描述:
-
Stage 0/Strawperson: 潜在的可能被纳⼊规范的⼀些想法;
-
Stage 1/Proposal:为该想法设想⼀些适⽤场景,可能的 case。提出解决实现⽅案以及可能的变更;
-
Stage 2/Draft:经过上⼀步验证讨论后,这⼀阶段开始起草语⾔层⾯的语义语法,准备正式的规范⽂档;
-
Stage 3/Candidate:提案进⼊到了候选阶段。开始接收⼀些反馈对提案进⾏完善;
-
Stage 4/Finished:可以被纳⼊到正式的 ECMAScript 语⾔规范中了;
2. ES6新增API
ES6完整内容学习可查阅阮一峰老师的:es6.ruanyifeng.com/
ES6内容较多,这里主要列举一些常用的:
-
类;
-
模块化;
-
箭头函数;
-
函数参数默认值;
-
模板字符串;
-
解构赋值;
-
延展操作符;
-
对象属性简写;
-
Promise;
-
Let与Const;
2.1 类(class)
对熟悉Java,object-c,c#等纯⾯向对象语⾔的开发者来说,都会对class有⼀种特殊的情怀。ES6 引⼊了class(类),让JavaScript的⾯向对象编程变得更加简单和易于理解。
class Animal {
constructor(name, color) {
this.name = name;
this.color = color;
}
toString() {
console.log('name:' + this.name + ',color:' + this.color);
}
}
var animal = new Animal('dog', 'white');
animal.toString();
console.log(animal);
console.log(animal.hasOwnProperty('name'));
console.log(animal.hasOwnProperty('toString'));
console.log(animal.__proto__.hasOwnProperty('toString'));
class Cat extends Animal {
constructor(action) {
// 如果没有置顶consructor,默认带super函数的constructor将会被添加
super('cat', 'white');
this.action = action;
}
toString() {
console.log(super.toString());
}
}
var cat = new Cat('catch')
cat.toString();
console.log(cat instanceof Cat);
console.log(cat instanceof Animal);
2.2 模块化(Module)
ES5不⽀持原⽣的模块化,在ES6中模块作为重要的组成部分被添加进来。模块的功能主要由 export 和import 组成。每⼀个模块都有⾃⼰单独的作⽤域,模块之间的相互调⽤关系是通过 export 来规定模块对外暴露的接⼝,通过import来引⽤其它模块提供的接⼝。同时还为模块创造了命名空间,防⽌函数的命名冲突。
2.2.1 导出(export)
ES6允许在⼀个模块中使⽤export来导出多个变量或函数。
导出变量
export var name = 'Rainbow'
注意:ES6不仅⽀持变量的导出,也⽀持常量的导出。
例如:export const sqrt =Math.sqrt; //导出常量
ES6将⼀个⽂件视为⼀个模块,上⾯的模块通过 export 向外输出了⼀个变量。⼀个模块也可以同时往外⾯输出多个变量。
var name = 'Rainbow';
var age = '24';
export {name, age};
// 导出函数
export function myModule(someArg) {
return someArg;
}
2.2.2 导入(import)
定义好模块的输出以后就可以在另外⼀个模块通过import引⽤。
import { myModule } from 'myModule';
import { name, age } from 'test';
注意:⼀条import 语句可以同时导⼊默认函数和其它变量。
import defaultMethod, { otherMethod } from 'xxx.js';
2.3 箭头(Arrow)函数
这是ES6中最令⼈激动的特性之⼀。 => 不只是关键字function的简写,它还带来了其它好处。箭头函数与包围它的代码共享同⼀个 this,能帮你很好的解决this的指向问题。
2.3.1 箭头函数的结构
箭头函数的箭头=>之前是⼀个空括号、单个的参数名、或⽤括号括起的多个参数名,⽽箭头之后可以是⼀个表达式(作为函数的返回值),或者是⽤花括号括起的函数体(需要⾃⾏通过return来返回值,否则返回的是undefined)。
() => 1;
v => v + 1;
(a, b) => a + b;
() => {
alert("foo");
};
e => {
if (e == 0) {
return 0;
}
return 1000 / e;
}
注意:不论是箭头函数还是bind,每次被执⾏都返回的是⼀个新的函数引⽤,因此如果你还需要函数的 引⽤去做⼀些别的事情(譬如卸载监听器),那么你必须⾃⼰保存这个引⽤。
2.3.2 卸载监听器时的陷阱
- 错误的做法
class PauseMenu extends React.Component {
componentWillMount() {
AppStateIOS.addEventListener('change', this.onAppPaused.bind(this));
}
componentWillUnmount() {
AppStateIOS.removeEventListener('change', this.onAppPaused.bind(this));
}
onAppPaused(event) { }
}
- 正确的做法
class PauseMenu extends React.Component {
constructor(props) {
super(props);
this._onAppPaused = this.onAppPaused.bind(this);
}
componentWillMount() {
AppStateIOS.addEventListener('change', this._onAppPaused);
}
componentWillUnmount() {
AppStateIOS.removeEventListener('change', this._onAppPaused);
}
onAppPaused(event) { }
}
我们还可以:
class PauseMenu extends React.Component {
componentWillMount() {
AppStateIOS.addEventListener('change', this.onAppPaused);
}
componentWillUnmount() {
AppStateIOS.removeEventListener('change', this.onAppPaused);
}
onAppPaused = (event) => { }
}
2.4 函数⼊参默认值
ES6⽀持在定义函数的时候为其设置默认值:
function foo(height = 50, color = 'red') { }
不使⽤默认值:
function foo(height, color) {
var height = height || 50;
var color = color || 'red';
}
这样写⼀般没问题,但当 参数的布尔值为false时,就会有问题了。⽐如,我们这样调⽤foo函数:
foo(0, "")
因为 0的布尔值为false,这样height的取值将是50。同理color的取值为‘red’。
所以说, 函数参数默认值不仅能是代码变得更加简洁⽽且能规避⼀些问题。
2.5 模板字符串
ES6⽀持 模板字符串,使得字符串的拼接更加的简洁、直观。
不使⽤模板字符串:
var name = 'Your name is ' + first + ' ' + last + '.'
使⽤模板字符串:
var name = `Your name is ${first} ${last}.`
在ES6中通过 ${ } 就可以完成字符串的拼接,只需要将变量放在⼤括号之中。
2.6 解构
解构赋值语法是JavaScript的⼀种表达式,可以⽅便的从数组或者对象中快速提取值赋给定义的变量。
2.6.1 获取数组中的值
从数组中获取值并赋值到变量中,变量的顺序与数组中对象顺序对应。
var foo = ["one", "two", "three", "four"];
var [one, two, three] = foo;
console.log(one);
console.log(two);
console.log(three);
//如果你要忽略某些值,你可以按照下⾯的写法获取你想要的值var [first, , , last] = foo; console.log(first); console.log(last);
//你也可以这样写var a, b;
var [a, b] = [1, 2];
console.log(a);
console.log(b);
如果没有从数组中的获取到值,你可以为变量设置⼀个默认值。
var [a = 5, b = 7] = [1];
console.log(a); // 1
console.log(b); // 7
通过解构赋值可以⽅便的交换两个变量的值。
var a = 1;
var b = 3;
[a, b] = [b, a];
console.log(a);
console.log(b);
2.6.2 获取对象中的值
const student = {
name: 'lyl',
age: '18',
city: 'Shanghai'
};
const { name, age, city } = student;
console.log(name);
console.log(age);
console.log(city);
2.7 延展操作符(Spread operator)
延展操作符 ...
可以在函数调⽤/数组构造时, 将数组表达式或者string在语法层⾯展开;
还可以在构造对象时, 将对象表达式按key-value的⽅式展开。
2.7.1 用法
- 函数调用
myFunction(...iterableObj);
- 数组构造或字符串
const iterableObj = [1, 2, 3]
console.log([...iterableObj, '4', ...'hello', 6]);
// [1, 2, 3, '4', 'h', 'e', 'l', 'l', 'o', 6]
- 构造对象时,进⾏克隆或者属性拷⻉(ECMAScript 2018规范新增特性)
let objClone = { ...obj };
2.7.2 应用场景
- 在函数调⽤时使⽤延展操作符
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum.apply(null, numbers));
console.log(sum(...numbers));
- 构造数组
没有展开语法的时候,只能组合使⽤ push,splice,concat 等⽅法,来将已有数组元素变成新数组的⼀部分。有了展开语法, 构造新数组会变得更简单、更优雅:
const stuendts = ['Jine', 'Tom'];
const persons = ['Tony', ...stuendts, 'Aaron', 'Anna'];
console.log(persons)
和参数列表的展开类似, ... 在构造数组时, 可以在任意位置多次使⽤。
- 数组拷贝
var arr1 = [1, 2, 3];
var arr2 = [...arr1];
arr2.push(4);
console.log(arr1) // [ 1, 2, 3 ]
console.log(arr2) // [ 1, 2, 3, 4 ]
展开语法和 Object.assign() ⾏为⼀致, 执⾏的都是浅拷⻉(只遍历⼀层)。
- 在ECMAScript 2018中延展操作符增加了对对象的⽀持
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
var clonedObj = { ...obj1 };
var mergedObj = { ...obj1, ...obj2 };
- 在React中的应⽤
通常我们在封装⼀个组件时,会对外公开⼀些 props ⽤于实现功能。⼤部分情况下在外部使⽤都应显式的传递 props 。但是当传递⼤量的props时,会⾮常繁琐,这时我们可以使⽤ ...(延展操作符,⽤于取出参数对象的所有可遍历属性) 来进⾏传递。
<CustomComponent name='Jine' age={21} />
使⽤ ... ,等同于上⾯的写法
const params = {
name: 'Jine',
age: 21
}
<CustomComponent {...params} />
配合解构赋值避免传⼊⼀些不需要的参数
var params = {
name: '123',
title: '456',
type: 'aaa'
}
var { type, ...other } = params;
<CustomComponent type='normal' number={2} {...other} />
// 等同于<CustomComponent type='normal' number={2} name='123' title='456' />
2.8 对象属性简写
const name = 'Ming',
age = '18',
city = 'Shanghai';
const student = {
name,
age,
city
};
console.log(student);
对象中直接写变量,⾮常简洁。
2.9 Promise
Promise 是异步编程的⼀种解决⽅案,⽐传统的解决⽅案callback更加的优雅。它最早由社区提出和实现的,ES6 将其写进了语⾔标准,统⼀了⽤法,原⽣提供了Promise对象。
不使⽤ES6
嵌套两个setTimeout回调函数:
setTimeout(function () {
console.log('Hello');
setTimeout(function () {
console.log('Hi');
}, 1000);
}, 1000);
使⽤ES6
var waitSecond = new Promise(function (resolve, reject) {
setTimeout(resolve, 1000);
});
waitSecond.then(function () {
console.log("Hello");
}).then(function () {
console.log("Hi");
});
上⾯的的代码使⽤两个then来进⾏异步编程串⾏化,避免了回调地狱:
2.10 ⽀持let与const
在之前JS是没有块级作⽤域的,const与let填补了这⽅便的空⽩,const与let都是块级作⽤域。
使⽤var定义的变量为函数级作⽤域:
{ var a = 10 }
console.log(a)
使⽤let与const定义的变量为块级作⽤域:
{ let a = 10 }
console.log(a) // ReferenceError: a is not defined
3. ESNext新增API
此处包含ES6以后的版本
3.1 ES7新特性(2016)
ES2016添加了两个⼩的特性来说明标准化过程:
-
数组includes()⽅法,⽤来判断⼀个数组是否包含⼀个指定的值,根据情况,如果包含则返回true,否则返回false。
-
a ** b指数运算符,它与 Math.pow(a, b)相同。
3.1.1 Array.prototype.includes()
includes() 函数⽤来判断⼀个数组是否包含⼀个指定的值,如果包含则返回 true,否则返回 false。
includes 函数与 indexOf 函数很相似,下⾯两个表达式是等价的:
arr.includes(x) arr.indexOf(x) >= 0
接下来我们来判断数字中是否包含某个元素:
在ES7之前的做法
使⽤ indexOf()验证数组中是否存在某个元素,这时需要根据返回值是否为-1来判断:
let arr = ['react', 'angular', 'vue'];
if (arr.indexOf('react') !== -1) {
console.log('react存在');
}
使⽤ES7的includes()
let arr = ['react', 'angular', 'vue'];
if (arr.includes('react')) {
console.log('react存在');
}
使⽤includes()验证数组中是否存在某个元素,这样更加直观简单
3.1.2 指数操作符
在ES7中引⼊了指数运算符 **, **具有与 Math.pow(..)等效的计算结果。
不使⽤指数操作符
使⽤⾃定义的递归函数calculateExponent或者Math.pow()进⾏指数运算:
function calculateExponent(base, exponent) {
if (exponent === 1) {
return base;
} else {
return base * calculateExponent(base, exponent - 1);
}
}
console.log(calculateExponent(2, 10));
console.log(Math.pow(2, 10));
使⽤指数操作符
使⽤指数运算符**,就像+、-等操作符⼀样:
console.log(2**10)
3.2 ES8 新特性(2017)
-
async/await;
-
Object.values();
-
Object.entries();
-
String padding: padStart()和 padEnd(),填充字符串达到当前⻓度;
-
函数参数列表结尾允许逗号;
-
Object.getOwnPropertyDescriptors();
-
ShareArrayBuffer和 Atomics对象,⽤于从共享内存位置读取和写⼊;
3.2.1 async/await
ES2018引⼊异步迭代器(asynchronous iterators),这就像常规迭代器,除了 next()⽅法返回⼀个Promise。
因此 await可以和 for...of循环⼀起使⽤,以串⾏的⽅式运⾏异步操作。例如:
async function process(array) {
for await (let i of array) {
setTimeout(() => {
doSomething(i)
}, Math.random() * 1000);
}
}
function doSomething(i) {
console.log(i);
}
process([1, 2, 3])
// 根据定时器中的时间数,时间数越小,越早打印
// 若定时器计数固定,那就是按顺序打印
3.2.2 Object.values()
Object.values()是⼀个与 Object.keys()类似的新函数,但返回的是Object⾃身属性的所有值,不包括继承的值。
假设我们要遍历如下对象 obj的所有值:
const obj = {a: 1, b: 2, c: 3};
const values = Object.values(obj);
console.log(values);
3.2.3 Object.entries()
Object.entries()函数返回⼀个给定对象⾃身可枚举属性的键值对的数组。
const obj = { a: 1, b: 2, c: 3 };
for (let [key, value] of Object.entries(obj)) {
console.log(`key: ${key}, value: ${value}`)
}
console.log(Object.entries(obj));
// key: a, value: 1
// key: b, value: 2
// key: c, value: 3
// [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
3.2.4 String padding
在ES8中String新增了两个实例函数 String.prototype.padStart和 String.prototype.padEnd,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
String.padStart(targetLength,[padString])
-
targetLength:当前字符串需要填充到的⽬标⻓度。如果这个数值⼩于当前字符串的⻓度,则返回当前字符串本身;
-
padString:(可选)填充字符串。如果字符串太⻓,使填充后的字符串⻓度超过了⽬标⻓度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 " ";
console.log('9.0'.padStart(4, '10'))
console.log('0.0'.padStart(20))
// 19.0
// 0.0
String.padEnd(targetLength,padString])
-
targetLength:当前字符串需要填充到的⽬标⻓度。如果这个数值⼩于当前字符串的⻓度,则返回当前字符串本身。
-
padString:(可选) 填充字符串。如果字符串太⻓,使填充后的字符串⻓度超过了⽬标⻓度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 " ";
console.log('0.0'.padEnd(4, '123'))
console.log('0.0'.padEnd(10, '1123'))
// 0.01
// 0.01123112
3.2.5 函数参数列表结尾允许逗号
主要作⽤是⽅便使⽤git进⾏多⼈协作开发时修改同⼀个函数减少不必要的⾏变更。
3.2.6 Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
函数⽤来获取⼀个对象的所有⾃身属性的描述符,如果没有任何⾃身属性,则返回空对象。
const obj2 = {
name: 'Jine',
get age() {
return '18'
}
};
console.log(Object.getOwnPropertyDescriptors(obj2));
// {
// name: {
// value: 'Jine',
// writable: true,
// enumerable: true,
// configurable: true
// },
// age: {
// get: [Function: get age],
// set: undefined,
// enumerable: true,
// configurable: true
// }
// }
obj2.age = 20
console.log(obj2.age); // 18
3.2.7 SharedArrayBuffer对象
SharedArrayBuffer 对象⽤来表示⼀个通⽤的,固定⻓度的原始⼆进制数据缓冲区,类似于 ArrayBuffer对象,它们都可以⽤来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。
new SharedArrayBuffer(length)
3.3 ES9新特性(2018)
3.3.1 异步迭代
在 async/await的某些时刻,你可能尝试在同步循环中调⽤异步函数。例如:
async function process(array) {
for (let i of array) {
await doSomething(i);
}
}
async function process(array) {
array.forEach(async (i) => {
await doSomething(i);
});
}
这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调⽤完成。
ES2018引⼊异步迭代器(asynchronous iterators),这就像常规迭代器,除了 next()⽅法返回⼀个Promise。因此 await可以和 for...of循环⼀起使⽤,以串⾏的⽅式运⾏异步操作。例如:
async function doSomething(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i)
}, 1000);
})
}
async function process(array) {
for await (let i of array) {
console.log(await doSomething(i));
}
console.log(1548);
}
process([1, 2, 3])
// 1
// 2
// 3
// 1548
3.3.2 Promise.finally()
⼀个Promise调⽤链要么成功到达最后⼀个 .then(),要么失败触发 .catch()。在某些情况下,你想要在⽆论Promise运⾏成功还是失败,运⾏相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()允许你指定最终的逻辑:
function doSomething() {
doSomething1().then(doSomething2).then(doSomething3).
catch((err) => {
console.log(err);
}).
finally(() => { });
}
function doSomething2() { }
function doSomething3() { }
3.3.3 Rest/Spread 属性
ES2015引⼊了Rest参数和扩展运算符。三个点(...)仅⽤于数组。Rest参数语法允许我们将⼀个不定数量的参数表示为⼀个数组。
restParam(1, 2, 3, 4, 5);
function restParam(p1, p2, ...p3) { } // p2 = 2
展开操作符以相反的⽅式⼯作,将数组转换成可传递给函数的单独参数。例如 Math.max()返回给定数字中的最⼤值:
const values = [99, 100, -1, 48, 16];
console.log(Math.max(...values)); // 100
ES2018为对象解构提供了和数组⼀样的Rest参数和展开操作符,⼀个简单的例⼦:
const myObject = {
a: 1,
b: 2,
c: 3
};
const {
a,
...x
} = myObject; // x = { b: 2, c: 3 }
跟数组⼀样,Rest参数只能在声明的结尾处使⽤。此外,它只适⽤于每个对象的顶层,如果对象中嵌套对象则⽆法适⽤。
扩展运算符可以在其他对象内使⽤,例如:
const obj1 = {
a: 1,
b: 2,
c: 3
};
const obj2 = {
...obj1,
z: 26
};
可以使⽤扩展运算符拷⻉⼀个对象,像是这样 obj2 ={...obj1},但是 这只是⼀个对象的浅拷⻉。另外,如果⼀个对象A的属性值是对象B,那么在克隆后的对象cloneB中,该属性指向对象B。
const obj1 = {
a: 1,
b: 2,
c: 3
};
const obj2 = {
obj1,
z: 26
};
obj2.obj1.a = 22
console.log(obj2);
console.log(obj1);
// { obj1: { a: 22, b: 2, c: 3 }, z: 26 }
// { a: 22, b: 2, c: 3 }
3.3.4 正则表达式命名捕获组
JavaScript正则表达式可以返回⼀个匹配的对象——⼀个包含匹配字符串的类数组,例如:以 YYYY-MM-DD的格式解析⽇期:
const reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match[1],
month = match[2],
day = match[3];
这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。
ES2018允许命名捕获组使⽤符号 ?<name>
,在打开捕获括号 (后⽴即命名,示例如下:
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match.groups.year,
month = match.groups.month, // 04
day = match.groups.day; // 30
任何匹配失败的命名组都将返回 undefined。
命名捕获也可以使⽤在 replace()⽅法中。例如将⽇期转换为美国的 MM-DD-YYYY 格式:
const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
d = '2018-04-30',
usDate = d.replace(reDate, '$<month>-$<day>-$<year>');
console.log(usDate); // 04-30-2018
3.3.5 正则表达式反向断⾔
⽬前JavaScript在正则表达式中⽀持先⾏断⾔(lookahead)。这意味着匹配会发⽣,但不会有任何捕获,并且断⾔没有包含在整个匹配字段中。例如从价格中捕获货币符号:
const reLookahead = /\D(?=\d+)/,
match = reLookahead.exec('$123.89');
console.log(match[0]); // $
ES2018引⼊以相同⽅式⼯作但是匹配前⾯的反向断⾔(lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字:
const reLookbehind = /(?<=\D)\d+/,
match = reLookbehind.exec('$123.89');
console.log(match[0]); // 123
以上是 肯定反向断⾔,⾮数字 \D必须存在。同样的,还存在 否定反向断⾔,表示⼀个值必须不存在,例如:
const reLookbehindNeg = /(?<!\D)\d+/,
match = reLookbehindNeg.exec('$123.89');
console.log(match[0]); // 23
3.3.6 正则表达式dotAll模式
正则表达式中点 .
匹配除回⻋外的任何单字符,标记 s改变这种⾏为,允许⾏终⽌符的出现,例如:
console.log(/hello.world/.test('hello\nworld')); //false
console.log(/hello.world/s.test('hello\nworld')); //true
3.3.7 正则表达式 Unicode 转义
到⽬前为⽌,在正则表达式中本地访问 Unicode 字符属性是不被允许的。ES2018添加了 Unicode 属性转义——形式为 \p{...}和 \P{...},在正则表达式中使⽤标记 u (unicode) 设置,在 \p块⼉内,可以以键值对的⽅式设置需要匹配的属性⽽⾮具体内容。例如:
const reGreekSymbol = /\p{Script=Greek}/u;
reGreekSymbol.test('π');
3.4 ES10新特性(2019)
-
⾏分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串⽂字中,与JSON匹配;
-
更加友好的 JSON.stringify;
-
新增了Array的 flat()⽅法和 flatMap()⽅法;
-
新增了String的 trimStart()⽅法和 trimEnd()⽅法;
-
Object.fromEntries();
-
Symbol.prototype.description;
-
String.prototype.matchAll;
-
Function.prototype.toString()现在返回精确字符,包括空格和注释;
-
简化 try{}catch{},修改 catch 绑定;
-
新的基本数据类型 BigInt;
-
globalThis;
-
import();
-
Legacy RegEx;
-
私有的实例⽅法和访问器;
3.4.1 ⾏分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串⽂字中,与JSON匹配
以前,这些符号在字符串⽂字中被视为⾏终⽌符,因此使⽤它们会导致SyntaxError异常。
3.4.2 更加友好的 JSON.stringify
如果输⼊ Unicode 格式但是超出范围的字符,在原先JSON.stringify返回格式错误的Unicode字符串。现在实现了⼀个改变JSON.stringify的第3阶段提案,因此它为其输出转义序列,使其成为有效Unicode(并以UTF-8表示)
3.4.3 新增了Array的 flat()方法和 flatMap()方法
flat()和 flatMap()本质上就是是归纳(reduce) 与 合并(concat)的操作。
3.4.3.1 Array.prototype.flat()
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个
新
数组返回。
var arr1 = [1, 2, [3, 4]];
console.log(arr1.flat())
var arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat())
var arr3 = [1, 2, [3, 4, [5, 6]]];
console.log(arr3.flat(2))
//使用 Infinity 作为深度,展开任意深度的嵌套数组arr3.flat(Infinity);
// [ 1, 2, 3, 4 ]
// [ 1, 2, 3, 4, [ 5, 6 ] ]
// [ 1, 2, 3, 4, 5, 6 ]
其次,还可以利⽤ flat()⽅法的特性来去除数组的空项
var arr4 = [1, 2, , 4, 5];
arr4.flat();
3.4.3.2 Array.prototype.flatMap()
flatMap() ⽅法⾸先使⽤映射函数映射每个元素,然后将结果压缩成⼀个新数组。它与 map 和 深度值 1 的 flat ⼏乎相同,但 flatMap 通常在合并成⼀种⽅法的效率稍微⾼⼀些。 这⾥我们拿map⽅法与flatMap方法做一个比较。
var arr1 = [1, 2, 3, 4];
console.log(arr1.map(x => [x * 2]));
console.log(arr1.flatMap(x => [x * 2]));
// 只会将 flatMap 中的函数返回的数组 “压平” ⼀层arr1.flatMap(x => [[x * 2]]);
// [ [ 2 ], [ 4 ], [ 6 ], [ 8 ] ]
// [ 2, 4, 6, 8 ]
3.4.4 Object.fromEntries()
Object.entries()⽅法的作⽤是返回⼀个给定对象⾃身可枚举属性的键值对数组,其排列与使⽤ for...in 循环遍历该对象时返回的顺序⼀致(区别在于 for-in 循环也枚举原型链中的属性)。
⽽ Object.fromEntries() 则是 Object.entries() 的反转。
Object.fromEntries() 函数传⼊⼀个键值对的列表,并返回⼀个带有这些键值对的新对象。这个迭代参数应该是⼀个能够实现@iterator⽅法的的对象,返回⼀个迭代器对象。
它⽣成⼀个具有两个元素的类似数组的对象,第⼀个元素是将⽤作属性键的值,第⼆个元素是与该属性键关联的值。
- 通过 Object.fromEntries, 可以将 Map/Array 转化为 Object:
const map = new Map([['foo', 'bar'], ['baz', 42]]);
const obj = Object.fromEntries(map);
console.log(map); // Map(2) { 'foo' => 'bar', 'baz' => 42 }
console.log(obj); // { foo: 'bar', baz: 42 }
const mapArr = [['foo', 'bar'], ['baz', 42]]
const obj2 = Object.fromEntries(mapArr);
console.log(mapArr); // [ [ 'foo', 'bar' ], [ 'baz', 42 ] ]
console.log(obj2); // { foo: 'bar', baz: 42 }
3.4.5 Symbol.prototype.description
通过⼯⼚函数Symbol()创建符号时,您可以选择通过参数提供字符串作为描述:
const sym = Symbol('The description');
以前,访问描述的唯⼀⽅法是将符号转换为字符串:
const assert = require('assert');
// assert.equal可用来比较传入的两个参数是否一样
assert.equal(String(sym), 'Symbol(The description)');
现在引⼊了getter Symbol.prototype.description以直接访问描述:
assert.equal(sym.description, 'The description');
3.4.6 String.prototype.matchAll
matchAll() ⽅法返回⼀个包含所有匹配正则表达式及分组捕获结果的迭代器。 在 matchAll 出现之前,通过在循环中调⽤regexp.exec来获取所有匹配项信息(regexp需使⽤/g标志:
const regexp = RegExp('foo*', 'g');
const str = 'table football, foosball';
while ((matches = regexp.exec(str)) !== null) {
console.log(`Found ${matches[0]}.Next starts at ${regexp.lastIndex}.`)
}
// Found foo.Next starts at 9.
// Found foo.Next starts at 19.
如果使⽤matchAll ,就可以不必使⽤while循环加exec⽅式(且正则表达式需使⽤/g标志)。使⽤matchAll 会得到⼀个迭代器的返回值,配合 for...of, array spread, or Array.from() 可以更⽅便实现功能:
const regexp = RegExp('foo*', 'g');
const str = 'table football, foosball';
let matches = str.matchAll(regexp);
for (const match of matches) {
console.log(match);
}
console.log(matches); // Object [RegExp String Iterator] {}
// [
// 'foo',
// index: 6,
// input: 'table football, foosball',
// groups: undefined
// ]
// [
// 'foo',
// index: 16,
// input: 'table football, foosball',
// groups: undefined
// ]
- matchAll可以更好的⽤于分组
var regexp = /t(e)(st(\d?))/g;
var str = 'test1test2';
console.log(str.match(regexp)); // [ 'test1', 'test2' ]
let array = [...str.matchAll(regexp)];
array[0];
array[1];
console.log(array);
// [
// [
// 'test1',
// 'e',
// 'st1',
// '1',
// index: 0,
// input: 'test1test2',
// groups: undefined
// ],
// [
// 'test2',
// 'e',
// 'st2',
// '2',
// index: 5,
// input: 'test1test2',
// groups: undefined
// ]
// ]
3.4.7 Function.prototype.toString()现在返回精确字符,包括空格和注释
function
/* comment */
foo
/* another comment */
() { }
console.log(foo.toString()); // 将上面五行原模原样打印出来
// ES2019 会把注释⼀同打印console.log(foo.toString());
// 箭头函数const bar = /* another comment */ () => {}; console.log(bar.toString());
3.4.8 修改 catch 绑定
在 ES10 之前,我们必须通过语法为 catch ⼦句绑定异常变量,⽆论是否有必要。很多时候 catch 块是多余的。ES10 提案使我们能够简单的把变量省略掉。
不算⼤的改动。
// 以前
try {} catch(e) {}
// 现在
try {} catch {}
3.4.9 新的基本数据类型 BigInt
现在的基本数据类型(值类型)不⽌5种(ES6之后是六种)了哦!加上BigInt⼀共有七种基本数据类型,分别是:String、Number、Boolean、Null、Undefined、Symbol、BigInt
3.5 ES11新特性(2020)
3.5.1 Promise.allSettled
Promise.all 缺陷
都知道 Promise.all 具有并发执⾏异步任务的能⼒。但它的最⼤问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise 直接进⼊ reject 状态。
想象这个场景:你的⻚⾯有三个区域,分别对应三个独⽴的接⼝数据,使⽤ Promise.all 来并发三个接⼝,如果其中任意⼀个接⼝服务异常,状态是 reject,这会导致⻚⾯中该三个区域数据全都⽆法渲染出来,因为任何 reject 都会进⼊ catch 回调, 很明显,这是⽆法接受的,如下:
Promise.all([Promise.reject({
code: 500,
msg: '服务异常'
}), Promise.resolve({
code: 200,
list: []
}), Promise.resolve({
code: 200,
list: []
})])
.then((res) => { // 如果其中⼀个任务是 reject,则不会执⾏到这个回调。
RenderContent(res);
})
.catch((error) => {
// 本例中会执⾏到这个回调 // error: { code: 500, msg: "服务异常" }
})
Promise.allSettled 的优势
我们需要⼀种机制,如果并发任务中,⽆论⼀个任务正常或者异常,都会返回对应的的状态(fulfilled 或者 rejected)与结果(业务 value 或者 拒因 reason),在 then ⾥⾯通过 filter 来过滤出想要的业务逻辑结果,这就能最⼤限度的保障业务当前状态的可访问性,⽽ Promise.allSettled 就是解决这问题的。
Promise.allSettled([Promise.reject({
code: 500,
msg: '服务异常'
}), Promise.resolve({
code: 200,
list: []
}), Promise.resolve({
code: 200,
list: []
})]).then((res) => {
console.log(res);
// [
// { status: 'rejected', reason: { code: 500, msg: '服务异常' } },
// { status: 'fulfilled', value: { code: 200, list: [] } },
// { status: 'fulfilled', value: { code: 200, list: [] } }
// ]
// 过滤掉 rejected 状态,尽可能多的保证⻚⾯区域数据渲染
RenderContent(res.filter((el) => { return el.status !== 'rejected'; }));
});
3.5.2 可选链
可选链 可让我们在查询具有多层级的对象时,不再需要进⾏冗余的各种前置校验。
⽇常开发中,我们经常会遇到这种查询
var name = user && user.info && user.info.name;
var age = user && user.info && user.info.getAge && user.info.getAge();
这是⼀种丑陋但⼜不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property...这种错误,这极有可能让你整个应⽤挂掉。
⽤了 Optional Chaining ,上⾯代码会变成
var name = user?.info?.name;
var age = user?.info?.getAge?.();
可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后⾯的字段。根据上⾯可以看出,⽤可选链可以⼤量简化类似繁琐的前置校验操作,⽽且更安全。
3.5.3 空值合并运算符
当我们查询某个属性时,经常会遇到,如果没有该属性就会设置⼀个默认的值。⽐如下⾯代码中查询玩家等级:
var level = (user.data && user.data.level) || '暂⽆等级';
在 JS 中,空字符串、0 等,当进⾏逻辑操作符判断时,会⾃动转化为 false。在上⾯的代码⾥,如果玩家等级本身就是 0 级, 变量 level 就会被赋值 暂⽆等级 字符串,这是逻辑错误。
var level;
if (typeof user.level === 'number') {
level = user.level;
}
else if (!user.level) {
level = '暂⽆等级';
} else {
level = user.level;
}
⽤空值合并运算在逻辑正确的前提下,代码更加简洁。
空值合并运算符 与 可选链 相结合,可以很轻松处理多级查询并赋予默认值问题。
const user = {
data: {
level: 0
}
}
var level = user.data?.level ?? '暂⽆等级';
console.log(level);
3.5.4 dynamic-import
按需 import 提案⼏年前就已提出,如今终于能进⼊ ES 正式规范。这⾥个⼈理解成 "按需" 更为贴切。现代前端打包资源越来越⼤,打包成⼏ M 的 JS 资源已成常态,⽽往往前端应⽤初始化时根本不需要全量加载逻辑资源,为了⾸屏渲染速度更快,很多时候都是按需加载,⽐如懒加载图⽚等。⽽这些按需执⾏逻辑资源都体现在某⼀个事件回调中去加载。
当然,webpack ⽬前已很好的⽀持了该特性。
el.onclick = () => {
import(`/path/current-logic.js`).then((module) => {
module.doSomthing();
}).catch((err) => {
// load error;
})
}
当然,webpack ⽬前已很好的⽀持了该特性。
3.5.5 globalThis
JavaScript 在不同的环境获取全局对象有不同的⽅式,NodeJS 中通过 global, Web 中通过 window, self等,有些甚⾄通过 this 获取,但通过 this 是及其危险的,this 在 JavaScript 中异常复杂,它严重依赖当前的执⾏上下⽂,这些⽆疑增加了获取全局对象的复杂性。
过去获取全局对象,可通过⼀个全局函数:
var getGlobal = function () {
if (typeof self !== 'undefined') {
return self;
}
if (typeof window !== 'undefined') {
returnwindow;
}
if (typeof global !== 'undefined') {
return global;
}
thrownewError('unable to locate global object');
};
var globals = getGlobal();
// https://developer.mozilla.org/zh-CN/docs/Web / JavaScript / Reference / Global_Objects / globalThis
⽽ globalThis ⽬的就是提供⼀种标准化⽅式访问全局对象,有了 globalThis后,你可以在任意上下⽂, 任意时刻都能获取到全局对象。
3.5.6 BigInt
JavaScript 中 Number 类型只能安全的表示-(2^53-1)⾄ 2^53-1 范的值,即 Number.MIN_SAFE_INTEGER ⾄ Number.MAX_SAFE_INTEGER,超出这个范围的整数计算或者表示会丢失精度。
var num = Number.MAX_SAFE_INTEGER;
// num ==> 9007199254740991
num = num + 1;
// ==> 9007199254740992
// 再次加 +1 后⽆法正常运算
num = num + 1;
// ==> 9007199254740992
// 两个不同的值,却返回了true
console.log(9007199254740992 === 9007199254740993);
// -> true
为解决此问题,ES2020 提供⼀种新的数据类型:BigInt。使⽤ BigInt 有两种⽅式:
// 在整数字⾯量后⾯加n
var bigIntNum = 9007199254740993n;
// 使⽤ BigInt 函数
// var bigIntNum = BigInt(9007199254740);
var anOtherBigIntNum = BigInt('9007199254740993');
// 通过 BigInt, 我们可以安全的进⾏⼤数整型计算。
var bigNumRet = 9007199254740993n + 9007199254740993n;
console.log(bigNumRet);// -> -> 18014398509481986n
console.log(bigNumRet.toString());
// -> '18014398509481986'
注意:BigInt 是⼀种新的数据原始(primitive)类型。
console.log(typeof 9007199254740993n); // -> 'bigint'
尽可能避免通过调⽤函数 BigInt ⽅式来实例化超⼤整型。因为参数的字⾯量实际也是 Number 类型 的⼀次实例化,超出安全范围的数字,可能会引起精度丢失。
4. babel
这里极其建议去看babel官网推荐的项目the-super-tiny-compiler,它还高屋建瓴地解释了 Babel 的工作方式
// 词法分析 =》 语法分析 =》 代码转换 =》 代码生成
1. input => tokenizer => tokens
2. tokens => parser => ast
3. ast => transformer => newAst
4. newAst => generator => output
关于编译器
这里的知识可看第 5 章(编译器)的详细描述;本章主要讲babel
4.1 babel的介绍
Babel 是⼀个⼯具链,主要⽤于将采⽤ ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运⾏在当前和旧版本的浏览器或其他环境中。下⾯列出的是 Babel 能为你做 的事情:
- 语法转换;
- 通过 Polyfill ⽅式在⽬标环境中添加缺失的特性 (通过引⼊第三⽅ polyfill 模块,例如 core-js);
- 源码转换(codemods);
// Babel 输⼊: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) { return n + 1; });
4.1.1 ES2015 及更新版本
Babel 通过语法转换器来⽀持新版本的 JavaScript 语法。
这些 插件 让你现在就能使⽤新的语法,⽆需等待浏览器的⽀持。
4.1.2 JSX和React
Babel 能够转换 JSX 语法!通过和 babel-sublime ⼀起使⽤还可以把语法⾼亮的功能提升到⼀个新的⽔平。
通过以下命令安装此 preset
npm install --save-dev @babel/preset-react
并将 @babel/preset-react 添加到你的 Babel 配置⽂件中。
export default function DiceRoll() {
const [num, setNum] = useState(getRandomNumber());
const getRandomNumber = () => {
return Math.ceil(Math.random() * 6);
};
return (
<div>
Your dice roll:
{num}
</div>
);
};
4.1.3 类型注释 (Flow 和 TypeScript)
Babel 可以删除类型注释。
// 通过以下命令安装 flow preset
// npm install --save-dev @babel/preset-flow
// @flow
function square(n: number): number {
return n * n;
}
// 或通过以下命令安装 typescript preset
// npm install --save-dev @babel/preset-typescript
function Greeter(greeting: string) {
this.greeting = greeting;
}
4.1.4 插件化
Babel 构建在插件之上。
使⽤现有的或者⾃⼰编写的插件可以组成⼀个转换管道。
通过使⽤或创建⼀个 preset 即可轻松使⽤⼀组插件。 可以使⽤ generator-babel-plugin ⽣成⼀个插件模板。
// ⼀个插件就是⼀个函数
export default function ({ types: t }) {
return {
visitor: {
Identifier(path) {
let name = path.node.name; // 反转字符串: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
4.1.5 可调试
由于 Babel ⽀持 Source map,因此你可以轻松调试编译后的代码。
4.1.6 符合规范
Babel 尽最⼤可能遵循 ECMAScript 标准。不过,Babel 还提供了特定的选项来对标准和性能做权衡。
4.1.7 代码紧凑
Babel 尽可能⽤最少的代码并且不依赖太⼤量的运⾏环境。
有些情况是很难达成的这⼀愿望的,因此 Babel 提供了 "assumptions" 选项,⽤以在符合规范、⽂件⼤⼩和编译速度之间做折中。
4.2 babel的使⽤
4.2.1 概述
整个配置过程包括:
- 运⾏以下命令安装所需的包(package):
npm install --save-dev @babel/core @babel/cli @babel/preset-env
- 在项⽬的根⽬录下创建⼀个命名为 babel.config.json 的配置⽂件(需要 v7.8.0 或更⾼版本),并将 以下内容复制到此⽂件中:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}
如果你使⽤的是 Babel 的旧版本,则⽂件名为 babel.config.js 。
const presets = [
[
"@babel/preset-env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
corejs: "3.6.4",
},
],
];
module.exports = { presets };
- 运⾏此命令将 src ⽬录下的所有代码编译到 lib ⽬录:
./node_modules/.bin/babel src --out-dir lib
4.2.2 CLI 命令⾏的基本⽤法
你所需要的所有的 Babel 模块都是作为独⽴的 npm 包发布的,并且(从版本 7 开始)都是以 @babel 作为冠名的。这种模块化的设计能够让每种⼯具都针对特定使⽤情况进⾏设计。 下⾯我们着重看⼀下 @babel/core
和 @babel/cli
。
4.2.2.1 核心库
Babel 的核⼼功能包含在 @babel/core 模块中。通过以下命令安装:
npm install --save-dev @babel/core
你可以在 JavaScript 程序中直接 require 并使⽤它:
const babel = require("@babel/core");
babel.transformSync("code", optionsObject);
4.2.2.2 CLI命令行工具
@babel/cli 是⼀个能够从终端(命令⾏)使⽤的⼯具。下⾯是其安装命令和基本⽤法:
npm install --save-dev @babel/core @babel/cli
./node_modules/.bin/babel src --out-dir lib
这将解析 src ⽬录下的所有 JavaScript ⽂件,并应⽤我们所指定的代码转换功能,然后把每个⽂件输出 到 lib ⽬录下。由于我们还没有指定任何代码转换功能,所以输出的代码将与输⼊的代码相同(不保留原代码格式)。我们可以将我们所需要的代码转换功能作为参数传递进去。
4.2.3 插件和预设(preset)
代码转换功能以插件的形式出现,插件是⼩型的 JavaScript 程序,⽤于指导 Babel 如何对代码进⾏转换。你甚⾄可以编写⾃⼰的插件将你所需要的任何代码转换功能应⽤到你的代码上。例如将 ES2015+ 语法转换为 ES5 语法,我们可以使⽤诸如 @babel/plugin-transform-arrow-functions 之类的官⽅插件:
npm install --save-dev @babel/plugin-transform-arrow-functions
./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
现在,我们代码中的所有箭头函数(arrow functions)都将被转换为 ES5 兼容的函数表达式了:
const fn = () => 1; // converted to var fn = function fn() { return 1; };
但是我们的代码中仍然残留了其他 ES2015+ 的特性,我们希望对它们也进⾏转换。我们不需要⼀个接⼀个地添加所有需要的插件,我们可以使⽤⼀个 "preset" (即⼀组预先设定的插件)。
就像插件⼀样,你也可以根据⾃⼰所需要的插件组合创建⼀个⾃⼰的 preset 并将其分享出去。对于当前的⽤例⽽⾔,我们可以使⽤⼀个名称为 env 的 preset。
npm install --save-dev @babel/preset-env
./node_modules/.bin/babel src --out-dir lib --presets=@babel/env
如果不进⾏任何配置,上述 preset 所包含的插件将⽀持所有最新的 JavaScript (ES2015、ES2016 等)特性。但是 preset 也是⽀持参数的。
我们来看看另⼀种传递参数的⽅法:配置⽂件,⽽不是通过终端控制台同时传递 cli 和 preset 的参数。
4.2.4 配置
根据你的需要,可以通过⼏种不同的⽅式来使⽤配置⽂件。另外,请务必阅读我们关于如何 配置 Babel 的深⼊指南以了解更多信息。
现在,我们⾸先创建⼀个名为 babel.config.json 的⽂件(需要 v7.8.0 或更⾼版本),并包含如下内容:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
}
}
]
]
}
现在,名为 env 的 preset 只会为⽬标浏览器中没有的功能加载转换插件。
babel在线使用: Babel · The compiler for next generation JavaScript (babeljs.io)
5. 编译器
5.1 什么是编译器
上图来自babel官网
Babel is a JavaScript compiler
compiler也叫编译器,是⼀种电脑程序,它会将⽤某种编程语⾔写成的源代码,转换成另⼀种编程语⾔。
从维基百科的定义来看,编译器就是个将当前语⾔转为其他语⾔的过程,回到babel上,它所做的事就是语法糖之类的转换,⽐如ES6/ES7/JSX转为ES5或者其他指定版本,因此称之为compiler也是正确的,换⾔之,像我们平时开发过程中所谓的其他⼯具,如:
● Less/Saas
● TypeScript/coffeeScript
● Eslint
● etc...
都可以看到compiler的身影
下面详细介绍compiler的实现思路以及 了解the-super-tiny-compiler的基本实现。
5.2 编译器的基本思路
5.2.1 词法分析(Lexical Analysis)
- 目的
将⽂本分割成⼀个个的“token”,例如:init、main、init、x、;、x、=、3、;、}等等。同时它可以去掉⼀些注释、空格、回⻋等等⽆效字符;
- 生成方式
词法分析⽣成token的办法有2种:
-
使⽤正则进⾏词法分析
需要写⼤量的正则表达式,正则之间还有冲突需要处理,不容易维护,性能不⾼,所以正则只适合⼀些简单的模板语法,真正复杂的语⾔并不合适。并且有的语⾔并不⼀定⾃带正则引擎。
-
使⽤⾃动机进⾏词法分析
⾃动机可以很好的⽣成token;
有穷状态⾃动机(finite state machine):在有限个输⼊的情况下,在这些状态中转移并期望最终达到终⽌状态。
有穷状态⾃动机根据确定性可以分为:
“确定有穷状态⾃动机”(DFA - Deterministic finite automaton)
在输⼊⼀个状态时,只得到⼀个固定的状态。DFA 可以认为是⼀种特殊的 NFA;
“⾮确定有穷⾃动机”(NFA - Non-deterministic finite automaton)
当输⼊⼀个字符或者条件得到⼀个状态机的集合。JavaScript 正则采⽤的是 NFA 引擎,具体看后⽂;
5.2.2 语法分析(Syntactic Analysis))
我们⽇常所说的编译原理就是将⼀种语⾔转换为另⼀种语⾔。编译原理被称为形式语⾔,它是⼀类⽆需知道太多语⾔背景、⽆歧义的语⾔。⽽⾃然语⾔通常难以处理,主要是因为难以识别语⾔中哪些是名词哪些是动词哪些是形容词。例如:“进⼝汽⻋”这句话,“进⼝”到底是动词还是形容词?所以我们要解析⼀⻔语⾔,前提是这⻔语⾔有严格的语法规定的语⾔,⽽定义语⾔的语法规格称为⽂法
。
1956年,乔姆斯基将⽂法按照规范的严格性分为0型、1型、2型和3型共4中⽂法,从0到3⽂法规则是逐渐增加严的。⼀般的计算机语⾔是2型
,因为0和1型⽂法定义宽松,将⼤⼤增加解析难度、降低解析效率,⽽3型⽂法限制⼜多,不利于语⾔设计灵活性。2型⽂法也叫做上下⽂⽆关⽂法(CFG)
。
语法分析的⽬的就是通过词法分析器拿到的token流 + 结合⽂法规则
,通过⼀定算法得到⼀颗抽象语法树(AST)。抽象语法树是⾮常重要的概念,尤其在前端领域应⽤很⼴。典型应⽤如babel插件,它的原理就是:es6代码 → Babylon.parse → AST → babel-traverse → 新的AST → es5代码
。
从⽣成AST效率和实现难度上,前⼈总结主要有2种解析算法:⾃顶向下的分析⽅法和⾃底向上的分析⽅法。⾃底向上算法分析⽂法范围⼴,但实现难度⼤。⽽⾃顶向下
算法实现相对简单,并且能够解析⽂法的范围也不错,所以⼀般的compiler都是采⽤深度优先索引的⽅式。
5.2.3 代码转换(Transformation)
在得到AST后,我们⼀般会先将AST转为另⼀种AST,⽬的是⽣成更符合预期的AST,这⼀步称为代码转换。
代码转换的优势:主要是产⽣⼯程上的意义
- 易移植:与机器⽆关,所以它作为中间语⾔可以为⽣成多种不同型号的⽬标机器码服务;
- 机器⽆关优化:对中间码进⾏机器⽆关优化,利于提⾼代码质量;
- 层次清晰:将AST映射成中间代码表示,再映射成⽬标代码的⼯作分层进⾏,使编译算法更加清晰;
对于⼀个Compiler⽽⾔,在转换阶段通常有两种形式:
-
同语⾔的AST转换;
-
AST转换为新语⾔的AST;
这⾥有⼀种通⽤的做法是,对我们之前的AST从上⾄下的解析(称为traversal),然后会有个映射表(称为visitor
),把对应的类型做相应的转换。
这里的visitor很重要,可以理解为钩子,每一个对应的类型就会触发相应的钩子去转换代码,详细可看the-super-tiny-compiler
5.2.4 代码生成(Code Generation)
在实际的代码处理过程中,可能会递归的分析(recursive)我们最终⽣成的AST,然后对于每种type都有个对应的函数处理,当然,这可能是最简单的做法。总之,我们的⽬标代码会在这⼀步输出,对于我们的⽬标语⾔,它就是HTML了。
5.2.5 完整链路(Compiler)
// 词法分析 =》 语法分析 =》 代码转换 =》 代码生成
1. input => tokenizer => tokens
2. tokens => parser => ast // 语法分析,⽣成AST
3. ast => transformer => newAst // 中间层代码转换
4. newAst => generator => output // ⽣成⽬标代码
5.3 the-super-tiny-compiler
一个基础的compiler
这里只是将前置讲解中文化了,关于代码的逻辑思路,及其建议跟着上面的思路去看代码
/**
* 今天我们要⼀起写⼀个编译器。但不仅仅是任何编译器......
* 超级⼩的编译器!⼀个很⼩的编译器,如果你
* 删除所有注释,这个⽂件只有⼤约 200 ⾏实际代码。
*
* 我们将把⼀些类似语义化代码的函数调⽤编译成⼀些类似 C 的函数
* 函数调⽤。
*
* 如果您不熟悉其中之⼀。我只是给你⼀个快速的介绍。
*
* 如果我们有两个函数 `add` 和 `subtract` 他们会写成这样:
*
* 类似 C
*
* 2 + 2 (加 2 2) 加 (2, 2)
* 4 - 2 (减 4 2) 减 (4, 2)
* 2 + (4 - 2) (加 2 (减 4 2)) 加 (2, 减 (4, 2))
*
*
* 很好,因为这正是我们要编译的。虽然这
* 不是完整的 C 语法,它的语法⾜以
* 演示现代编译器的许多主要部分。
*/
/**
* ⼤多数编译器分为三个主要阶段:解析、转换、
* 和代码⽣成
*
* 1. *解析* 将原始代码转化为更抽象的代码
* 代码的表示。
*
* 2. *转换* 采⽤这种抽象表示并进⾏操作
* ⽆论编译器想要什么。
*
* 3. *代码⽣成*采⽤转换后的代码表示,并
* 将其转换为新代码。
*/
/**
* 解析
* --------
*
* 解析通常分为两个阶段:词法分析和
* 句法分析。
*
* 1. *词法分析*获取原始代码并将其拆分成这些东⻄
* 被称为标记器(或词法分析器)的东⻄称为标记。
*
* Tokens 是⼀组微⼩的对象,描述了⼀个孤⽴的部分
* 的语法。它们可以是数字、标签、标点符号、运算符、
* 任何。
*
* 2. *句法分析*获取标记并将它们重新格式化为
* 描述语法的每个部分及其关系的表示
* 彼此。这被称为中间表示或
* 抽象语法树。
*
* 抽象语法树,简称 AST,是⼀个深度嵌套的对象,
* 以⼀种既易于使⽤⼜能告诉我们很多信息的⽅式表示代码
* 信息。
*
* 对于以下语法:
*
* (加 2 (减 4 2))
*
* 令牌可能看起来像这样:
*
* [
* { type: 'paren', value: '(' },
* { type: 'name', value: 'add' },
* { type: 'number', value: '2' },
* { type: 'paren', value: '(' },
* { type: 'name', value: 'subtract' },
* { type: 'number', value: '4' },
* { type: 'number', value: '2' },
* { type: 'paren', value: ')' },
* { type: 'paren', value: ')' },
* ]
*
* 抽象语法树 (AST) 可能如下所示:
* {
* type: 'Program',
* * body: [{
* type: 'CallExpression',
* name: 'add',
* params: [{
* type: 'NumberLiteral',
* value: '2',
* }, {
* type: 'CallExpression',
* name: 'subtract',
* params: [{
* type: 'NumberLiteral',
* value: '4',
* }, {
* type: 'NumberLiteral',
* value: '2',
* }]
* }]
* }]
* }
*/
/**
* 转换
* --------------
*
* 编译器的下⼀个阶段是转换。再次,这只是
* 从最后⼀步获取 AST 并对其进⾏更改。它可以操纵
* 使⽤相同语⾔的 AST,或者它可以将其翻译成全新的
* 语。
*
* 让我们看看如何转换 AST。
*
* 您可能会注意到我们的 AST 中的元素看起来⾮常相似。
* 这些对象具有类型属性。这些中的每⼀个都被称为
* AST 节点。这些节点在它们上定义了描述⼀个
* 树的隔离部分。
*
* 我们可以有⼀个“NumberLiteral”的节点:
*
* {
* type: 'NumberLiteral',
* value: '2',
* }
*
* Or maybe a node for a "CallExpression":
*
* {
* type: 'CallExpression',
* name: 'subtract',
* params: [...nested nodes go here...],
* }
*
* 转换 AST 时,我们可以通过以下⽅式操作节点
* 添加/删除/替换属性,我们可以添加新节点,删除节点,或者
* 我们可以不理会现有的 AST 并创建⼀个全新的基于
* 在上⾯。
*
* 由于我们的⽬标是⼀种新语⾔,我们将专注于创建⼀个
* 特定于⽬标语⾔的全新 AST。
*
* 遍历
* ---------
*
* 为了浏览所有这些节点,我们需要能够
* 遍历它们。这个遍历过程会到达 AST 中的每个节点
* 深度优先。
*
* {
* type: 'Program',
* body: [{
* type: 'CallExpression',
* name: 'add',
* params: [{
* type: 'NumberLiteral',
* value: '2'
* }, {
* type: 'CallExpression',
* name: 'subtract',
* params: [{
* type: 'NumberLiteral',
* value: '4'
* }, {
* type: 'NumberLiteral',
* value: '2'
* }]
* }]
* }]
* }
*
* 所以对于上⾯的 AST,我们会去:
*
* 1. Program - 从 AST 的顶层开始
* 2. CallExpression (add) - 移动到程序主体的第⼀个元素
* 3. NumberLiteral (2) - 移动到 CallExpression 参数的第⼀个元素
* 4. CallExpression (subtract) - 移动到 CallExpression 参数的第⼆个元素
* 5. NumberLiteral (4) - 移动到 CallExpression 参数的第⼀个元素
* 6. NumberLiteral (2) - 移动到 CallExpression 参数的第⼆个元素
*
* 如果我们直接操作这个 AST,⽽不是创建⼀个单独的 AST,
* 我们可能会在这⾥引⼊各种抽象。但只是参观
* 树中的每个节点都⾜以完成我们正在尝试做的事情。
*
* 我使⽤“访问”这个词的原因是因为有这样的模式
* 表示对对象结构元素的操作。
*
* Visitors
* --------
*
* 这⾥的基本思想是我们将创建⼀个“访问者”对象,
* 具有将接受不同节点类型的⽅法。
*
* var visitor = {
* NumberLiteral() {},
* CallExpression() {},
* };
*
* 当我们遍历我们的 AST 时,我们会在任何时候调⽤这个访问者的⽅法
* “输⼊”⼀个匹配类型的节点。
*
* 为了使它有⽤,我们还将传递节点和引⽤
* ⽗节点。
*
* var visitor = {
* NumberLiteral(node, parent) {},
* CallExpression(node, parent) {},
* };
*
* 但是,也存在在“退出”时调⽤事物的可能性。想象
* 我们之前的树形结构以列表形式:
*
* - Program
* - CallExpression
* - NumberLiteral
* - CallExpression
* - NumberLiteral
* - NumberLiteral
*
* 当我们向下遍历时,我们将到达有死胡同的分⽀。正如我们
* 完成我们“退出”它的树的每个分⽀。所以我们顺着树⾛
*“进⼊”每个节点,然后返回我们“退出”。
*
* -> Program (enter)
* -> CallExpression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Call Expression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Number Literal (enter)
* <- Number Literal (exit)
* <- CallExpression (exit)
* <- CallExpression (exit)
* <- Program (exit)
*
* 为了⽀持这⼀点,我们的访问者的最终形式将如下所示:
*
* var visitor = {
* NumberLiteral: {
* enter(node, parent) {},
* exit(node, parent) {},
* }
* };
*/
/**
* 代码⽣成
* ---------------
*
* 编译器的最后阶段是代码⽣成。有时编译器会做
* 与转换重叠的东⻄,但⼤部分是代码
* ⽣成只是意味着取出我们的 AST 和字符串化代码。
*
* 代码⽣成器有⼏种不同的⼯作⽅式,⼀些编译器会重⽤
* 早期的令牌,其他⼈将创建⼀个单独的表示
*代码,以便他们可以线性打印节点,但据我所知
* 将使⽤我们刚刚创建的相同 AST,这是我们将重点关注的内容。
*
* 实际上,我们的代码⽣成器将知道如何“打印”所有不同的
* AST的节点类型,它会递归调⽤⾃⼰打印嵌套
* 节点,直到所有内容都打印成⼀⻓串代码。
*/
/**
*就是这样!这就是编译器的所有不同部分。
*
* 现在这并不是说每个编译器看起来都和我在这⾥描述的完全⼀样。
* 编译器有许多不同的⽤途,它们可能需要更多的步骤
* 我有详细的。
*
* 但是现在您应该对⼤多数编译器的外观有⼀个⼤致的⾼级概念
* 喜欢。
*
* 现在我已经解释了所有这些,你们都可以⾃⼰写了
* 编译器对吗?
*
* 开个玩笑,这就是我来帮忙的:P
*
* 那么让我们开始吧...
*/
附录
转载自:https://juejin.cn/post/7357142305424719926