第一章 ECMAScript6 基础

时间:2024-11-06 10:54:16

学习目标

  • 1.1 ECMAScript6 介绍
    • 1.1.1 ECMAScript 的概念
    • 1.1.2 各浏览器对ES6的支持
    • 1.1.3 ES6转码ES5支持老版浏览器
  • 1.2 let与const命令
    • 1.2.1let命令
    • 1.2.2 const命令
  • 1.3 变量的解构赋值
    • 1.3.1 数组解构赋值
    • 1.3.2 对象解构赋值
    • 1.3.3 字符串解构赋值
    • 1.3.4 函数参数解构赋值
  • 1.4 函数扩展
    • 1.4.1 函数参数默认值
    • 1.4.2 rest参数与name属性
    • 1.4.3 箭头函数

1.1 ECMAScript6 介绍

  ECMAScript6 (简称ES6)是与2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015)。它的目标是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

1.1.1 ECMAScript 的概念

  ECMAScript是一种由欧洲计算机制造商协会(Ecma International,简称Ecma)标准化的脚本语言规范。以下是对ECMAScript概念的详细解释:

一、定义与背景

  • 定义:ECMAScript是定义脚本编程语言的标准,它为脚本语言定义了语法、类型、对象模型等核心内容,确保不同环境下脚本语言的兼容性和一致性。
  • 起源:ECMAScript最初由Netscape公司发起,作为Netscape Navigator浏览器的脚本语言(最初名为LiveScript,后改名为JavaScript)。后来,Ecma国际组织将其标准化为ECMA-262标准。

二、与JavaScript的关系

  • ECMAScript是JavaScript的核心语言规范,而JavaScript是ECMAScript标准的具体实现之一。
  • ECMAScript定义了基础语法和语义规则,而JavaScript在此基础上添加了Web相关的API和功能,例如DOM操作、事件处理、浏览器交互等。

三、版本演进

  • 自1997年发布第一版ECMAScript以来,该标准经历了多次更新和演进,包括ECMAScript 2、3、5、6(也称为ES2015)、7、8、9、10、11、12等版本。
  • 每个新版本都引入了新的语言特性和功能,以满足不断增长的Web开发需求。例如,ECMAScript 6引入了箭头函数、类、模块化等特性,极大地丰富了ECMAScript的编程能力。

四、主要特性

  • 动态类型:ECMAScript是一种动态类型语言,变量的类型在运行时确定,而不是在编译时确定。这使得编程更灵活,但也需要开发人员注意类型转换和类型错误。
  • 原型继承:ECMAScript使用原型继承作为对象之间的主要机制。每个对象都有一个原型对象,可以从中继承属性和方法。这种原型链的设计使得对象之间可以共享属性和方法,实现代码的重用。
  • 函数式编程:ECMAScript支持函数作为一等公民,可以将函数作为参数传递给其他函数,也可以将函数作为返回值。这种函数式编程的特性使得编写高阶函数和实现函数组合变得更加容易。
  • 闭包:ECMAScript支持闭包,即函数可以访问其定义时的作用域外部的变量。闭包在事件处理、模块化等方面发挥着重要作用,可以帮助开发人员管理状态和数据。
  • 异步编程:ECMAScript通过回调函数、Promise、async/await等机制支持异步编程。这使得在Web应用程序中处理异步操作(如网络请求、定时器等)变得更加简单和高效。
  • 模块化:ECMAScript支持模块化编程,可以将代码分割为多个模块,每个模块负责特定功能。这种模块化的设计有助于代码的组织和维护,同时也提高了代码的可重用性和可扩展性。

五、应用场景

  • Web前端开发:ECMAScript是Web前端开发的核心技术之一,用于实现各种动态效果和交互逻辑。
  • 服务器端开发:随着Node.js的兴起,ECMAScript也被广泛用于服务器端开发,包括构建RESTful API、后端服务等。
  • 移动应用开发:使用React Native等框架,开发者可以使用ECMAScript来构建跨平台的移动应用。

