MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript ES6语法新特性概述

2022-11-282.3k 阅读

块级作用域与 let 和 const 关键字

在 ES6 之前,JavaScript 只有函数作用域和全局作用域,没有块级作用域。这就导致了一些不符合预期的行为。例如:

function test() {
    for (var i = 0; i < 5; i++) {
        setTimeout(() => {
            console.log(i);
        }, 1000);
    }
}
test();
// 输出 5 5 5 5 5

这里使用 var 声明的 i 是函数作用域,在循环结束后 i 的值变为 5,所以在定时器回调中输出的都是 5。

ES6 引入了 letconst 关键字,它们具有块级作用域。以 let 为例:

function test() {
    for (let i = 0; i < 5; i++) {
        setTimeout(() => {
            console.log(i);
        }, 1000);
    }
}
test();
// 输出 0 1 2 3 4

在这个例子中,let 声明的 i 具有块级作用域,每次循环都会创建一个新的 i 变量,所以定时器回调中输出的是循环的当前值。

const 用于声明常量,一旦声明,值就不能被改变。例如:

const PI = 3.14159;
// PI = 3.14; // 这会报错,常量不能重新赋值

const 声明的变量也具有块级作用域。并且,如果 const 声明的是一个对象或数组,虽然不能重新赋值整个对象或数组,但可以修改其内部属性或元素。例如:

const obj = { name: 'John' };
obj.age = 30;
console.log(obj); // { name: 'John', age: 30 }

箭头函数

箭头函数是 ES6 引入的一种简洁的函数定义方式。传统函数定义方式如下:

function add(a, b) {
    return a + b;
}

使用箭头函数可以写成:

const add = (a, b) => a + b;

当箭头函数只有一个参数时,可以省略参数的括号:

const square = num => num * num;

如果箭头函数没有参数,需要保留括号:

const getRandom = () => Math.random();

箭头函数与传统函数有几个重要区别。首先,箭头函数没有自己的 this 绑定,它的 this 继承自外层作用域。例如:

const obj = {
    name: 'John',
    sayName: function() {
        setTimeout(() => {
            console.log(this.name);
        }, 1000);
    }
};
obj.sayName(); // 输出 'John'

在这个例子中,箭头函数中的 this 指向 obj,因为箭头函数没有自己的 this,它继承了外层 sayName 函数的 this

而传统函数在 setTimeoutthis 会指向全局对象(在浏览器中是 window),除非使用 bindcallapply 改变 this 的指向。

其次,箭头函数没有 arguments 对象。可以使用剩余参数(rest 参数)来替代获取函数的所有参数。例如:

const sum = (...nums) => nums.reduce((acc, num) => acc + num, 0);
console.log(sum(1, 2, 3)); // 输出 6

模板字符串

ES6 之前,拼接字符串通常使用 + 运算符,这在处理复杂字符串时会变得很繁琐。例如:

const name = 'John';
const age = 30;
const message = 'My name is'+ name +'and I am'+ age +'years old.';

ES6 引入了模板字符串,使用反引号()来定义。模板字符串可以包含变量和表达式,通过 ${}` 语法嵌入。例如:

const name = 'John';
const age = 30;
const message = `My name is ${name} and I am ${age} years old.`;
console.log(message);

模板字符串还支持多行字符串。例如:

const html = `
    <div>
        <h1>Hello, World!</h1>
        <p>This is a multi - line string in JavaScript.</p>
    </div>
