面试深入ES6 新特性:从基础到高级,全面掌握

485 阅读13分钟

ECMAScript 6(ES6)也被称为 ECMAScript 2015,它为 JavaScript 带来了许多强大的新特性。ES6 使 JavaScript 变得更加简洁、高效,并且为开发者提供了更多的工具来解决常见的编程问题。从 变量声明数组和函数扩展对象的扩展异步编程模块化新数据结构装饰器,ES6 中的每个新特性都为我们提供了更加丰富的功能。下面,我将详细解析这些特性,并解释如何在日常开发中应用它们。


1. varletconst 的区别

在 ES6 之前,JavaScript 只有 var 用于声明变量,var 存在一些问题,比如作用域不清晰和变量提升。ES6 引入了 letconst,解决了这些问题,并使变量声明更加灵活和可控。

1.1 var 的特点

  • 作用域问题: var 声明的变量具有 函数作用域,如果声明在函数内,则变量只在该函数内有效;如果声明在函数外,则变量具有 全局作用域。这会导致一些意外的结果,尤其是在使用循环和条件语句时。
  • 变量提升: var 声明的变量会被提升到当前作用域的顶部,但其值为 undefined

示例:

function testVar() {
  console.log(a); // undefined,变量提升
  var a = 10;
  console.log(a); // 10
}
testVar();

1.2 let 的特点

  • 块级作用域: let 声明的变量是 块级作用域,即变量仅在最近的 {} 块中有效,适用于 forif 等控制结构。这使得变量作用域更为清晰。
  • 没有变量提升: let 声明的变量不会提升,访问声明前的变量会导致 ReferenceError 错误。
  • 不可重复声明: 在同一作用域中,let 不允许重复声明变量。

示例:

function testLet() {
  // console.log(b); // ReferenceError: Cannot access 'b' before initialization
  let b = 20;
  console.log(b); // 20
}
testLet();

1.3 const 的特点

  • 常量声明: const 用于声明常量,必须在声明时初始化,并且之后不能重新赋值。const 只保证变量本身不可重新赋值,对于引用类型(如数组和对象),引用指向的内容可以改变。
  • 块级作用域: const 也有块级作用域。

示例:

const c = 30;
// c = 40; // TypeError: Assignment to constant variable
const obj = { name: 'Alice' };
obj.age = 25; // 合法,修改对象属性
console.log(obj); // { name: 'Alice', age: 25 }

const 通常用于声明不需要修改值的变量,如常量、配置参数等。


2. 数组的扩展

ES6 为数组引入了许多新特性,使得数组的操作变得更加简洁和强大。扩展运算符(...)的引入让我们能够方便地展开数组、合并数组以及进行浅拷贝。除此之外,ES6 还新增了一些数组方法,使得数组的操作更加高效和灵活。

