JS知識點:ES6 中常見的知識點

蜜汁微笑發表於2019-09-11

前言

ES6(ESMAScript 6, ESMAScript2015)是指ESMA組織在2015年釋出的ESMAScript 2015標準,由於相比ES5(2009年釋出),ES6的改動非常大,故廣義上,我們也將ES6統稱所有ES5之後的新特性,包括ES7(ESMAScript2016),ES8(ESMAScript2017)...ES10(ESMAScript2019)等,這裡專指ES6

1、var,letconst

我們知道在ES5中,用var關鍵字宣告的變數存在變數提升,函式及變數的宣告都將被提升到函式的最頂部,這就導致可以在使用之後在宣告變數,這種不規範的行為很多時候都依靠程式設計習慣來約束,尤其對於新手來說存在不少隱患,於是letconst應運而生,二者的作用域都是當前程式碼塊域,let宣告的變數可以修改,const宣告的是常量,宣告後不可修改。

let foo = 10;
const bar = {key: 20};
foo = 20;
bar.key = 30;
bar = 30; // TypeError
複製程式碼

這裡解釋一下bar.key = 30這句話改變的不是bar的值,只是修改了其中的屬性,就好比古代的女人嫁雞隨雞、從一而終,後來這隻雞變了,那也只是變成了一隻“大雞”,但雞還是這隻雞……

1.1、 作用域的差別

JavaScript中,我們總能聽到這樣的話:在JavaScript中只有函式作用域,沒有塊級作用域。那麼他們到底是什麼呢?

以一個function宣告的程式碼塊是一個函式作用域,如下程式碼塊中的scope 1scope 2就是同一個函式作用域,但他們是兩個不同的程式碼塊

function fn(){

    console.log(foo1); // undefined
    console.log(bar1); // undefined
    
    // scope 1
    var foo1 = 10;
    let foo2 = 20;
    if(true) {
        // scope 2
        var bar1 = 30;
        let bar2 = 40;
    }
    
    console.log(foo1); // 10
    console.log(foo2); // 20
    console.log(bar1); // 30
    console.log(bar2); // ReferenceError
}
複製程式碼

我們可以看出用let宣告的變數bar2if語句外是不能訪問的(ReferenceError引用錯誤,代表引用了一個未宣告的變數)

1.2、 變數提升

在上面的例子中第一句console.log(bar1);輸出了undefined,並未報錯,說明用var宣告的變數在函式開始時就已經定義(即變數提升),但需要注意的是此時並未初始化。但採用letconst宣告的變數則不存在變數提升,必須先宣告後使用,否則就會報錯:ReferenceError,同時我們把這種在宣告之前不可使用這種特性成為 暫時性死區(Temproal Dead Zone, TDZ)。

console.log(foo); // ReferenceError
let foo = 10;
複製程式碼

1.3、 重複宣告

var foo = 10;
var foo = 20;

let bar = 20;
let bar = 20; // SyntaxError
複製程式碼

當我們用var宣告變數的時候,是可以重複宣告的,後宣告的會覆蓋前面的宣告,但當我們用letconst宣告的時候則會報語法錯誤SyntaxError,注意,function宣告的函式也和var有相似的表現:存在 變數提升重複宣告

2、解構賦值

解構顧名思義按照一定的結構“解析”一個物件,通過這種方式我們可以從陣列或物件中取值,本質上屬於 模式匹配,這是ES6給我們提供的新語法,只要等號兩邊的模式相同即可解析相應的值。

// 解構一個物件,可以只解構我們需要的部分
const {foo, bar} = {foo: 1, bar: 2, other: 3};
console.log(foo, bar); // 1 2

// 解構一個陣列
const [first, second] = [10, 20, 30];
console.log(first, second); // 10 20

// 忽略陣列的某個值
const [first, , third] = [10, 20, 30];
console.log(first, third); // 10 30

// 解構一個不存在的鍵時,其值為 undefined
const {foo, bar} = {foo: 1}; // undefined

// 注意陣列的 length
const [length] = [12];
console.log(length); // 12
// 用方括號可花括號解析的差別
const {length} = [12];
console.log(length); // 1

// 當屬性中包含關鍵字或保留字時需要重新命名
const {class: clazz} = {class: 'class-name'};

// 動態匹配,同樣需要重新命名
const key = 'thisKey';
const {[key]: value} = {thisKey: 23};
console.log(value); // 23

// 同樣動態匹配也可用在物件宣告中
const key = 'currentKey';
const obj = {[key]: 20};
console.log(obj); // {currentKey: 20}

// 用解構賦值交換兩個變數,不需要宣告第三個變數
let foo = 10;
let bar = 20;
console.log(foo, bar); // 10 20
[foo, bar] = [bar, foo];
console.log(foo, bar); // 20 10
複製程式碼

3、 擴充套件運算子

在使用解構賦值的時候,可以使用擴充套件運算子...來將剩餘值統一放入一個物件中,

// 用於物件
const {foo, ...rest} = {foo: 1, bar: 2, other: 3};
console.log(foo, rest); // 1 {bar: 2, other: 3}

