深入理解 ES6

yck發表於2017-09-08

本文篇幅較長,有興趣的可以先收藏再看。本文將重要的 ES6 特性介紹了一遍,並且詳細解釋了一些重難點。

let && const

letvar 的宣告用法相同,但是多了一個臨時死區(Temporal Distonrtion Zone)的概念。

console.log(a) // -> undefined
var a = 1
console.log(b) // -> Uncaught ReferenceError: b is not defined
let b = 1複製程式碼

可以發現在宣告前使用 let 宣告的變數會導致報錯,這解決了 JS 很多奇怪的問題。並且使用 let 會生成一個塊級作用域,作用域外不能訪問該變數。

{
let a = 1;
var b = 1;
}
console.log(b); // -> 1
console.log(a); //  -> Uncaught ReferenceError: b is not defined複製程式碼

在 JS 中,宣告變數都會提升,不論用什麼關鍵字宣告。當使用 let 時變數也會被提升至塊級作用域的頂部,但是隻提升宣告,不提升初始化。並且會產生臨時死區,該區域會存放變數,直到執行過宣告語句後,方可使用該變數。

在迴圈中 let 會與前面有些不同,每次迭代都會產生一個新的變數,並用之前的值初始化,如何理解這句話呢,請看以下程式碼。

for(let i = 0; i < 10; i++) {
    console.log(i) // -> 輸入 0 - 9
}
// 上面的迴圈程式碼可以這樣看
{ // 形成塊級作用域
    let i = 0
    {
        let ii = i
        console.log(ii)
    }
    i++
    {
        let ii = i
        console.log(ii)
    }
    i++
    {
        let ii = i
        console.log(ii)
    }
    ...
}複製程式碼

constlet 基本類似,只是用 const 宣告必須賦值,並且不得修改繫結,什麼意思呢,請看程式碼。

const a = 1;
a = 2 // -> Uncaught TypeError: Assignment to constant variable
// but
const b = {a: 1};
b.a = 2 // 起效複製程式碼

當然了,有辦法讓這個不能改變

const b = Object.freeze({a: 1})
b.a = 2 // 沒有報錯,但是 b.a 沒有被改變複製程式碼

但是 Object.freeze 只能在這裡有效,對於陣列這些可以看看這個提案

這兩個新的宣告方式在全域性作用域下不會自動加上 window

字串相關

部分新增的字串函式

let string = 'startend'
string.includes('a') // -> true 是否包含
string.endsWith('end') // -> true 是否由 end 結尾
string.startsWith('start') // -> true 是否由 start 開頭複製程式碼

模板字面量

很棒的新功能,解決了之前很多麻煩的寫法。

// 語法就是 `` 代替之前的引號,在 `` 中使用引號不需要轉義
let s = `it's string`複製程式碼

多行字串

// 這樣在語法中就可以換行了
let s = `start \
end`
// 注意在模板字面量中的任何空白符都是起效的
let s = `start \
    end`  // ->  start     end複製程式碼

佔位符和標籤模板

let s = 'string'
let message = `start${s}`  // -> startstring
// ${} 就是佔位符語法,可以更簡便的實現字串插入

// 定義一個 tag 函式,然後直接在 `` 前使用就可以
let m = tag`s${s}e${message}`
// strings 是一個陣列,value 是模板字面量中所有的佔位符的值
function tag(strings, ...value) {
    // -> ['s', 'e', '']
    console.log(strings)
    // -> ['string', 'startstring']
    console.log(value)
}
// 上面的 ...value 也是 ES6新出的擴充套件語句,在這裡代表不定引數的寫法,用於替換 arguments
// 不定引數使用上也是有限制的,必須放在所有引數的末尾,並且在每個函式中只能宣告一次
// 擴充套件語句和 arguments 區別就是代表了 strings 引數後面的所有引數
// 除了上面的寫法,還可以用於展開可以迭代(有Symbol.iterator屬性)的物件
let array = [1, 2, 3]
console.log(...array)
// 該語法可以解決之前很多地方只能傳入單個引數,只能使用 apply 解決的問題
Array.prototype.unshift.apply([4, 5], array) // -> [1, 2, 3, 4, 5]
// 現在可以直接這樣寫
[4, 5].unshift(...array)
// 展開運算不受不定引數的條件限制,可以一起用複製程式碼

函式

預設引數

ES6 允許給函式增加預設引數

function fn(a = 1, b = 2) {}
// 預設值也可以通過呼叫函式獲得,注意必須呼叫函式
function fn1(a = 1, b = fn()) {}複製程式碼

新增函式內部方法

