JavaScript
写给后端的前端 ES6+ 新特性指南
11/5/2021
·
266
·
profile photo
大家好,今天主要为大家介绍的是 ES6 之后新增的语法特性以及部分实用的 API。

ECMAScript 简介

什么是 ECMAScript

ECMAScript 是由国际标准化组织 ECMA 定义的一种脚本语言规范;JavaScript 是 ES 的一种实现;
ECMAScript 简称 ES,是由国际标准化组织 ECMA定义的一种规范。ES 并不是什么新鲜东西,是由网景在浏览器大战中失利后,将自己浏览器的脚本语言捐献给了ECMA 组织,并且成立了Mozilla 基金会并参与其中,可以说现代浏览器中运行的的 JavaScript 只是 ES 的一种实现形式。
这里也可以顺带提一句,出了 Chrome 的 V8 引擎是最常用的 JS 引擎以外,Firefox 和苹果的 Safari 同样拥有自己的 JS 引擎,而前端开发最经常打交道的 Node.js 也是基于 V8 引擎的。
我们还经常见到另一种语言 TypeScript,TS 是由微软发明的,可以简单理解为添加了类型系统的 JavaScript 语言,由于它给 JavaScript 添加了编译器的类型检查,并且对于最新的 ES 标准支持非常好,所以通常推荐在大型项目中使用。
TypeScript 给开发带来的最大的好处,可以说就是类型签名,原本 JS 也可以通过 JSDoc 来给API 加类型注解,但是这并不是强制性的约束,而 TS 的类型问题会在编译期被检查出来,这除了给开发者不需要点到库里面查看源码细节的便利外,还可以更有信心的传递参数,也可以规避很多 JS 变量的隐式转换问题。

ECMAScript 与 JavaScript/TypeScript 的关系

  • JavaScript 是 ECMAScript 标准的实现;
  • JavaScript 前身是由 Brendan Eich 提出并实现的一种脚本语言(最初被称为 LiveScript),最早应用于网景浏览器;
  • TypeScript 是由微软创造的一种编程语言,可以简单理解为 TypeScript = ECMAScript + Type definition
  • TypeScript 由于为 JavaScript 添加了编译期类型检查,并且积极实现 ECMAScript 标准,通常为大型应用程序采用。

什么是 ES6、ECMAScript 2015

我们还经常听到 ES6、ES2015 这样的名词,它的具体含义是指的 ES 规范的版本,比如 ES6 就是指的 ES 标准的第六版,可以类比为 JDK 6;ES2015 表示这个标准发布于 2015 年,在此之后ES 标准通常保持一年一个大版本的更新速度,现在已经更新到 ES2021,也就是 ES12。
  • ES 版本有两种公开名称:ES + 版本号或者 ES + 发布年份:
    • 例如:ES6 是 2015 年发布的,那么 ES6 = ES2015 = ECMAScript 2015;
    • 在此之前,还有 ES3、ES5 等标准,但 ES6 引入了自发明以来最多的特性,所以尤为重要;
notion image

新增语法/关键字

新增关键字:let

  • let 用于声明一个变量;
  • 基本用法:
let a = 1;
let b = a;

b = 2; // let 声明的变脸可以被重新赋值

console.log(a); // ===> 1
console.log(a); // ===> 2

letvar 的异同

  • 相同点:
    • let/var 都用于声明变量,const 用于声明常量;
  • 不同点:
    • let 声明的变量具有暂时性死区(TDZ);
    • let 声明的变量具有块级作用域,并且在块级作用域中不能重复声明;
    • var 声明的变量具有变量提升特性,在块级作用域外部可以访问,甚至声明之前也能访问到;var 可以重复声明变量;
    • 在非严格模式下,var 可以将变量声明到全局作用域;
什么是变量/函数提升:使用 var 声明的变量,会提升到函数作用域的顶部;
function foo() {
	console.log(a); // ===> undefined

	if (true) {
		var a = 1;

		console.log(a); // ===> 1
	}
}
提升特性(Hoisting)不符合变量先声明后使用的原则;
  • 变量/函数提升的作用:使用 var 声明的变量,会提升到函数作用域的顶部
function tribonacci(n) {
	if (n == 0) return 0;
	if (n <= 2) return 2;
  return tribonacci(n - 2) + tribonacci(n - 1);
}
这种写法等价于下面的形式:
var tribonacci;

