目录

ES6新特性

ES6新特性

块级作用域(Block Scope)

随着ES6的引入,JavaScript增加了对块级作用域的支持,这主要是通过 letconst 关键字实现的。块级作用域限制了变量的作用范围到最近的一对大括号 {} 内,比如在一个 if 语句、 for 循环或简单的代码块中。

  • 特点
    • letconst 声明的变量只在它们所在的块内有效。
    • 不允许重复声明相同的变量名。
    • 解决了 var 带来的变量提升问题。
if (true) {
    let blockScopedVar = "I'm scoped to this block";
    const anotherBlockScopedVar = "Also scoped to this block";
    console.log(blockScopedVar); // 正常工作
    console.log(anotherBlockScopedVar); // 正常工作
}

console.log(blockScopedVar); // 报错:blockScopedVar is not defined
console.log(anotherBlockScopedVar); // 报错:anotherBlockScopedVar is not defined

暂时性死区(Temporal Dead Zone)

暂时性死区(Temporal Dead Zone,简称TDZ)是ECMAScript 6(ES6或ES2015)引入的一个概念,主要与使用 letconst 关键字声明的变量相关。 它定义了一个区域,在这个区域内尝试访问尚未声明的变量会导致运行时错误。

本质

当控制流进入一个新的作用域(如一个代码块),在这个作用域内用 letconst 声明的变量会被创建,但在此之前,这些变量不能被访问或使用。如果试图在声明之前访问它们,JavaScript引擎将抛出 ReferenceError 。换句话说,即使变量已经存在于作用域中,但在其声明之前访问它们是非法的,并且会导致错误。

示例

考虑以下代码:

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 2;

这里,在 let a = 2; 语句执行前,任何对 a 的访问都会导致错误。这是因为从进入作用域开始直到 let 声明语句的位置,构成了 a 的暂时性死区。

再来看一个稍微复杂一点的例子:

if (true) {
    // TDZ starts at the beginning of this block
    console.log(tmp); // ReferenceError
    let tmp = 3; // TDZ ends here
}

在这个例子中,从 if 语句的开始到 let tmp = 3; 这一行之间,构成了 tmp 的暂时性死区。在这一区域内尝试访问 tmp 会导致 ReferenceError

暂时性死区的影响

  • typeof操作符 :通常情况下, typeof 操作符对于未定义的变量不会抛出错误,而是返回 "undefined" 。然而,对于处于暂时性死区中的变量,使用 typeof 同样会抛出 ReferenceError

    typeof b; // ReferenceError: Cannot access 'b' before initialization
    let b = 4;
  • 函数参数 :如果函数参数依赖于其他参数的值作为默认值,而后者还未声明,则也可能遇到暂时性死区的问题。

    function foo(x = y, y = 2) {
        return [x, y];
    }
    foo(); // ReferenceError: Cannot access 'y' before initialization
    

为什么需要暂时性死区?

暂时性死区的设计是为了减少编程错误。通过强制要求变量必须先声明后使用,可以避免一些由于变量提升带来的意外行为,尤其是在使用 var 关键字时可能出现的情况。

在JavaScript中, arguments 对象和剩余参数(Rest Parameters)都是用于处理函数调用时传入的参数,但它们之间存在一些关键的区别。下面我将详细解释两者,并结合实际应用示例来说明它们的使用方法。

函数剩余参数(Rest Parameters)

函数剩余参数(Rest Parameters)是ES6(ECMAScript 2015)引入的一种语法特性,它提供了一种更简洁的方式来处理传递给函数的不定数量的参数。通过使用三个点 ... 前缀,剩余参数允许我们将一个不定数量的实参表示为一个数组。

基本概念
  • 剩余参数 是一个真正的数组实例,与传统的 arguments 对象不同,后者不是一个真正的数组。
  • 它只能出现在函数参数列表的最后,并且会收集从该位置开始的所有参数到一个数组中。

例如:

function sum(...theArgs) {
    return theArgs.reduce((previous, current) => previous + current);
}

console.log(sum(1, 2, 3)); // 输出: 6
console.log(sum(4, 5, 6, 7, 8)); // 输出: 30

在这个例子中, sum 函数使用了剩余参数 theArgs 来接收任意数量的参数,并利用数组的 reduce 方法来计算这些参数的总和。

特性与优点
  • 灵活性 :剩余参数使得函数能够接受任意数量的参数,增加了函数的灵活性和可重用性。
  • 简洁性 :相比手动创建数组来存储参数,剩余参数减少了代码量并提高了可读性。
  • 兼容性 :可以与其他ES6特性如箭头函数、解构赋值等结合使用,以更加高效地编写代码。
