大家好,今天主要为大家介绍的是 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); // ===> 2let 与 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' } ==> ktMap 与 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); // falseObject.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); // trueevery()方法用于判断数组中的每一项是否均可通过判断函数:
/**
* 判断数组中是否每一项都小于给定值
* @params {number[]} arr
* @params {number} target
* @returns {boolean}
*/
const isAllLt = (arr, target) => arr.every((t) => t < target);
isAllLt(nums, 10); // trueArray.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;
}