ECMAScript学习笔记

学习来源:尚硅谷

笔记参考:爱学习的汪

学习时间:2022年2月16日

1 概述

1.1 ECMA简介

ECMA(European Computer Manufacturers Association)中文名称为欧洲计算机制造商协会,这个组织的目标是评估、开发和认可电信和计算机标准。1994 年后该组织改名为 Ecma 国际。

ECMAScript 是由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言。

1.2 ECMA-262

Ecma 国际制定了许多标准,而 ECMA-262 只是其中的一个。

版数 年份 内容
第 1 版 1997 制定了语言的基本语法
第 2 版 1998 较小改动
第 3 版 1999 引入正则、异常处理、格式化输出等. IE 开始支持
第 4 版 2007 过于激进,未发布
第 5 版 2009 引入严格模式、JSON,扩展对象、数组、原型、字符串、日期方法
第 6 版 2015 模块化、面向对象语法、 Promise、箭头函数、let、 const、数组解构赋值等等因为发布内容很多,堪称里程碑,所以我们目前通常主要学这个
第 7 版 2016 幂运算符、数组扩展、 Async/await 关键字
第 8 版 2017 Async/await、字符串扩展
第 9 版 2018 对象解构赋值、正则扩展
第 10 版 2019 扩展对象、数组方法
ES.next 动态指向下一个版本

注:从 ES6 开始,每年发布一个版本,版本号比年份最后一位大 1。

TC39(Technical Committee 39)是推进 ECMAScript 发展的委员会。其会员都是公司(其中主要是浏览器厂商,有苹果、谷歌、微软、因特尔等)。TC39 定期召开会议,会议由会员公司的代表与特邀专家出席。

1.3 ES6更新内容概括

表达式: 声明、解构赋值

内置对象: 字符串扩展、数值扩展、对象扩展、数组扩展、函数扩展、正则扩展、Symbol、Set、Map、Proxy、Reflect

语句与运算: Class、Module、Iterator

异步编程: Promise、Generator、Async

2 ES6新特性

2.1 let、const关键字和作用域

2.1.1 let关键字

1) 基本使用

let 关键字用来声明变量。

1
2
3
4
5
// 声明变量和赋值
let a;
let b, c, d;
let e = 100;
let f = 521, g = "Hongyi", h = [];

使用 let 声明的变量有如下特点:

  • 不允许重复声明

  • 块级作用域

    • js中存在三种作用域:全局(作为window的属性)、函数(function() {})和块级({},例如for、while、if、else等)
    • var关键字在全局代码中运行
    • let命令只能在代码块中执行
    1
    2
    3
    4
    {
    var a = 123;
    }
    console.log(a); // 123
    1
    2
    3
    4
    {
    let a = 123;
    }
    console.log(a); // undefined
  • 不存在变量提升

1
2
console.log(a); // undefined
var a = 123;
1
2
console.log(a);
let a = 123; // Uncaught ReferenceError: Cannot access 'a' before initialization
  • 不影响作用域链
1
2
3
4
5
6
7
{
let name = "Hongyi";
function func() {
console.log(name);
}
func(); // Hongyi
}

如上,虽然在函数级作用域中没有name变量,但函数func会沿着作用域链向上寻找至块级作用域中的name变量。

应用场景:以后声明变量使用 let 就对了

2) 实践案例

需求:三个div块,点击时改变块的颜色

方式①:在遍历绑定事件,使用var时,不生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link crossorigin="anonymous" href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet">
<style>
.item {
width: 100px;
height: 50px;
border: solid 1px rgb(252, 7, 7);
float: left;
margin-right: 10px;
}
</style>
<title>Document</title>
</head>
<body>
<div class="container">
<h2 class="page-header">点击切换颜色</h2>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<script>
// 获取div元素对象
let items = document.getElementsByClassName("item");
// 遍历并绑定事件
// 使用var关键字
for(var i = 0;i < items.length; i++) {
items[i].onclick = function() {
items[i].style.background = "pink";
}
}
</script>
</body>
</html>
  • 失败原因

    • var不存在块作用域,而是在全局代码中运行(作为window对象的属性),当for循环遍历结束时,i的值变为了3,实际执行的语句为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 以下均在for的块内
    {
    var i = 0;
    // 绑定事件,等待点击执行回调
    items[0].onclick = function() {
    items[i].style.background = "pink"; // 这里的i并不确定,因为尚未点击触发事件
    }
    }
    {
    var i = 1;
    // 绑定事件,等待点击执行回调
    items[1].onclick = function() {
    items[i].style.background = "pink"; // 这里的i并不确定,因为尚未点击触发事件
    }
    }
    {
    var i = 2;
    // ...
    }
    // 最后跳出循环时
    {
    var i = 3;
    // ...
    }
    • 外层for循环只是给每个item绑定了点击事件,然后点击事件是异步任务,此时for已经执行完了。每次点击块时,回调函数在函数作用域找不到i,向外在全局中找到i,所以点击任何一个按钮,回调函数实际上执行的语句为:
    1
    items[3].style.background = "pink";

    超出了边界,因此不生效

