重整旗鼓,2019自结前端面试小册【ECMAScript 6】

2,797 阅读32分钟

前言

2020年已经到来,是不是该为了更好的2020年再战一回呢? ‘胜败兵家事不期,包羞忍耻是男儿。江东子弟多才俊,卷土重来未可知’,那些在秋招失利的人,难道就心甘情愿放弃吗!

此文总结2019年以来本人经历以及浏览文章中,较热门的一些面试题,涵盖从CSS到JS再到Vue再到网络等前端基础到进阶的一些知识。

总结面试题涉及的知识点是对自己的一个提升,也希望可以帮助到同学们,在2020年会有一个更好的竞争能力。

Module Three - ECMAScript 6

1 - ECMAScript 是什么?

ECMAScript 是编写脚本语言的标准,意味着Javascript遵循ECMAScript标准中的规范变化,可以说是Javascript的蓝图

ECMAScriptJavascript本质上都跟一门语言有关,一个是语言本身,一个是语言的约束条件。

谈一谈对ECMAScript 6的理解

ECMAScript 6是一个新的标准,它包含了许多新的语言特性和库,是Javascript最实质的一次升级,比如箭头函数、字符串模板、generator(生成器)、async/await、解构赋值、class等等,以及引入了module模块概念

2 - ECMAScript 6 / ECMAScript 2015 新增了哪些新特性?

  • 箭头函数
  • Class
  • 模板字符串
  • 加强的对象字面量
  • 对象解构赋值
  • Promise
  • Generator生成器
  • 模块概念
  • Symbol类型
  • Proxy代理
  • Set & Map
  • 函数默认参数
  • rest与展开运算符...
  • 块级作用域

3 - 什么是块级作用域?为什么需要块级作用域?

什么是块级作用域? 由一对花括号{}中的语句集都属于一个块,在这个{}代码块中定义的所有变量在这个代码块之外都是不可见的,因此称为块级作用域

为什么需要块级作用域?

由于ECMAScript 6之前只有全局作用域函数作用域eval作用域,没有块级概念,这会带来很多不合理的场景:

  • 变量提升导致内层变量可能会覆盖外层变量
var i = 5
function fun(){
    console.log(i)
    if(true){
        var i = 6
    }
}
fun() // undefined
  • 用来计数的循环变量被泄漏成全局变量
for (var i = 0; i < 10; i++) {  
    	console.log(i);  
}  
console.log(i);  // 10

为了解决这些问题,ECMAScript 6新增了let / const 实现块级作用域

4 - 细品 letconst,与var区别在哪?

let

  • let - 用于声明变量,用法与var类似,但其声明的变量,只在let所在的代码块中有效
{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1
for (let i = 0; i < 10; i++) {
  // ...
}

console.log(i)  // ReferenceError: i is not defined
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

-------------- var → let ------------------

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
  • let不存在变量提升 - var所定义的变量,存在变量提升现象,即可以在声明之前使用(值为undefined),let改变了这一语法行为,它所声明的变量一定要在声明之后才能使用,否在报错
console.log(bar); // ReferenceError
let bar = 2;
  • 暂时性死区 - 只要块级作用域内存在let,它所声明的变量就'绑定'在这个区域,不受外部影响
var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错

❗ 小知识:

ECMAScript 6明确规定,如果区块中存在let | const命令,这个区块对这些命令声明的变量,从一开始就形成封闭作用域,凡是在let声明变量前,对该变量操作,都会报错(使用let、const命令声明变量之前,该变量都是不可用的,这在语法上,成为'暂时性死区')

  • 不允许重复声明 - let不允许在相同作用域内,重复声明同一个变量

const

  • const - 声明一个只读的常量,一旦声明,常量的值就不能改变
const PI = 3.1415;
PI // 3.1415

PI = 3; // TypeError: Assignment to constant variable.
  • const 声明的变量不得改变值,这意味const一旦声明变量,就必须立即初始化,不能留到以后赋值
const foo  // SyntaxError: Missing initializer in const declaration
  • constlet 相同,只在声明所在的块级作用域内有效
if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined
  • const 同样存在暂时性死区概念,只能在声明的位置之后使用
if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}
  • const 声明的变量,也不能重复声明
var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

var、let、const 三者区别

1 - var【声明变量】
    var 没有块的概念,可以跨块访问,无法跨函数访问

2 - let【声明块中的变量】 - 存在暂时性死区
    let 只能在块作用域里访问,不能跨块访问,更不能跨函数访问

3 - const【声明常量,一旦赋值便不可修改】 - 存在暂时性死区
    const 只能在块级作用域里访问,而且不能修改值
    
    Tips: 这里的不能修改,并不是变量的值不能改动,而是变量所指向的那个内存地址保存的指针不能改动

4 - 在全局作用域下使用letconst声明变量,变量并不会被挂载到window上,这一点与var不同

★ 5 - 什么是箭头函数 ?

ECMAScript 6标准新增了一种新的函数:Arrow Function(箭头函数)