注意事项
  • 剩余参数必须是函数参数列表中的最后一个参数,因为它将捕获所有剩余的参数。
  • 剩余参数不能用于箭头函数中,因为箭头函数没有自己的 this , arguments , supernew.target
  • 可以在剩余参数上使用任何数组方法,而 arguments 对象则不可以直接使用数组的方法。
实际应用示例

下面的例子展示了如何使用剩余参数实现一个简单的加法器函数,它可以对任意数量的数字进行求和:

function sum(first, ...rest) {
    let result = first;
    for (let number of rest) {
        result += number;
    }
    return result;
}

console.log(sum(1, 2, 3)); // 输出: 6
console.log(sum(4, 5, 6, 7, 8)); // 输出: 30
console.log(sum()); // 输出: NaN,因为没有提供第一个参数

展开运算符(Spread Operator)

展开运算符(Spread Operator)是ES6(ECMAScript 2015)引入的一种语法特性,它允许数组、字符串或对象的元素被“展开”为独立的元素。展开运算符使用三个连续的点号 ... 来表示,并且可以应用于多种场合,包括函数调用、数组字面量构造、对象字面量构造等。

在函数调用时展开数组元素

当你需要将一个数组作为参数传递给一个函数时,可以使用展开运算符来代替 apply() 方法:

function sum(x, y, z) {
    return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 输出: 6 
在数组字面量中合并多个数组

展开运算符可以用来轻松地合并两个或更多的数组:

const fruits = ['apple', 'banana'];
const moreFruits = ['orange', 'grape'];
const allFruits = [...fruits, ...moreFruits];
console.log(allFruits); // 输出: ['apple', 'banana', 'orange', 'grape'] 
复制数组

使用展开运算符可以创建一个现有数组的浅拷贝:

const arr = [1, 2, 3];
const arrCopy = [...arr]; // 创建arr的一个浅拷贝 

需要注意的是,这种复制方式只适用于数组的第一层,对于嵌套的对象或数组,展开运算符只会复制引用而不是深层的内容。

在对象字面量中合并对象

在ES7及以后版本中,可以使用展开运算符来合并对象:

const obj1 = { foo: 'bar', x: 42 };
const obj2 = { foo: 'baz', y: 13 };
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // 输出: { foo: 'baz', x: 42, y: 13 } 

如果存在相同的键名,后出现的对象属性会覆盖前面的对象属性。

使用展开运算符代替 apply 方法

展开运算符还可以用于简化某些原本需要用 apply 方法实现的操作,比如求一组数中的最大值:

const numbers = [9, 3, 2];
const maxNumNew = Math.max(...numbers); // 使用展开运算符的写法 

注意事项

  • 展开运算符只能用于可迭代对象(如数组、字符串、Map 和 Set),不能直接用于普通对象。
  • 对于对象的展开,只有对象自身的可枚举属性会被展开,不包括从原型链继承来的属性。
  • 展开运算符进行的是浅拷贝,这意味着如果数组或对象中包含其他对象,那么这些内部对象的引用也会被复制,而不是深拷贝整个结构。

箭头函数(Arrow Functions)

箭头函数(Arrow Functions)是ECMAScript 2015(ES6)引入的一种新的函数定义方式,它提供了一种更加简洁的语法来编写匿名函数。

箭头函数的基本语法

箭头函数的基本结构如下:

(param1, param2, ..., paramN) => { statements }

如果只有一个参数,可以省略括号:

param => { statements }

对于单个表达式返回值的情况,可以进一步简化为:

(param1, param2, ..., paramN) => expression

例如:

// 普通函数写法
var add = function(x, y) {
    return x + y;
};

// 使用箭头函数简化
const add = (x, y) => x + y;

this 关键字的行为

箭头函数与普通函数的一个重要区别在于 this 关键字的行为。在普通函数中, this 的值取决于函数是如何被调用的;而在箭头函数中, this 是在函数创建时就确定了,并且总是指向其外层作用域中的 this

const obj = {
    method: function() {
        // 这里的 'this' 指向 obj 对象
        setTimeout(() => {
            console.log(this); // 同样指向 obj 对象
        }, 100);
    }
};
obj.method(); // 输出 obj 对象两次

arguments 和剩余参数

箭头函数没有自己的 arguments 对象,取而代之的是可以通过剩余参数(Rest Parameters)来获取传入的所有参数。

function regularFunction() {
    console.log(arguments);
}

const arrowFunction = (...args) => {
    console.log(args);
};

regularFunction(1, 2, 3); // 输出 Arguments 对象
arrowFunction(1, 2, 3);   // 输出 [1, 2, 3]

不能作为构造器

由于箭头函数没有自己的 thisprototype 属性,所以它们不能通过 new 关键字来实例化对象。

const ArrowFunc = () => {};
// new ArrowFunc(); // 抛出错误

其他特性

  • supernew.target :箭头函数不支持这些关键字。
  • 隐式的返回值 :当箭头函数只包含一个表达式时,可以省略 return 关键字和大括号。
  • 不能用作Generator函数 :因为箭头函数不支持 yield 关键字。

使用场景

箭头函数非常适合用于那些不需要独立上下文的小型回调函数,比如数组方法中的回调(如 mapfilter 等)、事件处理器等。

[1, 2, 3].map(n => n * 2); // 返回 [2, 4, 6]

总之,箭头函数以其简洁的语法和固定的 this 绑定机制,为JavaScript开发者提供了更加强大和灵活的工具。不过,在需要动态 this 或者需要使用 arguments 对象的情况下,还是应该选择普通函数。

数组解构(Array Destructuring)

数组解构是ES6(ECMAScript 2015)引入的一种语法特性,它提供了一种简洁的方式来从数组中提取数据,并将其赋值给变量。这种特性不仅使代码更加直观和易读,而且也提高了开发效率。

基本用法

最基本的数组解构形式是从一个已知结构的数组中提取元素并赋值给对应的变量。例如:

let [a, b] = [1, 2];
console.log(a); // 输出: 1
console.log(b); // 输出: 2

这里, [1, 2] 是一个数组,而 [a, b] 是解构模式,用于将数组中的第一个元素赋值给变量 a ,第二个元素赋值给变量 b

跳过元素

在解构过程中,如果不需要某些元素,可以通过跳过它们来实现:

let [a,,b] = [1, 2, 3];
console.log(a); // 输出: 1
console.log(b); // 输出: 3

这里,通过使用两个逗号,我们跳过了数组中的第二个元素。

使用默认值

当数组中的元素不存在或为 undefined 时,可以指定默认值:

let [a = 1, b = 2] = [undefined, 3];
console.log(a); // 输出: 1 (因为第一个元素是undefined,所以使用了默认值)
console.log(b); // 输出: 3

解构剩余部分

使用 ... 操作符,我们可以将数组剩下的部分收集到一个新的数组中:

let [a, ...rest] = [1, 2, 3, 4];
console.log(a); // 输出: 1
console.log(rest); // 输出: [2, 3, 4]

函数返回值解构

函数也可以返回数组,然后你可以直接对返回值进行解构:

function returnArray() {
    return [1, 2, 3];
}
let [a, b, c] = returnArray();
console.log(a, b, c); // 输出: 1 2 3

交换变量值

解构赋值使得交换两个变量的值变得非常简单:

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 输出: 2
console.log(b); // 输出: 1

复杂场景

对于更复杂的数据结构,比如嵌套数组,也可以使用解构赋值:

let nested = [1, [2, 3]];
let [a, [b, c]] = nested;
console.log(a); // 输出: 1
console.log(b); // 输出: 2
console.log(c); // 输出: 3

对象解构(Object Destructuring)

对象解构是ES6(ECMAScript 2015)引入的一种特性,它允许我们从对象中提取属性并将其赋值给变量。这种语法不仅让代码更加简洁和易读,而且提高了开发效率。

基本用法

对象解构的基本形式是从一个对象中提取属性,并将这些属性的值赋给同名的变量:

let person = { name: "Sarah", country: "Nigeria", job: "Developer" };
let { name, country, job } = person;
console.log(name); // 输出: Sarah
console.log(country); // 输出: Nigeria
console.log(job); // 输出: Developer

这里, { name, country, job } 是解构模式,用于从 person 对象中提取相应的属性并赋值给同名的变量。

设置别名

有时我们可能想要为提取的属性设置不同的变量名。这可以通过在解构模式中指定属性名后跟冒号和新的变量名来实现:

let { name: personName, country: personCountry } = person;
console.log(personName); // 输出: Sarah
console.log(personCountry); // 输出: Nigeria

在这个例子中, namecountry 属性被分别赋值给了 personNamepersonCountry 变量。

默认值

当对象中没有某个属性或属性值为 undefined 时,可以提供默认值:

let { name = "Unknown", age = 30 } = {};
console.log(name); // 输出: Unknown
console.log(age); // 输出: 30

解构嵌套对象

对于包含嵌套结构的对象,也可以使用解构赋值:

let employee = {
    name: "John",
    address: {
        city: "New York",
        zipCode: "10001"
    }
};
let { name, address: { city, zipCode } } = employee;
console.log(name); // 输出: John
console.log(city); // 输出: New York
console.log(zipCode); // 输出: 10001

函数参数解构

解构赋值同样可以应用于函数参数,从而简化函数签名和内部逻辑:

function printDetails({ name, job }) {
    console.log(`${name} works as a ${job}`);
}

printDetails(person); // 输出: Sarah works as a Developer

使用场景

对象解构在许多情况下都非常有用,比如:

  • 简化从复杂对象中提取数据的过程。
  • 在函数参数中直接解构传入的对象,减少临时变量的创建。
  • 结合扩展运算符(spread operator),灵活地处理对象中的剩余部分。

class 关键字

ES6(ECMAScript 2015)引入了 class 关键字,为JavaScript带来了更接近传统面向对象编程语言的语法。尽管这种新的语法看起来像是引入了一种全新的机制来定义类和创建对象,但实际上它只是基于原型继承的一种“语法糖”,并没有改变JavaScript原有的原型继承的本质。

基本语法

在ES6中,你可以使用 class 关键字来声明一个类,并通过构造函数 constructor 来初始化实例对象。下面是一个简单的例子:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

在这个例子中, Point 类有一个构造方法 constructor 用于接收参数并初始化对象属性,还有一个名为 toString 的方法用于返回点的位置信息。

类的继承

ES6还引入了 extends 关键字来实现类的继承,使得子类可以继承父类的所有属性和方法。例如:

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的构造函数
    this.color = color;
  }

  toString() {
    return super.toString() + ' in ' + this.color;
  }
}

