写给C++程序员的ES6语法基础
ECMAScript 标准的发展经历了一个漫长的过程,从 1997 年发布的ECMAScript 1.0,到 2011 年的ECMAScript 5.1,再到 2015 年的ECMAScript 2015,以及后面每年都会发布的一个新的 ECMAScript 版本,版本众多,而且每个版本都有新特性的加入。
我们通常用ECMAScript 6来统称ECMAScript 2015及其之后的的版本。
但ECMAScript 2015可谓是 ECMAScript 标准的分水岭,其从制定到发布前后经历了 15 年。目前绝大多数浏览器都已支持ECMAScript 2015特性,并部分的支持了后面版本的新特性,所以对于需要快速入门的初学者,只需要直接学习ECMAScript 2015标准即可,编写完全遵守ECMAScript 2015标准的代码不仅更加严谨,而且可以节省不少学习时间。
即便对于那些不支持ECMAScript 6的浏览器(如 IE11),我们也可以使用Babel之类的工具将其转换为ECMAScript 5标准的代码。
查看浏览器对 ES 特性的支持情况,可以访问:https://kangax.github.io/compat-table/es6/
下文讲述的都是支持 ECMAScript 6 标准的 JavaScript 语法
零
- JavaScript 是大小写敏感的
- 在每行只写一条语句时,结尾可以不加分号
;,但建议每条语句都以分号结尾
七种基本数据类型
基本数据类型有以下 7 种:
- undefined
- null
- Boolean
- String
- Number
- Object
- Symbol
JavaScript 虽然是弱类型的语言,但不代表其没有数据类型,其弱类型指的是在声明变量的时候不需要(也不能)指明变量类型,编译器会根据“值”的类型自动将变量转换成合适的类型。
所以无论是值还是变量终究还是有类型的,对于开发者而言,不需要关注什么类型的值应该赋值给什么类型的变量。
我们可以使用typeof查看每个变量或值的类型:
1 | console.log(typeof 123); // number |
值得注意的是undefined的类型是undefined,但null的类型是object,下面的章节会介绍undefined和null的区别。
变量声明方式
声明变量的方式有 3 种:
- let 用来声明变量
- var 也是用来声明变量的
- const 用来声明常量,必须在声明的时候赋初始值,且以后不能再修改
var是ES6之前的,建议不要在代码中使用 var,这样可以避免很多不必要的问题,比如变量提升、暂时性死区等,本文也只讲述let和const,忘记var吧,一起走向新世界。
变量具有作用域的概念
用 let 和 const 声明的变量或常量的作用域可以精确到“块级别”
1 | { |
1 | for (let i = 0; i < 10; i++) { |
1 | let a = 123; |
1 | // 无论是let还是const都是一样的,在同一个作用域内不能重复声明 |
块级作用域内的 let
只要“块级作用域内”存在 let 命令,它所声明的变量就“绑定”到这个区域,不再受外部的影响
1 | let a = 123; |
这个和其他语言不太一样,如果是 C/C++这样的语言是不会报错的,只会在let a = 789;给出“局部变量覆盖了全局变量”的警告。
但在 ES6 中却会报错,我们可以这样理解为“强龙压不过地头蛇”:
我用let a = 789;在蓝色区域内声明了变量a,那么在这个蓝色区域的一亩三分地内的都是我说了算,a = 456;敢在我还没声明前都赋值,那我还不给你报个错。
for 循环的变量作用域
for 循环设置循环变量的部分是一个父作用域,而循环体内部是一个单独的子作用域
1 | for (let i = 0; i < 10; i++) { |
虽然编译器没有报错,但尽量不要写这样代码,不利于后期维护。
Object 与 Class
Object(对象)是 ES5 就已经有的概念,Class(类)是 ES6 引入的概念。
关于 JavaScript 中类和对象的叫法
学过任何一个面向对象编程语言的同学都知道,对象是类的实例,比如在 Java 中:
1 | A a = new A(); |
a为对象,A为类,也就是“对象 a 为类型 A 的实例”。
而大家可能也发现了很多 JavaScript 教程称Object、Number、String为对象,如果根据这个称呼来,下面的代码岂不是“对象 a 为对象 Number 的实例”,很显然在这种叫法很别扭,也不科学:
1 | let a = new Number(123); |
为什么会出现这种蹩脚的叫法了?因为在 ES6 之前,JavaScript 中没有完全引入Class类的概念,其最多只能算是一个伪的面向对象语言,估计也是为了降低学习理解的难度,所以统称为对象吧。
既然在 ES6 中明确引入了 Class 概念,我们在学习的时候就要明确区分“类”和“对象”的概念了。
万物皆对象
在 JavaScript 中万物皆对象。学过其他面向对象编程语言的同学知道,对象是类的实例,每个类都可以有构造函数、析构函数、属性和方法。
在 JavaScript 中,每个对象都有构造函数、属性、方法,但没有析构函数,我们可以通过下面的代码来验证“万物皆对象”:
1 | console.log("John".constructor); // [Function: String] |
隐式声明对象
JavaScript 访问属性和方法的方式和其他面向对象语言类似:
1 | let msg1 = "Hello World!"; // msg1为对象,String类的实例 |
大家可以看到上面的代码中都没有使用new来声明对象,但这些对象确确实实存在了,不然我们也无法访问其“属性”和“方法”,我们可以姑且认为这是 JavaScript 的语法糖吧,或者叫“隐式声明对象”吧。
显式声明对象
前面介绍的 8 种基本数据类型中除了null和undefined之外,都可以使用 new 来显示的声明对象:
1 | let a1 = new Number(123); |
使用 Class 定义类
在 ES6 中可以使用 Class 关键字定义类,且每个类都有默认的 name 属性。
1 | class Rectangle { |
Class 的成员都是公共的,外部都可以访问,目前没有从语法层面规定如何定义一个私有的成员
类表达式
一个类表达式是定义一个类的另一种方式。类表达式可以是具名的或匿名的。
一个具名类表达式的名称是类内的一个局部属性,它可以通过类的 name 属性来获取。
1 | // 匿名类 |
函数
函数参数是默认声明的变量
1 | function foo1(x) { |
函数参数可以有默认值
1 | function foo2(x, y = 1) { |
与 C++不同,JavaSript 没有要求拥有默认值的参数必须放在参数列表的尾部,所以可以这样写:
1 | function foo2(x = 1, y) { |
但仔细想一想,这样写并没有什么实际意义。
另外值得注意的是,函数参数的默认值是每次函数调用时都会动态计算的:
1 | let x = 99; |
箭头函数
1 | function foo(x, y) { |
如果函数只有一个参数:
1 | function foo(x) { |
如果函数没有返回值大括号可以省略:
1 | function foo() { |
4.4 函数的 name,length 属性
name可以返回函数名;length可以返回没有指定默认值的参数个数;
数据结构
Object
我们可以向 Object 对象添加任何属性,所以 Object 也是一个容器或集合:
1 | let obj = new Object("hello"); |
数组
JavaScript 中使用 Array 类创建数组对象:
1 | // 使用Array类创建数组 |
Array 类完整的属性
| 属性 | 描述 |
|---|---|
| length | 设置或获取数组元素的个数 |
| prototype | 允许你向数组对象添加属性或方法 |
Array 类完整的方法
| 方法 | 描述 |
|---|---|
| concat() | 连接两个或更多的数组,并返回结果 |
| copyWithin() | 从数组的指定位置拷贝元素到数组的另一个指定位置中 |
| entries() | 返回数组的可迭代对象 |
| every() | 检测数值元素的每个元素是否都符合条件 |
| fill() | 使用一个固定值来填充数组 |
| filter() | 检测数值元素,并返回符合条件所有元素的数组 |
| find() | 返回符合传入测试(函数)条件的数组元素 |
| findIndex() | 返回符合传入测试(函数)条件的数组元素索引 |
| forEach() | 数组每个元素都执行一次回调函数 |
| from() | 通过给定的对象中创建一个数组 |
| includes() | 判断一个数组是否包含一个指定的值 |
| indexOf() | 搜索数组中的元素,并返回它所在的位置 |
| isArray() | 判断对象是否为数组 |
| join() | 把数组的所有元素放入一个字符串 |
| keys() | 返回数组的可迭代对象,包含原始数组的键(key) |
| lastIndexOf() | 搜索数组中的元素,并返回它最后出现的位置 |
| map() | 通过指定函数处理数组的每个元素,并返回处理后的数组 |
| pop() | 删除数组的最后一个元素并返回删除的元素 |
| push() | 向数组的末尾添加一个或更多元素,并返回新的长度 |
| reduce() | 将数组元素计算为一个值(从左到右) |
| reduceRight() | 将数组元素计算为一个值(从右到左) |
| reverse() | 反转数组的元素顺序 |
| shift() | 删除并返回数组的第一个元素 |
| slice() | 选取数组的一部分,并返回一个新数组 |
| some() | 检测数组元素中是否有元素符合指定条件 |
| sort() | 对数组的元素进行排序 |
| splice() | 从数组中添加或删除元素 |
| toString() | 把数组转换为字符串,并返回结果 |
| unshift() | 向数组的开头添加一个或更多元素,并返回新的长度 |
| valueOf() | 返回数组对象的原始值 |
数组的复制
1 | let a1 = [1, 2, 3]; |
从上面的例子中,我们发现改变数组a2[1]的值时,数组a1[1]的值也随着改变了,这说明 a1 和 a2 指向的是同一块内存区域,这个在 C/C++中就是指针的概念,let a2 = a1; 做的是浅拷贝操作。
如果需要做深拷贝,也就是将数组 a1 的所有元素克隆一份给 a2,可以通过下面的两种方式:
1 | let a1 = [1, 2, 3]; |
1 | let a1 = [1, 2, 3]; |
Map
Map 对象用来存储键值对(key-value),并且能够记住键的原始插入顺序。任何对象都可以作为键或值。
在 Map 中 Key 是唯一的。
1 | let myMap = new Map(); |
Map 与 Array 相互转换
Array => Map
1 | let map = new Map([ |
Map => Array
1 | let map = new Map(); |
Map 的复制
Map 在直接赋值的时候会遇到和 Array 同样的“浅拷贝”的问题,如:
1 | let map1 = new Map(); |
可以通过下面的方式完成 Map 的深拷贝:
1 | let map1 = new Map(); |
Set
Map 是键值对的集合,而 Set 则只是键(key)的集合。
Set 中的每个元素都是唯一的。
任何对象都可以作为 Set 的元素。
1 | let keys = [1, 1, 2, 3, "str"]; |
迭代器
前面介绍 Object、Array、Map、Set 这些容器的时候,都避开了一个话题:遍历。本节主要介绍如何遍历 JavaScript 中的容器或集合。
迭代器(Iterator)就是一个接口,为各种不同的数据结构提供统一的遍历访问机制。任何数据结构只要实现 Iterator 接口,就可以完成遍历操作。
在学习如何自定义迭代器之前,我们先学习一下如何遍历 JavaScript 常用的数据集合:
遍历字符串
1 | let str = "hello"; |
遍历数组
1 | let arr = [1, "str", 2]; |
遍历 Map
1 | let map = new Map([ |
解构赋值
解构(Destructuring)赋值分为“数组的解构赋值”和“对象的解构赋值”。
数组的解构赋值
1 | let [a, b, c] = [1, 2, 3]; // 根据位置依次取值 |
对象的解构赋值
1 | let { foo, bar } = { foo: "aaa", bar: "bbb" }; // 和顺序没关系,根据属性名来取值 |
promise、async、await
promise、async、await 这三个关键字都和异步编程有关。
Promise
Promise 翻译成中文就是“承诺”的意思,声明一个 Promise 就是立下了一个承诺,无论怎么样,都会给被承诺人一个结果,而且这个结果是板上钉钉的,不会再变。
Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
声明 Promise 对象时需要传入一个函数对象作为参数,这个函数对象的 2 个参数也是函数对象(resolve, reject),resolve和reject不需要开发者定义,Javascript 引擎会自动生成这 2 个函数。
当 Promise 对象生成后会立即变成pending状态,调用resolve函数会将 Promise 对象标记为fulfilled状态,而调用reject函数则会将当前 Promise 对象标记rejected状态。
resolve和rejected函数,我们只能调用它们中的一个,不能即调用resolve又调用rejected。如果我们先调用了resolve,此时 Promise 状态会标记为fulfilled,然后又调用了rejected函数,此时 Promise 状态并不会再改变,仍然使fulfilled状态,因为承诺的结果是板上钉钉的,不会再变。建议将resolve和reject作为最后一行代码调用,简单起见,可以在这 2 个函数前面加上return, 即return resolve();或return reject();
1 | // Promise的参数为一个函数对象,函数有2个参数resolve, reject |
那么,我想在这个异步操作完成之后,再根据结果(是成功了,还是失败了)来继续做下一件事情,那我们该怎么做了?
Promise 对象提供了then方法,该方法接受 2 个函数对象作为参数:
第一个回调函数是 Promise 对象的状态变为resolved时调用;
第二个回调函数是 Promise 对象的状态变为rejected时调用。
其中,第二个函数是可选的,不一定要提供。每个回调函数都可以接受一个参数,这个参数就是上一步调用 resolve 或 reject 时传入的。
1 | // 上面的耗时操作完成之后,我们可能还需要根据结果来继续做一些事情 |
在这个例子中,我在第一个参数中直接通过 return 返回了”hello”字符串,那这个返回值的意义在哪里了?还有其他人可以使用到这个返回值吗?
是的,还可以继续使用。因为then的返回值是一个 Promise 对象,虽然我只是使用的return "hello";,并没有new Promise,但 JavaScript 引擎会自动包装成一个 Promise 对象,等同于:
1 | promise.then( |
到这儿了,我们知道了then的返回的是一个Promise对象。既然then返回的是Promise对象,那么Promise就可以继续then呀,然后一直then下去….这样我们就可以将一系列异步的操作串联起来了:
1 | let p = new Promise((resolve, reject) => { |
但多个异步操作串联执行,还有一点需要注意,我们看下面的例子:
1 | let promise = new Promise((resolve, reject) => { |
我们期望的输出是:
1 | resolve1: ok1; |
但实际的输出却是:
1 | resolve1: ok1; |
问题出在第二个 setTimeout 模拟的耗时操作,我们以为程序会等第二个 setTimeout 执行完了再执行第二个 then,但事实上 setTimeout 也是一个异步操作,虽然其延时了一秒执行其回调函数,但 setTimeout 这条语句却马上执行完成了,导致第一个 then 没有任何返回,针对这种情况,我们需要将代码改成下面的:
1 | let promise = new Promise((resolve, reject) => { |
Promise 异常捕获
Promise 对象还提供了catch方法,用来捕获异常。在介绍catch前,我们先看看下面的代码:
1 | let promise = new Promise((resolve, reject) => { |
我们在 Promise 中人为抛出了一个异常,但是程序却还是没有中止,而是运行到了 reject 过程中去了。
这是因为 Promise 默认会捕获其操作过程中的异常,如果有异常发生,其状态就会自动变成rejected,还记得前面说过 Promise 状态一旦确定就不会再改变了吧,所以即便后面的resolve("ok");执行了,也不会改变 promise 状态(事实上 throw 语句后的代码并没有机会执行)。
那么,假如我们没有写 reject 回调函数会怎么样了?看看下面的代码:
1 | let promise = new Promise((resolve, reject) => { |
上面的代码中由于没有指定异常处理函数,所以程序抛出了异常信息,中止执行了。
另外,Promise 的异常是会一直向下传递的,直到最后有人处理,如果始终没人处理,程序就会抛出异常信息,然后中止:
1 | let promise = new Promise((resolve, reject) => { |
上面的代码中,第一个 then 没有处理异常,异常向下传递给第二个 then, 第二个 then 处理了该异常,程序继续运行。
现在理解Promise.catch()方法就容易多了。catch()方法其实就是.then(null, rejectiion)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
我们一般总是建议,Promise 对象后面要跟 catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then()方法。
async, await
async 是ES7才有的与异步操作有关的关键字,需要和Promise配合使用,async函数返回一个 Promise对象,可以使用then方法添加回调函数:
1 | async function helloAsync() { |
await关键字只能用在被async标记的函数体内,async函数执行时,如果遇到await就会先暂停执行,等到触发的异步操作完成后,恢复async函数的执行并返回解析值。
1 | function testAwait() { |