1.1.2 各浏览器对ES6的支持

  ES6(ECMAScript 2015)引入了许多新的语法和特性,以增强JavaScript编程语言的功能。目前,各大浏览器对ES6的支持情况如下:
一、桌面端浏览器

  • Chrome:从Chrome 51版起,便可以支持97%的ES6新特性。Chrome浏览器对ES6新特性的支持较为友好,且随着版本的更新,支持度也在不断提升。
  • Firefox:Firefox从53版起便可以支持97%的ES6新特性。Firefox浏览器同样对ES6新特性有较好的支持,是开发者常用的浏览器之一。
  • Safari:从Safari 10版起,便可以支持99%的ES6新特性。Safari浏览器在iOS和macOS平台上都有广泛的应用,对ES6的支持也相对较高。
  • Edge:Edge 15可以支持96%的ES6新特性,而Edge 14可以支持93%的ES6新特性。需要注意的是,早期的IE浏览器(如IE7~11)基本不支持ES6。但随着Edge浏览器的推出和不断更新,其对ES6的支持度也在逐渐提升。

二、移动端浏览器
对于移动端浏览器,2016年以后发布的Safari on iOS和Chrome等全部都支持ES6。这些浏览器在移动设备上有着广泛的应用,对ES6的支持使得开发者可以在移动应用中使用更多的ES6特性。

三、兼容性解决方案
尽管现代浏览器对ES6的支持度已经很高,但仍有一些老旧浏览器或特定环境下的浏览器可能不支持ES6。为了兼容这些浏览器,开发者通常会使用Babel等转码工具将ES6代码转码为ES5代码。这样可以确保代码在不同浏览器环境下都能正常运行。

1.1.3 ES6转码ES5支持老版浏览器

  为了兼容老版浏览器,特别是那些不支持ES6的浏览器(如Internet Explorer 11及以下版本),开发者通常会将ES6代码转码为ES5代码。以下是对这一过程的详细解释:

一、转码工具

  • Babel:Babel是一个广泛使用的JavaScript编译器,可以将ES6+代码转换为向后兼容的JavaScript版本(如ES5)。通过使用Babel,开发者可以确保他们的代码在老旧浏览器上也能正常运行。

二、转码过程

  • 安装Babel:首先,开发者需要在他们的项目中安装Babel。这通常通过npm(Node Package Manager)来完成。
  • 配置Babel:安装完成后,开发者需要配置Babel以指定要转换的代码和转换规则。这通常通过在项目中创建一个.babelrc文件或在package.json中添加一个babel字段来完成。
  • 编写ES6代码:在配置好Babel后,开发者可以开始编写ES6代码。这些代码将包含ES6的新特性,如箭头函数、类、模块化等。
  • 运行Babel进行转码:当开发者准备好将ES6代码转换为ES5代码时,他们可以运行Babel。Babel将读取源代码,应用转换规则,并生成兼容ES5的JavaScript代码。

三、Polyfill的使用
除了Babel之外,开发者可能还需要使用Polyfill来提供老旧浏览器缺少的ES6功能。Polyfill是一种浏览器端的技术,用于在旧版浏览器中提供那些原本不支持的现代功能。

  • 安装Polyfill:开发者可以通过npm安装Polyfill库,如babel-polyfill或core-js。
  • 引入Polyfill:在项目的入口文件中(如main.js),开发者需要引入Polyfill。这通常通过import语句来完成。

四、兼容性测试
在将ES6代码转码为ES5并应用Polyfill后,开发者需要进行兼容性测试以确保他们的代码在老旧浏览器上也能正常运行。这可以通过在目标浏览器上手动测试或使用自动化测试工具来完成。

五、注意事项

  • 性能考虑:虽然Polyfill可以提供老旧浏览器缺少的功能,但它们可能会增加代码的体积并影响性能。因此,开发者需要权衡功能需求和性能要求。
  • 持续更新:随着浏览器版本的更新和新特性的引入,开发者需要定期更新他们的Babel配置和Polyfill库以确保兼容性。
  • 代码质量:在转码过程中,开发者需要确保生成的ES5代码保持与原始ES6代码相同的逻辑和功能。这可能需要额外的测试和调试工作。