image-20220219151314014

方式②:使用let关键字

1
2
3
4
5
for(let i = 0;i < items.length; i++) {
items[i].onclick = function() {
items[i].style.background = "pink";
}
}
  • 成功原因

    • let关键字只在块级生效,当每次点击块时,回调函数在函数作用域找不到i,向外(在for块内)找到i,实际执行的语句为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 以下均在for的块内
    {
    let i = 0;
    // 绑定事件,等待点击执行回调
    items[0].onclick = function() {
    items[i].style.background = "pink"; // 这里的i并不确定,因为尚未点击触发事件
    }
    }
    {
    let i = 1;
    // 绑定事件,等待点击执行回调
    items[1].onclick = function() {
    items[i].style.background = "pink"; // 这里的i并不确定,因为尚未点击触发事件
    }
    }
    {
    let i = 2;
    // ...
    }
    // 最后跳出循环时
    {
    let i = 3;
    // ...
    }
    • 执行回调时,找到块内的i,并执行,此时,window对象里并没有i的属性

    image-20220219152057300

    • 执行结果:

image-20220219150931624

2.1.2 const关键字

const 关键字用来声明常量。

1
const NAME = "Hongyi";

使用 const 声明的变量有如下特点:

  • 不允许重复声明
  • 值不允许修改
  • 不存在变量提升
  • 块级作用域
  • 声明必须赋初始值
  • 标识符一般为大写
  • 对象属性修改和数组元素变化不会触发 const 错误

    • const实际上保证的, 并不是变量的值不得改动, 而是变量指向的那个内存地址所保存的数据不得改动
    1
    2
    const TEAM = ["UZI", "MING", "LETME"];
    TEAM.push("MEIKO"); // 允许

**应用场景:声明对象类型使用 const,非对象类型声明选择 let **

2.1.3 作用域

1) 为什么需要块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

例如用来计数的循环变量泄露为全局变量

1
2
3
4
5
var s = 'Hongyi';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 6

上面代码中,变量i只用来控制循环,但是循环结束后它并没有消失,泄露成了全局变量。

2) ES6 的块级作用域

let实际上为 JavaScript 新增了块级作用域

1
2
3
4
5
6
7
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}

上面的函数有两个代码块, 都声明了变量n, 运行后输出 5. 这表示外层代码块不受内层代码块的影响. 如果两次都使用var定义变量n, 最后输出的值才是 10.

  • ES6 允许块级作用域的任意嵌套,并且外层不能读取内层的变量
1
2
3
4
5
6
{{{{
{
let insane = 'Hello World';
}
console.log(insane); // 报错 因为外层不能取到内层数据
}}}};

上面代码使用了一个五层的块级作用域, 每一层都是一个单独的作用域. 第四层作用域无法读取第五层作用域的内部变量.

  • 内层作用域可以定义外层作用域的同名变量
1
2
3
4
5
6
{{{{
let insane = 'Hello World';
{
let insane = 'Hello World';
} //可以这样命名,不会报错
}}}};

块级作用域的出现, 实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了

1
2
3
4
5
6
7
8
9
10
11
// IIFE 写法
(function () {
var tmp ;
...
}());

// 块级作用域写法
{
let tmp ;
...
}
  • ES6 的块级作用域必须有大括号, 如果没有大括号 , JavaScript 引擎就认为不存在块级作用域.
1
2
3
4
5
6
7
// 第一种写法, 报错
if (true) let x = 1;

// 第二种写法, 不报错
if (true) {
let x = 1;
}

上面代码中, 第一种写法没有大括号, 所以不存在块级作用域, 而let只能出现在当前作用域的顶层, 所以报错.

2.2 解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

2.2.1 数组的解构赋值

以前,为变量赋值,只能直接指定值:

1
2
3
let a = 1;
let b = 2;
let c = 3;

ES6 允许写成下面这样.:

1
let [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的栗子。

1
2
3
4
5
6
7
8
9
10
11
12
let [foo, [[bar], baz]] = [1, [[2], 3]];//foo : 1 bar : 2 baz : 3

let [ , , third] = ["foo", "bar", "baz"];//third : "baz"

let [x, , y] = [1, 2, 3];//x : 1 y : 3

let [head, ...tail] = [1, 2, 3, 4];//head : 1 tail : [2, 3, 4]

let [x, y, ...z] = ['a'];//x : "a" y : undefined z : []

const F4 = ['a', 'b', 'c', 'd'];
let [xiao, liu, zhao, song] = F4;

如果解构不成功, 变量的值就等于 undefined

1
2
let [foo] = []; // foo: undefined
let [bar, foo] = [1]; // bar: 1, foo: undefined

2.2.2 对象的解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象的解构赋值
const obj = {
name: "赵本山",
age: 60,
xiaopin: function() {
console.log("Hello World");
}
};

// 变量必须和对象的属性名同名
let {name, age, xiaopin} = obj;
console.log(name);
console.log(age);
xiaopin();

对象的解构与数组有一个重要的不同:数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

如果变量名与属性名不一致,必须写成下面这样–>取别名

1
2
3
4
5
6
7
8
// 左侧:给foo属性取一个别名baz,并赋值给baz
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };//baz = "aaa"

