阅读 1083

我理解的ES6

前言

我是通过阮一峰老师的ES6教程入门的,基本上是把ES6的几个核心特性过了一遍,但是面试官一问深我就???了,还是实际运用的太少。

本篇文章也偏总结类,结合我亲身经历的高频面试题,建议大家必须要对箭头函数、Promise、Generator、async等内容深入理解。

工具:Babel是一个 ES6 转码器,可以将 ES6 代码转为 ES5 代码,以便兼容那些还没支持ES6的平台。

String字符串优化

新增了字符串模板,在拼接大段字符串时,用反斜杠取代以往的字符串相加的形式,能保留所有空格和换行,使得字符串拼接看起来更加直观,更加优雅。

新增了includes()方法,用于取代传统的只能用indexOf查找包含字符的方法, 此外还新增了startsWith(), endsWith(), padStart(),padEnd(),repeat()等方法,可方便的用于查找,补全字符串。

Array数组优化

数组解构赋值: 如ES6可以直接以let [a,b,c] = [1,2,3]形式进行变量赋值,映射关系更清晰。

扩展运算符

  • 可以将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])  // 1 2 3
复制代码
  • 可以实现数组的复制和解构赋值 (let a = [2,3,4]; let b = [...a])

  • 可以取代arguments对象和apply方法,轻松获取未知参数个数情况下的参数集合。

使用扩展运算符替代函数的apply方法:

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6 写法
let args = [0, 1, 2];
f(...args);
复制代码

JS中遍历数组的方法:

  1. for循环
  2. forEach
myArray.forEach(function(value){
    console.log(value);
})
复制代码

无法中途跳出forEach循环,break命令或return命令都不能奏效。

  1. for... in

for…in主要是为遍历对象而设计的,不适用于遍历数组。

遍历数组的缺点:

  • 数组的键名是数字,但是for…in循环是以字符串作为键名的。
  • 某些情况下,for…in循环会以任意顺序遍历键名。
  1. for...of (ES6)
for(let value of myArray){
    console.log(value);
}
复制代码
  • 不同用于forEach方法,它可以与break、continue和return配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

for of 和 for in 的总结:

  • 推荐在循环对象属性的时候,使用for...in, 在遍历数组的时候的时候使用for...of。
  • for...in循环出的是key,for...of循环出的是value
  • 注意,for...of是ES6新引入的特性。修复了ES5引入的for...in的不足
  • for...of不能循环普通的对象,需要通过和Object.keys()搭配使用

Object类型优化

  1. 对象属性变量式声明:

ES6可以直接以变量形式声明对象属性或者方法。比传统的键值对形式声明更加简洁,更加方便,语义更加清晰。

let [apple, orange] = ['red appe', 'yellow orange'];
let myFruits = {apple, orange};    
// let myFruits = {apple: 'red appe', orange: 'yellow orange'};
复制代码
  1. 对象的扩展运算符(...)

可将一个数组转为用逗号分隔的参数序列,主要用于函数调用。 console.log(...[1, 2, 3]) // 1 2 3

  1. super 关键字

ES6在Class类里新增了类似this的关键字super。同this总是指向当前函数所在的对象不同,super关键字总是指向当前函数所在对象的原型对象。

箭头函数

基本使用:

如果 return 值就只有一行表达式,可以省去 return,默认表示该行是返回值,否则需要加一个大括号和 return。如果参数只有一个,也可以省去括号,两个则需要加上括号。

var f = v => v*2;
// 等价于
var f = function(v){
  return v*2;
}

// 判断偶数
var isEven = n => n % 2 == 0;

// 需要加 return
var = (a, b) => {
  if(a >= b)
    return a;
  return b;
}
复制代码

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。

rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

//利用 rest 参数,可以向该函数传入任意数目的参数。
function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 5, 3) // 10
复制代码

面试问题:箭头函数和普通函数的区别

  1. 箭头函数没有自己的this对象

函数中的 this 始终是指向函数执行时所在的对象。比如全局函数执行时,this 指向 window,对象的方法执行时,this 指向该对象,这就是函数 this 的可变。

而箭头函数中的 this 是固定的,箭头函数继承自己作用域的上一层的this,就是上一级外部函数的 this 的指向。任何方法都改变不了其指向,如call(), bind(), apply()。

一个例子:

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;
foo.call({ id: 42 });  // id: 42
复制代码

执行的结果是 42 而不是全局的 21,表示 setTimeout 函数执行的时候,this 指向的不是 window。因此箭头函数很好地解决了匿名函数和setTimeoutsetInterval的this指向问题,不用再去给其用that变量存储this。

对箭头函数中关于 this 的总结:在对象的方法中直接使用箭头函数,会指向 window,其他箭头函数 this 会指向上一层的 this,箭头函数并没有存储 this。

var obj = {
  id: 1,
  foo: ()=>{
    return this.id;
  }
}
var id = 2;
obj.foo(); // 2
复制代码
  1. 箭头函数不能当做构造函数,不能使用new,因为它没有自己的this,无法实例化。
  2. 箭头函数不绑定arguments, 取而代之用rest参数(形式为...变量名)。也没有supernew.target
  3. 不可以使用yield命令,箭头函数不可用作Generator函数。
  4. 箭头函数没有原型属性。

