写给后端的前端 ES6+ 新特性指南

998 阅读12分钟

最近在公司做了一次内部分享,给后端 TL 介绍 ES6 之后引入的新特性,这里整理成文字稿分享出来,附 PPT 下载链接

📖 博文地址: blog.mitscherlich.me/2021/11/es6…

大家好,今天主要为大家介绍的是 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 引入了自发明以来最多的特性,所以尤为重要;

Untitled.png

新增语法/关键字

新增关键字: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
  }
}
  • 变量/函数提升的作用:使用 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 实现继承:

Untitled 1.png

有了 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;

async-await.gif

新增语法:箭头函数

基本用法:

  • 定义一个函数:
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
}

新增数据类结构/类型

新类型: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 是一个有限状态机:

Untitled 2.png

一个  Promise  必然处于以下几种状态之一:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled): 意味着操作成功完成。
  • 已拒绝(rejected): 意味着操作失败。

Untitled 3.png

这时候,让我们再来回顾前面实现的 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 回调函数继续完成外层函数的后续逻辑。

Untitled 4.png

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;
}

Untitled 5.png