[关闭]
@gyyin 2020-03-07T18:27:15.000000Z 字数 6957 阅读 321

ES6 中常用的基本语法

慕课专栏


前言

2015年6月17日发布的 ECMAScript 2015(ES6)是一个具有里程碑意义的语言标准。在该标准中,不仅引入了诸如类、模块、Promise、Map 等众多新的语言特性,还制定了更加规范化和快速的标准制定发布流程。
虽然浏览器还未全部实现对 ES6 标准的支持,但通过 Babel 等转译器,人们已经可以使用全部 ES6 特性。
一些情况下,我们也常常泛指 ES6 之后的新增特性,这些特性可能原属于 ES7、ES8 等等。

let/const

在 ES6 之前,声明变量常用的是 varvar 有很多奇怪的表现。比如用 var 声明的变量会挂载到 window 对象上。

  1. var a = 10;
  2. console.log(window.a); // 10

var 还存在变量提升的问题。

  1. console.log(a); // undefined
  2. var a = 10;

除此之外,在 ES6 之前一直有一个问题困扰着开发者,那就是块作用域的问题。

  1. for(var i = 0; i < 10; i++) {
  2. setTimeout(function() {
  3. console.log(i);
  4. }, 1000)
  5. }

以上面这段代码为例,原本希望在 1s 后打印出 0-9,但最后却打印出了十个10,这就是因为在 for 循环中没有块作用域导致的。

let

因此,ES6 中引入了新的 let 关键字,提供了除 var 之外的变量声明方式。
var 不同的地方在于,使用 let 声明的变量无法重复声明,不会出现变量提升,也不会被挂载到 window 上面。

  1. console.log(a); // ReferenceError: a is not defined
  2. let a = 10;
  3. console.log(window.a); // undefined
  4. let a = 20; // Identifier 'a' has already been declared

let 的更大作用体现在块作用域上。在 ES5 中只有全局作用域和函数作用域,没有块级作用域也常常导致一些诡异的问题。比如上面说的 for 循环,最后 i 会暴露到全局作用域中。

  1. var a = 10;
  2. if (true) {
  3. var a = 20;
  4. }
  5. console.log(a); // 20

从上面这段程序可以看出,由于没有块级作用域,导致了 if 里面重新声明的 a 修改了全局作用域中的 a 的值。
而 let 为 ES6 新增了块级作用域,块级作用域一般是指 {}。上面这段代码将 var 换成 let 后就会得到截然不同的效果。

  1. var a = 10;
  2. if (true) {
  3. let a = 20;
  4. }
  5. console.log(a); // 10

有了块级作用域后,我们可以随便嵌套。

  1. {
  2. let i = 10;
  3. }
  4. console.log(i); // ReferenceError: i is not defined

需要注意的是,虽然 {} 一般指块级作用域,但是对象字面量中的 {} 却不属于块级作用域。

  1. var name = "tom";
  2. let obj = {
  3. name: "jerry",
  4. name2: this.name
  5. }
  6. console.log(obj.name2); // "tom"

const

const 和 let 基本上一致,唯一的不同点在于用 const 声明的是常量,无法重新赋值。

  1. const a = 10;
  2. a = 20; // TypeError: Assignment to constant variable.

但是用 const 声明的复杂类型,还是可以修改其中的某一项。

  1. const arr = [1, 2, 3];
  2. arr[0] = 2;
  3. const obj = {};
  4. obj.name = "tom";

箭头函数

ES6 允许我们用箭头来定义一个函数。我们以 test 函数的三种声明方式为例:

  1. // 函数声明
  2. function test() {}
  3. // 函数表达式
  4. const test = function() {}
  5. // 箭头函数
  6. const test = () => {}

可以看到箭头函数省略了 function 的关键字,在其后面用 => 来代替。
箭头函数还允许我们省略花括号和 return 关键字,这在一定程度上简化了函数声明。

  1. function add(a, b) {
  2. return a + b;
  3. }
  4. const add = (a, b) => a + b;