x => x*x

相当于

function(x) {
    return x*x
}
  • 箭头函数相当于匿名函数,并且简化了函数定义,并且没有自己的thisargumentssuper或者new.target
  • 箭头函数更适合用于那些本来需要匿名函数的地方,并且不能用作构造函数
// Es 5
var getDate = function(){
    reutrn new Date()
}

// Es 6
var getDate = () => new Date()

在箭头函数版本中,我们只需要()括号,不需要 return 语句,因为如果我们只有一个表达式或值需要返回,箭头函数就会有一个隐式的返回
  • 箭头函数不能访问arguments对象,所以调用第一个getArgs()时会抛出错误,我们可以通过...rest来存储所有参数,并获取
const getArgs = () => arguments
getArgs('1','2','3') // ReferenceError: arguments is not defined

const getArgs2 = (...rest) => rest
getArgs('1','2','3') // ["1", "2", "3"]
  • 箭头函数没有自己的this,它捕获词法作用域函数的this
const data = {
    result:0,
    nums:[1,2,3,4,5],
    computeResult(){
        // this → data对象
        const addAll = () => {
            return this.nums.reduce((total, cur) => total + cur, 0)
        }
        this.result = addAll()
    }
}

这个例子中,addAll函数将复制computeResult方法中的this值,如果我们在全局作用域声明箭头函数,则this值为window对象

箭头函数需要注意的地方

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new关键字,否则抛出错误
  • 不可以使用arguments对象,该对象在函数体内不存在(可用rest参数代替)
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数

上面四点,第一点尤其重要,this对象的指向是可变的,但在箭头函数中,this是固定的,不可变的

const obj = {
    a: () => {
        console.log(this.id)
    }
}
var id = '1'
obj.a() // '1'
obj.a.call({
    id:'2'
}) // '1'

❗ 小知识: 当箭头函数箭头后面是简单操作时,直接去掉“{ }”,这样可以不使用return 就能会返回值

// 箭头函数常规写法
console.log(Array.from([1, 2, 3], (x) => { return x + x}))  // expected output: Array [2, 4, 6]

// 箭头函数简单操作
console.log(Array.from([1, 2, 3], (x) =>  x + x))  // expected output: Array [2, 4, 6]

★ 6 - 什么是Class?

类的由来

Javascript中,生成实例对象的方法是通过构造函数,这种方式与传统的面向对象语言(比如c++,java)差异很大,ECMAScript 6提供了更接近于传统面向对象的写法,引入了Class类的概念,作为对象的模板。通过Class关键字来定义类

什么是类?

