那些必會用到的 ES6 精粹

前言

最新的 ECMAScript 都已經到發佈到 2019 版了。

我們應該有的態度是: Stay hungry ! Stay young !

從接觸 vue 到工作中用到 vue 將近 2 年了,在開發 vue 項目中用到了很多 es6 的 api ,es6 給我的開發帶來了很大便利。

本文只總結小汪在工作和麪試中經常遇到的 ES6 及之後的新 api 。

有空就得多總結,一邊總結,一邊重溫學習!!!

正文

1 let 和 const

let 的作用域與 const 命令相同:只在聲明所在的塊級作用域內有效。且不存在變量提升 。

1.1 let

let 所聲明的變量,可以改變。

let a = 123
a = 456 // 正確,可以改變

let b = [123]
b = [456] // 正確,可以改變

1.2 const

const 聲明一個只讀的常量。一旦聲明,常量的值就不能改變。

簡單類型的數據(數值、字符串、布爾值),不可以變動

const a = 123
a = 456 // 報錯,不可改變

let b = [123]
b = [456] // 報錯,不可以重新賦值,不可改變

複合類型的數據(主要是對象和數組),可以這樣子變動

const a = [123]
a.push(456) // 成功

const b = {}
b.name = 'demo'  // 成功

1.3 不存在變量提升

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

所以 for循環的計數器,就很合適使用 let 命令。

let a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

1.4 推薦

對於 數值、字符串、布爾值 經常會變的,用 let 聲明。

對象、數組和函數用 const 來聲明。

// 如經常用到的導出 函數
export const funA = function(){
    // ....
}

2 解構(Destructuring)

2.1 數組

一次性聲明多個變量:

let [a, b, c] = [1, 2, 3];
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3

結合擴展運算符:

let [head, ...tail] = [1, 2, 3, 4];
console.log(head) // 1
console.log(tail) // [2, 3, 4]

解構賦值允許指定默認值:

let [foo = true] = [];
foo // true

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

2.2 對象

解構不僅可以用於數組,還可以用於對象。

let { a, b } = { a: "aaa", b: "bbb" };
a // "aaa"
b // "bbb"

數組中,變量的取值由它 排列的位置 決定;而對象中,變量必須與 屬性 同名,才能取到正確的值。

對象的解構也可以指定默認值。

let {x = 3} = {};
x // 3

let {x, y = 5} = {x: 1};
x // 1
y // 5

2.3 字符串

字符串也可以解構賦值。這是因爲此時,字符串被轉換成了一個類似數組的對象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

2.4 用途

  1. 交換變量的值
let x = 1;
let y = 2;

[x, y] = [y, x];
  1. 從函數返回多個值
// 返回一個數組

function example() {
  let [a, b, c] = [1, 2, 3]
  return  [a, b, c] 
}
let [a, b, c] = example();

// 返回一個對象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
  1. 函數參數的默認值
function funA (a = 1, b = 2){
      return a + b;
}

funA(3) // 5 因爲 a 是 3, b 是 2
funA(3,3) // 6 因爲 a 是 3, b 是 3
  1. 輸入模塊的指定方法

加載模塊時,往往需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

在 utils.js 中:

export const function A (){
    console.log('A')
}

export const function B (){
   console.log('B')
}

export const function C (){
     console.log('C')
}

在 組件中引用時:

import { A, B, C } from "./utils.js" 

//調用
A() // 輸出 A 

3. 模板字符串(template string)