1.2 let与const命令

  ES5中使用的是var来声明变量,存在不少的问题,也不支持常量定义。在ES6中新增了let命令和const命令来弥补var的不足。

1.2.1let命令

  let 命令是ECMAScript 6(ES6)中引入的用于声明块作用域的本地变量的关键字。与 var 关键字不同,let 声明的变量只在其所在的块或子块中可用,而不会污染全局作用域或函数作用域。以下是一些关于 let 命令的示例代码:

{
  let a = 10;
  console.log(a); // 输出: 10
}
console.log(a); // 报错: a is not defined,因为a的作用域仅限于其所在的块

块作用域
let 声明的变量具有块作用域,这意味着它们只在其所在的块(由 {} 包围的代码块)中有效。

if (true) {
  let x = 5;
  console.log(x); // 输出: 5
}
// console.log(x); // 报错: x is not defined,因为x的作用域仅限于if块内

不允许重复声明
在同一作用域内,let 不允许重复声明同一个变量。

let y = 10;
// let y = 20; // 报错: Identifier 'y' has already been declared

暂时性死区(Temporal Dead Zone, TDZ)
在 let 变量被声明之前的区域被称为暂时性死区。在这个区域内,访问该变量会导致引用错误(ReferenceError)。

console.log(z); // 报错: Cannot access 'z' before initialization,因为z处于暂时性死区
let z = 5;

用于循环中的计数器
在 for 循环中使用 let 声明的计数器变量每次迭代都会创建一个新的块作用域,从而避免了使用 var 时可能导致的变量污染问题。

for (let i = 0; i < 3; i++) {
  console.log(i); // 输出: 0, 1, 2
}
// console.log(i); // 报错: i is not defined,因为i的作用域仅限于for循环块内

函数作用域与全局作用域
尽管 let 引入了块作用域,但在函数内部声明的 let 变量仍然遵循函数作用域的原则(在函数体内部有效),只是不再像 var 那样会“泄漏”到包含它的函数外部的全局作用域中。

function example() {
  let foo = 'Hello, world!';
  console.log(foo); // 输出: Hello, world!
}
example();
// console.log(foo); // 报错: foo is not defined,因为foo的作用域仅限于example函数内部

总之,let 命令提供了一种更灵活且更安全的变量声明方式,通过引入块作用域来避免变量污染和意外覆盖的问题。在编写现代JavaScript代码时,建议使用 let 和 const(用于声明常量)来替代 var。

1.2.2 const命令

  const 是 ECMAScript 6(ES6)中引入的另一个用于声明变量的关键字,与 let 类似,const 声明的变量也具有块作用域,并且不会在全局作用域或函数作用域中创建可变的绑定。然而,与 let 不同的是,const 声明的变量必须被初始化,并且一旦赋值后,其值就不能被重新改变(尽管如果变量是一个对象或数组,其内容仍然可以被修改)。

以下是一些关于 const 命令的示例代码:

const PI = 3.14159;
console.log(PI); // 输出: 3.14159
 
// PI = 3.14; // 报错: Assignment to constant variable.

块作用域
与 let 一样,const 声明的变量也具有块作用域。

if (true) {
  const message = "Hello, block scope!";
  console.log(message); // 输出: Hello, block scope!
}
 
// console.log(message); // 报错: message is not defined

必须初始化
const 声明的变量在声明时必须被初始化。

// const name; // 报错: Missing initializer in const declaration
const age = 25; // 正确

不可重新赋值
一旦 const 变量被赋值,其值就不能被重新改变。

const country = "USA";
// country = "Canada"; // 报错: Assignment to constant variable.

对象和数组的内容可以修改
尽管 const 变量的值不能重新赋值,但如果该变量是一个对象或数组,其内容仍然可以被修改。

const person = { name: "John", age: 30 };
person.age = 31; // 正确,修改了对象的属性
console.log(person.age); // 输出: 31
 