Class类是在Js中编写构造函数的另一种方式,本质上它就是使用构造函数的语法糖,在底层中使用仍然是原型和基`于原型的继承

// Es 5
function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
}
Person.prototype.getPerson = function(){
    return name + '|' + age + '|' + job
}

// Es 6
class Person {
    constructor(name, age, job){
        this.name = name
        this.age = age
        this.job = job
    }
    getPerson(){
        return this.name + '|' + this.age + '|' + this.job
    }
}
var person = new Person('chicago',22,'student')
person.getPerson()  // chicago|22|student
  • 构造函数的prototype属性,在Class类中继续存在,实际上,类的所有方法都定义在类的prototype属性上
class Person {
    constructor(name, age, job){
        this.name = name
        this.age = age
        this.job = job
    }
    getPerson(){
        return this.name + '|' + this.age + '|' + this.job
    }
}
console.log(Person.prototype)  // {constructor: ƒ, getPerson: ƒ}
  • 在类的实例上调用方法,实际上也是调用原型上的方法
class A{
    // ...
}
let a = new A()
console.log(a.constructor === A.prototype.constructor)

// 这其实很好理解,a实例上并没有constructor属性,所以会通过原型链向上查找属性,最后在A类中找到constructor
  • 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
class Point{
    constructor(x,y){
        // ...
    }
    toString(){
        // ...
    }
}
Object.keys(Point.prototype) // [] 这里toString方法是Point类内部定义的方法,是不可枚举的

Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]

constructor方法

  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动会调用该方法,一个类必须有constructor方法,如果没有显式定义,一个空的constructor会被默认添加
class A{}

等同于

class A{
    constructor(){}
}
  • constructor方法默认返回实例对象(即this),我们完全可以指定返回另一个对象
class Foo{
    constructor(){
        return Object.create(null);
    }
}
new Foo() instanceof Foo // false → constructor返回了一个全新的对象,导致实例对象不再是Foo类的实例
  • 实例的属性除非显式定义在其本身(即定义在this上),否则都是定义在原型上
class A{
    constructor(x, y){
        this.x = x
        this.y = y
        this.getA = function(){
            console.log('A')
        }
    }
    toString(){
        return '(' + this.x + ', ' + this.y + ')'
    }
}

var a = new A(2,3)
point.toString(); // (2,3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('getA') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty("toString") // true

x、y、getA都是实例对象a自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true ,而toString是原型对象的属性(因为定义在A类上),所以hasOwnProperty方法返回false

  • Es5一致,类的所有实例也共享一个原型对象
var p1 = new Person('chicago')
var p2 = new Person('Amy')
p1.__proto__ === p2.__proto__  // true

- p1 / p2 都是Person的实例,它们的原型都是`Person.prototype`,所以`__proto__`自然是相同的
- 这也意味着可以通过实例的`__proto__`属性为'类'添加方法

p1.__proto__.getName = function(){
    return 'i get name'
}
p1.getName() // i get name
p2.getName() // i get name
var p3 = new Person('Jack')
p3.getName() // i get name

Class中的 getter / setter

  • Es 5一样,在'类'的内部可以通过使用getset关键字,对某个属性设置存值函数与取值函数
class A{
    constructor(){
        //...
    }
    get prop(){
        return 'getter'
    }
    set prop(value){
        console.log('setter:' + value)
    }
}
let a = new A()
a.prop = 123 // setter:123
a.prop // getter

这里`prop`属性有对应的存值函数和取值函数。因此赋值和读取行为都被自定义了
❗ Ps - 存值函数和取值函数是设置在属性的Descriptor对象上的

关于Class的几个注意点

  • 类必须通过new来调用,否则会报错,这是与普通构造函数的一个主要区别,后者不需要new也可以执行
class Foo{
    constructor(){}
}
Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
  • 类的内部,默认就是严格模式,所以不需要使用use strict指定运行模式(只要代码写在类之中,就只有严格模式可用)
  • 不存在提升 - 类不存在变量提升,这一点与Es5不同
new Foo() // ReferenceError: Cannot access 'Foo' before initialization
class Foo{}

❗ Ps - Es6不会把类的声明提升到代码头部
  • name属性 - 本质上,Es6的类只是Es5的构造函数的一层包装,所以函数的许多特性都被class继承,包括name
class A{}
A.name // A → name属性总是返回跟在`class`关键字后面的类名
  • this指向 - 类的方法内部如果含有this,this指向类的实例,通过this.xxx = ...的方式赋值,都是在实例本身上操作
    • 特殊:当类中的静态方法(static)含有this关键字,则this指向的是类,而不是实例

7 - ECMAScript 6 模板字符串

模板字符串是在Js中创建字符串的一种新方式,我们可以通过使用反引号让模板字符串化

// Es 5 
var greet = 'Hi I\'m Chicago'

// Es 6
var greet = `Hi I'm Chicago`
  • 基本用途:
    • 基本的字符串格式化,将表达式嵌入字符串中进行拼接,用${expr}来界定
    // Es 5
    var name = 'chicago'
    console.log('hello' + name) // hello chicago
    
    // Es 6
    var name = 'chicago'
    console.log(`hello ${name}`) // hello chicgao
    
    • 在Es 5时我们通过反斜杠来做一些转义操作,例如多行字符串等
    //ES5
    var str = '\n' + '   I  \n' + '   Am  \n' + 'Iron Man \n';
    
    // Es 6
    var str = `
        I
        Am
      Iron Man   
    `
    

8 - ECMAScript 6 对象字面量增强

在ES6中当你的对象属性名和当前作用域中的变量名相同时,ES6的对象会自动的帮你完成键到值的赋值

// Es 5
var foo = 'Foo'
var bar = 'Bar'
var A = {
    foo:foo,
    bar:bar
}

// Es 6
var foo = 'Foo'
var bar = 'Bar'
var A = {
    foo,
    bar
}

9 - 对象解构赋值

什么是解构赋值? 从数组和对象中提取值,对变量进行赋值,就称为解构赋值

let a = 1,b = 2,c = 3

等同于

let [a, b, c] = [1, 2, 3]

从对象中获取属性,Es6前的做法是创建一个与对象属性同名的变量,这种方式较繁琐,因为每一个属性都需要一个新变量,Es6中解构赋值就完美的解决这一问题

const person = {
  name: "Chicago",
  age: "22",
  job:'student'
}

// Es 5
var name = person.name
var age = person.age
var job = person.job

// Es 6 解构赋值
var {name, age, job} = person
  • 本质上,这种写法属于'模式匹配',只要等号两边模式相同,左边的变量就会被赋予对应的值
let [foo,[bar],baz] = [1,[2],3]

let [,,c] = ['a','b','c'] 

let [a,,c] = ['a','b','c']

let [a,b,...c] = ['a']  // a:'a'  b:undefined  c:[]
  • 如果解构不成功,变量的值就会赋予undefined
let [a] = []  // a:undefined
  • 不完全解构,即等号左边的模式,只匹配到等号右边的一部分
let [a,b] = [1,2,3]

let [a,[b],d] = [1,[2,3],4]  // a:1  b:2  c:4
  • 如果等号右边不是数组(不是可遍历)时,将会抛出错误
let [foo] = 1  // TypeError: 1 is not iterable
let [foo] = {} // TypeError: {} is not iterable
...
  • 只要某种数据结构具有Iterable接口,都可以采用数组形式的解构赋值