tribonacci = function (n) {
	if (n == 0) return 0;
	if (n <= 2) return 2;
  return tribonacci(n - 2) + tribonacci(n - 1);
}

变量/函数提升导致的问题:

  1. 变量相互覆盖;
var message = 'hello world';

function foo() {
  console.log(message);

  if (false) { // even falsy
		var message = 'bad apple!';
  }
}

foo(); // print what ???
  1. 变量生命周期延长,未即时销毁
for (var i = 0; i < 5; i++) {
	continue;
}

console.log(i); // print what ???

新增关键字:class

class 用于在 JavaScript 中定义一个类,本质上是对 prototype 包装的语法糖;
class Person {
	constructor(name) {
		this.name = name;
  }

	great(message) {
		console.log(`hi there, I'm ${this.name}. ${message}`);
  }
}

const bob = new Person('Bob');

bob.great('Never gonna give you up!');
在 ES6 之前,我们必须得基于 Functionprototype 来自定义类型:
function Person(name) {
  this.name = name;
}

Person.prototype.great = function (message) {
  console.log(`hi there, I'm ${this.name}. ${message}`);
}

const bob = new Person('Bob');

bob.great('Never gonna let you down!');
到百度或者 Google 上搜索下,一个经典的问题就是如何使用 JS 实现继承:
notion image
有了 class 之后,我们可以方便的通过 extends 关键字来实现继承:
class Person {
	constructor(name, age, sex) {
		this.name = name;
		this.age = age;
		this.sex = sex;
	}
}

class Student extends Person {
	constructor(name, age, sex, major) {
    super(name, age, sex); // 使用 super 关键字函数来调用父类构造函数

		this.major = major;
  }
}

const alice = new Student('Alice', 21, 'female', 'CS');

alice instanceof Person; // ===> true
上述的例子等价于用 Function + Prototype 实现继承的形式;
function Person(name, age, sex) {
	this.name = name;
	this.age = age;
	this.sex = sex;
}