在 JS 中,函式有多種用法,可以直接呼叫,也可以通過 new 建構函式。

在 ES6中,函式內部新增了 [[Call]] 和 [[Construct]] 兩個方法。後者會在使用 new 建構函式時執行,其他情況會執行前者方法。

當一個函式必須使用 new 構造時,你可以使用這個新屬性 new.target 判斷

// new.target 只能在函式中使用
function Fn() {
    if (typeof new.target === 'underfined') { throw ....... }
}複製程式碼

箭頭函式

這個特性真的很棒,先介紹下他的幾種語法

// 最簡單的寫法,只有一個引數,單行表示式
value => value
// 多個引數需要使用小括號包裹
(v1, v2) => v2 + v1
// 沒有引數需要使用小括號包裹
() => "balabala"
// 多行表示式需要大括號包裹
(v1, v2) => {
    return v1 + v2
}
// 返回一個物件,需要用小括號包裹
() => ({a: 1})
// 立即執行函式,注意普通的立即執行函式的小括號包裹在最外面,箭頭函式不需要
((value) => value)("balabala")複製程式碼

箭頭函式和普通函式區別還是蠻大的,說幾個常用的

  • 沒有 this,不能改變 this 繫結
  • 不能通過 new 呼叫,當然也沒有原型
  • 沒有 arguments 物件,不能有相同命名引數

箭頭函式雖然沒有 this ,但是還是可以在內部使用 this

  • this 的繫結取決於定義函式時的上下文環境
  • 一旦函式呼叫,任何改變 this 的方法都無效
// let 有個細節
let x = 11111
let a = {
    x: 1,
    init() {
        // 箭頭函式的 this 取決於 init,所以可以列印出 1
        document.addEventListener('click', () => console.log(this.x))
    },
    allowInit: () => {
        // allowInit 直接是個箭頭函式,所以這時的 this 變成了 window
        // 但是並不會列印出 11111,忘了 let 的一個細節的可以回到上面看看
        console.log(this.x)) 
    }
    otherInit() {
        // 普通函式的 this 取決於呼叫函式的位置,this 指向 document
        // 如果想列印出 x,可以使用 bind
        document.addEventListener('click', function() {
            console.log(this.x)
        })
    }
}
a.init() // -> 1
a.allowInit() // -> undefined
a.otherInit() // -> undefined複製程式碼

物件相關

let a = 1
// 當 key 和 value 名字相同時可以簡寫
let b = { a }
// 物件中的方法也可以簡寫
let a = {
    init() {}
}
// 物件屬性名也可以計算 
let name = 'name'
b[name + '1'] = 2 // === b['name1'] = 2複製程式碼

ES6 也新增了幾個物件方法

Object.is(NaN, NaN) // ->true
// 結果基本於 === 相似,除了 NaN 和 +0 -0
Object.is(+0, -0) // -> false
let o = {a: 1}
let a = Object.assign({}, o) // -> {a: 1}
// 第一個引數為目標引數,後面的引數是不定的,引數屬性名如果有重複,後面的會覆蓋之前的複製程式碼

原型相關

ES6 之前改變物件原型很麻煩

let obj = {a: 1}
let obj1 = {a: 2}
// 已 obj 為原型
let a = Object.create(obj)
// 改變 a 的原型為 obj1
Object.setPrototypeOf(a, obj1) // a.a === 2複製程式碼

訪問原型

Object.getPrototypeOf(a) // 訪問原型
// ES6 中可以直接通過 super 代表原型
let a = {
    init() {
        return 'Hello'
    }
}
let b = {
    init() {
    // 不能在 super 之前訪問 this
        return super.init() + 'World'
    }
}
Object.setPrototypeOf(b, a)
b.init() // -> 'HelloWorld'複製程式碼

但是 super 不是每個函式都可以使用的,只有在函式的簡寫語法中方可使用。因為在 ES6中新增了一個函式內部屬性 [[HomeObject]],這個屬性決定了是否可以訪問到 super。首先在該屬性上呼叫 Object.getPrototypeOf(繫結的物件),然後找到原型中的同名函式,在設定 this 繫結並且呼叫函式,其實就是一個新增的語法糖。

解構賦值

該特性可以用於物件,陣列和傳參。

