ES6 經常使用新特性講解

圖片描述

上週在公司組織了 ES6 新特性的分享會,主要講了工程化簡介、ES6 的新特性與前端經常使用的幾種構建工具的配合使用。ES6 這塊主要講了一些咱們平時開發中常常會用到的新特性。在這裏整理一下關於 ES6 的部分。
<!--more-->
一共講解了 8 個經常使用的 ES6 新特性,講解過程也是由淺入深。廢話很少說,下面進入正文。前端


函數默認值

特性 & 語法

// Before
function decimal(num, fix) {
    fix = fix === void(0) ? 2 : fix;

    return +num.toFixed(fix);
}
// After
function decimal(num, fix = 2) {
    return +num.toFixed(fix);
}

  首先,咱們看一下以前咱們是怎麼寫函數默認值的:咱們一般會使用三元運算符來判斷入參是否有值,而後決定是否使用默認值運行函數(如示例中 fix = fix === void(0) ? 2 : fix面試

  而在 ES6 中,咱們能夠直接在函數的顯示入參中指定函數默認值(function decimal(num, fix = 2){}),很明顯,這種寫法更天然易懂,也更加方便,不過有一點須要注意:ajax

  • 設定了默認值的入參,應該放在沒有設置默認值的參數以後,也就是咱們不該該這樣寫:function decimal(fix = 2, num){},雖然經過變通手段也能夠正常運行,但不符合規範算法


模板字符串

特性 & 語法

// Before
// Before.1
var type = 'simple';
'This is a ' + type + ' string join.'

// Before.2
var type = 'multiline';
'This \nis \na \n' + type + '\nstring.'

// Before.3
var type = 'pretty singleline';
'This \
is \
a \
' + type + '\
string.'
// OR
// Before.4
'This ' +
'is' +
'a' +
type +
'string.'
// After
var type = 'singleline';
`This is a ${type} string.`

var type = 'multiline';
`This
is
a
${type}
string.`

var type = 'pretty singleline';
`This \
is \
a \
${type} \
string.`

  咱們以前在對字符串和變量進行拼接的時候,一般都是反覆一段一段使用引號包裹的字符串,再反覆使用加號進行拼接(Before.1)。多行字符串的時候咱們還要寫上蹩腳的 \n 來換行以獲得一個多行的字符串(Before.2)。數組

  在字符串過長的時候可能會使用 \ 在編輯器中書寫多行字符串來表示單行字符串,用來方便較長的字符串在編輯器中的閱讀(Before.3),或者簡單粗暴的反覆引號加號這樣多行拼接(Before.4)。閉包

  ES6 中咱們可使用反引號(`,位於 TAB 上方)來輸入一段簡單明瞭的多行字符串,還能夠在字符串中經過 ${變量名} 的形式方便地插入一個變量,是否是方便多了!app


解構賦值

數組解構

var [a, ,b] = [1, 2, 3, 4, 5];
console.log(a); // => 1
console.log(b); // => 3

  數組解構,使用變量聲明關鍵字聲明一個形參數組([a, , b]),等號後跟一個待解構目標數組([1, 2, 3]),解構時能夠經過留空的方式跳過數組中間的個別元素,可是在形參數組中必須留有相應空位才能夠繼續解構以後的元素,若是要跳過的元素處於數組末端,則在形參數組中能夠不予留空async

對象解構

var {b, c} = {a: 1, b: 2, c: 3};
console.log(b); // => 2
console.log(c); // => 3

  對象解構與數組解構大致相同,不過須要注意一點編輯器

  • 形參對象({b, c})的屬性或方法名必須與待解構的目標對象中的屬性或方法名徹底相同才能解構到對應的屬性或方法模塊化

對象匹配解構

var example = function() {
    return {a: 1, b: 2, c: 3};
}
var {a: d, b: e, c: f} = example();
console.log(d, e, f); // => 1, 2, 3

  對象匹配解構是對象解構的一種延伸用法,咱們能夠在形參對象中使用:來更改解構後的變量名。

函數入參解構

function example({param: value}) {
    return value;
}
console.log(example({param: 5})); // => 5

  函數的入參解構也是對象解構的一種延伸用法,咱們能夠經過改寫入參對象目標值爲變量名的方式,在函數內部直接獲取到入參對象中某個屬性或方法的值。

函數入參默認值解構

function example({x, y, z = 0}) {
    return x + y + z;
}
console.log(example({x: 1, y: 2}));       // => 3
console.log(example({x: 1, y: 2, z: 3})); // => 6

  這是入參解構的另外一種用法,咱們能夠在入參對象的形參屬性或方法中使用等號的方式給入參對象的某些屬性或方法設定默認值。


Let & Const

Let

  • 變量提高

// Before
console.log(num); // => undefined
var num = 1;
// After
console.log(num); // => ReferenceError
let num = 1;

  使用 var 聲明的變量會自動提高到當前做用域的頂部,若是聲明位置與做用域頂部之間有另外一個同名變量,很容易引發難以預知的錯誤。使用 let 聲明的變量則不會進行變成提高,規避了這個隱患。

注意:var 聲明的變量提高後雖然在聲明語句以前輸出爲 undefined,但這並不表明 num 變量尚未被聲明,此時 num 變量已經完成聲明並分配了相應內存,只不過該變量目前的值undefined,並非咱們聲明語句中賦的初始值 1

  • 塊級做用域

// Before
{
    var num = 1;

    console.log(num); // => 1
}
console.log(num);     // => 1
// After
{
    let num = 1;
    
    console.log(num); // => 1
}
console.log(num);     // => ReferenceError

  let 聲明的變量只能在當前塊級做用域中使用,最多見的應用大概就是 for(let i = 0, i < 10; i++) {},相信許多小夥伴在面試題中見過,哈哈。

  • 禁止重複聲明

// Before
var dev = true;
var dev = false;

console.log(dev); // => false
// After
let dev = true;
let dev = false; // => SyntaxError

  var 聲明的變量能夠重複聲明,並且不會有任何警告或者提示,就這樣悄悄的覆蓋了一個值,隱患如變量提高同樣讓人擔心。( ̄┰ ̄*)

  而 let 聲明的變量若是進行重複聲明,則會直接拋出一個語法錯誤(是的,就是直接明確地告訴你:你犯了一個至關低級的語法錯誤哦)

Const

  • 無變量提高

  • 有塊級做用域

  • 禁止重複聲明

前 3 點跟 let 一個套路,就很少說了

  • 禁止重複賦值

const DEV = true;
DEV = false; // => TypeError

  基於靜態常量的定義咱們能夠很明顯知道,const 聲明的常量一經聲明便不能再更改其值,無需多說。

  • 必須附初始值

const DEV; // => SyntaxError

  也是基於定義,const 聲明的常量既然一經聲明便不能再更改其值,那聲明的時候沒有附初始值顯然是不合理的,一個沒有任何值的常量是沒有意義的,浪費內存。


新增庫函數

  ES6 新增了許多(至關多)的庫函數,這裏只介紹一些比較經常使用的。

題外話:多瞭解一下內建函數與方法有時候能夠很方便高效地解決問題。有時候絞盡腦汁寫好的一個算法,沒準已經有內建函數實現了!並且內建函數通過四海八荒衆神的考驗,性能必定不錯,哈哈。

Number

Number.EPSILON
Number.isInteger(Infinity); // => false
Number.isNaN('NaN');        // => false

  首先是 ᶓ 這個常量屬性,表示小數的極小值,主要用來判斷浮點數計算是否精確,若是計算偏差小於該閾值,則能夠認爲計算結果是正確的。

  而後是 isInteger() 這個方法用來判斷一個數是否爲整數,返回布爾值。

  最後是 isNaN() 用來判斷入參是否爲 NaN。是的,咱們不再用經過 NaN 不等於 NaN 才能肯定一個 NaN 就是 NaN 這種反人類的邏輯來判斷一個 NaN 值了!

if(NaN !== NaN) {
    console.log("Yes! This is actually the NaN!");
}

  另外還有兩個小改動:兩個全局函數 parseInt()parseFloat() 被移植到 Number 中,入參反參保持不變。這樣全部數字處理相關的都在 Number 對象上嘞!規範多了。

String

'abcde'.includes('cd'); // => true
'abc'.repeat(3);        // => 'abcabcabc'
'abc'.startsWith('a');  // => true
'abc'.endsWith('c');    // => true
  • inclueds() 方法用來判斷一個字符串中是否存在指定字符串

  • repeat() 方法用來重複一個字符串生成一個新的字符串

  • startsWith() 方法用來判斷一個字符串是否以指定字符串開頭,能夠傳入一個整數做爲第二個參數,用來設置查找的起點,默認爲 0,即從字符串第一位開始查找

  • endsWith()startsWith() 方法相反

Array

Array.from(document.querySelectorAll('*')); // => returns a real array.
[0, 0, 0].fill(7, 1); // => [0, 7, 7]
[1, 2, 3].findIndex(function(x) {
    return x === 2;
}); // => 1
['a', 'b', 'c'].entries(); // => Iterator [0: 'a'], [1: 'b'], [2: 'c']
['a', 'b', 'c'].keys();    // => Iterator 0, 1, 2
['a', 'b', 'c'].values();  // => Iterator 'a', 'b', 'c'
// Before
new Array();        // => []
new Array(4);       // => [,,,]
new Array(4, 5, 6); // => [4, 5, 6]
// After
Array.of();         // => []
Array.of(4);        // => [4]
Array.of(4, 5, 6);  // => [4, 5, 6]

  首先是 from() 方法,該方法能夠將一個類數組對象轉換成一個真正的數組。還記得咱們以前常寫的 Array.prototype.slice.call(arguments) 嗎?如今能夠跟他說拜拜了~

  以後的 fill() 方法,用來填充一個數組,第一個參數爲將要被填充到數組中的值,可選第二個參數爲填充起始索引(默認爲 0),可選第三參數爲填充終止索引(默認填充到數組末端)。

  findIndex() 用來查找指定元素的索引值,入參爲函數,函數形參跟 map() 方法一致,很少說。最終輸出符合該條件的元素的索引值。

  entries()keys()values() 三個方法各自返回對應鍵值對、鍵、值的遍歷器,可供循環結構使用。

  最後一個新增的 of() 方法主要是爲了彌補 Array 當作構造函數使用時產生的怪異結果。

Object

let target = {
    a: 1,
    b: 3
};
let source = {
    b: 2,
    c: 3
};

Object.assign(target, source); // => { a: 1, b: 2, c: 3}

  assign() 方法用於合併兩個對象,不過須要注意的是這種合併是淺拷貝。可能看到這個方法咱們還比較陌生,不過了解過 jQuery 源碼的應該知道 $.extend() 這個方法,例如在下面這個粗糙的 $.ajax() 模型中的應用:

$.ajax = function(opts) {
    var defaultOpts = {
        method: 'GET',
        async: true,
        //...
    };
    opts = $.extend(defaultOpts, opts);
}

  從這咱們能夠看到 TC39 也是在慢慢吸取百家所長,努力讓 JavaScript 變得更好,更方便開發者的使用。

Object 新增的特性固然不止這一個 assign() 方法,一共增長了十多個新特性,特別是對屬性或方法名字面量定義的加強方面,很值得一看,感興趣的自行查找資料進行了解哈,印象會更深入!

Math

  Math 對象上一樣增長了許多新特性,大部分都是數學計算方法,這裏只介紹兩個經常使用的

Math.sign(5);     // => +1
Math.sign(0);     // => 0
Math.sign(-5);    // => -1

Math.trunc(4.1);  // => 4
Math.trunc(-4.1); // => -4

  sign() 方法用來判斷一個函數的正負,使用與對應返回值如上。

  trunc() 用來取數值的整數部分,咱們以前可能常用 floor() 方法進行取整操做,不過這個方法有一個問題就是:它自己是向下取整,當被取整值爲正數的時候計算結果徹底 OK,可是當被取整值爲負數的時候:

Math.floor(-4.1); // => -5

插播一個小 Tip:使用位操做符也能夠很方便的進行取整操做,例如:~~3.14 or 3.14 | 0,也許這更加方便 : )


箭頭函數

  箭頭函數無疑是 ES6 中一個至關重要的新特性。

特性

  • 共享父級 this 對象

  • 共享父級 arguments

  • 不能當作構造函數

語法

最簡表達式
var arr = [1, 2, 3, 4, 5, 6];

// Before
arr.filter(function(v) {
    return v > 3;
});
// After
arr.filter(v => v > 3); // => [4, 5, 6]

  先後對比很容易理解,能夠明顯看出箭頭函數極大地減小了代碼量。

完整語法
var arr = [1, 2, 3, 4, 5, 6];

arr.map((v, k, thisArr) => {
    return thisArr.reverse()[k] * v;
})  // => [6, 10, 12, 12, 10, 6]

  一個簡單的首尾相乘的算法,對比最簡表達式咱們能夠發現,函數的前邊都省略了 function 關鍵字,可是多個入參時需用括號包裹入參,單個入參是時可省略括號,入參寫法保持一致。後面使用胖箭頭 => 鏈接函數名與函數體,函數體的寫法保持不變。

函數上下文 this
// Before
var obj = {
    arr: [1, 2, 3, 4, 5, 6],
    getMaxPow2: function() {
        var that = this,
            getMax = function() {
                return Math.max.apply({}, that.arr);
            };
        
        return Math.pow(getMax(), 2);
    }
}
// After
var obj = {
    arr: [1, 2, 3, 4, 5, 6],
    getMaxPow2: function() {
        var getMax = () => {
            return Math.max.apply({}, this.arr);
        }

        return Math.pow(getMax(), 2);
    }
}

  注意看中第 5 行 var that = this 這裏聲明的一個臨時變量 that。在對象或者原型鏈中,咱們之前常常會寫這樣一個臨時變量,或 that_this,諸如此類,以達到在一個函數內部訪問到父級或者祖先級 this 對象的目的。

  現在在箭頭函數中,函數體內部沒有本身的 this,默認在其內部調用 this 的時候,會自動查找其父級上下文的 this 對象(若是父級一樣是箭頭函數,則會按照做用域鏈繼續向上查找),這無疑方便了許多,咱們無需在多餘地聲明一個臨時變量來作這件事了。

  注意

  1. 某些狀況下咱們可能須要函數有本身的 this,例如 DOM 事件綁定時事件回調函數中,咱們每每須要使用 this 來操做當前的 DOM,這時候就須要使用傳統匿名函數而非箭頭函數。

  2. 在嚴格模式下,若是箭頭函數的上層函數均爲箭頭函數,那麼 this 對象將不可用。

另,因爲箭頭函數沒有本身的 this 對象,因此箭頭函數不能當作構造函數。

父級函數 arguments

  咱們知道在函數體中有 arguments 這樣一個僞數組對象,該對象中包含該函數全部的入參(顯示入參 + 隱式入參),當函數體中有另一個函數,而且該函數爲箭頭函數時,該箭頭函數的函數體中能夠直接訪問父級函數的 arguments 對象。

function getSum() {
    var example = () => {
        return Array
            .prototype
            .reduce
            .call(arguments, (pre, cur) => pre + cur);
    }

    return example();
}
getSum(1, 2, 3); // => 6

因爲箭頭函數自己沒有 arguments 對象,因此若是他的上層函數都是箭頭函數的話,那麼 arguments 對象將不可用。

  最後再鞏固一下箭頭函數的語法:

  1. 當箭頭函數入參只有一個時能夠省略入參括號;

  2. 當入參多餘一個或沒有入參時必須寫括號;

  3. 當函數體只有一個 return 語句時能夠省略函數體的花括號與 return 關鍵字。


類 & 繼承

  類也是 ES6 一個不可忽視的新特性,雖然只是句法上的語法糖,可是相對於 ES5,學習 ES6 的類以後對原型鏈會有更加清晰的認識。

特性

  • 本質爲對原型鏈的二次包裝

  • 類沒有提高

  • 不能使用字面量定義屬性

  • 動態繼承類的構造方法中 super 優先 this

類的定義

/* 類不會被提高 */
let puppy = new Animal('puppy'); // => ReferenceError

class Animal {
    constructor(name) {
        this.name = name;
    }

    sleep() {
        console.log(`The ${this.name} is sleeping...`);
    }

    static type() {
        console.log('This is an Animal class.');
    }
}

let puppy = new Animal('puppy');

puppy.sleep();    // => The puppy is sleeping...

/* 實例化後沒法訪問靜態方法 */
puppy.type();     // => TypeError

Animal.type();    // => This is an Animal class.

/* 實例化前沒法訪問動態方法 */
Animal.sleep();   // => TypeError

/* 類不能重複定義 */
class Animal() {} // => SyntaxError

  以上咱們使用 class 關鍵字聲明瞭一個名爲 Animal 的類。

雖然類的定義中並未要求類名的大小寫,但鑑於代碼規範,推薦類名的首字母大寫。

  兩點注意事項:

  1. 在類的定義中有一個特殊方法 constructor(),該方法名固定,表示該類的構造函數(方法),在類的實例化過程當中會被調用(new Animal('puppy'));

  2. 類中沒法像對象同樣使用 prop: value 或者 prop = value 的形式定義一個類的屬性,咱們只能在類的構造方法或其餘方法中使用 this.prop = value 的形式爲類添加屬性。

  最後對比一下咱們以前是怎樣寫類的:

function Animal(name) {
    this.name = name;
}

Animal.prototype = {
    sleep: function(){
        console.log('The ' + this.name + 'is sleeping...');
    }
};

Animal.type = function() {
    console.log('This is an Animal class.');
}

class 關鍵字真真讓這一切變得清晰易懂了~

類的繼承

class Programmer extends Animal {
    constructor(name) {
        /* 在 super 方法以前 this 不可用 */
        console.log(this); // => ReferenceError
        super(name);
        console.log(this); // Right!
    }
    
    program() {
        console.log("I'm coding...");
    }

    sleep() {
        console.log('Save all files.');
        console.log('Get into bed.');
        super.sleep();
    }
}

let coder = new Programmer('coder');
coder.program(); // => I'm coding...
coder.sleep();   // => Save all files. => Get into bed. => The coder is sleeping.

  這裏咱們使用 class 定義了一個類 Programmer,使用 extends 關鍵字讓該類繼承於另外一個類 Animal

  若是子類有構造方法,那麼在子類構造方法中使用 this 對象以前必須使用 super() 方法運行父類的構造方法以對父類進行初始化。

  在子類方法中咱們也可使用 super 對象來調用父類上的方法。如示例代碼中子類的 sleep() 方法:在這裏咱們重寫了父類中的 sleep() 方法,添加了兩條語句,並在方法末尾使用 super 對象調用了父類上的 sleep() 方法。

  俗話講:沒有對比就沒有傷害 (*゜ー゜*),咱們最後來看一下之前咱們是怎麼來寫繼承的:

function Programmer(name) {
    Animal.call(this, name);
}

Programmer.prototype = Object.create(Animal.prototype, {
    program: {
        value: function() {
            console.log("I'm coding...");
        }
    },
    sleep: {
        value: function() {
            console.log('Save all files.');
            console.log('Get into bed.');
            Animal.prototype.sleep.apply(this, arguments);
        }
    }
});

Programmer.prototype.constructor = Programmer;

  若是前文類的定義中的先後對比不足爲奇,那麼這個。。。

  給你一個眼神,本身去體會 (⊙ˍ⊙),一臉懵逼.jpg


模塊

啊哈,終於寫到最後一部分了。

  模塊系統是一切模塊化的前提,在未推出 ES6 Module 標準以前,相信大夥兒已經被滿世界飛的 AMDCMDUMDCommonJS 等等百花齊放的模塊化標準搞的暈頭轉向了吧。可是,如今 TC39ECMAScript2015(ES6) 版本里終於推出了正式的模塊化規範,前端模塊系統的大一統時代已經到來了!

OMG,這段話寫的好燃 orz

  廢話有點多。。。

  下面我們來了解一個這個模塊系統的基本規範。

爲方便描述,下文中導出對象指一切可導出的內容(變量、函數、對象、類等),勿與對象(Object)混淆。
導入對象同理。

特性

  • 封閉的代碼塊
    每一個模塊都有本身徹底獨立的代碼塊,跟做用域相似,可是更加封閉。

  • 無限制導出導出
    一個模塊理論上能夠導出無數個變量、函數、對象屬性、對象方法,甚至一個完整的類。可是咱們應該時刻牢記單一職責這一程序設計的基本原則,不要試圖去開發一個臃腫的巨大的面面俱到的模塊,合理控制代碼的顆粒度也是開發可維護系統必不可少的一部分。

  • 嚴格模式下運行
    模塊默認狀況下在嚴格模式下運行('use strict;'),這時候要注意一些取巧甚至有風險的寫法應該避免,這也是保證代碼健壯性的前提。

模塊的定義與導出

內聯導出
export const DEV = true;
export function example() {
    //...
}
export class expClass {
    //...
}
export let obj = {
    DEV,
    example,
    expClass,
    //...
}

  使用 export 關鍵字,後面緊跟聲明關鍵字(letfunction 等)聲明一個導出對象,這種聲明並同時導出的導出方式稱做內聯導出
  未被導出的內容(變量、函數、類等)因爲獨立代碼塊的緣由,將僅供模塊內部使用(可類比成一種閉包)。

對象導出
// module example.js
const DEV = true;
function example() {
    //...
}
class expClass {
    //...
}
let obj = {
    DEV,
    example,
    expClass,
    //...
}
// module example.js
export {DEV, example, expClass, obj};
export {DEV, example as exp, expClass, obj};

  相對於內聯導出,上邊的這種方式爲對象導出。咱們能夠像寫普通 JS 文件同樣寫主要的功能邏輯,最後經過 export 集中導出。

  在導出時咱們可使用 as 關鍵字改變導出對象的名稱。

默認導出
export default {DEV, example as exp, expClass, obj};
// OR
export default obj;
// OR
export default const DEV = true;

  咱們能夠在 export 關鍵字後接 default 來設置模塊的默認導出對象,須要注意的是:一個模塊只能有一個默認導出

  先很少說,後面講導入的時候再細講相互之間的關聯。

模塊的導入與使用

自定義模塊

  前文咱們定義了一個名爲 example 的模塊,寫在文件 example.js中,下面咱們來導入並使用這個模塊。

import example from './example.js';
// OR 
import default as example from './example.js';

  使用 import 關鍵字導入一個模塊,上邊這兩種寫法是等效的。默認導入對象既是模塊默認導出對象,即對應模塊定義中的 export default 所導出的內容。

  此外咱們還能夠這樣導入一個模塊:

import {DEV, example} from './example.js';
import * as exp from './example.js';
import {default as expMod, * as expAll, DEV, example as exp} from './example.js';

  這種導入方式對應模塊定義中的 export {DEV, example, expClass, obj}export const DEV = true。下面咱們逐行分析:

  第一行,咱們使用對象導入的方式導入一個模塊內容,可能有些人已經發現,這跟解構賦值很類似,但也有不一樣,下面會講到。須要注意的是形參對象({DEV, example})與模塊定義中導出的名稱必須保持一致

  第二行,導入時可使用通配符 * 配合 as 關鍵字一次性導出模塊中全部內容,最終導入的內容放在 exp 對象中。

  第三行,在使用對象導入來導入一個模塊的指定內容時,也可使用 as 關鍵字更改最終導入對象的名稱,這裏表現出與解構賦值的一個不一樣之處,忘記解構賦值的小夥伴能夠翻翻前文對比一下哈~

  最後,在導入一個模塊後咱們就能夠直接使用模塊的函數、變量、類等了,完整的代碼示例:

import {DEV, example, expClass as EC} from './example.js';

if(DEV) {
    let exp = new EC();
    // anything you want...
    example();
}

  好嘞!到這裏,ES6 經常使用的 8 個新特性就講完了,恭喜你耐心地看完了。固然,還有許多地方沒有講到,有時間的話會考慮繼續寫一些。

  好嘞,就這樣吧,但願對你有所幫助,拜拜~<(* ̄▽ ̄*)/。

文中部分專業名詞因爲未找到合適譯文,最後自行翻譯,若有不妥,歡迎指正。

相關文章
相關標籤/搜索