let obj = {
first: 'hello',
last: 'world'
};
let { first: f, last: l } = obj;//f = 'hello' ; l = 'world'

2.2.3 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象

1
2
const [a, b, c, d, e] = 'hello';
//a == "h" ;b == "e" ; c == "l" ; d == "l" ;e == "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值

1
let {length : len} = 'hello';//len == 5

2.2.4 函数参数的解构赋值

1
2
3
4
function add([x, y]){
return x + y;
}
add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy

2.2.5 应用举例

  • 交换变量的值
1
2
3
let x = 1;
let y = 2;
[x, y] = [y, x];
  • 从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

1
2
3
4
5
6
7
8
9
10
11
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
return { foo: 1,bar: 2};
}
let { foo, bar } = example();
  • 提取 JSON 数据
1
2
3
4
5
6
7
8
9
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};

let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
  • 输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

1
2
const { SourceMapConsumer, SourceNode } = require("source-map");
const { readFile, writeFile } = require("fs")

2.3 模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识

1
2
3
// 声明
let str = `我也是一个字符串`;
console.log(str, typeof str); // string

特点:

  • 字符串中可以出现换行符

    • 单引号或双引号的情况:报错

    image-20220219161605573

    • 使用模板字符串:
    1
    2
    3
    4
    5
    let str = `<ul>
    <li>沈腾</li>
    <li>马丽</li>
    <li>艾伦</li>
    </ul>`;
  • 可以使用 ${xxx} 形式输出变量,拼接字符串

1
2
3
let name = "Hongyi";
let age = `${name}24`;
console.log(age); // Hongyi24

注意:当遇到字符串与变量拼接的情况使用模板字符串

2.4 简化对象写法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

原始写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let name = "Hongyi";
let change = function() {
console.log("change...");
}

let obj = {
// 前者为变量名,后者为值,为1,2声明的
name: name,
change: change,
improve: function() {
console.log("improve...");
}
}

console.log(obj);

ES6简化写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let name = "Hongyi";
let change = function() {
console.log("change...");
}

let obj = {
// 直接写变量名
name,
change,
// 省略了function关键字
improve() {
console.log("improve...");
}
}

console.log(obj);

image-20220219162526006

2.5 箭头函数

2.5.1 基本使用

ES6 允许使用「箭头」(=>)定义函数。

原始写法:

1
2
3
let fn = function(a, b) {
return a + b;
}

简化的通用写法:

1
2
3
let fn = (a, b) => {
return a + b;
}

注意点

  • this是静态的,始终指向函数声明时所在的作用域下的this的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function getName() {
console.log(this.name);
}
// 该箭头函数在全局作用域下声明,this指向window
let getName2 = () => {
console.log(this.name);
}

window.name = "Hongyi";
let obj = {
name: "Yiming"
}

// 以函数形式调用
// this --> window
getName(); // "Hongyi"
// this --> window
getName2(); // "Hongyi"

// call调用
// this --> obj
getName.call(obj); // "Yiming"
// this --> window,依然指向window
getName2.call(obj); // "Hongyi"
  • 箭头函数不能作为构造函数实例化
1
2
3
4
5
6
7
let Person = (name, age) => {
this.name = name;
this.age = age;
}

let me = new Person("Hongyi", 24);
console.log(me); // Uncaught TypeError: Person is not a constructor
  • 不能使用 arguments
1
2
3
4
let fn = () => {
console.log(arguments);
}
fn(1, 2, 3); // Uncaught ReferenceError: arguments is not defined

更多的简写形式

  • 如果形参只有一个,则小括号可以省略
1
2
3
let add = n => {
return n + n;
}
  • 函数体如果只有一条语句,则花括号可以省略,return必须省略,函数的返回值为该条语句的执行结果
1
let pow = n => n * n;

2.5.2 实践案例

需求-1:点击一个div块,2s后块变色

  • 原始方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
background: blue;
}
</style>
</head>
<body>
<div id="ad"></div>
<script>
let ad = document.getElementById("ad");
// 绑定事件
// 这里ad以方法调用函数addEventListener(),this指向调用函数的ad对象
ad.addEventListener("click", function() {
// 保存this的值 这里this --> ad,因此self --> ad
let self = this;
// 定时器
setTimeout(function() {
// 闭包
// 修改背景颜色 this
// setTimeout在当前作用域下找不到self,往外找到self
// self指向ad
self.style.background = "pink";
}, 2000);
});
</script>
</body>
</html>
  • 利用箭头函数