模板字符串(template string)用反引號(`)標識。

3.1 純字符串

所有模板字符串的空格和換行,都是被保留的.

console.log(`輸出值爲 N, 

換行`)
// "輸出值爲 N

換行"

3.2 字符串中加變量

模板字符串中嵌入變量,需要將變量名寫在 ${ } 之中

let x = 1;
let y = 2;

console.log(`輸出值爲:${x}`) // "輸出值爲:1"
console.log(`輸出值爲:${x + y}`) // "輸出值爲:3"

3.3 模板字符串之中還能調用函數。

function fn() {
  return "Hello World";
}

console.log(`輸出值爲:${fn()}`) // "輸出值爲:Hello World"

4. 字符串函數擴展

  • includes():返回布爾值,表示是否找到了參數字符串。
  • startsWith():返回布爾值,表示參數字符串是否在原字符串的頭部。
  • endsWith():返回布爾值,表示參數字符串是否在原字符串的尾部。
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

這三個方法都支持第二個參數,表示開始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

5. 數值擴展

5.1 指數運算符

ES2016 新增了一個指數運算符(**)。

2 ** 2 // 4
2 ** 3 // 8

這個運算符的一個特點是右結合,而不是常見的左結合。多個指數運算符連用時,是從最右邊開始計算的。

// 相當於 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

上面代碼中,首先計算的是第二個指數運算符,而不是第一個。

指數運算符可以與等號結合,形成一個新的賦值運算符(**=)。

let a = 1.5;
a **= 2;
// 等同於 a = a * a;

let b = 4;
b **= 3;
// 等同於 b = b * b * b;

6. 函數的擴展

除了在解構中說到的函數參數的默認值,還有不少經常會用到的方法。

6. 1 rest 參數

ES6 引入 rest 參數(形式爲...變量名),用於獲取函數的多餘參數,這樣就不需要使用 arguments 對象了。rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。

function add(...values) {
  let sum = 0;

  for (let val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面代碼的 add 函數是一個求和函數,利用 rest 參數,可以向該函數傳入任意數目的參數。

注意,rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。

// 報錯
function f(a, ...b, c) {
  // ...
}

6.2 箭頭函數

ES6 允許使用“箭頭”(=>)定義函數。

const f = v => v;
console.log('輸出值:', f(3)) // 輸出值: 3
// 等同於
const f = function (v) {
  return v;
};

如果箭頭函數不需要參數或需要多個參數,就使用一個圓括號代表參數部分。

// 等同於
const f = function () { return 5 };

const sum = (num1, num2) => num1 + num2;
// 等同於
const sum = function(num1, num2) {
  return num1 + num2;
};

如果箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用 return 語句返回。

const sum = (num1, num2) => { return num1 + num2; }

箭頭函數的一個用處是簡化回調函數。

const square = n => n * n;

// 正常函數寫法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭頭函數寫法
[1,2,3].map(x => x * x);

注意: 函數體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象。

this 對象的指向是可變的,但是在箭頭函數中,它是固定的。

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

let id = 21;

foo.call({ id: 42 });
// id: 42

上面代碼中,setTimeout 的參數是一個箭頭函數,這個箭頭函數的定義生效是在 foo 函數生成時,而它的真正執行要等到 100 毫秒後。如果是普通函數,執行時 this 應該指向全局對象window,這時應該輸出 21。但是,箭頭函數導致 this 總是指向函數定義生效時所在的對象(本例是{ id: 42}),所以輸出的是 42。

7. 數組的擴展

擴展運算符(spread)是三個點(...)。它好比 rest 參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。

7.1 數組合並的新寫法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合併數組
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合併數組
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

7.2 函數調用。

function add(x, y) {
  return x + y;
}

const numbers = [4, 4];
add(...numbers) // 8

7.3 複製數組的簡便寫法。

const a1 = [1, 2];
// 寫法一
const a2 = [...a1];
a2[0] = 2;
a1 // [1, 2]
// 寫法二
const [...a2] = a1;
a2[0] = 2;
a1 // [1, 2]

上面的兩種寫法,a2 都是 a1 的克隆,且不會修改原來的數組。

7.4 將字符串轉爲真正的數組。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

7.5 數組實例的 entries(),keys() 和 values()

用 for...of 循環進行遍歷,唯一的區別是 keys() 是對鍵名的遍歷、values() 是對鍵值的遍歷,entries() 是對鍵值對的遍歷。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

7.6 includes()

Array.prototype.includes 方法返回一個布爾值,表示某個數組是否包含給定的值,與字符串的 includes 方法類似。ES2016 引入了該方法。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

該方法的第二個參數表示搜索的起始位置,默認爲 0。如果第二個參數爲負數,則表示倒數的位置,如果這時它大於數組長度(比如第二個參數爲 -4,但數組長度爲 3 ),則會重置爲從 0 開始。

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

8. 對象的擴展

8.1 屬性和方法 的簡潔表示法

let birth = '2000/01/01';

const Person = {

  name: '張三',

  //等同於birth: birth
  birth,

  // 等同於hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

8.2 Object.assign()

Object.assign方法用於對象的合併,將源對象(source)的所有可枚舉屬性,複製到目標對象(target)。

const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign方法的第一個參數是目標對象,後面的參數都是源對象。

注意,如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。

const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign 方法實行的是淺拷貝,而不是深拷貝。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

上面代碼中,源對象 obj1 的 a 屬性的值是一個對象,Object.assign 拷貝得到的是這個對象的引用。這個對象的任何變化,都會反映到目標對象上面。

9. Set

ES6 提供了新的數據結構 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


// 去除數組的重複成員
const array = [1, 1, 2, 3, 4, 4]
[...new Set(array)]
// [1, 2, 3, 4]

10. Promise 對象

Promise 是異步編程的一種解決方案。

Promise 對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。

Promise 對象的狀態改變,只有兩種可能:從 pending 變爲 fulfilled 和從 pending 變爲
rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)

const someAsyncThing = function(flag) {
  return new Promise(function(resolve, reject) {
    if(flag){
        resolve('ok');
    }else{
        reject('error')
    }
  });
};

someAsyncThing(true).then((data)=> {
  console.log('data:',data); // 輸出 'ok'
}).catch((error)=>{
  console.log('error:', error); // 不執行
})

someAsyncThing(false).then((data)=> {
  console.log('data:',data); // 不執行
}).catch((error)=>{
  console.log('error:', error); // 輸出 'error'
})

上面代碼中,someAsyncThing 函數成功返回 ‘OK’, 失敗返回 ‘error’, 只有失敗時纔會被 catch 捕捉到。

最簡單實現:

// 發起異步請求
    fetch('/api/todos')
      .then(res => res.json())
      .then(data => ({ data }))
      .catch(err => ({ err }));

來看一道有意思的面試題:

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function() {
  console.log(4);
});
console.log(5);

這道題應該考察 JavaScript 的運行機制的。
首先先碰到一個 setTimeout,於是會先設置一個定時,在定時結束後將傳遞這個函數放到任務隊列裏面,因此開始肯定不會輸出 1 。
然後是一個 Promise,裏面的函數是直接執行的,因此應該直接輸出 2 3 。
然後,Promise 的 then 應當會放到當前 tick 的最後,但是還是在當前 tick 中。
因此,應當先輸出 5,然後再輸出 4 。
最後在到下一個 tick,就是 1 。
答案: “2 3 5 4 1”

11. async 函數

ES2017 標準引入了 async 函數,使得異步操作變得更加方便。

async 函數的使用方式,直接在普通函數前面加上 async,表示這是一個異步函數,在要異步執行的語句前面加上 await,表示後面的表達式需要等待。async 是 Generator 的語法糖

    1. async 函數內部 return 語句返回的值,會成爲 then 方法回調函數的參數。
async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

上面代碼中,函數 f 內部 return 命令返回的值,會被 then 方法回調函數接收到。

    1. async 函數內部拋出錯誤,會導致返回的 Promise 對象變爲 reject 狀態。拋出的錯誤對象會被 catch 方法回調函數接收到。
async function f() {
  throw new Error('出錯了');
}

f().then(
  result => console.log(result),
  error => console.log(error)
)
// Error: 出錯了
    1. async 函數返回的 Promise 對象,必須等到內部所有 await 命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到 return 語句或者拋出錯誤。也就是說,只有 async 函數內部的異步操作執行完,纔會執行 then 方法指定的回調函數。

下面是一個例子:

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log('完成'))
// "ECMAScript 2017 Language Specification"

上面代碼中,函數 getTitle 內部有三個操作:抓取網頁、取出文本、匹配頁面標題。只有這三個操作全部完成,纔會執行 then 方法裏面的 console.log。

    1. 在 vue 中,我們可能要先獲取 token ,之後再用 token 來請求用戶數據什麼的,可以這樣子用:
methods:{
        getToken() {
            return new Promise((resolve, reject) => {
                this.$http.post('/token')
                    .then(res => {
                        if (res.data.code === 200) {
                           resolve(res.data.data)
                        } else {
                            reject()
                        }
                    })
                    .catch(error => {
                        console.error(error);
                    });
            })
       },
       getUserInfo(token) {
            return new Promise((resolve, reject) => {
                this.$http.post('/userInfo',{
                        token: token
                    })
                    .then(res => {
                        if (res.data.code === 200) {
                           resolve(res.data.data)
                        } else {
                            reject()
                        }
                    })
                    .catch(error => {
                        console.error(error);
                    });
            })
       },
       async initData() {
            let token = await this.getToken()
            this.userInfo = this.getUserInfo(token)
       },
}

12. import 和 export

import 導入模塊、export 導出模塊

// example2.js  // 導出默認, 有且只有一個默認
export default const example2 = {
  name : 'my name',
  age : 'my age',
  getName  = function(){  return 'my name' }
}
//全部導入 // 名字可以修改
import people from './example2.js'

-------------------我是一條華麗的分界線---------------------------

// example1.js // 部分導出
export let name  = 'my name'
export let age  = 'my age'
export let getName  = function(){ return 'my name'}

// 導入部分 // 名字必須和 定義的名字一樣。
import  {name, age} from './example1.js'

//有一種特殊情況,即允許你將整個模塊當作單一對象進行導入
//該模塊的所有導出都會作爲對象的屬性存在
import * as example from "./example1.js"
console.log(example.name)
console.log(example.age)
console.log(example.getName())

-------------------我是一條華麗的分界線---------------------------

// example3.js  // 有導出默認, 有且只有一個默認,// 又有部分導出
export default const example3 = {
  birthday : '2018 09 20'
}
export let name  = 'my name'
export let age  = 'my age'
export let getName  = function(){ return 'my name'}

// 導入默認與部分
import example3, {name, age} from './example1.js'

總結:

1.當用 export default people 導出時,就用 import people 導入(不帶大括號)

2.一個文件裏,有且只能有一個 export default。但可以有多個 export。

3.當用 export name 時,就用 import { name }導入(記得帶上大括號)

4.當一個文件裏,既有一個 export default people, 又有多個 export name 或者 export age 時,導入就用 import people, { name, age } 

5.當一個文件裏出現 n 多個 export 導出很多模塊,導入時除了一個一個導入,也可以用 import * as example

13. Class

對於 Class ,小汪用在 react 中較多。

13.1基本用法:

//定義類
class FunSum {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  sum() {
    console.log( this.x +this.y')
  }
}

// 使用的時候,也是直接對類使用new命令,跟構造函數的用法完全一致。
let f = new FunSum(10, 20);
f.sum() // 30

13.2 繼承

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}

上面代碼中,constructor 方法和 toString 方法之中,都出現了super關鍵字,它在這裏表示父類的構造函數,用來新建父類的 this 對象。

子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯。這是因爲子類自己的 this 對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法。如果不調用 super 方法,子類就得不到 this 對象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代碼中,ColorPoint 繼承了父類 Point,但是它的構造函數沒有調用 super 方法,導致新建實例時報錯。

最後

總結和寫博客的過程就是學習的過程,是一個享受的過程 !!!

好了,面試和工作中用到 ES6 精粹幾乎都在這了。

文章很多內容參考了:ECMAScript 6 標準入門

如果你是 JavaScript 語言的初學者,建議先看 《JavaScript 語言入門教程》

你以爲本文就這麼結束了 ? 精彩在後面 !!!

對 全棧開發 有興趣的朋友可以掃下方二維碼關注我的公衆號

我會不定期更新有價值的內容。

微信公衆號:愛寫bugger的阿拉斯加
分享 前端開發、後端開發 等相關的技術文章,熱點資源,全棧程序員的成長之路。

關注公衆號並回復 福利 便免費送你視頻資源,絕對乾貨。

福利詳情請點擊: 免費資源分享——Python、Java、Linux、Go、node、vue、react、javaScript

愛寫bugger的阿拉斯加

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章