这里, ColorPoint 类继承自 Point 类,并添加了一个额外的属性 color 。它重写了 toString 方法,并通过 super 关键字调用了父类的同名方法。

静态方法

你还可以在类中定义静态方法,这些方法不会被实例化,而是直接通过类本身来调用:

class MyClass {
  static myStaticMethod() {
    return 'Hello World';
  }
}

console.log(MyClass.myStaticMethod()); // 输出 "Hello World"
Getter 和 Setter

ES6中的类支持getter和setter方法,它们允许你控制对对象属性的访问和修改:

class Temperature {
  constructor(celsius) {
    this.celsius = celsius;
  }

  get fahrenheit() {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

以上代码展示了如何定义获取和设置华氏温度的getter和setter方法。

注意事项

  • 类的定义不会被提升到作用域顶部,这意味着必须在使用之前定义类。
  • 在类的构造函数中,如果需要引用父类的构造函数,必须先调用 super()
  • 类内部的方法不需要使用 function 关键字,并且方法之间不应该加分号。

其他

模块(Modules)

  • importexport :ES6引入了原生的模块系统,允许开发者定义模块并通过 import 语句导入其他模块的功能,或通过 export 导出自己的功能供其他模块使用。这极大地简化了代码组织和复用。

    // math.js
    export function square(x) {
      return x * x;
    }
    
    // main.js
    import { square } from './math.js';
    console.log(square(2)); // 输出: 4
    

Promises

  • 异步编程改进 :Promises提供了一种更清晰的方式来处理异步操作,替代了传统的回调地狱模式。Promises表示一个异步操作的最终完成(或失败)及其结果值。

    let promise = new Promise(function(resolve, reject) {
      setTimeout(() => resolve("done"), 1000);
    });
    
    promise.then(
      result => console.log(result), // "done"
      error => console.log(error)
    );

Sets 和 Maps

  • 新的集合类型 :ES6引入了 SetMap 两种新的数据结构,分别用于存储不重复的值集和键值对。它们提供了比传统对象和数组更强大的功能来管理集合数据。

Proxy 和 Reflect

  • 元编程支持 :Proxy可以用来创建对象的代理,从而拦截并自定义基本操作(如属性查找、赋值等)。Reflect则提供了一组方法,对应于Proxy所拦截的操作,为反射式操作对象提供了API。

    let handler = {
      get: function(target, name) {
        return name in target ? target[name] : 42;
      }
    };
    
    let p = new Proxy({}, handler);
    p.a = 1;
    console.log(p.a, p.b); // 输出: 1 42