大家好,今天主要为大家介绍的是 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);
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; }