`;
console.log(html);

解构赋值

解构赋值是一种从数组或对象中提取值并赋给变量的简洁方式。

数组解构

从数组中提取值:

const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

可以跳过某些值:

const numbers = [1, 2, 3];
const [a,, c] = numbers;
console.log(a); // 1
console.log(c); // 3

还可以使用剩余参数来获取剩余的值:

const numbers = [1, 2, 3, 4, 5];
const [a, b,...rest] = numbers;
console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]

对象解构

从对象中提取值:

const person = { name: 'John', age: 30, city: 'New York' };
const { name, age, city } = person;
console.log(name); // 'John'
console.log(age); // 30
console.log(city); // 'New York'

可以给变量取不同的名字:

const person = { name: 'John', age: 30, city: 'New York' };
const { name: fullName, age: yearsOld, city: livingCity } = person;
console.log(fullName); // 'John'
console.log(yearsOld); // 30
console.log(livingCity); // 'New York'

也可以设置默认值:

const person = { name: 'John' };
const { name, age = 18 } = person;
console.log(name); // 'John'
console.log(age); // 18

函数参数的默认值

在 ES6 之前,如果要给函数参数设置默认值,通常需要在函数内部进行判断。例如:

function greet(name) {
    name = name || 'Guest';
    console.log('Hello,'+ name);
}
greet(); // Hello, Guest
greet('John'); // Hello, John

ES6 可以直接在函数参数中设置默认值:

function greet(name = 'Guest') {
    console.log('Hello,'+ name);
}
greet(); // Hello, Guest
greet('John'); // Hello, John

这样代码更加简洁明了。当函数调用时,如果没有传递参数,就会使用默认值。

剩余参数与扩展运算符

剩余参数

剩余参数允许将不定数量的参数表示为一个数组。例如,传统方式获取函数的所有参数需要使用 arguments 对象:

function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}
console.log(sum(1, 2, 3)); // 6

使用剩余参数可以这样写:

function sum(...nums) {
    return nums.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 6

这里的 ...nums 就是剩余参数,它将所有传入的参数收集到一个数组 nums 中。

扩展运算符

扩展运算符也是 ...,但它的作用与剩余参数相反。它可以将一个数组展开成多个元素。例如,合并两个数组:

const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = [...arr1,...arr2];
console.log(combined); // [1, 2, 3, 4]

扩展运算符还可以用于函数调用,将数组作为参数传递。例如:

function add(a, b) {
    return a + b;
}
const numbers = [1, 2];
const result = add(...numbers);
console.log(result); // 3

在对象中也可以使用扩展运算符来合并对象:

const obj1 = { name: 'John' };
const obj2 = { age: 30 };
const combinedObj = {...obj1,...obj2 };
console.log(combinedObj); // { name: 'John', age: 30 }

对象字面量增强

在 ES6 中,对象字面量有了一些增强的功能。

属性简写

当对象的属性名和变量名相同时,可以简写。例如:

const name = 'John';
const age = 30;
const person = { name, age };
console.log(person); // { name: 'John', age: 30 }

这等价于:

const name = 'John';
const age = 30;
const person = { name: name, age: age };
console.log(person);

方法简写

定义对象方法时可以省略 function 关键字。例如:

const person = {
    name: 'John',
    sayHello() {
        console.log('Hello, my name is'+ this.name);
    }
};
person.sayHello(); // Hello, my name is John

这比传统的写法更加简洁:

const person = {
    name: 'John',
    sayHello: function() {
        console.log('Hello, my name is'+ this.name);
    }
};
person.sayHello();

计算属性名

在 ES6 中,可以在对象字面量中使用计算属性名。例如:

const key = 'name';
const person = {
    [key]: 'John',
    age: 30
};
console.log(person); // { name: 'John', age: 30 }

这在需要动态生成属性名时非常有用。

类的引入

ES6 引入了类的概念,虽然 JavaScript 本质上仍然是基于原型的面向对象语言,但类的语法让代码更加清晰和易于理解。

类的定义

定义一个简单的类:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayHello() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
}

这里的 constructor 是类的构造函数,用于初始化对象的属性。sayHello 是类的方法。

类的实例化

使用 new 关键字创建类的实例:

const john = new Person('John', 30);
john.sayHello(); // Hello, my name is John and I am 30 years old.

继承

类可以通过 extends 关键字实现继承。例如:

class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);
        this.grade = grade;
    }
    study() {
        console.log(`${this.name} is studying in grade ${this.grade}`);
    }
}
const tom = new Student('Tom', 15, 9);
tom.sayHello(); // Hello, my name is Tom and I am 15 years old.
tom.study(); // Tom is studying in grade 9

在子类的构造函数中,需要先调用 super() 来调用父类的构造函数,以初始化继承自父类的属性。

模块

ES6 引入了模块系统,用于更好地组织和管理代码。

模块的定义

一个模块可以是一个单独的 JavaScript 文件。例如,创建一个 math.js 文件:

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

这里使用 export 关键字导出函数。也可以使用默认导出:

// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
export default { add, subtract };

模块的导入

在另一个文件中导入模块:

// main.js
import { add, subtract } from './math.js';
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2

如果是默认导出,可以这样导入:

// main.js
import math from './math.js';
console.log(math.add(1, 2)); // 3
console.log(math.subtract(5, 3)); // 2

Promise

Promise 是 ES6 引入的用于处理异步操作的一种机制,它解决了回调地狱(callback hell)的问题。

创建 Promise

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('Operation successful');
        } else {
            reject('Operation failed');
        }
    }, 1000);
});

Promise 构造函数接受一个回调函数,该回调函数有两个参数 resolverejectresolve 用于表示异步操作成功,reject 用于表示异步操作失败。

处理 Promise

promise.then(value => {
    console.log(value); // Operation successful
}).catch(error => {
    console.error(error);
});

then 方法用于处理成功的情况,catch 方法用于处理失败的情况。

Promise 链

可以将多个 then 方法连接起来形成 Promise 链。例如:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const promise2 = promise1.then(value => {
    return value * 2;
});
const promise3 = promise2.then(value => {
    return value + 3;
});
promise3.then(finalValue => {
    console.log(finalValue); // 5
}).catch(error => {
    console.error(error);
});

在这个例子中,promise1 成功后返回 1promise2 将其乘以 2 得到 2promise3 再将其加 3 得到 5

Async/await

async/await 是基于 Promise 的更简洁的异步处理方式,它让异步代码看起来更像同步代码。

async 函数

async 函数总是返回一个 Promise。例如:

async function getData() {
    return 'Data fetched';
}
const result = getData();
result.then(value => {
    console.log(value); // Data fetched
});

这里 getData 函数返回的是一个已解决(resolved)的 Promise。

await

await 只能在 async 函数内部使用,它用于暂停 async 函数的执行,直到 Promise 被解决(resolved)或被拒绝(rejected)。例如:

function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
async function asyncFunction() {
    console.log('Start');
    await delay(2000);
    console.log('End');
}
asyncFunction();

在这个例子中,await delay(2000) 会暂停 asyncFunction 的执行,直到 delay 返回的 Promise 被解决,也就是 2 秒后,然后继续执行后面的代码。

处理错误

async 函数中,可以使用 try...catch 来处理错误。例如:

async function asyncFunction() {
    try {
        await Promise.reject('Error occurred');
    } catch (error) {
        console.error(error); // Error occurred
    }
}
asyncFunction();

这样可以更优雅地处理异步操作中的错误,而不是像 Promise 那样使用 catch 方法。

Symbol

Symbol 是 ES6 引入的一种新的原始数据类型。每个 Symbol 值都是唯一的。

创建 Symbol

const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false

也可以给 Symbol 提供一个描述:

const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // false

使用 Symbol 作为对象属性

Symbol 可以作为对象的属性,并且这些属性不会被常规的对象遍历方法(如 for...inObject.keys)枚举到。例如:

const mySymbol = Symbol('myProperty');
const obj = {
    [mySymbol]: 'Value associated with symbol'
};
console.log(obj[mySymbol]); // Value associated with symbol

Map 和 Set

Map

Map 是一种键值对的集合,与普通对象不同的是,Map 的键可以是任意类型。例如:

const map = new Map();
const key1 = { name: 'John' };
const key2 = [1, 2, 3];
map.set(key1, 'Value for key1');
map.set(key2, 'Value for key2');
console.log(map.get(key1)); // Value for key1
console.log(map.get(key2)); // Value for key2

Map 还提供了一些有用的方法,如 size 获取键值对的数量,has 检查是否存在某个键,delete 删除某个键值对。

Set

Set 是一种无序的唯一值集合。例如:

const set = new Set();
set.add(1);
set.add(2);
set.add(2); // 重复值不会被添加
console.log(set.has(1)); // true
console.log(set.has(3)); // false
console.log(set.size); // 2

Set 也有一些方法,如 add 添加值,delete 删除值,clear 清空集合。

通过以上对 JavaScript ES6 语法新特性的详细介绍,我们可以看到这些新特性大大增强了 JavaScript 的功能和灵活性,使得代码更加简洁、易读和维护。无论是在前端开发还是后端开发中,合理运用这些新特性都能提高开发效率和代码质量。在实际项目中,应该根据具体需求和场景,充分发挥这些新特性的优势,打造出更优秀的 JavaScript 应用程序。