JavaScript ES6 功能

在本教程中,你將瞭解最新版 JavaScript 中的新功能。

什麼是 ECMAScript 6(或 ES6)

ECMAScript 2015(或 ES6)是 ECMAScript 語言規範標準的第六版和主要版本。它定義了 JavaScript 實現的標準。

ES6 為 JavaScript 語言帶來了重大變化。它引入了一些新功能,例如塊範圍變數,用於迭代陣列和物件的新迴圈,模板文字以及許多其他增強功能,使 JavaScript 程式設計更容易,更有趣。在本章中,我們將討論你可以在日常 JavaScript 編碼中使用的一些最佳 ES6 功能。

let 關鍵字

ES6 引入了用於宣告變數的新關鍵字 let 。在 ES6 之前,在 JavaScript 中宣告變數的唯一方法是 var 關鍵字。讓我們看看它們之間的區別是什麼。

varlet 之間有兩個關鍵的區別,使用 var 關鍵字宣告的變數是函式範圍的,並在其範圍內的頂部提升,而使用 let 關鍵字宣告的變數是塊作用域的({}),並且它們不會被提升。

塊作用域只是意味著在一對大括號 {} 之間建立一個新的作用域。因此,如果在迴圈內宣告帶有 let 關鍵字的變數,則它不會存在於迴圈外部,如以下示例所示:

// ES6 syntax
for(let i = 0; i < 5; i++) {
    console.log(i); // 0,1,2,3,4
}
console.log(i); // undefined

// ES5 syntax
for(var i = 0; i < 5; i++) {
    console.log(i); // 0,1,2,3,4
}
console.log(i); // 5

正如你在上面的示例中所看到的,第一個塊中的變數 ifor 迴圈外部是不可訪問的。這也使我們可以多次重複使用相同的變數名,因為它的作用域僅限於塊({}),這樣可以減少變數宣告和編寫更清晰的程式碼。

const 關鍵字

新的 const 關鍵字可以定義常量。常量是隻讀的,你無法為其重新分配新值。它們也是塊狀的 let

const PI = 3.14;
console.log(PI); // 3.14

PI = 10; // error

但是,你仍然可以更改物件屬性或陣列元素:

// Changing object property value
const PERSON = {name: "Peter", age: 28};
console.log(PERSON.age); // 28
PERSON.age = 30;
console.log(PERSON.age); // 30

// Changing array element
const COLORS = ["red", "green", "blue"];
console.log(COLORS[0]); // red
COLORS[0] = "yellow";
console.log(COLORS[0]); // yellow

for...of 迴圈

新的 for...of 迴圈允許我們非常容易地迭代陣列或其他可迭代物件。此外,迴圈內的程式碼是針對可迭代物件的每個元素執行的。這是一個例子:

// Iterating over array
let letters = ["a", "b", "c", "d", "e", "f"];

for(let letter of letters) {
    console.log(letter); // a,b,c,d,e,f
}

// Iterating over string
let greet = "Hello World!";

for(let character of greet) {
    console.log(character); // H,e,l,l,o, ,W,o,r,l,d,!
}

for...of 迴圈不能跟物件一起使用,因為物件是不可迭代的。如果要迭代物件的屬性,可以使用 for-in迴圈。

模板文字

模板文字提供了一種簡單而乾淨的方式來建立多行字串並執行字串插值。現在我們可以在任何地方將變數或表示式嵌入到字串中而不會有任何麻煩。

使用反向單引號(``)(重音符號)而不是通常的雙引號或單引號建立模板文字。可以使用 ${...} 語法將變數或表示式放在字串內。比較以下示例,看看它有多大用處:

// Simple multi-line string
let str = `The quick brown fox
    jumps over the lazy dog.`;

// String with embedded variables and expression
let a = 10;
let b = 20;
let result = `The sum of ${a} and ${b} is ${a+b}.`;
console.log(result); // The sum of 10 and 20 is 30.

