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 增强语法和功能的优势。