Set 和 Map

  1. Set

ES6引入的一种类似Array的新的数据结构,Set实例的成员类似于数组item成员,区别是Set实例的成员都是唯一,不重复的。这个特性可以轻松地实现数组去重

Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4
复制代码
  1. Map

JavaScript 的对象(Object),本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

Map 是ES6引入的一种类似Object的新的数据结构,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"
复制代码

let 和 const

没有块级作用域回来带很多难以理解的问题,比如for循环var变量泄露,变量覆盖等问题。

let 声明的变量拥有自己的块级作用域,形如for (let x...)的循环在每次迭代时都为x创建新的绑定。且修复了var声明变量带来的变量提升问题。(必须声明 'use strict' 后才能使用let声明变量,否则浏览并不能显示结果)

“变量提升”现象:即变量可以在声明之前使用,值为undefined。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

ES5 只有全局作用域和函数作用域,没有块级作用域。

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}
复制代码

const 声明一个只读的常量。一旦声明,常量的值就不能改变。 const 声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化。

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

总结: 使用var声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象; 使用let声明的变量,其作用域为该语句所在的代码块内,不存在变量提升; 使用const声明的是常量,在后面出现的代码中不能再修改该常量的值。

Promise

主要作用是用来解决JS回调机制产生的“回调地狱”。 回调地狱带来的负面作用有以下几点:

  • 代码臃肿, 可读性差, 复用性差, 容易滋生 bug。
  • 耦合度过高,可维护性差。
  • 只能在回调里处理异常。

Promise它不是新的语法功能,而是一种新的写法,将回调函数的嵌套,改成链式调用。

new Promise(请求1)
    .then(请求2(请求结果1))
    .then(请求3(请求结果2))
    .catch(处理异常(异常信息))
复制代码

Promise 使用总结:

  1. 可以通过两种方式初始化一个 Promise 对象,都会返回一个 Promise 对象。
  • new Promise(fn)
  • Promise.resolve(fn)
  1. 然后调用上一步返回的 promise 对象的 then 方法,注册回调函数。 then 中的回调函数可以有一个参数,也可以不带参数。如果 then 中的回调函数依赖上一步的返回结果,那么要带上参数。

  2. 最后注册 catch 异常处理函数,处理前面回调中可能抛出的异常。

简单例子:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});
复制代码

timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

一些常用API:

Promise.race

类方法,多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。

Promise.all

类方法,多个 Promise 任务同时执行。 如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。

如果后续任务是异步任务的话,必须return 一个 新的 promise 对象。如果后续任务是同步任务,只需 return 一个结果即可。

new Promise(买菜)
//用买好的菜做饭
.then((买好的菜)=>{
    return new Promise(做饭);
})
复制代码

一个 Promise 对象有三个状态,并且状态一旦改变,便不能再被更改为其他状态:

  • pending,异步任务正在进行。
  • resolved (也可以叫fulfilled),异步任务执行成功。
  • rejected,异步任务执行失败。

generator 以及 async/await 语法使异步处理更加接近同步代码写法,可读性更好,同时异常捕获和同步代码的书写趋于一致。

(async ()=>{
    let 蔬菜 = await 买菜();
    let 饭菜 = await 做饭(蔬菜);
    let 送饭结果 = await 送饭(饭菜);
    let 通知结果 = await 通知我(送饭结果);
})();
复制代码

Generator

Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。

  • function关键字与函数名之间有一个星号;
  • 函数体内部使用yield表达式,定义不同的内部状态。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }
复制代码

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

async和await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数就是 Generator 函数的语法糖。

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。并且返回一个 Promise,可以使用then方法添加回调函数。 当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async函数对 Generator 函数的改进:

  1. 内置执行器 async函数自带执行器,不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行。
  2. 更好的语义
  3. 更好的适用性
  4. 返回值是 Promise 进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

例子:getJSON函数返回一个promise,这个promise成功resolve时会返回一个json对象。我们只是调用这个函数,打印返回的JSON对象,然后返回”done”。

// promise
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
makeRequest()
复制代码
//使用Async/Await
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
makeRequest()

//async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)
复制代码

Async的优缺点:

优势: 处理 then 的调用链能够更清晰准确的写出代码。

缺点: 滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

Iterator

是ES6中一个很重要概念,它并不是对象,也不是任何一种数据类型。为Set、Map、Array、Object新增一个统一的遍历API。部署了Iterator接口的对象(可遍历对象)都可以通过for...of去遍历。

class

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已,可以看做是构造函数的另一种写法。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

//ES6的class改写
class Point {
  //构造方法
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
复制代码

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

class实现继承: Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

总结

日常前端代码开发中,有哪些值得用ES6去改进的编程优化或者规范:

  • 常用箭头函数来取代var self = this;的做法。
  • 常用let取代var命令。
  • 常用数组/对象的结构赋值来命名变量,结构更清晰,语义更明确,可读性更好。
  • 在长字符串多变量组合场合,用模板字符串来取代字符串累加,能取得更好地效果和阅读体验。
  • 用Class类取代传统的构造函数,来生成实例化对象。
  • 在大型应用开发中,要保持module模块化开发思维,分清模块之间的关系,常用import、export方法。