在 ES5 中,要達到同樣的目的,我們必須寫下這樣的東西:

// Multi-line string
var str = 'The quick brown fox\n\t'
    + 'jumps over the lazy dog.';

// Creating string using variables and expression
var a = 10;
var b = 20;
var result = 'The sum of ' + a + ' and ' + b + ' is ' + (a+b) + '.';
console.log(result); // The sum of 10 and 20 is 30.

函式引數的預設值

現在,在 ES6 中,你可以為函式引數指定預設值。這意味著如果在呼叫時沒有為函式提供引數,則將使用這些預設引數值。這是 JavaScript 中最受期待的功能之一。這是一個例子:

function sayHello(name='World') {
    return `Hello ${name}!`;
}

console.log(sayHello()); // Hello World!
console.log(sayHello('John')); // Hello John!

在 ES5 中,要達到同樣的目的,我們必須寫下這樣的東西:

function sayHello(name) {
    var name = name || 'World'; 
    return 'Hello ' +  name + '!';
}

console.log(sayHello()); // Hello World!
console.log(sayHello('John')); // Hello John!

箭頭功能

箭頭函式是 ES6 中另一個有趣的功能。它通過選擇 functionreturn 關鍵字提供了更簡潔的語法來編寫函式表示式

箭頭函式使用新的語法胖箭頭(=>)表示法定義。讓我們看看它的樣子:

// Function Expression
var sum = function(a, b) {
    return a + b;
}
console.log(sum(2, 3)); // 5

// Arrow function
var sum = (a, b) => a + b;
console.log(sum(2, 3)); // 5

如你所見在箭頭函式宣告中,沒有 functionreturn 關鍵字。

你也可以跳過括號,即 () 如果只有一個引數,但是當你有零個或多個引數時,總是需要使用它。

另外,如果函式體中有多個表示式,則需要將其括起來({})。在這種情況下,你還需要使用 return 語句來返回值。

有幾種不同的方法可以編寫箭頭功能。以下是最常用的:

// Single parameter, single statement
var greet = name => alert("Hi " + name + "!");
greet("Peter"); // Hi Peter!

// Multiple arguments, single statement
var multiply = (x, y) => x * y;
alert(multiply(2, 3)); // 6

// Single parameter, multiple statements
var test = age => {
    if(age > 18) {
        alert("Adult");
    } else {
        alert("Teenager");
    }
}
test(21); // Adult

// Multiple parameters, multiple statements
var divide = (x, y) => {
    if(y != 0) {
        return x / y;
    }
}
alert(divide(10, 2)); // 5

// No parameter, single statement
var hello = () => alert('Hello World!');
hello(); // Hello World!

常規函式和箭頭函式之間存在重要差異。不同於一般的函式,箭頭函式不具有其自己的 this ,它需要從被定義的外部函式得到 this。在 JavaScript 中, this 是函式的當前執行上下文。

為了清楚地理解這一點,讓我們看看以下示例:

function Person(nickname, country) {
    this.nickname = nickname;
    this.country = country;
    
    this.getInfo = function() {
        // Outer function context (Person object)
        return function() {
            // Inner function context (Global object 'Window')
            alert(this.constructor.name); // Window
            alert("Hi, I'm " + this.nickname + " from " + this.country);
        };
    }
}

var p = new Person('Rick', 'Argentina');
var printInfo = p.getInfo();
printInfo(); // Hi, I'm undefined from undefined

使用 ES6 模板文字和箭頭函式重寫相同的示例:

function Person(nickname, country) {
    this.nickname = nickname;
    this.country = country;
    
    this.getInfo = function() {
        // Outer function context (Person object)
        return () => {
            // Inner function context (Person object)
            alert(this.constructor.name); // Person
            alert(`Hi, I'm ${this.nickname} from ${this.country}`);
        };
    }
}