let obj = {a: 1, b: 2}
// 物件解構使用 {},陣列解構使用 [],因為這裡是物件解構,c 不是 obj 的屬性,所以 underfined
// 陣列解構中,如果需要解構的變數大於陣列索引,多出來的變數也是 undefined
// 解構必須賦值,否則報錯。不能 let {a, b, c};
// 賦值不能為 null 或者 undefined,會報錯
let {a, b, c} = obj
// 等於 let a = obj.a,可以看做之前介紹的物件屬性簡寫
console.log(a, b, c) // -> 1, 2, underfined
// 如果已經宣告瞭變數並且想使用解構,必須最外面是小括號
({a, b} = obj)
// 如果不想使用 obj 中的物件名,又想使用解構賦值
let {x: a} = obj
// 如果想使用預設值
let {a = 2, c= 3} = obj // -> 1, 3
// 因為 a 是 obj 中的物件,所以預設值被覆蓋
// 解構也可以巢狀
let obj = {data: {code: 1}, message: [1, 2]}
// 這個寫法在 json 中很好用
// 注意在這個寫法中,data 和 message 都是指代了 obj 的屬性,並沒有被宣告變數
let { data: {code}, message: [a] } = obj
console.log(code, a)
// 陣列解構和物件解構基本相似,並且簡單多了
let message = [1, 2, 3, 4]
// 因為陣列取值只能索引取,所以想跳過某幾個索引,就用逗號代替
// 同樣,陣列解構也可以使用預設值和巢狀解構,和物件解構一模一樣就不贅述了
let [a, , b] = message // -> 1, 3
// 在上面章節介紹了擴充套件語法,同樣也可以使用在陣列解構中
// 可以看到 b 變成了一個陣列
let [a, ...b] = message // -> 1, [2, 3, 4]
// 傳參使用解構可以讓要傳的引數更加清晰
function fn(name, {key, value}) {
    console.log(name, key, value)
}
// 使用,注意:傳參解構必須起碼傳入一個值,否則報錯
fn(1, {key: 2, value: 3})
// 因為傳參解構類似以下寫法
function fn(name, {key, value}) {
    let {key, value} = null // 這個上面講過不能這樣寫
}複製程式碼

Symbol

ES6 新出的第六個原始型別。多用於避免程式碼衝突,作為一個私有屬性使用,不會被屬性遍歷出來。可以使用 Object.getOwnPropertySymbols() 檢索 Symbol 屬性。

建立和使用

// 建立
 let a = Symbol()
 // 更推薦這種寫法,可以更加明確這個Symbol的用途
 // 並且有函式可以通過這個字串取到相應的Symbol
 let b = Symbol('is b')
 // 使用,一般作為可計算屬性使用
 let a = {}
 let b = Symbol('is b')
 a[b] = 1
 // 可以在全域性登錄檔中共享同一個 Symbol,但不推薦使用
 // 不存在 is a 會自動建立
 let a = Symbol.for('is a')複製程式碼

暴露內部操作

Symbol 中預定義了一些 well-know Symbol,這些 Symbol 定義了一些語言的內部實現

  • Symbol.hasinstance,用於執行 instanceof 時檢測物件的繼承資訊
  • Symbol.isConcatSpreadable,布林值,用於判斷當使用 concat 函式時是否將陣列展開
  • Symbol.iterator,迭代器,後面會講到
  • Symbol.match,Symbol.replace,Symbol.search,Symbol.split,字串相關方法的對應內部實現
  • Symbol.toPrimitive,返回物件原始值
  • Symbol.toStringTag,呼叫 toString

Set 和 Map

Set

Set 是新增的無重複的有序集合,多用於集合去重或者判斷集合中是否含有某個元素。

// 建立
let set = new Set()
// 新增元素
set.add(1)
set.add('1')
// 重複的元素不會被新增
set.add(1)
// 判斷是否包含元素
set.has(1) // -> true
// 判斷長度
set.size() // -> 2
// 刪除某個元素
set.delete()
// 移除所有元素
set.clear()複製程式碼

Map

Map 是新增的有序鍵值對列表,鍵值可以是任何型別。

// 建立
let map = new Map()
// 設定鍵值對
map.set('year', "2017")
map.set({}, 'obj')
// 取值
map.get('year') // -> '2017'
// 判斷是否有該鍵值
map.has('year') // -> true
// 獲得長度
map.size() // -> 2
// 刪除某個鍵值
map.delete('year')
// 移除所有鍵值
map.clear()複製程式碼

迭代器和 Generator 函式

迭代器

顧名思義,用來迭代的。之前介紹過 Symbol.iterator,可以迭代的物件都有這個屬性,包括陣列,Set,Map,字串和 NodeList。ES6新增的 for-of 就用到了迭代器的功能,但是預設只有上面這些物件能使用。