function Student(name, age, sex, major) {
  Person.call(this, name, age, sex);

  this.major = major;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

新增关键字:async + await

async 用于修饰一个函数,使其内部执行到 await 修饰的语句时,可以等待其执行结果返回后再向下执行。
这么说可能比较抽象,举一个具体的例子,如何使用 async/await 实现一个 sleep 函数,这里稍微涉及到一点点关于 Promise 的知识,我们下面再介绍它:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function foo() {
  console.log('Before sleep');
	await sleep(3 * 1000); // sleep for 3s
  console.log('After sleep');
}
到这里可能已经有同学比较迷惑了,让我们暂且放下,看下基于回调形式如何实现同样的功能:
const sleep = (ms, cb) => setTimeout(cb, ms);

function bar() {
  console.log('Before sleep');
	sleep(3000, () => { // but in callback
    console.log('After sleep');
  });
}
这个有一定前端基础的同学肯定会说了:我知道,sleep 函数会在 setTimeout 结束之后执行 cb 回调函数,从而继续执行 bar 函数中 sleep 之后的逻辑。
那么同样的道理,我们回过头来看前面的例子:setTimeout 结束后,会调用 Promise.resolve 使 sleep 函数返回,而 await 起到的就是这样一个作用:等待一个同步操作返回,或者等待一个 Promise settle;

新增语法:箭头函数

基本用法:

  • 定义一个函数:
const foo = (param) => {
  console.log('bar', param);
}
  • 直接返回一个表达式:
const toArray = (val) => (Array.isArray(val) ? val : [val]);

function 定义函数的区别

  • 使用 function 关键字定义的函数同样具有提升特性;
  • 使用 function 关键字定义的函数,上下文中的 this 会随着调用方的改变而改变;
以一个具体的例子来看,假如有以下函数定义:
'use strict';

function outer() {
	var outer_this = this;

	console.log('outer this', outer_this); // what will print ?

	function inner() {
		var inner_this = this;

		console.log('inner this', inner_this); // what will print ?

		// is this true ?
		console.log('outer this == inner this', outer_this == inner_this);
  }

	inner();
}
根据上述函数定义,思考以下表达式的结果
outer(); // 没有明确的调用方

const kt = {};

kt.outer = outer;

kt.outer(); // `kt` 对象作为调用方

setTimeout(kt.outer, 3000); // 此时真实的调用方是 `kt` 还是 `window` ?
,接下来,我们使用箭头函数改写函数定义:
'use strict';

const outer = () => {
	var outer_this = this;

	console.log('outer this', outer_this); // always `undefined`

	const inner = () => {
		var inner_this = this;

		console.log('inner this', inner_this); // always `undefined`

		// always be `true`
		console.log('outer this == inner this', outer_this == inner_this);
  }

	inner();
}
这个时候再看上面输出的结果,this 指向不再受函数调用者的影响了,总是符合我们开发时预期的结果。
另一种方式,也可以使用 ES6 新增函数方法 bind 绑定 this
const bounded_fn = outer.bind(null);
不过需要注意的是,bind 对箭头函数形式声明的函数不起作用,它的上下文依然指向编写时的调用栈。

新增语法:解构赋值

  • 对一个对象使用解构:
const bob = { name: 'Bob', girlfriend: 'Alice' };

const { girlfriend } = bob; 
  • 对一个数组使用解构:
const [a1, a2] = [1, 2, 3]; // a1 == 1, a2 == 2
对数组使用解构,可以方便的交换两个变量的值,而无需第三个变量:
let x = 1, y = 2;

[x, y] = [y, x];

console.log(x, y); // 2 1
  • 一个常见的使用场景:
function fetchUserList({ clinicId }) {
	return axios({
		url: '/api/user/list',
		params: { clinicId },
	});
}

const {
	userList = [],                   // 如果解构出来的变量是 undefined,这里可以设置默认值
} = await fetchUserList(clinicVo); // 假设 clinicVo 中有一个 key 是 clinicId

新增语法:展开运算符

展开语法可以将一个对象/数组/字符串按 key-value 方式展开,或者在函数参数层面展开为位置参数:
  • 展开一个对象
const obj = { name: 'foo' };
const obj_clone = { ...obj };
console.log(obj_clone);        // { name: 'foo' }
console.log(obj == obj_clone); // false
  • 展开一个数组
const arr = [1, 2, 3];
const arr2 = [...arr];    // 类似 arr.slice()
console.log(arr == arr2); // false
arr2.push(4);             // 对 arr2 的操作不会影响 arr
console.log(arr, arr2);   // [ 1, 2, 3 ] [ 1, 2, 3, 4 ]
  • 展开函数参数为位置参数
const sum = (x, y, z) => x + y + z;

const nums = [1, 2, 3];

console.log(sum(...nums)); // 6

一个常见的使用场景:合并对象/数组:

  • 合并两个数组,类似 Array.concat
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

const arr = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
  • 合并两个对象,类似于 Object.assign,相同字段后面的对象中的 value 会覆盖前面的:
const obj1 = { name: 'obj1', foo: 1 };
const obj2 = { name: 'obj2', bar: 'baz' };

const obj = { ...obj1, ...obj2 }; // { name: 'obj2', foo: 1, bar: 'baz' }

新增语法:for .. of 语句

for .. of 语句允许在任意可迭代对象(ArrayMapSetString 等)上循环调用每一个元素(item):

基本用法

const arr = [1, 2, 3];

for (const item of arr) {
	console.log(item); // 最终输出: 1 2 3
}
这个例子形式上等价于:
const arr = [1, 2, 3];

for (let i = 0; i < arr.length; i++) {
	console.log(arr[i]);
}

for 循环语句、for .. in 语句、forEach 方法的区别:

  • for .. in 用于对象上的可枚举(Enumerable)属性;
  • forEach 方法能在任何实现的 forEach 的对象上调用,例如:NodeList
  • for .. of 仅可在具有 [Symbol.iterator] 属性的对象上使用;
Array.prototype.customKey = 'kt';

const array = [1, 2, 3];

for (const k in array) {
	console.log(k, '->', array[k]); // 最终输出: 0 -> 1  1 -> 2  2 -> 3  customKey -> kt
}

for (const i of array) {
	console.log(i); // 最终输出: 1 2 3
}

其他新增语法

对象属性简写(属性,方法)

const data = { name: 'kt' };

const kt = {
	data,    // 与 data: data 等价

	init() { // 与 init: function() { /* ... */ } 等价
		// ...
	}
}

模版字符串

  • 单行文本
const api_url = `/api/code?cat=${cat || 'HBO'}`;
  • 多行文本
const template = `
	<header>
		<h4>hello world</h4>
		<small>Template string is very useful</small>
	</header>
`;

可选链 ?.

  • 取属性
const data = response?.data;
// 等价于
const data = response != null ? response.data : undefined;
  • 取方法
const result = object.method?.();
// 等价于
const result = object.method != null ? object.method() : undefined;

空值合并 ??

const data = response?.data ?? defaultData;
// 等价于
const data = response != null ? response.data : defaultData;
  • || 运算符的区别在于,?? 仅在左值为 nullundefined 的时候生效;而 || 运算符在左值为假值 (0, '', false, null, undefined) 的时候均返回右值:
// 假设我们有一个 response = { data: 0 }, defaultData = 1
const data = response.data ?? defaultData; // 这时能正确取到 0
// 假如使用 || 运算符,可能返回不符合预期
const data = response.data || defaultData; // 此时返回 defaultData 1

迭代器函数

function* makeGenerator(...args) {
	for (const arg of args) {
		yield arg;
	}
}

const g = makeGenerator(1, 2, 3);

let current;
while ((current = g.next()).done != true) {
	console.log(current.value); // 最终输出: 1 2 3
}
https://codepen.io/mitscherlich/pen/XWROjmz

新增数据类结构/类型

新类型:Symbol

Symbol 是 ES6 新增的基本类型,可以简单理解为全局唯一的常量;

基本用法

const sym1 = Symbol();
const sym2 = Symbol('foo');
const sym3 = Symbol('foo');

console.log(sym2 == sym3); // false
需要注意的是,Symbol 只能用作包装类型,不能被 new 操作调用:
const sym = new Symbol(); // TypeError

新对象:Map

Map 类似于 Java 中的 HashMap,用于以 Key-Value 形式存储数据,Key 可以不为 string

基本用法

const map = new Map();

map.set('name', 'foo');            // key 也可以是 string
map.set(1, 'bar');                 // 也可以是 number
map.set(Symbol('map.foo'), 'baz'); // 还可以是 symbol
map.set({ name: 'kt' }, 'kt');     // 甚至可以是 object

const size = map.size;

for (const [key, value] of map) {
  console.log(key, '==>', value);
}
// name ==> foo
// 1 ==> bar
// Symbol(map.foo) ==> baz
// { name: 'kt' } ==> kt

MapObject 的异同

  • 相同点:
    • 都可以 Key-Value 形式存储数据;
  • 不同点:
    • Map 的 key 可以是任意值,Object 的 key 只能是 stringsymbol
    • Map 的 key 是有序的,Object 的 key 在 ES6 之前是无序的;
    • Map 可以通过 for .. of 遍历,Object 只能通过 for .. in 或先通过 Object.keys 获取到所有的 key 后遍历;
    • Map 在频繁增删键值对的场景下性能更好,Object 依赖与 JS 引擎自身的优化;

新对象:Map

Set 基本等同于 Java 中的 Set<E>,用于存储不重复的数据;

基本用法

const set = new Set();

set.add(1);               // Set(1) { 1 }
set.add(5);               // Set(2) { 1, 5 }
set.add(5);               // Set(2) { 1, 5 }
set.add('foo');           // Set(3) { 1, 5, 'foo' }
const o = { a: 1, b: 2 };
set.add(o);               // Set(4) { 1, 5, 'foo', { a: 1, b: 2 } }
set.add({ a: 1, b: 2 });  // Set(5) { 1, 5, 'foo', { a: 1, b: 2 }, { a: 1, b: 2 } }
即使数据字面相等,也不会视为相同的值

常见用法

  • 数组去重
const arr = [1, 2, 2, undefined, 3, 4, 5, undefined, null];

const set = new Set(arr);

const uniqed_arr = [...set]; // [ 1, 2, undefined, 3, 4, 5, null ]

新增全局变量:Promise

Promise 是一个特殊的类型,用于表示一个异步操作的完成状态与结果值,可以认为 Promise 是一个有限状态机:
notion image
一个 Promise 必然处于以下几种状态之一:
  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled): 意味着操作成功完成。
  • 已拒绝(rejected): 意味着操作失败。