2.1 扩展运算符(...

扩展运算符可以将数组拆分为单个元素,也可以将多个元素合并为一个数组。它不仅用于数组的展开,还可用于函数调用中的参数传递。

  • 数组展开: 将数组展开成单独的元素。

    const arr1 = [1, 2, 3];
    const arr2 = [...arr1, 4, 5];
    console.log(arr2); // [1, 2, 3, 4, 5]
    
  • 浅拷贝: 使用扩展运算符进行浅拷贝,即拷贝原数组的元素,但引用类型的元素不会被复制,而是保持引用关系。

    const arr3 = [1, 2, 3];
    const arr4 = [...arr3];
    arr4[0] = 10;
    console.log(arr3); // [1, 2, 3]
    console.log(arr4); // [10, 2, 3]
    
  • 函数调用: 将数组展开为单独的参数传递给函数。

    function sum(a, b, c) {
      return a + b + c;
    }
    const arr5 = [1, 2, 3];
    console.log(sum(...arr5)); // 6
    

2.2 数组新增方法

ES6 为数组引入了许多新的方法,这些方法提高了数组的操作效率和可读性。

  • Array.from() :将类数组对象(如 arguments)或可迭代对象(如 SetMap)转换为数组。

    const str = 'hello';
    const arr6 = Array.from(str); // ['h', 'e', 'l', 'l', 'o']
    
  • Array.of() :将多个值转换为一个数组。

    const arr7 = Array.of(1, 2, 3); // [1, 2, 3]
    
  • copyWithin() :将数组中的部分元素复制到其他位置。

    const arr8 = [1, 2, 3, 4, 5];
    arr8.copyWithin(0, 3); // [4, 5, 3, 4, 5]
    
  • find()findIndex() :返回满足条件的第一个元素或其索引。

    const arr9 = [1, 2, 3, 4, 5];
    const found = arr9.find(x => x > 3);
    console.log(found); // 4
    
  • includes() :判断数组是否包含某个元素。

    console.log(arr9.includes(3)); // true
    
  • flat()flatMap() :用来将多维数组扁平化。

    const arr10 = [1, [2, 3], [4, 5]];
    console.log(arr10.flat()); // [1, 2, 3, 4, 5]
    

3. 函数的新增特性

ES6 使得函数的声明变得更加简洁,同时引入了一些新的功能,提升了 JavaScript 函数的表达能力。

3.1 默认参数

ES6 允许为函数的参数设置默认值,如果调用时没有传入对应的参数,则使用默认值。

function greet(name = 'Guest') {
  console.log(`Hello, ${name}!`);
}
greet(); // Hello, Guest!
greet('Alice'); // Hello, Alice!

默认参数也支持与解构赋值结合使用:

function greet({ name = 'Guest', age = 18 } = {}) {
  console.log(`Hello, ${name}. You are ${age} years old.`);
}
greet({ name: 'Bob' }); // Hello, Bob. You are 18 years old.
greet(); // Hello, Guest. You are 18 years old.

3.2 箭头函数

箭头函数提供了更简洁的函数定义方式,并且会继承外部作用域的 this,避免了传统函数中 this 绑定的问题。

  • 简洁语法: 不需要 function 关键字,可以省略 {}return(如果只有一个表达式)。

    const add = (a, b) => a + b;
    console.log(add(2, 3)); // 5
    
  • 继承 this 箭头函数不会有自己的 this,它会继承外部作用域的 this。这对于回调函数非常有用,避免了 this 指向不明确的问题。

    const obj = {
      name: 'Alice',
      greet: function() {
        setTimeout(() => {
          console.log(`Hello, ${this.name}`);
        }, 100)
    }  
    obj.greet(); // Hello, Alice
    

在上述例子中,箭头函数的 this 会继承 greet 方法中的 this(即 obj),因此 this.name 正确指向 Alice。而如果使用普通函数,则 this 会指向 setTimeout 的上下文,可能导致错误的结果。

注意事项:

  • 箭头函数不适用于构造函数,也不能使用 argumentsyield
  • 对于需要独立绑定 this 的场景,仍然需要使用普通函数。

4. 对象的扩展

ES6 对对象的功能进行了扩展,提供了更简洁、更灵活的语法和新功能,使得对象的操作更加直观和强大。

4.1 属性简写与方法简写

ES6 引入了对象字面量的简写语法,在对象中,如果属性名与变量名相同,可以省略属性名。

  • 属性简写:

    const name = 'Alice';
    const age = 25;
    const person = { name, age }; // 相当于 { name: name, age: age }
    console.log(person); // { name: 'Alice', age: 25 }
    
  • 方法简写: 在对象字面量中,方法定义时不需要使用 function 关键字。

    const person = {
      name: 'Alice',
      greet() {
        console.log('Hello, ' + this.name);
      }
    };
    person.greet(); // Hello, Alice
    

4.2 属性名表达式

ES6 允许使用表达式作为对象的属性名。这使得在动态生成对象时更加灵活。

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

4.3 super 关键字

super 用于调用父类的构造函数或方法,主要用于继承中。

  • 访问父类的方法:

    class Animal {
      speak() {
        console.log('Animal speaks');
      }
    }
    
    class Dog extends Animal {
      speak() {
        super.speak(); // 调用父类的 speak 方法
        console.log('Dog barks');
      }
    }
    
    const dog = new Dog();
    dog.speak(); 
    // Output:
    // Animal speaks
    // Dog barks
    
  • 调用父类构造函数:

    class Person {
      constructor(name) {
        this.name = name;
      }
    }
    
    class Employee extends Person {
      constructor(name, position) {
        super(name); // 调用父类构造函数
        this.position = position;
      }
    }
    
    const emp = new Employee('Alice', 'Developer');
    console.log(emp.name); // Alice
    console.log(emp.position); // Developer
    

4.4 扩展运算符

扩展运算符不仅可以用于数组的合并和展开,也可以用于对象的合并。它用于将一个对象的所有属性复制到另一个对象,支持浅拷贝。

const person = { name: 'Alice', age: 25 };
const address = { city: 'New York', country: 'USA' };
const user = { ...person, ...address };
console.log(user); // { name: 'Alice', age: 25, city: 'New York', country: 'USA' }

扩展运算符可以非常简洁地合并多个对象或创建新对象。注意,它是浅拷贝,引用类型的属性仍然是共享的。


5. Promise 异步编程

ES6 引入了 Promise,这是处理异步操作的一个重要工具。Promise 使得异步操作的管理和错误处理变得更加容易,避免了回调地狱(callback hell)。

5.1 Promise 状态

Promise 有三种状态:

  • pending(待定): 初始状态,表示异步操作尚未完成。
  • fulfilled(已完成): 操作成功完成,Promise 返回结果。
  • rejected(已拒绝): 操作失败,Promise 返回错误。
const promise = new Promise((resolve, reject) => {
  let success = true;
  if (success) {
    resolve('Operation was successful');
  } else {
    reject('Operation failed');
  }
});

promise
  .then(result => console.log(result)) // "Operation was successful"
  .catch(error => console.log(error)); // 如果失败则输出 "Operation failed"

5.2 链式调用与错误捕获

Promise 支持链式调用,多个 .then() 方法可以串联在一起,形成操作链条。每个 then 都返回一个新的 Promise,使得可以处理不同的异步结果。

const promise = new Promise((resolve, reject) => {
  resolve(5);
});

promise
  .then(result => {
    console.log(result); // 5
    return result * 2;
  })
  .then(result => {
    console.log(result); // 10
    return result * 2;
  })
  .then(result => {
    console.log(result); // 20
  });

5.3 Promise 组合

  • Promise.all() :等待所有 Promise 完成后执行。若其中一个 Promise 失败,则立即返回失败状态。

    const p1 = Promise.resolve(5);
    const p2 = Promise.resolve(10);
    
    Promise.all([p1, p2]).then(results => {
      console.log(results); // [5, 10]
    });
    
  • Promise.race() :返回第一个完成的 Promise

    const p1 = new Promise(resolve => setTimeout(resolve, 500, 'First'));
    const p2 = new Promise(resolve => setTimeout(resolve, 100, 'Second'));
    
    Promise.race([p1, p2]).then(result => {
      console.log(result); // 'Second'
    });
    

6. 模块化(Module)

ES6 提供了内置的模块系统,使得 JavaScript 代码更具模块化,易于维护和组织。

6.1 exportimport

  • export:用于从一个模块导出代码,使其可以在其他模块中使用。
  • import:用于导入其他模块的代码。

导出模块:

// math.js
export const PI = 3.14159;
export function calculateArea(radius) {
  return PI * radius * radius;
}

导入模块:

// app.js
import { PI, calculateArea } from './math.js';
console.log(PI); // 3.14159
console.log(calculateArea(5)); // 78.53975

6.2 动态加载模块

import() 函数允许动态加载模块。它返回一个 Promise,适合按需加载模块,尤其在大型应用中可以提升性能。

import('./math.js').then(module => {
  console.log(module.PI); // 3.14159
  console.log(module.calculateArea(5)); // 78.53975
});

7. Generator 函数

Generator 函数是一个能够暂停执行并在之后恢复执行的函数,使用 function* 声明,yield 用于暂停。

7.1 基本用法

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

7.2 应用场景

Generator 可以用于处理异步操作,例如,控制异步流程,或者用于数据流的迭代。

Promise 配合使用:

function* fetchData() {
  const data1 = yield fetch('/data1.json').then(res => res.json());
  console.log(data1);
  const data2 = yield fetch('/data2.json').then(res => res.json());
  console.log(data2);
}

const gen = fetchData();
gen.next().value.then(data1 => gen.next(data1).value).then(data2 => gen.next(data2));

8. 装饰器(Decorator)

装饰器是 ES7 提案中的一部分,它使得我们可以动态地修改类或方法的行为。

8.1 用法

装饰器用于修改类或类方法的行为。例如,添加日志、权限检查等。

类装饰器:

function logClass(target) {
  console.log(`
  console.log(`Class ${target.name} has been decorated`);
}

@logClass
class Person {
  constructor(name) {
    this.name = name;
  }
}

const person = new Person('Alice');
// Console Output: Class Person has been decorated

在上面的代码中,logClass 装饰器会在类 Person 被创建时输出日志。通过使用 @logClass,我们能够动态地修改类的行为或进行额外的操作。

方法装饰器:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

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

  @readonly
  getName() {
    return this.name;
  }
}

const person = new Person('Alice');
console.log(person.getName()); // Alice
person.getName = function() { return 'Bob'; }; // Error: Cannot assign to read only property 'getName'

这里的 readonly 装饰器使得 getName 方法成为只读,无法修改。

装饰器广泛应用于框架中,如 Angular 或 React 的高阶组件(HOC)中,也用于函数的验证、权限控制、缓存等功能。


9. 新数据结构:Set 和 Map

ES6 引入了 SetMap 这两种新的数据结构,它们具有更高效的查找、删除操作,并且提供了不同的行为特性,适用于不同的使用场景。

9.1 Set

Set 是一种集合数据结构,它存储的是唯一的值,重复的值会被自动去除。Set 不允许存储相同的元素。

基本用法:

const set = new Set();
set.add(1);
set.add(2);
set.add(3);
set.add(2); // 重复值不会被添加

console.log(set); // Set { 1, 2, 3 }
console.log(set.has(2)); // true
set.delete(3);
console.log(set); // Set { 1, 2 }
  • 增、删、查操作: add(), delete(), has()clear()(清空集合)。
  • 迭代: Set 提供了多种迭代方式,如 forEach(),以及 values(), keys(), entries() 等。

特点:

  • Set 是一个 无序的集合,它的元素不按任何顺序排列。
  • Set 的元素是唯一的,没有重复值。

9.2 Map

Map 是一种键值对(key-value)数据结构,可以存储任意类型的键。与对象不同,Map 的键可以是任何数据类型,而不仅仅是字符串或符号。

基本用法:

const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);
map.set('city', 'New York');

console.log(map); // Map { 'name' => 'Alice', 'age' => 25, 'city' => 'New York' }

console.log(map.get('name')); // Alice
console.log(map.has('age')); // true
map.delete('city');
console.log(map.has('city')); // false
  • 增、删、查操作: set(), get(), has(), delete(), clear()
  • 遍历: Map 支持通过 forEach() 方法迭代,也可以通过 keys(), values(), entries() 获取迭代器。

特点:

  • Map 中的键值对是 有序的,且可以存储任何类型的键。
  • Map 具有更高效的查找和删除性能,尤其是在需要频繁查找和删除键值对的场景下。

10. 其他新增功能

ES6 还提供了一些其他非常实用的新功能,以下是一些常见的新增方法和功能:

10.1 Object.is

Object.is 用于比较两个值是否严格相等,它与 === 运算符相似,但在某些特殊情况下表现不同:

  • NaN 的比较: Object.is(NaN, NaN) 返回 true,而 === 返回 false
  • +0-0 的比较: Object.is(+0, -0) 返回 false,而 === 返回 true
console.log(Object.is(25, 25)); // true
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false

Object.is 更加严格地比较值,适用于一些需要精确比较的场景。


10.2 Object.assign

Object.assign 用于将一个或多个源对象的所有可枚举属性复制到目标对象中。它常用于合并对象或创建对象的浅拷贝。

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

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

注意: Object.assign 进行的是浅拷贝,如果源对象包含嵌套的引用类型(如对象或数组),目标对象和源对象会共享这些引用。

const obj1 = { nested: { x: 10 } };
const obj2 = { y: 20 };

const result = Object.assign({}, obj1, obj2);
result.nested.x = 30;

console.log(obj1.nested.x); // 30,obj1 和 result 中的 nested 引用的是同一个对象

10.3 Object.fromEntries

Object.fromEntries 方法接受一个键值对的数组或可迭代对象,并将其转换为一个对象。这个方法与 Object.entries 搭配使用非常方便。

const entries = [['name', 'Alice'], ['age', 25]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: 'Alice', age: 25 }

Object.fromEntries 非常适合将 Map 或其他键值对集合转换为普通对象,简化了对象转换过程。


11. 总结

ES6 为 JavaScript 语言带来了很多新特性,使得代码更加简洁、高效且易于维护。通过模块化、箭头函数、类、扩展运算符和新的数据结构,开发者能够更好地管理和组织代码,避免了许多传统 JavaScript 编程中常见的问题。

  • 块级作用域和常量声明letconst 提供了更强的作用域控制,避免了 var 带来的混乱。
  • 函数特性:箭头函数简化了函数定义,同时提供了更加直观的 this 绑定。
  • 数组和对象的扩展:新增的数组方法和对象的简写语法使得代码更加简洁,减少了样板代码。
  • 异步编程PromiseGenerator 提供了更强大的异步处理能力,使得异步操作更加可控和易于理解。
  • 新数据结构SetMap 提供了更高效的存储和查找操作,特别是在处理大量数据时,能提高性能。

对于前端开发者来说,ES6 是现代 JavaScript 编程的基石,掌握 ES6 的新特性是提升编程效率和代码质量的关键。希望通过本文的详细解析,能帮助你更好地理解并应用这些新特性,不断提升你的 JavaScript 编程能力。


进一步学习和实践

  • 模块化: 深入了解 ES6 的模块化,使用 exportimport 来管理大型应用的代码结构。
  • 异步编程: 掌握 async/await,使得异步代码更加易读,了解如何结合 Promiseasync/await 进行复杂的异步操作。
  • 新数据结构: 学习如何使用 SetMap 替代传统的对象和数组,优化性能,特别是在数据去重和映射的场景中。