const colors = ["red", "green", "blue"];
colors.push("yellow"); // 正确,修改了数组的内容
console.log(colors); // 输出: ["red", "green", "blue", "yellow"]
 
// person = { name: "Jane", age: 25 }; // 报错: Assignment to constant variable.
// colors = ["black", "white"]; // 报错: Assignment to constant variable.

函数声明与常量
在 ES6 中,可以在块作用域内使用 const 来声明函数,但这在严格模式下可能会有一些限制,特别是如果你尝试在一个已经声明了同名函数的外部作用域中这样做。然而,通常的做法是在顶层作用域或函数作用域内使用 const 来声明函数表达式(也称为箭头函数或匿名函数)。

const add = (a, b) => a + b;
console.log(add(2, 3)); // 输出: 5

总之,const 提供了一种声明不可重新赋值的变量的方法,这对于定义那些一旦设置就不应该改变的常量值非常有用。然而,需要注意的是,尽管 const 变量的引用不能改变,但如果该变量引用的是一个对象或数组,其内容仍然可以被修改。

1.3 变量的解构赋值

  解构赋值是对赋值运算符的扩展。ES6允许按照一定模式从数组和对象中提取值,再对变量赋值,这被称为解构(Destructuring)。在代码编写上,解构赋值的写法更加简洁易懂,语言更加清晰明了。同时,变量解构赋值还有如下用途。

  • 交换变量的值。
  • 从函数返回多个值。
  • 定义函数的参数。
  • 提取JSON数据。
  • 设置函数参数的默认值。

1.3.1 数组解构赋值

  数组解构赋值是ES6(ECMAScript 6)中引入的一种语法,它允许你从数组中提取值,并将这些值赋给单独的变量。这种语法简洁明了,特别适用于处理函数返回多个值或从数组中提取特定元素的情况。

以下是一些数组解构赋值的示例代码:

const arr = [1, 2, 3];
 
// 从数组中提取值并赋给变量
const [a, b, c] = arr;
 
console.log(a); // 输出: 1
console.log(b); // 输出: 2
console.log(c); // 输出: 3

跳过某些值
如果你不想提取数组中的某个值,可以在解构时使用逗号来跳过它。

const arr = [1, 2, 3, 4];
 
// 跳过第二个值
const [x, , y, z] = arr;
 
console.log(x); // 输出: 1
console.log(y); // 输出: 3
console.log(z); // 输出: 4

不完全解构
如果解构的变量少于数组中的元素数量,剩余的元素将被忽略。

const arr = [1, 2, 3, 4];
 
// 只提取前两个值
const [first, second] = arr;
 
console.log(first); // 输出: 1
console.log(second); // 输出: 2
// arr中的剩余元素(3和4)将被忽略

使用默认值
如果数组中的某个位置没有值,你可以在解构时为变量提供默认值。

const arr = [1];
 
// 为第二个变量提供默认值
const [a, b = 10] = arr;
 
console.log(a); // 输出: 1
console.log(b); // 输出: 10(因为数组中第二个位置没有值,所以使用默认值)

嵌套数组解构
你还可以解构嵌套数组。

const nestedArr = [1, [2, 3], 4];
 
// 解构嵌套数组
const [a, [b, c], d] = nestedArr;
 
console.log(a); // 输出: 1
console.log(b); // 输出: 2
console.log(c); // 输出: 3
console.log(d); // 输出: 4

与函数结合使用
解构赋值经常与函数结合使用,以返回多个值。

function getPoint() {
  return [1, 2];
}
 
// 从函数返回的数组中提取值
const [xPoint, yPoint] = getPoint();
 
console.log(xPoint); // 输出: 1
console.log(yPoint); // 输出: 2

注意事项

  • 解构赋值时,如果数组的长度小于变量的数量,且没有提供默认值,那么未赋值的变量将是undefined。
  • 如果尝试对一个非数组(如null或undefined)进行解构赋值,将会抛出一个错误。因此,在进行解构之前,最好先检查变量是否为数组。