function* fibs(){
    let a = 0
    let b = 1
    while(true){
        yield a;
        [a,b] = [b,a + b]
    }
}
let [first,second,third,fourth,fifth,sixth] = fibs()
sixth // 5
  • 解构赋值允许指定默认值
let [foo = 'Foo'] = []  // foo:'Foo'

let [x,y = 'b'] = ['a']  // x:'a'  y:'b'

let [x,y = 'b'] = ['a', undefined] // x:'a'  y:'b'

❗ Ps:由于Es6内部使用严格相等运算符(===)来进行判断是否有值,所以只有一个数组成员严格等于undefined时,默认值才生效

let [x = 1] = [null]  // x:null

❗ Ps:默认值可以引用解构赋值的其他变量,但该变量必须已经声明

let [x = 1, y = x] = [] // x:1  y:1
let [x = 1, y = x] = [2]  // x:2  y:2
let [x = 1, y = x] = [1, 2] // x:1  y:2
let [x = y, y = 1] = []  // ReferenceError: Cannot access 'y' before initialization
  • 对象的解构赋值
    • 对象的解构赋值与数组有一个重要的区别,数组的元素是按次序排列,变量的取值由它的位置决定,而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
    let {foo,bar} = { foo:'Foo', bar:'Bar' }  // foo:'Foo'  bar:'Bar'
    let {baz} = { foo:'Foo', bar:'Bar' }  // baz:undefined
    
    • 如果变量名与属性名不一致,必须这样实现
    let { foo:baz } = { foo:'Foo', bar:'Bar' }  // baz:'Foo'
    
    { 属性名:变量名 }  - 真正被赋值的是后者
    
    • 同样,对象的解构也可以指定默认值(同理,必须要严格等于undefined,才会生效)
    var { x = 3 } = {}  // x:3
    
    var { message: msg = 'Hello Es 6' } = {} // msg:'Hello Es 6'
    

★ 10 - Promise