notion image
这时候,让我们再来回顾前面实现的 sleep 函数:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const foo = () => {
	console.log('before sleep');
	sleep(3000).then(() => {
		console.log('after sleep');
  });
}
不难看出,当 setTimeout 结束时,调用了 Promise.resolve 方法,使得 sleep 函数返回的 Promise 进入 fulfilled 状态,从而可以进入 then 回调函数继续完成外层函数的后续逻辑。
notion image

Promise 链式调用

const fetchUserList = ({ clinicId }) => axios.get('/api/user/list', {
  params: { clinicId },
}).then(({ data: userList = [] }) => userList);

const fetchClinicVo = (clinicCode) => axios.get('/api/clinic', {
  params: { clinicCode },
});

const userList = await fetchClinicVo(clinicCode).then(fetchUserList);
Tips: Promise.all 可以实现并发调用

新增常用对象、数组 API

Object.assign

用于浅合并多个对象,返回第一个参数的应用:
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

const obj = Object.assign({}, obj1, obj2); // { a: 1, b: 2, c: 3, d: 4 }
如果只传入一个对象,也可以用于浅拷贝:
const shallowClone = (obj) => Object.assign({}, obj);

const objClone = shallowClone(obj);

console.log(objClone == obj); // false

Object.keys/Object.values

  • keys() 返回一个对象所有可枚举的属性名,通常用于遍历对象