1.3.2 对象解构赋值

  对象解构赋值是ECMAScript 6(ES6)中引入的一种语法特性,它允许你从对象中提取属性,并将这些属性的值赋给单独的变量。这种语法非常有用,特别是在处理函数返回多个值或从复杂对象中提取所需属性时。

以下是一些对象解构赋值的示例代码:

const person = {
  name: 'Alice',
  age: 25,
  job: 'Engineer'
};
 
// 从对象中提取属性并赋给变量
const { name, age, job } = person;
 
console.log(name); // 输出: Alice
console.log(age);  // 输出: 25
console.log(job);  // 输出: Engineer

重命名属性
在解构时,你可以为提取的属性指定一个新的变量名。

const person = {
  firstName: 'Alice',
  lastName: 'Smith',
  age: 25
};
 
// 重命名属性并提取
const { firstName: name, age: yearsOld } = person;
 
console.log(name);      // 输出: Alice
console.log(yearsOld);  // 输出: 25

默认值
如果对象中的某个属性不存在,你可以在解构时为变量提供一个默认值。

const person = {
  name: 'Alice',
  age: 25
};
 
// 为不存在的属性提供默认值
const { name, job = 'Unknown' } = person;
 
console.log(name); // 输出: Alice
console.log(job);  // 输出: Unknown(因为person对象中没有job属性)

嵌套对象解构
你还可以解构嵌套的对象。

const addressBook = {
  contacts: {
    Alice: {
      phone: '123-456-7890',
      email: 'alice@example.com'
    },
    Bob: {
      phone: '987-654-3210',
      email: 'bob@example.com'
    }
  }
};
 
// 解构嵌套对象
const { contacts: { Alice: aliceContact } } = addressBook;
 
console.log(aliceContact.phone); // 输出: 123-456-7890
console.log(aliceContact.email); // 输出: alice@example.com

提取函数参数中的对象属性
解构赋值经常与函数结合使用,以便从对象参数中提取属性。

function greet({ name, age }) {
  console.log(`Hello, my name is ${name} and I am ${age} years old.`);
}
 
const person = {
  name: 'Alice',
  age: 30
};
 
greet(person); // 输出: Hello, my name is Alice and I am 30 years old.

注意事项

  • 解构赋值时,如果对象中没有对应的属性,且没有提供默认值,那么变量将是undefined。
  • 如果尝试对一个非对象(如null或undefined)进行解构赋值,将会抛出一个错误。因此,在进行解构之前,最好先检查变量是否为对象。
  • 在函数参数中使用解构赋值时,如果传入的对象缺少某些属性,这些属性对应的参数将是undefined,除非在解构时提供了默认值。

1.3.3 字符串解构赋值

  在JavaScript中,解构赋值主要用于数组和对象,而字符串本身并不直接支持解构赋值,因为字符串不是由键值对或有序元素组成的集合,它们是由字符组成的序列。然而,你可以通过一些技巧将字符串转换为数组或对象,然后对其进行解构。

以下是一些将字符串转换为数组或对象,并进行解构赋值的示例代码:

将字符串转换为数组并进行解构
你可以使用split()方法将字符串转换为数组,然后进行解构赋值。

const str = "hello world";
 
// 将字符串转换为数组
const [firstWord, secondWord] = str.split(' ');
 
console.log(firstWord); // 输出: hello
console.log(secondWord); // 输出: world

使用对象模拟字符串解构(不推荐)
虽然这不是直接对字符串进行解构,但你可以创建一个对象来模拟字符串的解构过程。然而,这种方法并不直观,也不推荐在实际代码中使用。

const str = "hello";
 
// 创建一个对象来模拟字符串
const strObj = {
  0: 'h',
  1: 'e',
  2: 'l',
  3: 'l',
  4: 'o',
  length: 5
};
 
// 使用对象解构(但这不是真正的字符串解构)
// 注意:这里的解构是基于对象的属性名,而不是字符串的索引
// 而且,这种方法需要事先知道字符串的长度,并且非常不实用
const { 0: firstChar, 1: secondChar } = strObj;
 
