最近在公司做了一次内部分享,给后端 TL 介绍 ES6 之后引入的新特性,这里整理成文字稿分享出来,附 PPT 下载链接
大家好,今天主要为大家介绍的是 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 引入了自发明以来最多的特性,所以尤为重要;
新增语法/关键字
新增关键字:let
let
用于声明一个变量;- 基本用法:
let a = 1;
let b = a;
b = 2; // let 声明的变脸可以被重新赋值
console.log(a); // ===> 1
console.log(a); // ===> 2
let
与 var
的异同
- 相同点:
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
}
}
- 变量/函数提升的作用:使用
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);
};
变量/函数提升导致的问题:
- 变量相互覆盖;
var message = 'hello world';
function foo() {
console.log(message);
if (false) {
// even falsy
var message = 'bad apple!';
}
}
foo(); // print what ???
- 变量生命周期延长,未即时销毁
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 之前,我们必须得基于 Function
和 prototype
来自定义类型:
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 实现继承:
有了 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
语句允许在任意可迭代对象(Array
,Map
,Set
,String
等)上循环调用每一个元素(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;
- 与
||
运算符的区别在于,??
仅在左值为null
或undefined
的时候生效;而||
运算符在左值为假值 (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
}
新增数据类结构/类型
新类型: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
Map
与 Object
的异同
- 相同点:
- 都可以 Key-Value 形式存储数据;
- 不同点:
Map
的 key 可以是任意值,Object
的 key 只能是string
或symbol
;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
是一个有限状态机:
一个 Promise
必然处于以下几种状态之一:
- 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled): 意味着操作成功完成。
- 已拒绝(rejected): 意味着操作失败。
这时候,让我们再来回顾前面实现的 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
回调函数继续完成外层函数的后续逻辑。
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);
新增常用对象、数组 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);
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;
}