1
2
3
4
5
6
7
8
9
// 绑定事件
// 这里ad以方法调用函数addEventListener(),this指向调用函数的ad对象
ad.addEventListener("click", function() {
// 定时器
setTimeout(() => {
// 修改背景颜色 this
this.style.background = "pink";
}, 2000);
});

原因:()函数在function() { // 定时器... }的作用域中声明,因此()函数的this指向所在作用域下的this。

需求-2:从数组中返回偶数的元素

  • 原始方法
1
2
3
4
5
6
7
8
9
let arr = [1, 6, 9, 10, 100, 25];
let res = arr.filter(function(item) {
if(item % 2 === 0) {
return true;
} else {
return false;
}
});
console.log(res);
  • 使用箭头函数
1
2
3
let arr = [1, 6, 9, 10, 100, 25];
let res2 = arr.filter(item => item % 2 === 0);
console.log(res2);

总结

  • 箭头函数不会更改 this 指向,用来指定与this无关的回调函数会非常合适,即不新开辟this的指向。例如定时器,数组的方法回调
  • 箭头函数不适合与this有关的回调,例如事件的回调,对象的方法等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = {
name: "Hongyi",
getName: function() {
console.log(this.name);
}
}

// 以方法形式调用函数,this-->obj
obj.getName(); // "Hongyi"

window.name = "Mark";
var obj2 = {
name: "Hongyi",
getName: () => {
// 箭头函数中的this是静态的,只与外部代码块的this绑定
// 因此即使以方法形式调用,也不会改变this的指向
console.log(this.name);
}
}

obj2.getName(); // "Mark"

2.5.3 补充:回调函数和箭头函数的this指向

  • 什么是 this:自动引用正在调用当前方法的.前的对象

  • 正常情况下,this指向的三种情况

    • obj.fun():fun 中的 this->obj ,自动指向.前的对象
    • new Fun():Fun 中的 this->正在创建的新对象,new 改变了函数内部的 this 指向,导致 this 指向实例化 new 的对象
    • fun()直接调用和匿名函数自调:this 默认->window,即函数内部的 this 默认是指向 window 的

示例1

1
2
3
4
5
6
7
8
9
10
let Bob={
sname:"鲍勃",
friends:["Jack","Rose","Tom","Jerry"],
intr(){
this.friends.forEach(function(ele){
console.log(this.sname+"认识"+ele);
});
}
}
Bob.intr();

有三个函数:intr()forEach()和回调函数function(ele){...}

打印结果:

1
2
3
4
undefined认识Jack
undefined认识Rose
undefined认识Tom
undefined认识Jerry

可见,回调函数中的this默认是指向window的

示例2

1
2
3
4
5
6
7
8
9
10
var Bob={
sname:"鲍勃",
friends:["Jack","Rose","Tom","Jerry"],
intr(){
this.friends.forEach(ele => {
console.log(this.sname+"认识"+ele);
});
}
}
Bob.intr();

也有三个函数:intr()forEach()和箭头函数形式的回调函数ele => {...}

打印结果:

1
2
3
4
鲍勃认识Jack
鲍勃认识Rose
鲍勃认识Tom
鲍勃认识Jerry

可以看出箭头函数内的this自动指向了回调函数外层的 this 。

或者利用函数闭包,将外层函数的this赋值给另一个变量self

1
2
3
4
5
6
7
8
9
10
11
12
13
let Bob={
sname:"鲍勃",
friends:["Jack","Rose","Tom","Jerry"],
intr(){
// 保存this为self
let self = this;
this.friends.forEach(function(ele){
// 内层回调函数使用外部函数的self,闭包
console.log(self.sname+"认识"+ele);
});
}
}
Bob.intr();

总结

箭头函数中的 this:

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

  • this 指向的固定化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本没有自己的 this导致内部的 this 就是外层代码块的 this。正是因为它没有 this,所以也就不能用作构造函数

2.5.4 函数参数的默认值设置

  • ES6允许给函数参数赋值初始值。

    • 形参初始值:具有默认值的参数,一般位置要靠后(潜规则)
    1
    2
    3
    4
    5
    6
    function add(a, b, c = 10) {
    return a + b + c;
    }

    let result = add(1, 2);
    console.log(result); // 13
    • 与解构赋值结合
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function connect({host="127.0.0.1", username, password, port}) {
    console.log(host); // "127.0.0.1"
    console.log(username); // "root"
    console.log(password); // "root"
    console.log(port); // 3306
    }

    connect({
    username: "root",
    password: "root",
    port: 3306
    });

    这里host默认值为127.0.0.1

2.6 Rest参数

  • ES6 引入 rest 参数,用于获取函数的实参,用来代替 arguments
1
2
3
4
5
6
// ES5形式
function names() {
console.log(arguments); // arguments是一个对象
}

names("Hongyi", "Mark", "John");

image-20220227134040300

1
2
3
4
5
6
// rest参数
function names(...args) {
console.log(args); // args是一个数组
}

names("Hongyi", "Mark", "John");

image-20220227134137138

  • rest参数必须是最后一个形参
1
2
3
4
5
6
7
function fn(a, b, ...args) {
console.log(a); // 1
console.log(b); // 2
console.log(args); // [3, 4, 5]
}

fn(1, 2, 3, 4, 5);
  • rest 参数非常适合不定个数参数函数的场景

2.7 Spread扩展运算符

2.7.1 基本使用

扩展运算符(spread)也是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。

1
2
3
4
5
6
7
8
9
const names = ["Hongyi", "Mark", "Yiming"];

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

fn(...names);
// 等同于
// fn("Hongyi", "Mark", "Yiming");

image-20220227135446408

2.7.2 实践案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div></div>
<div></div>
<div></div>
<script>
// 1.数组的合并
const names = ["Hongyi", "Yiming"];
const countries = ["China", "Japan"];

const all = [...names, ...countries];
console.log(all);

// 2.数组克隆
const all2 = [...names];
console.log(all2);

// 3.将伪数组转换为真正的数组
const divs = document.querySelectorAll("div");
const divArr = [...divs];
console.log(divs);

</script>
</body>
</html>

执行结果:

image-20220227143322555

2.8 Symbol

2.8.1 介绍和创建

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是JavaScript 语言的第七种数据类型,是一种类似于字符串的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建symbol
let s = Symbol();
console.log(s, typeof s); // Symbol() "symbol"

// 添加标识的 Symbol
let s2 = Symbol("Hongyi");
let s3 = Symbol("Hongyi");
console.log(s2 === s3); // false

// 使用 Symbol for 定义
let s4 = Symbol.for("Hongyi");
let s5 = Symbol.for("Hongyi");
console.log(s4 === s5); // true
  • Symbol 特点

    • Symbol 的值是唯一的,用来解决命名冲突的问题
    • Symbol 值不能与其他数据进行运算,以下操作都是非法的
    1
    2
    3
    4
    5
    let s = Symbol();

    let res = s + 100;
    let res1 = s + "Hongyi";
    let res2 = s + s;
    • Symbol 定义 的 对象属 性 不能 使 用 for...in 循 环遍 历,但 是可 以 使 用Reflect.ownKeys来获取对象的所有键名
  • 遇到唯一性的场景时要想到 Symbol

总结——JS中的数据类型

  • USONBYou are so niubility
    • u : undefined
    • s : string symbol
    • o : object
    • n : null number
    • b : boolean

2.8.2 使用场景

  • 向对象添加属性和方法

暂略

2.8.3 内置值

暂略

2.9 迭代器

2.9.1 概述

遍历器(Iterator)就是一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。

  • ES6 创造了一种新的遍历命令 for...of循环,Iterator 接口主要供 for...of 消费
  • 原生具备 iterator 接口的数据结构(可用 for of 遍历)
    • Array
    • Arguments
    • Set
    • Map
    • String
    • TypedArray
    • NodeList

代码示例

1
2
3
4
5
6
7
// 定义一个数组
const arr = ["Hongyi", "Mark", "Yiming", "Marx"];
console.log(arr);
// 使用for...of遍历数组
for(let v of arr) {
console.log(v);
}

image-20220302211324000

工作原理

  • 创建一个指针对象,指向当前数据结构的起始位置
  • 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员
  • 接下来不断调用 next 方法,指针一直往后移动,直到指向最后一个成员
  • 每调用 next 方法返回一个包含 value 和 done 属性的对象
1
2
3
4
5
6
7
8
9
// 获取迭代器
let iterator = arr[Symbol.iterator]();
console.log(iterator);
// 调用对象的next方法
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

image-20220302212042543

2.9.2 应用:自定义遍历数据

对于不具有iterator 接口的数据结构,例如object对象,可以在其内部自定义一个迭代器属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const banji = {
name: "class 1",
stus: [
"Hongyi",
"Mark",
"Yiming"
],
// 为不可迭代的object对象,添加自定义迭代器方法
[Symbol.iterator]() {
// 索引变量
let index = 0;
// 保存this,或者使用箭头函数
let that = this;
return {
next: function() {
if(index < that.stus.length) {
const res = {
value: that.stus[index],
done: false
};
// 索引自增
index++;
return res;
}else {
// 当遍历完成时,返回undefined
return {
value: undefined,
done: true
}
}
}
}
}
};

// 遍历这个对象
for(let v of banji) {
console.log(v);
}

2.10 生成器

2.10.1 概述

生成器函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • * 的位置没有限制
  • 生成器函数返回的结果是迭代器对象,调用迭代器对象的 next 方法可以得到yield 语句前的值
  • yield 相当于函数的暂停标记,也可以认为是函数的分隔符,每调用一次 next方法,执行一段代码

代码示例1

1
2
3
4
5
6
7
8
// 声明生成器函数
function * gen() {
console.log("Hello World");
}
// 返回迭代器对象
let iterator = gen();
// 打印这个迭代器对象
console.log(iterator);

image-20220304172319728

代码示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function * gen() {
console.log("111");
yield "我是1";
console.log("222");
yield "我是2";
console.log("333");
yield "我是3";
console.log("444");
}
// 得到一个迭代器
let iterator = gen();

// 返回一个value和done属性的对象
// {value: "我是1", done: false}
// console.log(iterator.next());

iterator.next(); // 执行第一个yield前的代码
iterator.next(); // 执行第一个yield之后,第二个yield前的代码
iterator.next(); // 以此类推
iterator.next();

执行结果:

1
2
3
4
111
222
333
444

代码示例3

1
2
3
4
5
6
7
8
9
10
11
function * gen() {
yield "我是1";
yield "我是2";
yield "我是3";
}

let iterator = gen();
// 利用for..of遍历迭代器
for (let v of gen()) {
console.log(v);
}

执行结果:

1
2
3
我是1
我是2
我是3

2.10.2 参数传递

next 方法可以传递实参,作为上一个 yield 语句的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function * gen(arg) {
console.log(arg);
let first = yield 111;
console.log(first);
let second = yield 222;
console.log(second);
let third = yield 333;
console.log(third);
}
// 直接传入参数
let iterator = gen("AAA");
console.log(iterator.next());
// next方法可以传入实参,作为上一个yield的返回值,这里是第一个yield
console.log(iterator.next("BBB"));
console.log(iterator.next("CCC"));
console.log(iterator.next("DDD"));

image-20220304174303389

2.10.3 程序示例

  • 需求1:1s后输出111,然后再过2s后输出222,然后再过3s后输出333

定时器方法

1
2
3
4
5
6
7
8
9
10
// 回调地狱
setTimeout(() => {
console.log("111");
setTimeout(() => {
console.log("222");
setTimeout(() => {
console.log("333");
}, 3000);
}, 2000);
}, 1000);

生成器函数方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function one() {
setTimeout(() => {
console.log("111");
// 执行下一个yield
iter.next();
}, 1000);
}
function two() {
setTimeout(() => {
console.log("222");
// 执行下一个yield
iter.next();
}, 2000);
}
function three() {
setTimeout(() => {
console.log("333");
}, 3000);
}

function * gen() {
yield one();
yield two();
yield three();
}

let iter = gen();
iter.next();
  • 需求2:模拟按先后顺序获取数据:用户数据 –> 订单数据 –> 商品数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function getUsers() {
setTimeout(() => {
let data = "用户数据";
// 调用next方法,并传入参数
iter.next(data);
}, 1000);
}

function getOrders() {
setTimeout(() => {
let data = "订单数据";
// 调用next方法,并传入参数
iter.next(data);
}, 1000);
}

function getGoods() {
setTimeout(() => {
let data = "商品数据";
// 调用next方法,并传入参数
iter.next(data);
}, 1000);
}

function * gen() {
let users = yield getUsers();
console.log(users);
let orders = yield getOrders();
console.log(orders);
let goods = yield getGoods();
console.log(goods);
}
let iter = gen();
iter.next();

2.11 集合Set

2.11.1 概述

ES6 提供了新的数据结构 Set(集合)。它类似于数组,但成员的值都是唯一的,集合实现了 iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历,集合的属性和方法:

  • size:返回集合的元素个数
  • add:增加一个新元素,返回当前集合
  • delete:删除元素,返回 boolean 值
  • has:检测集合中是否包含某个元素,返回 boolean 值
  • clear:清空集合,返回 undefined

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明一个空set
let s = new Set();
// 声明一个非空set
let s2= new Set(["Hongyi", "Yiming", "Hello"]);
console.log(s2);

// 元素个数
console.log(s2.size); // 3
// 添加元素
s2.add("World");
// 删除元素
s2.delete("Hongyi");
// 检测
console.log(s2.has("Yiming")); // true
console.log(s2.has("Hongyi")); // false
// 清空
// s2.clear();
// console.log(s2);

for(let v of s2) {
console.log(v);
}

2.11.2 程序示例

  • 需求1:数组去重
1
2
3
4
let arr1 = [1, 2, 3, 4, 1, 2, 4];
// 先new了一个set用于去重,...扩展运算符又将该set转换为一个数组Array
let res1 = [...new Set(arr1)];
console.log(res1); // [1, 2, 3, 4]
  • 需求2:求交集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr1 = [1, 2, 3, 4, 1, 2, 4];
let arr2 = [1, 2, 3, 5, 5, 6, 6];
// 方式1
// filter是数组的方法,因此要使用扩展运算符进行转换
let res = [...new Set(arr1)].filter(item => {
// 先去重
let s = new Set(arr2);
if(s.has(item)) {
return true;
}
});
console.log(res); // [1, 2, 3]

// 方式2
let res = [...new Set(arr1)].filter(item => new Set(arr2).has(item));
  • 需求3:求并集
1
2
3
4
let arr1 = [1, 2, 3, 4, 1, 2, 4];
let arr2 = [1, 2, 3, 5, 5, 6, 6];
let union = [...new Set([...arr1, ...arr2])];
console.log(union); // [1, 2, 3, 4, 5, 6]
  • 需求4:求差集,即交集取反
1
2
3
4
5
let arr1 = [1, 2, 3, 4, 1, 2, 4];
let arr2 = [1, 2, 3, 5, 5, 6, 6];
// 求arr1-arr2
let diff = [...new Set(arr1)].filter(item => !(new Set(arr2).has(item)));
console.log(diff); // [4]

2.12 Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 也实现了iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历。Map 的属性和方法:

  • size:返回 Map 的元素个数

  • set:增加一个新元素,返回当前 Map

  • get:返回键名对象的键值

  • has:检测 Map 中是否包含某个元素,返回 boolean 值

  • clear:清空集合,返回 undefined

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 声明一个空map
let m = new Map();
// 添加元素
// 字符串做键值
m.set("name", "Hongyi");
// 函数做值
m.set("change", function() {
console.log("change...");
});
let key = {
name: "Mark"
};
// 对象做键,数组做值
m.set(key, ["Hongyi", "Mark", "World"]);
console.log(m);
console.log(m.size); // 3
// 删除 用键
// m.delete("name");

// 获取
console.log(m.get("name")); // Hongyi
// 遍历
for(let v of m) {
console.log(v); // 返回一个个的键值对
}

执行结果:

image-20220304213924958

2.13 class类

2.13.1 概述

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

代码示例

  • class 声明类
  • constructor 定义构造函数初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 采用构造函数的形式
// 手机
function Phone(brand, price) {
this.brand = brand;
this.price = price;
}

// 添加方法
Phone.prototype.call = function() {
console.log("打电话...");
}

// 实例化对象
let Huawei = new Phone("Huawei", 5999);
Huawei.call();
console.log(Huawei);

// 采用class类的形式
class Phone{
// 1.构造方法,实例化对象时自动执行
constructor(brand, price) {
this.brand = brand;
this.price = price;
}
// 2.方法必须采用该形式书写
call() {
console.log("打电话...");
}
}
// 实例化对象
let OnePlus = new Phone("OnePlus", 1999);
console.log(OnePlus);

2.13.2 静态成员

  • static 定义静态方法和属性,静态方法和属性属于类,不属于实例对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Phone{
// 静态属性
static name = "手机";
// 静态方法
static change(){
console.log("Change the world");
}
}

let nokia = new Phone();
console.log(nokia.name); // undefined
console.log(Phone.name); // 手机
Phone.change(); // Change the world

2.13.3 继承

  • 关于ES5的继承,可查看JS笔记中的9.2节

  • extends 继承父类

  • super 调用父级构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Phone {
constructor(brand, price) {
this.brand = brand;
this.price = price;
}
// 父类方法
call() {
console.log("打电话...");
}
}
class SmartPhone extends Phone {
// 构造方法
constructor(brand, price, color, size) {
// 调用父类的构造方法
super(brand, price);
this.color = color;
this.size = size;
}
// 子类方法
photo() {
console.log("拍照...");
}
}

let xiaomi = new SmartPhone("xiaomi", 5999, "red", "4.7inch");
console.log(xiaomi);
xiaomi.call();
xiaomi.photo();

执行结果:

image-20220305124112060

  • 父类方法可以重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Phone {
constructor(brand, price) {
this.brand = brand;
this.price = price;
}
// 父类方法
call() {
console.log("打电话...");
}
}
class SmartPhone extends Phone {
// 构造方法
constructor(brand, price, color, size) {
super(brand, price);
this.color = color;
this.size = size;
}
// 子类方法
photo() {
console.log("拍照...");
}
// 重写父类的方法
call() {
console.log("子类:打电话...");
}
}

let xiaomi = new SmartPhone("xiaomi", 5999, "red", "4.7inch");
xiaomi.call(); // 子类:打电话...

2.13.4 set和get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// set和get
class Phone{
get price() {
console.log("价值属性被读取了");
return 5999;
}

set price(newVal) {
console.log("价值属性被修改了");
}
}
let s = new Phone();

console.log(s.price); // 5999
s.price = 4999;

执行结果:

image-20220305141039151

2.14 对象方法的扩展

ES6 新增了一些 Object 对象的方法:

  • Object.is 比较两个值是否严格相等,与『===』行为基本一致(+0 与 NaN)
1
2
3
console.log(Object.is(120, 120)); // true
console.log(Object.is(NaN, NaN)); // true
console.log(NaN === NaN); // false
  • Object.assign 对象的合并,将源对象的所有可枚举属性,复制到目标对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let config1 = {
host: "localhost",
port: 3306,
name: "root",
password: "root",
test: "test"
};
let config2 = {
host: "192.168.0.1",
port: 3307,
name: "hongyi",
password: "hongyi",
scope: "compile"
};
// 合并:相同属性后者覆盖前者
let config3 = Object.assign(config1, config2);
console.log(config3);

image-20220305142257836

  • setPrototypeOf 可以直接设置对象的原型,不建议使用
1
2
3
4
5
6
7
8
9
10
let school = {
name: "atguigu"
};

let cities = {
xiaoqu: ["Beijing", "Shanghai", "Shenzheng"]
};
// 让后者成为前者的原型对象
Object.setPrototypeOf(school, cities);
console.log(school);

image-20220305142351677

2.15 模块化

模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。

2.15.1 概述

模块化的好处

  • 防止命名冲突

  • 代码复用

  • 高维护性

模块化规范产品

ES6之前的模块化规范和相应实现有:

规范 实现
CommonJS NodeJS,Browserify
AMD requireJS
CMD seaJS

2.15.2 模块化语法

模块功能主要由两个命令构成:export 和 import。

  • export 命令用于规定模块的对外接口
  • import 命令用于输入其他模块提供的功能

代码示例

  • 创建m1.js
1
2
3
4
5
6
// 使用export向外暴露属性和方法
export let name = "Hongyi";

export function sayName() {
console.log("Hi, I'm Hongyi");
}
  • 创建一个页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
// 引入m1.js模块
import * as m1 from "./m1.js";
console.log(m1);
// 使用模块里的属性和方法
console.log(m1.name);
m1.sayName();
</script>
</body>
</html>

注意:如果以静态页面的方式打开页面,会出现跨域错误。可在vs中下载live server插件,再打开页面即可。

image-20220305143556388

执行结果:

image-20220305143619435

2.15.3 暴露数据语法

2.15.2小节的暴露属于分别暴露,此小节介绍统一暴露默认暴露

  • 统一暴露
1
2
3
4
5
6
7
8
// 统一暴露
let school = "atguigu";

function findJob(){
console.log("我们可以帮助你找工作");
}

export {school, findJob};

测试:

1
2
import * as m2 from "./m2.js";
m2.findJob();
  • 默认暴露
1
2
3
4
5
6
7
// 默认暴露
export default {
school: "atguigu",
findJob: function(){
console.log("我们可以帮助你找工作");
}
}

测试:

1
2
3
import * as m3 from "./m3.js";
// 采用默认暴露,会加一层default结构
m3.default.findJob();

2.15.4 引入数据语法

2.15.2小节的引入属于通用的导入方式。此外还有解构赋值形式简便形式

  • 解构赋值形式:针对三种暴露数据形式都可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 分别暴露
import {name, sayName} from "./m1.js";
console.log(name);
sayName();

// 统一暴露,如果引入的数据命名冲突,可使用as取一个别名
import {school, findJob} from "./m2.js";
console.log(school);
findJob();

// 默认暴露
// 注意:对于默认暴露,default对象才包裹了暴露数据
// 因此引入default对象,并需要起一个别名m3
import {default as m3} from "./m3.js";
console.log(m3.school);
m3.findJob();
  • 简便形式:只针对默认暴露
1
2
3
import m3 from "./m3.js";
console.log(m3.school);
m3.findJob();

2.15.5 模块化方式二

将所有的模块引入都放在一个入口文件app.js中:

1
2
3
4
5
6
7
8
// 入口文件
// 模块引入
import * as m1 from "./m1.js";
import * as m2 from "./m2.js";
import * as m3 from "./m3.js";
console.log(m1);
console.log(m2);
console.log(m3);

然后在页面中引入该文件:

1
<script src="./app.js" type="module"></script>

执行结果:

image-20220305150355455

2.15.6 babel对ES6模块化代码转换

Babel 是一个 JavaScript 编译器。作用是将ES6的代码转换为ES5的代码,用以向下兼容仅支持ES5的浏览器。

步骤

  • 将之前编写的四个js文件放置于src/js
  • 安装babel工具:
    • babel-cli:bable命令行工具
    • babel-preset-env:预设包,将es6语法转换为es5语法
    • browserify:轻量级打包工具,项目中使用webpack
1
2
3
npm init --yes # 初始化
npm i babel-cli babel-preset-env browserify -D # 安装
npx babel src/js -d dist/js --presets=babel-preset-env # 转换,并将转换后的代码放置于dist/js
  • 转换执行结果:以m1.js为例
1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.sayName = sayName;
// 使用export向外暴露属性和方法
var name = exports.name = "Hongyi";

function sayName() {
console.log("Hi, I'm Hongyi");
}
  • 打包:以app.js为例
1
npx browserify dist/js/app.js -o dist/bundle.js
  • 在页面中引入这个打包文件
1
<script src="./dist/bundle.js"></script>

image-20220307153213708

3 ES7~ES11