console.log(firstChar); // 输出: h
console.log(secondChar); // 输出: e

然而,上面的代码并不是真正的字符串解构,而是基于对象的属性名进行解构。它并不实用,因为你需要事先知道字符串的确切长度,并且为每个字符创建一个属性,这在实际应用中是不可行的。

更实用的方法:使用数组解构和字符串方法
更实用的方法是继续使用split()方法将字符串转换为数组,然后进行解构。如果你需要解构字符串中的特定部分,可以结合使用其他字符串方法,如substring()、slice()或正则表达式。

const str = "hello123world";
 
// 使用正则表达式分割字符串,并解构赋值
const [, letters, numbers, , moreLetters] = str.match(/([a-z]+)(\d+)([a-z]*)/i);
 
console.log(letters);    // 输出: hello
console.log(numbers);    // 输出: 123
console.log(moreLetters); // 输出: world(注意:这里的moreLetters变量名可能不够准确,因为它只是匹配剩余的字母)

在这个例子中,我们使用了正则表达式来匹配和分割字符串。然而,这种方法依赖于正则表达式的准确性和字符串的格式。如果字符串的格式发生变化,正则表达式可能需要相应地调整。

总之,虽然JavaScript本身不支持对字符串进行直接解构赋值,但你可以通过将字符串转换为数组或对象(尽管后者不推荐),或者使用正则表达式和其他字符串方法来实现类似的功能。在实际应用中,最常用和推荐的方法是使用split()方法将字符串转换为数组,然后进行解构赋值。

1.3.4 函数参数解构赋值

  函数参数解构赋值是JavaScript ES6中引入的一种语法糖,它允许你在函数定义时直接对传入的对象或数组参数进行解构,从而更方便地访问其属性或元素。以下是一些关于函数参数解构赋值的示例代码:

对象参数解构赋值
当函数参数是一个对象时,你可以使用解构赋值来直接提取对象的属性。

function greet({ name, age }) {
  console.log(`Hello, my name is ${name} and I am ${age} years old.`);
}
 
const person = {
  name: 'Alice',
  age: 30
};
 
greet(person); // 输出: Hello, my name is Alice and I am 30 years old.

在这个例子中,函数greet接收一个对象参数,并使用解构赋值来提取name和age属性。

数组参数解构赋值
当函数参数是一个数组时,你可以使用解构赋值来直接访问数组的元素。

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

在这个例子中,函数sum接收一个数组参数,并使用解构赋值来提取数组的前三个元素。

默认值
你可以在解构赋值时为参数提供默认值,以防传入的对象或数组缺少某些属性或元素。

function describePerson({ name = 'Unknown', age = 0 }) {
  console.log(`Name: ${name}, Age: ${age}`);
}
 
describePerson({}); // 输出: Name: Unknown, Age: 0
describePerson({ name: 'Bob' }); // 输出: Name: Bob, Age: 0
describePerson({ age: 40 }); // 输出: Name: Unknown, Age: 40
describePerson({ name: 'Charlie', age: 25 }); // 输出: Name: Charlie, Age: 25

在这个例子中,如果传入的对象缺少name或age属性,函数将使用默认值Unknown和0。

嵌套解构
你还可以对嵌套的对象或数组进行解构赋值。

function printCoordinates({ location: { latitude, longitude } }) {
  console.log(`Latitude: ${latitude}, Longitude: ${longitude}`);
}
 
const place = {
  location: {
    latitude: 37.7749,
    longitude: -122.4194
  }
};
 
printCoordinates(place); // 输出: Latitude: 37.7749, Longitude: -122.4194

在这个例子中,函数printCoordinates接收一个对象参数,该参数有一个嵌套的对象location,然后进一步解构location对象来提取latitude和longitude属性。

函数默认参数与解构赋值结合
你还可以将函数默认参数与解构赋值结合使用。

function printDetails({ name, details: { age = 0, city = 'Unknown' } = {}