在高阶函数的场景下,箭头函数无疑是更加简洁的。

  1. const add = function(a) {
  2. return function(b) {
  3. return a + b;
  4. }
  5. }
  6. // 箭头函数
  7. const add = a => b => a + b;

箭头函数中的 this 默认指向了上上一级作用域,而不是调用时的对象。

  1. var name = "jerry"
  2. const obj = {
  3. name: 'tom',
  4. say: () => {
  5. console.log(this.name);
  6. }
  7. }
  8. obj.say();

在普通函数中,obj.say 里面的 this 指向了 obj 对象,所以最后打印出来的一定是 "tom"。而在箭头函数中,this 指向了上上一级的作用域,在这里是全局对象,所以打印出了 jerry

箭头函数和普通函数有几点不同之处:
1. 箭头函数不能通过 bind、call、apply 等函数修改 this 指向。
2. 箭头函数不能当做构造函数,也就是说不能和 new 一起使用。
3. 箭头函数中不能使用 arguments,必须用 rest 参数来代替。
4. 箭头函数不能当做 generator 函数来使用。

扩展运算符

扩展运算符是三个点,可以将对象按照键值对的形式展开,也可以将数组转换成按照逗号分开的序列。

数组中的扩展运算符

扩展运算符可以将数组展开成一个个项,可以将其当做参数传给函数。

  1. const arr = [1, 2, 3];
  2. console.log(...arr); // 1 2 3

也可以用一个新的数组来保存这些子项。

  1. const arr = [1, 2, 3];
  2. const arr2 = [1, ...arr]; // [1, 1, 2, 3]

通过扩展运算符展开的特性,可以将类数组转换为真正的数组。

  1. function test() {
  2. const args = [...arguments];
  3. }
  4. const divs = [...document.querySelectorAll("div")];

扩展运算符可以对数组进行一系列操作,比如数组合并、复制。

  1. // 数组的合并
  2. const arr = [1, 2, 3],
  3. arr2 = [4, 5];
  4. const arr3 = [...arr, ...arr2];
  5. // 数组的复制
  6. const arr3 = [...arr];

除了上面这些,还有一种逆向的用法,常常结合解构赋值使用,那就是 rest 剩余参数。

  1. const arr = [1, 2, 3];
  2. const [1, ...rest] = arr;
  3. console.log(rest); // [2, 3]

对象的扩展运算符

扩展运算符同样可以用在对象中,常用于对象的浅拷贝。

  1. const obj = {
  2. name: "tom"
  3. }
  4. const person = {age: 20, ...obj}; // {age: 20, name: "tom"}
  5. const obj2 = { ...obj }; // 相当于 Object.assign({}, obj);

注意:这里对象上的属性指的是对象自身且可枚举的。

解构赋值

解构赋值是对赋值运算符的一种扩展。这种方式在代码书写上更加简洁易读,方便了复杂对象类型中值的提取。
从语义上来理解,解构就是按照原有结构,将复杂类型展开,并重新赋值给新的变量。

基本用法

我们知道,想要在 JavaScript 中声明并赋值多个变量,只有一个个写,这样比较繁琐。

  1. let a = 10, b = 20, c = 30;

毕竟 JavaScript 不支持其他语言中直接对多个变量赋值的用法。

  1. let a, b, c = 10, 20, 30; // SyntaxError: Unexpected number

有了解构赋值之后,声明赋值多个变量就变得更加简洁。

  1. let [a, b, c] = [10, 20, 30];

解构的时候,还可以对变量设置默认值。

  1. let [a = 0, b, c] = [, 20, 30];

对象的解构

解构赋值在对象中有广泛的应用。

  1. const obj = {
  2. name: "tom",
  3. age: 20
  4. }
  5. // old
  6. const name = obj.name,
  7. age = obj.age;
  8. // 解构赋值
  9. const { name, age } = obj;

对象解构还能用于比较深层的嵌套对象,对属性值也可以做进一步解构。

  1. const country = {
  2. province: {
  3. city: {
  4. name: "shenzhen"
  5. }
  6. }
  7. }
  8. const {
  9. province: {
  10. city: {
  11. name
  12. }
  13. }
  14. } = country
  15. console.log(name) // "shenzhen"