let a = [1, 2]
for (let value of a) {
    console.log(value) // -> 1, 2
}
// 上面的程式碼其實就是呼叫了陣列的預設迭代器
let iterator = a[Symbol.iterator]()
// 當呼叫 next 時會輸出這次迭代的 value 和是否迭代完成
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 2, done: false}
// 已經沒元素可以迭代了
console.log(iterator.next()) // {value: undefined, done: true}
// 陣列的預設迭代器只會輸出 value,如果想同時輸出索引的話
// 這裡可以使用新特性陣列解構 let [index, value]
for (let value of a.entries()) {
    console.log(value) // -> [0, 1]   [1, 2]
}複製程式碼

對於自己建立的物件都是不可迭代的,當然我們也可以讓他變成迭代的

let a = {
    array: [],
    // 這是一個 Generator 函式,馬上就會講到
    *[Symbol.iterator]() {
        for(let item in this.array) {
            yield item
        }
    }
}
a.array.push(...[1, 2, 3])
for(let item of a) {
    console.log(item)
}複製程式碼

Generator 函式

用於非同步程式設計。該函式可以暫停和恢復執行,和同步寫法很像。

// 星號表示這是一個 Generator 函式
function *gen() {
// 第一次 next 只執行到等號右邊
    let first = yield 1
    // 第二次 next 執行 let first = 和 yield 2
    let second = yield 2
    // 不執行接下來的 next 就卡在上一步了
    let thrid = yield 3
}
let g = gen()
g.next() // -> {value: 1, done: false}
g.next() // -> {value: 2, done: false複製程式碼

接下來看下 Generator 函式如何用於非同步

function getFirstName() {
    setTimeout(function(){
        gen.next('alex')
    }, 1000);
}

function getSecondName() {
    setTimeout(function(){
        gen.next('perry')
    }, 2000);
}

function *sayHello() {
    var a = yield getFirstName();
    var b = yield getSecondName();
    // settimeout 本來是非同步的,通過 Generator 函式寫成了同步寫法
    console.log(a, b); // ->alex perry
}

var gen = sayHello();

gen.next();複製程式碼

JS 中的類不是其他語言中的類,只是個語法糖,寫法如下。

class Person {
// 建構函式
    constructor() {
        this.name = name
    }
    sayName() {
        console.log(this.name)
    }
}
let p = new Person('name')
p.sayName() // -> 'name'

// class 就是以下程式碼的語法糖
// 對應 constructor
function Person(name) {
    this.name = name
}
// 對應 sayName
Person.prototype.sayName = function() {
    console.log(this.name)
}複製程式碼

類宣告相比之前的寫法有以下幾點優點

  • 類宣告和 let 宣告一樣,有臨時死區
  • 類宣告中的程式碼全部執行在嚴格模式下
  • 必須使用 new 呼叫

繼承

在 ES6 之前寫繼承很麻煩,既然有個類,那麼必然也可以繼承類了

class Person {
// 建構函式
    constructor() {
        this.name = name
    }
    sayName() {
        console.log(this.name)
    }
}
// extends 代表繼承自Person
class Student extends Person {
    constructor(name, age) {
    // super 的注意事項之前有說過
        super(name)
        // 必須在 super 之後呼叫 this
        this.age = age
    }
    sayName() {
    // 如果像使用父類的方法就使用這個方法使用
    // 不像使用的話就不寫 super,會覆蓋掉父類的方法
        super.sayName(this.name)
        console.log(this.age)
    }
}複製程式碼

Promise

概念

用於非同步程式設計。

// 你可以使用 new 建立一個 Promise 物件
let promise = new Promise(function(resolve, reject)) {}
resole() // 代表成功
reject() // 代表失敗
promise.then(onFulfilled, onRejected) // 當呼叫 resole 或者 reject ,then 可以監聽到
promise.catch() // reject 或者 throw時可以監聽到複製程式碼

Promise 有三個狀態

  • pending,等待狀態,也就是既不是 resolve 也不是 reject 狀態
  • fulfilled,resolve 以後進入這個狀態
  • reject,reject 以後進入這個狀態

一般使用情況

function delay() {
// 建立一個 promise
    return new Promise((resolve, reject) => {
    // 當呼叫 promise 時,裡面的內容會立即執行
        console.log('in delay')
        setTimeout(() => {
          resolve(1)
        }, 1000)
    });
}
function otherDelay() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(1)
        }, 1000)
    });
}
// 這裡會先輸出 delay 函式中的 log,然後再輸出 outer,接下來1秒以後輸出3個1
delay()
// then 可以捕獲 resolve 和 reject
    .then((value) => {
        console.log(value)
    })
console.log('outer')

