重整旗鼓,2019自結前端面試小冊【ECMAScript 6】

CHICAGO發表於2020-01-23

前言

2020年已經到來,是不是該為了更好的2020年再戰一回呢? ‘勝敗兵家事不期,包羞忍恥是男兒。江東子弟多才俊,捲土重來未可知’,那些在秋招失利的人,難道就心甘情願放棄嗎!

此文總結2019年以來本人經歷以及瀏覽文章中,較熱門的一些面試題,涵蓋從CSS到JS再到Vue再到網路等前端基礎到進階的一些知識。

總結面試題涉及的知識點是對自己的一個提升,也希望可以幫助到同學們,在2020年會有一個更好的競爭能力。

重整旗鼓,2019自結前端面試小冊【ECMAScript 6】

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知識點

相關文章