注意,上面的 province 和 city 只是模式,并非变量,如果想将其作为变量赋值,需要单独写。

  1. const {
  2. province,
  3. province: {
  4. city,
  5. city: {
  6. name
  7. }
  8. }
  9. } = country
  10. console.log(province); // {city: {name: "shenzhen"}}

除此之外,我们还可以对函数的参数进行解构。

  1. const test = ({name, age}) => {
  2. console.log(`name=${name}|age=${age}`);
  3. }
  4. test({name: "tom", age: 20})

数组的解构

使用数组的解构赋值,可以简单地实现变量的值互换。

  1. let a = 10, b = 20;
  2. [b, a] = [a, b];
  3. console.log(a); // 20
  4. console.log(b); // 10

同样地,也可以对深层的数组进行解构。

  1. const [a, [b, c], d] = [1, [2, 3], 4];

模板字符串

如果你有经历过 jQuery 的时代,那么一定会对下面的代码深恶痛绝吧。

  1. var html = "<h1 class='title'>hello, " + name + "</h1>"

不仅需要考虑各种引号的问题,还要考虑字符串和变量的拼接,让人非常头疼。
ES6 中对字符串进行了扩展,增加了模板字符串的特性,我们使用反引号 ` 来表示模板字符串,可以在其中嵌套变量。

  1. const html = `<h1 class="title">hello, ${name}</h1>`

普通的字符串如果想要换行,只有在里面加入换行符,如果直接换行就是报错。

  1. // SyntaxError: Invalid or unexpected token
  2. "hello
  3. world
  4. "
  5. // good
  6. "hello\nworld"

而在模板字符串里面,可以直接输入回车。

  1. `hello
  2. world
  3. `

模板字符串里面潜入的变量,还支持运算。

  1. const a = 1, b = 2;
  2. const str = `sum is: ${a + b}`;

Symbol

Symbol 是 ES6 中新增加的一个基本类型,是 undefinednull、数值(Number)、布尔(Boolean)、对象(Object)、字符串(String)之外的第七种类型。
一般来说,Symbol 值都是通过 Symbol 函数来生成。Symbol 值是独一无二的,即使传入相同的参数,返回的结果依然不相等。

  1. Symbol("hello") === Symbol("hello"); // false

由于 Symbol 具有唯一值的特性,可以用在对象的属性名上。由于对象的属性名都是字符串,常常会造成冲突,这样会导致后面的覆盖掉前面的。

  1. const obj = {
  2. name: "tom",
  3. name: "jerry"
  4. }
  5. obj.name; // "jerry"
  6. const s1 = Symbol("jerry");
  7. const obj = {
  8. name: "tom",
  9. [s1]: "jerry"
  10. }
  11. obj[s1]; // "jerry"

在前面关于 JavaScript 类型转换这节中,我们介绍过使用 Symbol 可以重写 toPrimitive 方法的用法。
Symbol 中提供了 Symbol.toPrimitive 方法,该方法在类型转换的时候优先级最高。

  1. const obj = {
  2. toString() {
  3. return '1111'
  4. },
  5. valueOf() {
  6. return 222
  7. },
  8. [Symbol.toPrimitive]() {
  9. return 666
  10. }
  11. }
  12. const num = 1 + obj; // 667
  13. const str = '1' + obj; // '1666'

Set/Map

Set 和 Map 是 ES6 中提供的新的数据结构。

Set

Set 是一个构造函数,需要用 new 来创建一个 Set 对象。

  1. const set = new Set();

Set 也可以接收一个数组作为初始值。

  1. const set = new Set([1, 2, 3]);

Set 类似于数组,但里面的值都是唯一的,所以可以用来做数组去重。

  1. const set = new Set([1, 2, 3, 2]);
  2. [...set]; // [1, 2, 3]

虽然类似数组,但 Set 的操作方式和数组还是不一样,一共有下面这些方法。
1. add:添加一项,返回自身。
2. delete:删除一项,返回删除是否成功。
3. has:判断当前值是否在 Set 里面。
4. clear:清除所有值。
5. size:返回 Set 的成员数。
6. keys:返回 Set 的键名遍历器。
7. values:返回 Set 的值遍历器。
8. entries:返回 Set 键值对的遍历器。
9. forEach:类似数组的 forEach 用法。

  1. const s = Set();
  2. s.add(1).add(2).add(3);
  3. s.has(2); // true
  4. s.delete(3);
  5. s.size; // 2
  6. // 分别打印出1, 2
  7. for(let key of s.keys()) {
  8. console.log(key);
  9. }
  10. // 分别打印出1, 2
  11. for(let value of s.values()) {
  12. console.log(value);
  13. }
  14. // 分别打印出 key=1|value=1, key=2|value=2
  15. for(let [key, value] of s.entries()) {
  16. console.log(`key=${key}|value=${value}`);
  17. }
  18. // 分别打印出 key=1|value=1, key=2|value=2
  19. s.forEach((k, v) => {
  20. console.log(`key=${key}|value=${value}`);
  21. })

注意:Set 中的 key 和 value 都是一样的。

Map

前面我们讲过,对象的键都是字符串,所以容易造成冲突。如果键可以支持其他的数据类型呢?
ES6 中提供了 Map 这个数据结构,允许你使用各种数据类型,甚至包括对象。

  1. const m = new Map();
  2. const obj1 = {},
  3. obj2 = {};
  4. m.set(obj1, "obj1")
  5. m.set(obj2, "obj2")
  6. m.get(obj1) // "obj1"
  7. m.get(obj2) // "obj2"

Map 的遍历方法和 Set 保持一致,都有 keysvaluesentriesforEach
Map 的操作方法有下面这些:
1. set(key, value):和 Object 定义新属性用法一致,返回当前 Map 对象。
2. get(key): 根据键名获取值。
3. has(key): 判断 Map 对象是否有当前这个键名。
4. delete(key): 删除这个键值对,返回布尔值。
5. clear(): 清空所有成员。
6. size: 返回 Map 成员总数。

  1. const m = new Map();
  2. m.set(1, "a")
  3. .set(2, "b")
  4. .set(3, "c")
  5. m.has(3); // true
  6. m.size; // 2
  7. m.get(2); // b
  8. // 分别打印出1, 2, 3
  9. for(let key of m.keys()) {
  10. console.log(key);
  11. }
  12. // 分别打印出a, b, c
  13. for(let value of m.values()) {
  14. console.log(value);
  15. }
  16. // 分别打印出 key=1|value=a, key=2|value=b, key=3|value=c
  17. for(let [key, value] of m.entries()) {
  18. console.log(`key=${key}|value=${value}`);
  19. }
  20. // 分别打印出 key=1|value=a, key=2|value=b, key=3|value=c
  21. m.forEach((k, v) => {
  22. console.log(`key=${key}|value=${value}`);
  23. })

通过扩展运算符,Map 可以转换为数组。

  1. const m = new Map()
  2. .set(1, "a")
  3. .set(2, "b");
  4. [...m]; // [[1, "a"], [2, "b"]]

与此相反的是,数组也可以转换成 Map。

  1. const arr = [[1, "a"], [2, "b"]];
  2. const m = new Map(arr);

Map 也可以转换为对象,但如果 Map 的键不是字符串,就会被隐式转换为字符串类型,可能造成一些键值丢失。

  1. const m = new Map()
  2. .set(1, "a")
  3. .set(2, "b");
  4. const obj = {};
  5. m.forEach((k, v) => {
  6. obj[k] = v;
  7. })

对象转为 Map 则比较简单,可以直接使用 Object.entries() 来转。

  1. const obj = {
  2. 1: "a",
  3. 2: "b"
  4. }
  5. const m = new Map(Object.entries(obj));

总结

ES6 提供了许多新的语法,这些新语法在很大程度上都方便了我们开发。
时至今日,这些新特性在前端开发中已经被广泛使用了。作为前端开发,我们也应该时刻保持自身技术栈的更新,不断地去学习新语法,这样才不会被时代抛弃。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注