otherDelay()
// 捕獲 reject時,如果不需要捕獲 resolve 時可以這樣寫
    .then(null, (value) => {
        console.log(value)
    })
// 捕獲 reject 或者 throw 時推薦使用這個寫法,原因後面會說
otherDelay()
    .catch((value) => {
        console.log(value);
    })複製程式碼

以上是最常用的 Promise 寫法,現在介紹 Promise 鏈

delay()
// then 會返回一個新的 promise 物件
    .then((value) => {
     // 這樣就可以傳參了
        return value + 1
    }).then((value) => {
        console.log(value) // -> 2
        // then 裡面可以也可以傳入一個函式名,會自動呼叫
        // 如果傳入的函式有引數會自動傳入
    }).then(delay).then((value) => {
        console.log(value) // -> 1
        // 如果在then 中丟擲錯誤,只有 catch 才能監聽到,所以推薦使用 catch 監聽錯誤
        throw new Error('error')
    }).then((value) => {
        console.log(value) // 這個then 不會執行
    }).catch((error) => {
        console.log('catch' + error) // -> catch Error
    })複製程式碼

Promise 高階用法

開發中可能會有需求,需要一次上傳幾張圖片,全部上傳成功以後有個提示,這時候就可以用到 Promise.all()

function updateOne() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('one')    
        }, 1000)
    });
}
function updateTwo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('two')    
        }, 2000)
    });
}
// all 函式接收一個可迭代物件,注意這裡傳入函式必須呼叫
let promise = Promise.all([updateOne(), updateTwo()])
// 只有當 all 中的非同步全部完成了才會呼叫 then
promise
    .then((value) => {
    // value 是個函式,順序按照 all 裡的迭代物件的順序
        console.log(value) // -> ["one", "two"]
    })複製程式碼

如果一個非同步任務超時了,你想直接取消,可以通過 Promise.race()

// 假設該任務執行時間超過1秒就算超時,應該 cancel
function delay() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            resolve('finish')
        }, 1500);
    });
}
function cancel() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            resolve('cancel')
        }, 1000);
    });
}
// 接收的引數和 all 相同
let promise = Promise.race([delay(), cancel()])
// race 中只要有一個任務完成,then 就會被呼叫,這樣就可以 cancel 掉所有超時任務
promise
    .then((value) => {
        console.log(value) // -> cancel
    })複製程式碼

Proxy

Proxy 可以建立一個代替目標物件的代理,攔截語言內部的操作。

let handle = {}
let target = {}
// 這樣就建立了target 物件的代理,但是這個代理其實沒有任何用處
let p = new Proxy(target, handle)複製程式碼

上面的程式碼中可以看到傳入了一個 handle 的物件,只有當這個物件中包含一些代理行為的函式時,這個代理才有用。具有的代理行為函式可以去 MDN檢視,這裡舉例幾個用法。

let handle = {
    // 改變 set 的內部操作
        set(target, key, value) {
        // 當給 age 屬性賦值小於19時報錯
            console.log(value)
            if (key === 'age') {
                if (value < 19) {
                    throw new Error('未成年')
                }
            }
        }
    }
let target = {}
let p = new Proxy(target, handle)
p.age = 1 // -> 報錯
p.age = 19 // -> 沒問題複製程式碼

模組化

ES6 引入了原生的模組化,這樣就可以拋棄之前的 AMD 或者 CMD 規範了,如果對模組化還沒什麼瞭解,可以看下我之前的文章 明白 JS 模組化

// example.js 檔案下
// export 可以匯出任何變數,函式或者類
export var age = 14
export function sum(n1, n2) {
    return n1 + n2
}
export class Person {
    constructor(age) {
        this.age = age
    }
}
// 別的 JS 檔案中匯入
// 如果想匯入整個模組並且自己命名,就可以這樣使用
// import 後面代表模組名,from 後面代表要匯入的檔案地址
import * as Example from './example'
console.log(Example.age) // -> 14
// 當然你也可以只使用模組中的一個功能
// 這裡使用了物件解構的方法拿到需要的功能,注意這裡名字必須相同,否則使用會報錯
import { age, sum } from './example'
console.log(age) // -> 14
console.log(sum(1, 2)) // -> 3
// 現在我只想匯出一個功能,並且外部可以隨便命名該如何做呢?
// default 一個檔案中只能使用一次
export default var age = 14
// MyAge 可以隨便自己喜歡命名
import MyAge from './example'
console.log(MyAge) // -> 14複製程式碼

以上就是本文的全部內容了,感謝大家能看到這裡,如果有任何錯誤或者解釋的不明白的,可以留言回覆,謝謝!

相關文章