// 用於陣列
const [first, ...others] = [1, 2, 3];
console.log(first, others); // 1 [2, 3]

// 函式呼叫中傳遞引數
function callback(first, second){
    console.log(first, second);
}
let params = [1, 2];
callback(...params); // 輸出 1 2

// 用於獲取 arguments,這種特性主要用於箭頭函式
const arrow = (...args) => {
    console.log(args);
};
arrow(1, 2, 3); // 輸出陣列 [1, 2, 3]
複製程式碼

擴充套件運算子實際上是呼叫了Iterator介面,通常的Array,String,NodeList,Arguments,Set,Map都實現了這個介面,所以可通過擴充套件運算子獲得arguments,也可用於函式呼叫時傳遞引數,但Object並未實現Iterator,所以當我們呼叫fn(...obj)時,會報錯TypeError

4、預設值和預設引數

在我們使用解構賦值的時候,可以為不存在的值或值為undefined賦預設值

// 不存在指定對映
const {foo, bar = 20} = {foo: 10};
console.log(foo, bar); // 10 20

// 不存在指定值為 undefined
const {foo, bar = 20} = {foo: 10, bar: undefined};
console.log(foo, bar); // 10 20
複製程式碼

只有為undefined時才會執行預設值賦值步驟

const {foo, bar = (function(){
    console.log('預設值');
    return 20;
})()} = {foo: 10};
// 預設值
console.log(foo, bar); // 10 20

function defaultVal(){
    console.log('預設值');
    return 20;
}
const {key1, key2 = defaultVal()} = {key1: 10, key2: null};
console.log(key1, key2); // 10 null
// 可見 key2 為 null,並未輸出 “預設值”
複製程式碼

在宣告函式時,我們也可以為函式引數賦預設值,同樣只有在傳入引數為undefined時,才執行預設值賦值操作

function fn(foo, bar = 20){
    console.log(foo, bar);
}

fn(10); // 10 20
複製程式碼

5、箭頭函式

ES6給我們帶來了新的函式語法箭頭函式,在平常的書寫中,極大的提高了我們的開發效率,但是箭頭函式和普通函式卻有著很大的差別

// 基本使用
const arrow = () => {};

// 有一個引數
const arrow = (arg) => {};
const arrow = arg => {}; // 括號可省略

// 有多個引數
const arrow = (arg1, arg2, arg3) => {}

// 函式體只有一句
const arrow = () => 1; // 花括號可省略
// 等價於
const arrow = () => {return 1;};

// 函式體有多句
const arrow = arg => {
    if(arg){
        return 1;
    } else {
        return 2;
    }
};

// 返回一個空物件
// 這實際上只有一句,但括號不能省略,否則會被解析成程式碼塊
const arrow = () => ({});
複製程式碼

5.1、沒有自身this

箭頭函式的this是和宣告時的父作用域繫結的,不可通過其他方式(call,apply,bind)更改this指向,如:

'use strict';
function fn1(){
    console.log(this.foo);
    return function() {
        console.log(this.foo)
    }
}

const obj = {foo: 10};
const bar = fn1.call(obj); // 10
bar(); // TypeError(因為此時 this 指向 undefined)

const changedObj = {foo: 60};
bar.call(changedObj); // 60 (this 可以重新繫結)
複製程式碼

如果是箭頭函式:

'use strict';
function fn1(){
    console.log(this.foo);
    return () => {
        console.log(this.foo)
    }
}

const obj = {foo: 10};
const bar = fn1.call(obj); // 10
bar(); // 10 (此時 this 仍然指向 obj)
obj.foo = 20;
bar(); // 20

const changedObj = {foo: 60};
bar.call(changedObj); // 20
複製程式碼

5.2、不可使用new

const foo = () => {};
const bar = function(){
}

new bar(); // bar {}
new foo(); // TypeError
複製程式碼

5.3、沒有arguments

箭頭函式沒有arguments,但可以使用上面的解構語法獲得 引數列表,但得到的不是Arguments物件,而是一個包含所有引數的陣列

const arrow = (...args) => {
    console.log(args);
}
arrow(1, 2, 3); // [1, 2, 3]
複製程式碼

6、class語法

ES5中,只能通過Function.prototype來實現類的宣告和繼承,ES6提供了新的語法來宣告一個類,使其更加貼合物件導向程式設計,

// ES5
function Student(){}

Student.staticMethod = function(){
    // ES5 的靜態方法
}
Student.prototype.memberMethod = function(){
    // ES5 的例項方法
}

// ES6
class Person {
    constructor(name) {
        this.name = name;
        this.memberMethod = this.memberMethod.bind(this);
    }
    
    static staticMethod(){
        console.log('這是一個靜態方法');
    }
    
    static memberMethod(){
        // 注意例項方法 ES6 並未繫結 this
        // React 中也建議了,在構造器中應手動繫結 this
        // 如上
        console.log('這是例項方法');
    }
}
複製程式碼

7、Promise

(相比以上,這是大概是最複雜的,下一篇將專門將這一特性,後續還會介紹ES7async,await,他們都是依賴Promise,所以……很重要!)