const obj = { a: 1, b: 2, name: 'foo' };

console.log(Object.keys(obj)); // ['a', 'b', 'name']
  • values() 返回一个对象所有的 values,顺序等同于 keys() 返回的顺序:
console.log(Object.values(obj)); // [1, 2, 'foo']

Object.entries

返回一个对象所有 Key-Value 对:
const obj = { a: 1, b: 2, name: 'foo' };
const pairs = Object.entries(obj); // [['a', 1], ['b', 2], ['name', 'foo']]
可以用于 Object → Map 的转换:
const map = new Map(pairs); // Map(3) { 'a' => 1, 'b' => 2, 'name' => 'foo' }

Array.some/Array.every

  • some() 方法用于判断一个数组中是否有元素能通过判断(Judge)函数:
const nums = [1, 2, 3, 4, 5];

/**
 * 判断数组中是否有元素大于给定值
 * @params {number[]} arr
 * @params {number} target
 * @returns {boolean}
 */
const isAnyGt = (arr, target) => arr.some((t) => t > target);

isAnyGt(nums, 3); // true
  • every() 方法用于判断数组中的每一项是否均可通过判断函数:
/**
 * 判断数组中是否每一项都小于给定值
 * @params {number[]} arr
 * @params {number} target
 * @returns {boolean}
 */
const isAllLt = (arr, target) => arr.every((t) => t < target);

isAllLt(nums, 10); // true

Array.find/Array.findIndex

  • find() 方法用于查找一个数组中一个符合要求的元素:
const currentUser = '1';

const userList = [
  { id: 0, name: 'admin', defunct: false },
  { id: 1, name: 'bob', defunct: false },
  { id: 2, name: 'alice', defunct: true },
];

const currentUser = userList.find((user) => user.id == currentUser);
  • findIndex() 方法查找一个数组中一个符合要求的元素的下标(index):
const currentUserIndex = userList.findIndex((user) => user.id == currentUser);
不同于 indexOf() 方法,findIndex 可以自己控制查找逻辑;

Array.reduce

用于在数组的每一项上调用一次累加器(reducer)方法,并将结果汇总为单个返回值:
const accelerator = (sum, num) => (sum += num);

const nums = [1, 2, 3];

console.log(nums.reduce(accelerator, 0)); // 6
上面的例子用 for .. of 语句可以表达为:
const nums = [1, 2, 3];

let sum = 0;

for (const num of nums) sum += num;

一个更复杂的例子

const nodeList = [
{ id: 0, name: 'node0', parent: null },
{ id: 1, name: 'node1', parent: 0 },
{ id: 2, name: 'node2', parent: 1 },
{ id: 3, name: 'node3', parent: null },
{ id: 4, name: 'node4', parent: 2 },
{ id: 5, name: 'node5', parent: 1 },
];

const tree = nodeList.reduce((tree, node) => {
	if (node.parent != null) {
		const parent = findNodeById(tree, node.parent);
		if (!parent.children) {
			parent.children = [];
		}
		parent.children.push(node);
	} else {
		tree.push (node) ;
	}
	return tree;
}, []):

function findNodeById(tree, id) {
	let result = null;
	(function traverse(root) {
		for (const node of root) {
			const { children = [] } = node;
			if (children. length > 0) {
				traverse(children);
			}
			if (id === node. id && result == null) {
				result = node;
				break;
			}
	})(tree);
	return result;
}
notion image