let p = new Person('Rick', 'Argentina');
let printInfo = p.getInfo();
printInfo(); // Hi, I'm Rick from Argentina

正如你可以清楚地看到的,上面示例中的 this 關鍵字是指包含箭頭函式的函式的上下文,該函式是 Person 物件(第 9 行),與之前的示例不同,它引用了全域性物件 Window行號 - 9)。

在 ECMAScript 5 及更早版本中,JavaScript 從未存在過類。ES6 中引入了類,它們看起來類似於其他面嚮物件語言中的類,例如Java,PHP 等,但它們的工作方式並不完全相同。ES6 類使建立物件變得更容易,通過使用 extends 關鍵字實現繼承,並重用程式碼。

在 ES6 中,你可以使用新 class 關鍵字後跟類名宣告一個類。按照慣例,類名用 TitleCase 編寫(即將每個單詞的首字母大寫)。

class Rectangle {
    // Class constructor
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    
    // Class method
    getArea() {
        return this.length * this.width;
    }
}

// Square class inherits from the Rectangle class
class Square extends Rectangle {
    // Child class constructor
    constructor(length) {
        // Call parent's constructor
        super(length, length);
    }
    
    // Child class method
    getPerimeter() {
        return 2 * (this.length + this.width);
    }
}

let rectangle = new Rectangle(5, 10);
alert(rectangle.getArea()); // 50

let square = new Square(5);
alert(square.getArea()); // 25
alert(square.getPerimeter()); // 20

alert(square instanceof Square); // true
alert(square instanceof Rectangle); // true
alert(rectangle instanceof Square); // false

在上面的示例中,Square 類使用 extends 關鍵字從 Rectangle 繼承。從其他類繼承的類稱為派生類或子類。

此外, super() 在訪問 context(this) 之前,必須呼叫子類建構函式。例如,如果省略 super()getArea() 在方形物件上呼叫方法,則會導致錯誤,因為 getArea() 方法需要訪問 this 關鍵字。

注意: 與函式宣告不同,類宣告不會被掛起。類宣告駐留在直到執行到達類宣告,類似的點的時間的死區(TDZ) letconst 宣告。因此,你需要在訪問它之前宣告你的類,否則將發生 ReferenceError。

模組

在 ES6 之前,JavaScript 中的模組沒有原生支援。JavaScript 應用程式內的所有內容(例如跨不同 JavaScript 檔案的變數)共享相同的範圍。

ES6 引入了基於檔案的模組,其中每個模組由單獨的 .js 檔案表示。現在,你可以使用模組中的 export 或者 import 語句將變數、函式、類或任何其他實體匯出或匯入到其他模組或檔案中。

讓我們建立一個模組,即 JavaScript 檔案 main.js,並將以下程式碼放入其中:

let greet = "Hello World!";
const PI = 3.14; 

function multiplyNumbers(a, b) {
    return a * b;
}

// Exporting variables and functions
export { greet, PI, multiplyNumbers };

現在使用以下程式碼建立另一個 JavaScript 檔案 app.js

import { greet, PI, multiplyNumbers } from './main.js';

alert(greet); // Hello World!
alert(PI); // 3.14
alert(multiplyNumbers(6, 15)); // 90

最後建立一個 HTML 檔案 test.html 並使用以下程式碼,並使用 HTTP 協議(或使用 localhost)在瀏覽器中開啟此 HTML 檔案。另請注意 type="module" 指令碼標記。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>ES6 Module Demo</title>
</head>
<body>
    <script type="module" src="app.js"></script>
</body>
</html>

rest 引數

ES6 引入了 rest 引數,允許我們以陣列的形式將任意數量的引數傳遞給函式。當你想要將引數傳遞給函式但不知道需要多少時,這尤其有用。

通過在命名引數前加上 rest 運算子即三個點(...)來指定 rest 引數。Rest 引數只能是引數列表中的最後一個引數,並且只能有一個 rest 引數。看一下下面的例子,看它是如何工作的:

function sortNames(...names) {
    return names.sort();
}

alert(sortNames("Sarah", "Harry", "Peter")); // Harry,Peter,Sarah
alert(sortNames("Tony", "Ben", "Rick", "Jos")); // John,Jos,Rick,Tony

當 rest 引數是函式中唯一的引數時,它獲取傳遞給函式的所有引數,否則它將獲得超過命名引數數量的其餘引數。

function myFunction(a, b, ...args) {
    return args;
}

alert(myFunction(1, 2, 3, 4, 5)); // 3,4,5
alert(myFunction(-7, 5, 0, -2, 4.5, 1, 3)); // 0,-2,4.5,1,3

注意: 不要將術語 rest 引數與 REST(REpresentational State Transfer)混淆。這與 RESTful Web 服務無關。

展開運算子

展開運算子(也表示為(...))執行與 rest 運算子完全相反的函式。擴充套件運算子展開(即拆分)一個陣列並將值傳遞給指定的函式,如以下示例所示:

function addNumbers(a, b, c) {
    return a + b + c;
}

let numbers = [5, 12, 8];

// ES5 way of passing array as an argument of a function
alert(addNumbers.apply(null, numbers)); // 25

// ES6 spread operator
alert(addNumbers(...numbers)); // 25

展開運算子也可用於插入一個陣列到另一個陣列的元件而不使用陣列方法比如 push()unshift() concat() 等。

let pets = ["Cat", "Dog", "Parrot"];
let bugs = ["Ant", "Bee"];

// Creating an array by inserting elements from other arrays
let animals = [...pets, "Tiger", "Wolf", "Zebra", ...bugs];

alert(animals); // Cat,Dog,Parrot,Tiger,Wolf,Zebra,Ant,Bee

解構賦值

解構賦值是一種表示式,通過提供更短的語法,可以輕鬆地將陣列或物件屬性中的值提取到不同的變數中。

有兩種解構賦值表示式 - 陣列物件解構賦值。那麼,讓我們看看它們中的每一個是如何工作的:

陣列解構賦值

在 ES6 之前,要獲得陣列的單個值,我們需要編寫如下內容:

// ES5 syntax
var fruits = ["Apple", "Banana"];

var a = fruits[0];
var b = fruits[1];
alert(a); // Apple
alert(b); // Banana

在 ES6 中,我們可以使用陣列解構賦值在一行中執行相同的操作:

// ES6 syntax
let fruits = ["Apple", "Banana"];

let [a, b] = fruits; // Array destructuring assignment

alert(a); // Apple
alert(b); // Banana

你還可以在陣列解構賦值中使用rest 運算子 ,如下所示:

// ES6 syntax
let fruits = ["Apple", "Banana", "Mango"];

let [a, ...r] = fruits;

alert(a); // Apple
alert(r); // Banana,Mango
alert(Array.isArray(r)); // true

物件解構賦值

在 ES5 中提取物件的屬性值,我們需要編寫如下內容:

// ES5 syntax
var person = {name: "Peter", age: 28};

var name = person.name;
var age = person.age;

alert(name); // Peter
alert(age); // 28

但是在 ES6 中,你可以提取物件的屬性值並將它們輕鬆地分配給變數,如下所示:

// ES6 syntax
let person = {name: "Peter", age: 28};

let {name, age} = person; // Object destructuring assignment

alert(name); // Peter
alert(age); // 28

我們上面討論過的大部分功能都在最新版本的主要網路瀏覽器中得到支援,例如 Google Chrome,Mozilla Firefox,Microsoft Edge,Safari 等。

或者,你可以免費使用 Babel 等線上轉譯器(源到源編譯器) 將你當前的 ES6 程式碼轉換為 ES5,以獲得更好的瀏覽器相容性,同時不會遺漏 ES6 增強語法和功能的優勢。