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