Promise可以说是Es6中最重要的一个知识点,想要真正的学好Promise,需要较大篇幅,这里建议浏览阮一峰博客(es6.ruanyifeng.com/#docs/promi…

  • Promise的意义 - Promise异步编程的一种解决方案,是一个对象,通过它可以获取异步操作的消息
  • Promise对象有以下特点
    • 对象的状态不受外界影响Promise对象代表一个异步操作,具有三种状态:pending(进行中)fulfilled(已成功)rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
    • 一旦状态改变,就不会再次改变
    • 任何时候都可以得到结果

❗ 小知识:

Promise对象的状态改变,只有两种可能

  • pending变为fulfilled
  • pending变为rejected

只要这两种情况发生,状态就凝固了(不会再变化),会一直保持这个结果,这时就称为resolved(已定型),如果改变已经发生,再对Promise对象添加回调函数,也会立即得到这个结果

  • 这与Event完全不同,Event的特点是,如果你错过了它,再去监听,是得不到结果的

什么是回调地狱? Promise如何解决回调地狱?

回调地狱 - 如果我们在回调内部存在另外一个异步操作,即出现多层嵌套问题,导致变成一段混乱且不可读的代码,此代码就称为'回调地狱'

  • 多层嵌套问题
  • 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性

这两个问题在回调函数中尤为突出,Promise的诞生就是为了解决这两个问题

Promise解决'回调地狱'

  • 回调函数延迟绑定
  • 返回值穿透
  • 错误冒泡

Promise通过此三个方面解决问题:

let readFilePromise = (fileName) => {
    fs.readFile(fileName, (err, data) => {
        if(err){
            reject(err)
        }else{
            resolve(data)
        }
    })
}

readFilePromise('xxx1').then( res => {
    return readFilePromise('xxx2')
})

上面代码中,回调函数通过在then()中传入,实现了回调函数延迟绑定

let promise = readFilePromise('xxx1').then( res => {
    return readFilePromise('xxx2')  // 返回一个Promise对象
})

promise.then( //... )

根据在then()中回调函数的传入值创建不同的Promise,然后把返回的Promise穿透到外层,后续可继续使用,上面promise实际上就是内部返回的Promisepromise变量可以继续调用then(),这便是返回值穿透

解决多层嵌套,就是结合回调函数延迟绑定返回值穿透,实现链式调用来解决

readFilePromise('xxx1').then( res => {
    return readFilePromise('xxx2')
}).then( res => {
    return readFilePromise('xxx3')
}).then( res => {
    return readFilePromise('xxx4')
})

链式调用解决多层嵌套,那么每次任务执行结束后成功和失败的分别处理,又通过什么解决?

  • Promise通过错误冒泡的方式来解决问题
readFilePromise('xxx1').then( res => {
    return readFilePromise('xxx2')
}).then( res => {
    return readFilePromise('xxx3')
}).catch( err => {
    // 错误处理
})

上面的代码中,不论是前面的错误还是后面的错误,所有产生的错误都会一直向后传递,被catch()接收到,这样一来就不必重复就处理每一个任务的成功和失败结果

关于Promise,请留意then()

Promise具有许多Api

  • then - 为Promise实例添加状态改变时的回调函数
  • catch - 用于指定发生错误时的回调函数
  • finally - 用于指定不管Promise对象最后状态如何,都会执行的操作
  • all - 用于将多个Promise实例,包装成一个新的Promise实例
  • race - 与all一样,将多个实例包装成一个新的Promise实例
  • allSettled - 接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束
  • any - 方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态
  • resolve - 将现有对象转为 Promise 对象
  • reject - 返回一个新的 Promise 实例,该实例的状态为rejected
  • try

其中,then()Promise中出现几率最高的,因此then()返回值是必须理解的

对于一个Promise来说,当一个Promise完成(fulfilled)或者失败(rejected),返回函数将被异步调用。具体的返回值依据以下规则返回:

  • 如果then中的回调函数返回一个值,那么then返回的Promise就会变成接受状态(resolved),并且将返回的值作为接受状态的回调函数的参数值
promise1().then( () => {
  return 'I am return a value'  
}).then( res => {
  console.log(res) // I am return a value  
})
  • 如果then中的回调函数没有返回值,那么then返回的Promise将会成为接受状态(resolved),并且该接受状态的回调函数的参数值为undefined
promise1().then( () => {
  console.log('nothing return')
}).then( res => {
  console.log(res) // undefined 
})
  • 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态(rejected),并且将抛出的错误作为拒绝状态的回调函数的参数值
promise1().then( () => {
  throw new Error('I am Error')
}).then( res => {
  console.log(res) // 不执行
}).catch( err => {
  console.log(err)  // I am Error
})
  • 如果then中的回调函数返回一个已经是接受状态Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态的回调函数的参数值
var promise1 = function(){
   return new Promise((resolve,reject)=>{
       resolve()
   })
}
var promise2 = function(){
    return new Promise((resolve,reject) => {
        resolve('I am p2 and Im resolved')
    })
}

promise1().then( () => {
    // 返回一个已经是接受状态的Promise
    return promise2()  // 如果不加return,这个回调将没有返回值,参考第二点
}).then( res => {
    console.log(res)  // I am p2 and Im resolved  
})
  • 如果then中的回调函数返回一个已经是拒绝状态Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态的回调函数的参数值
var promise1 = function(){
   return new Promise((resolve,reject)=>{
       resolve()
   })
}
var promise2 = function(){
    return new Promise((resolve,reject) =>{
        reject('I am p2 and Im rejected')
    })
}

promise1().then( () => {
    // 返回一个已经是拒绝状态的Promise
    return promise2()  // 如果不加return,这个回调将没有返回值,参考第二点 
}).then( res => {
    console.log(res) // 不执行
}).catch( err => {
    console.log(err) // I am p2 and Im rejected
})
  • 如果then中的回调函数返回一个**未定状态(pending)**的Promise,那么then返回的Promise也是未定状态,并且它的最终状态会与那个Promise的最终状态相同,同时,它变为终态时调用的回调函数的参数值与那个Promise变为终态时的回调函数的参数值相同
var promise1 = function(){
   return new Promise((resolve,reject)=>{
       resolve()
   })
}
var promise2 = function(){
    // 定时器,初始状态未定,3s后更新状态
    return new Promise((resolve,reject) => {
        setTimeout(()=>{
             resolve('p2 after 3s resolve')
            // reject('p2 after 3s reject')
        },3000)
    })
}

promise1().then( () => {
    return promise2()  
}).then( res => {
    console.log(res)  // p2 resolve → p2 after 3s resolve
}).catch( err => {
    console.log(err)  // p2 reject → p2 after 3s reject
})

以上 then与catch均在3s后当promise2()状态改变才会执行

11 - Generator函数是什么,有什么用?

Generator函数是ECMAScript 6提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • Generator函数可以理解成状态机,封装了多个内部状态
  • Generator函数也可以是一个普通函数,但具有两个特征
    • function关键字与函数名之间有一个星好(*)
    • 函数体内部使用yield表达式,定义不同的内部状态

执行Generator函数会返回一个遍历器对象,每一次Generator函数里面的yield表达式都相当于一次遍历器对象的next()方法,并且可以通过next(value)的方式传入自定义的值,来改变Generator函数的行为

Generator的用途

Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景,例如:

  • 异步操作的同步化表达
  • 控制流管理
  • 部署Iterator接口
  • 作为数据结构

12 - ECMAScript 6 - 模块

模块使我们能够将代码基础分割成多个文件,以获得更高的可维护性,并且避免将所有代码放在一个大文件中

Es 6支持模块之前,有两个流行的模块

  • CommonJs - 【NodeJs】
  • AMD(异步模块) - 浏览器

基本上,Es 6使用模块的方式很简单,import用于从另一个文件中获取功能或值,export用于从文件中导出功能或值

Es 5 - CommonJs
// 导出
export.xxx = function(args){
    // todo
}
// 引入
const xxx = requir('xxx').xxx

Es 6 - CommonJs
// 导出
export function xxx(args){
    // todo
}
// 引入
import xxx from 'xxx'

Es 6模块与CommonJs模块的差异

  • CommonJs模块输出的是一个值的拷贝,Es 6模块输出的是值的引用
    • CommonJs模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
    • Es 6模块的运行机制与CommonJs不一样,JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,Es 6的模块化,原始值变了,import加载的值也会跟着变。(Es 6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块)
  • CommonJs模块是运行时加载,Es 6模块是编译时输出接口
    • 运行时加载:CommonJs模块就是对象,即再输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法
    • 编译时加载:Es 6模块不是对象,而是通过export命令显式指定输出的代码,import时采用静态命令的形式,即在import时可以指定加载某个输出值,而不是加载整个模块

CommonJs加载的是一个对象(model.export属性),该对象只有在脚本运行完才会生成

Es 6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成


13 - Symbol

Symbol是Es 6新增的一种数据类型,是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用,表示独一无二的值

语法 - Symbol([description])

description - 可选的字符串,可用于调式但不访问符号本身的符号的说明,如果不加参数,在控制台打印的都是Symbol,不利于区分

用途 - 作为属性名的Symbol

let symbolProp = Symbol()

var obj = {}
obj[symbolProp] = 'hello Symbol'

// Or
var obj = {
    [symbolProp] : 'hello Symbol';
}

// Or
var obj = {};
Object.defineProperty(obj,symbolProp,{value : 'hello Symbol'});

14 - Proxy

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须通过这层拦截,因此提供一种机制,可以对外部的访问进行过滤和修改

Es 6提供Proxy构造函数,用来生成Proxy实例 - var proxy = new Proxy(target,handler)

Proxy对象的所有用法,都是上面这种形式,不同的只是handle参数的写法,其中new Proxy用来生成Proxy实例,target表示所要拦截的对象,handle用来定制拦截行为的对象

Proxy设置默认值(零值)

Js中未设置的默认值是undefinedProxy可以改变这种情况

const withZeroValue = (target, zeroValue) => {
    new Proxy(target, {
        get:(obj,prop) => (prop in obj) ? obj[prop] : zeroValue
    })
}

> (obj, prop) => obj → target  prop → target每一个属性

let pos = {
    x: 4,
    y: 19
}

pos = withZeroValue(pos, 0)
console.log(pos.x, pos.y, pos.z) // 4 19 0

15 - ECMAScript 6 - 新的数据解构 Set

Es 6 提供了新的数据结构Set,它类似于数组,但是成员的值是唯一的,没有重复的值

Set()接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化

const arr = new Set([1,2,3,4,4])
console.log([...set])  // Array(4) [1, 2, 3, 4]   [...xxx]转化为Array

const items = new Set([1,2,3,4,5,5,5,5])
console.log(items) // Set(5) {1, 2, 3, 4, 5}

经典面试考题:一行代码实现数组去重(字符串去重重复字符)

- 去除数组重复成员的方法
[...new Set(array)]

- 去除字符串的重复字符
[...new Set('abbbbc')].join('') // 'abc'  Set → 数组 → 字符串

❗ 小知识:

向Set加入值的时候,不会发生类型转换,所以5和'5'是两个不同的值。Set内部判断两个值是否不同,使用的算法成为Same-value-zero equality,它类似于精确相等运算符(===),主要区别是向Set加入值时认为 NaN 等于自身,而 === 认为 NaN 不等于自身

let set = new Set()
let a = NaN
let b = NaN
set.add(a)
set.add(b)
console.log(set)  // Set(1) { NaN }

同时,Set中两个对象总是不相同的

let set = new Set()
set.add({})
set.add({})
console.log(set)  // Set(2) { {}, {} }

总结Set实例的属性和方法

  • 属性
    • Set.prototype.constructor - 构造函数,默认就是Set函数
    • Set.prototype.size - 返回Set实例的成员总数
  • 方法(操作方法 & 遍历方法)
    • 操作方法
      • Set.prototype.add(value) - 添加某个值,返回Set结构本身
      • Set.prototype.delete(value) - 删除某个值,返回一个布尔值
      • Set.prototype.has(value) - 返回一个布尔值,表示该值是否为Set成员
      • Set.prototype.clear() - 清除所有成员,没有返回值
    • 遍历方法
      • Set.prototype.keys() - 返回键名的遍历器
      • Set.prototype.values() - 返回键值的遍历器
      • Set.prototype.entries() - 返回键值对的遍历器
      • Set.prototype.forEach() - 使用回调函数遍历每个成员

❗ Ps: 由于keys()、values()、entries()返回的都是遍历器对象,且Set结构没有键名,只有键值,所以keys(),values()效果相同,但entries()会输出带有2个相同元素的数组

let set = new Set(['red','green','blue'])
for(let item of set.keys()){
    console.log(item)
}
// red
// green
// blue

for(let item of set.values()){
    console.log(item)
}
// red
// green
// blue

for(let item of set.entries()){
    console.log(item)
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

16 - ECMAScript 6 - 新的数据结构 Map

Map 是 Es6 提供的一种新的数据结构,它类似于对象,也是键值对的集合,但是'键'的范围不限于字符串,各种类型的值(包括对象)都可以当作键,即 Map 提供了一种值 - 值的对应,是一种更完善的Hash结构实现

作为构造函数,Map 可以接受一个数组作为参数,该数组的成员是一个个表示键值对的数组

const map = new Map([
    ['name','张三'],
    ['title','Author']
])
map.size // 2
map.has('name') // true
map.get('name') // '张三'
map.has('title') // true
map.get('title') // 'Author'

❗ 小知识:

事实上,不仅仅是数组,任何具有Iterable接口,且每个成员都是一个双元素数组的数据结构,都可以当作 Map 构造函数的参数,即 Set 和 Map 都可以用来生成新的 Map

总结Map实例的属性和方法

  • 属性
    • Map.prototype.size - 返回Map实例的成员总数
  • 方法(操作方法 & 遍历方法)
    • 操作方法
      • Map.prototype.set(key,value) - 设置键名Key对应的键值Value,然后返回整个Map结构,如果Key已经存在,则键值更新
      • Map.prototype.get(key) - 读取Key对应的键值,如果找不到则返回undefined
      • Map.prototype.has(key) - 返回一个布尔值,表示某个键是否在当前Map对象中存在
      • Map.prototype.delete(key) - 删除某个键,返回true,如果删除失败,则返回false
      • Map.prototype.clear() - 清除所有成员,没有返回值
    • 遍历方法
      • Map.prototype.keys() - 返回键名的遍历器
      • Map.prototype.values() - 返回键值的遍历器
      • Map.prototype.entries() - 返回键值对的遍历器
      • Map.prototype.forEach() - 使用回调函数遍历每个成员

经典面试考题:实现对象数组去重

function unique(arr) {
  return [...new Set(arr.map(e => JSON.stringify(e)))].map(e => JSON.parse(e))
}

17 - rest与展开运算符(...)

展开运算符

展开运算符(spread) 是三个点(...),可以将一个数组转为用逗号分隔的参数序列

基本用法:拆解字符串与数组

var arr = [1,2,3,4]
console.log(...arr) // 1 2 3 4
var str = 'String'
console.log(...str) // S t r i n g

展开运算符的应用

  • 某些场景中可以替代apply
在使用Math.max()求数组最大值时,Es5可以通过apply做到(不友好且繁琐)
var array = [1,2,3,4,3]
var maxItem = Math.max.apply(null,array)
console.log(maxItem)

在Es6中,展开运算符可用于数组的解析,优雅的解决了这个问题
var array = [1,2,3,4,3]
var maxItem = Math.max(...array)
console.log(maxItem) // 4
  • 代替数组的pushconcat等方法
- 把 arr2 塞进 arr1 中
// Es5
var arr1 = [0,1,2]
var arr2 = [3,4,5]
Array.prototype.push.apply(arr1,arr2)
// arr1 → [0,1,2,3,4,5]

// Es6
var arr1 = [0,1,2]
var arr2 = [3,4,5]
arr1.push(...arr2)
// arr1 → [0,1,2,3,4,5]
  • 拷贝数组或对象
var arr = [1,2,3]
var copyArr = [...arr]
console.log(copyArr)  // [1,2,3]

let obj = {
    a: 1
    b:{
        foo:'foo',
        bar:'bar'
    }
}
let objCopy = { ...obj }
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}

obj.a = 2
console.log(objCopy.a) // 1
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'}

展开运算符...来实现拷贝属于浅拷贝,如果属性值是一个对象,拷贝的是地址
  • 将伪数组转化为数组
var nodeList = document.querySelectorAll('div')  
// querySelectorAll 方法返回的是一个 nodeList 对象。它不是数组,而是一个类似数组的对象。
console.log([...nodeList]) // [div,div,div,...] 

rest运算符

rest运算符(剩余运算符) 看起来与展开运算符一样,但是它是用于解构数组和对象。在某种程度上,剩余元素和展开元素相反,展开元素会'展开'数组变成多个元素,剩余元素会收集多个元素和'压缩'成一个单一的元素

rest可以看作是扩展运算符的一个逆运算,它是一个数组

rest参数用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中

例如实现计算传入所有参数的和

使用rest参数:

function sumRest(...m) {
    var total = 0
    for(var i of m){
        total += i
    }
    return total
}
console.log(sumRest(1,2,3))  // 6

rest运算符的应用

  • rest参数代替arguments变量
// arguments写法
function sortNumbers(){
    return Array.prototype.slice.call(arguments).sort()
}

// Es6 rest参数写法
const sortNumbers = (...numbers) => {
    numbers.sort()
}
  • 与解构赋值组合使用
var array = [1,2,3,4,5,6]
var [a,b,...c] = array
console.log(a)  // 1
console.log(b)  // 2
console.log(c)  // [3,4,5,6]

❗ 小知识:

rest参数可理解为剩余的参数,所以必须在最后一位定义,如果定义在中间会报错。

var array = [1,2,3,4,5,6];
var [a,b,...c,d,e] = array;
//  Uncaught SyntaxError: Rest element must be last element

【 十道题通关Promise 】

题目一
const promise = new Promise((resovle,reject) => {
    console.log(1)
    resolve()
    conosole.log(2)
})

promise.then(() => {
    console.log(3)  
})
console.log(4)
Result:

1
2
4
3
题目二
const promise1 = new Promise((resolve,reject) => {
    setTimeout(() => {
        resolve('success')
    }, 1000)
})
const promise2 = new Promise((resolve,reject) => {
    throw new Error('error!!!')
})

console.log('promise1', promise1)
console.log('promise2', promise2)

setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)
Result:

promise1 Promise { <pending> }
promise2 Promise { <pending> }

Uncaught (in promise) Error: error!!!

promise1 Promise { <resolved>: "success" }
promise2 Promise { <rejected>: Error: error!!! }

解析:

promise有三种状态:pending,fulfilled或rejected,状态改变只能是 pending → fulfilledpending → rejected,状态一旦改变则不能再变。上面 promise2并不是promise1,而是返回的一个新的 Promise 实例

题目三
const promise = new Promise((resolve,reject) => {
    resolve('success1')
    reject('error')
    resolve('success2')
})

promise.then(res => {
    console.log('then:', res)
}).catch(err => {
    console.log('catch:', err)
})
Result:

then:success1

解析:

Promise的resolvereject只有第一次执行有效,多次调用没有任何作用(Promise状态一旦改变则不能再变)

题目四
Promise.resolve(1).then( res => {
    console.log(res)
    return 2
}).catch( err => {
    return 3
}).then( res => {
    console.log(res)
})
Result:

1
2

解析:

promise每次调用.then或者.catch都返回一个新的Promise,从而实现链式调用

题目五
const promise = new Promise((resolve,reject) => {
    setTimeout(() => {
        console.log('once')
        resolve('success')
    },1000)
})
const start = Date.now()
promise.then(res => {
    console.log(res, Date.now() - start)
})
promise.then(res => {
    console.log(res, Date.now() - start)
})
Result:

once
success 1001
success 1002

解析:

Promise的thencatch都可以被多次调用,这里promise实例状态一旦改变,并且有了一个值,那么后续每次调用promise.then或者promise.catch都会拿到这个值

题目六
Promise.resolve().then(() => {
    return new Error('error!')
}).then( res => {
    console.log('then:',res)
}).catch( err => {
    console.log('catch:', err)
})
Result:

then: Error: error!

解析:

.then或者.catch中return一个error对象并不会抛出错误,所以不会被后续的.catch捕获,而是进行.then,需要改成以下方式才会被.catch捕获

1 - return Promise.reject(new Error('error!'))
2 - throw new Error('error!')

因为返回任意一个非Promise的值都会被包裹成Promise对象,即return new Error('error!')等于return Promise.resolve(new Error('error!')

题目七
const promise = Promise.resolve().then( () => {
    return promise
})
promise.catch(console.error)
Result:

TypeError: Chaining cycle detected for promise #<Promise>

解析:

.then.catch返回的值不能是promise本身,否则会造成死循环

题目八
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
Result:

1

解析:

.then.catch的参数期望是函数,当传入的是非函数则会发生值穿透

题目九
Promise.resolve(1).then(function success(res){
    console.log('success',res)
    throw new Error('error')
},function fail1(err){
    console.log('fail1',err)  
}).catch(function fail2(err){
    console.log('fail2',err)  
})
Result:

success 1
fail2 Error: error
    at success (...)

解析:

.then可以接受两个参数,第一个是处理成功的参数,第二个是处理错误的函数,.catch实际上是.then第二个参数的简便写法,但是用法上有一点需要注意:

  • .then的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的.catch可以捕获之前的错误
Promise.resolve().then(function success1(res){
    throw new Error('error')
},function fail1(err){
    console.log('fail1',err)  
}).then(function success2 (res) {}, function fail2 (err) {
    console.error('fail2: ', err)
})
题目十
process.nextTick(() => {
    console.log('nextTick')
})
Promise.resolve().then( () => {
    console.log('then')
})
setImmediate(() => {
  console.log('setImmediate')
})
console.log('end')
Result:

end
nextTick
then
setImmediate

解析:

process.nextTickpromise.then均属于微任务microtask,而setImmediate属于宏任务macrotask,仔事件循环EventLoop中,每个宏任务执行完后都会清空当前所有微任务,再进行下一个宏任务


温馨提示😀

  • 关于Es 6相关的手写题,会与JavaScript一起总结,集中成一篇
  • 下一期 - 总结Vue.Js知识点