JavaScript学习文档

JavaScript


1.入门


字面量和变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
字面量
- 字面量其实就是一个值,它所代表的含义就是它字面量的意思
- 比如:1 2 3 4 100 "hello" true null ...
- 在js中所有的字面量都可以直接使用,但是直接使用字面量并不方便

变量
- 变量可以用来存储字面量
- 并且变量中存储的字面量可以随意的修改
- 通过变量可以对字面量进行描述,并且变量比较方便修改

- 变量的使用
声明变量 -> let 变量名 / var 变量
变量赋值 -> a = xx
声明和赋值同时进行 -> let 变量 = 值
*/

变量的内存

变量中并不存储任何值,而是存储值的内存地址!

  1. 基本类型

JavaScript 中的基本类型(如 numberstringbooleannullundefinedsymbolbigint)是按值存储的。
当你执行 let b = a; 时,JavaScript 会复制 a 的值到 b,两者存储在不同的内存空间中。

例子:

1
2
let a = 10; // a 的值是 10,存储在某个内存位置
let b = a; // b 是 a 的拷贝,它也存储 10,但在另一个内存位置

如果后来你修改了 ab 的值,比如:

1
a = 20;

此时,b 的值仍然是 10,因为它们彼此独立。

  1. 引用类型

如果是对象(如数组、对象、函数等),它们是按引用存储的。赋值时,新的变量会共享同一个引用,而不是创建副本。

例子:

1
2
3
4
let obj1 = { value: 10 };
let obj2 = obj1; // obj2 和 obj1 指向同一个内存地址
obj1.value = 20;
console.log(obj2.value); // 输出 20

在这种情况下,obj1obj2 指向同一块内存,因此修改 obj1 的属性会影响到 obj2


常量

1
2
3
4
5
6
/*
在JS中,使用const声明常量,常量只能赋值一次,重复赋值会报错
在JS中除了常规的常量外,有一些对象类型的数据我们也会声明为常量
*/

const PI = 3.1415926;

标识符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
在JS中,所有可以由我们自主命名的内容,都可以认为是一个标识符
像 变量名 函数名 类名...
使用标识符需要遵循如下的命名规范:
1.标识符只能含有字母、数值、下划线、$,且不能以数字开头
2.标识符不能是JS中的关键字和保留字,也不建议使用内置的函数或类名作为变量名
3.命名规范:
- 通常会使用驼峰命名法
- 首字母小写,每个单词开头大写
- maxlength -> maxLength
- borderleftwidth -> borderLeftWidth

- 类名会使用大驼峰命名法
- 首字母大写,每个单词开头大写
- maxlength -> MaxLength

- 常量的字母会全部大写
- PI MAX_LLENGTH
*/

2.数据类型


let和const

1
2
3
4
5
6
7
8
9
10
// 暂时性死区形成之后,在该区域内这个标识符不能访问
let message = "hello";

function foo() {
console.log(message);

let message = "haha";
}

foo(); // 报错 Cannot access 'message' before initialization

数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
数值(Number)
- 在JS中所有的整数和浮点数都是Number类型
- JS中的数值并不是无限大的,当数值超过一定范围后会显示近似值
- Infinity 是一个特殊的数值,表示无穷
- 所以在JS中进行一些精度较高的运算时要十分注意
- NaN 也是一个特殊的数值,表示非法的数值

大整数(BigInt)
- 大整数用来表示一些比较大的整数
- 大整数使用n结尾,它可以表示的数字范围是无限大
a = 9999999999999999999999999n

其他进制的数字:
二进制 0b
a = 0b1010
八进制 0o
a = 0o10
十六进制 0x
a = 0xff
*/

类型检查

1
2
3
4
5
6
7
8
9
10
/* 
typeof 运算符
- typeof用来检查不同的值的类型
- 它会更具不同的值返回不同的结果
*/
let a = 10;
let b = 10n;

console.log(typeof a); // number
console.log(typeof b); // bigint

字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
字符串(String)
- 在JS中使用单引号或双引号来表示字符串
- 转义字符 \
\" -> "
\' -> '
\\ -> \
\t -> 制表符
\n -> 换行
- 模板字符串
- 使用反单引号 ` 来表示模板字符串
- 模板字符串中可以嵌入变量
- 使用typeof检查一个字符串时会返回 "string"
*/

// 标签模板字符串的特殊用法
const name = "ear";
const age = 18;

function foo(...args) {
console.log(args);
}

foo`my name is ${name}, age is ${age}, haha`; // [['my name is ', ', age is ', ', haha'], "ear", 18]

其他数据类型

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
布尔值(Boolean)
- 布尔值主要用来进行逻辑判断
- 布尔值只有两个true 和 false
- 使用typeof检查一个布尔值时会返回 "boolean"

空值(Null)
- 空值用来表示空对象
- 空值只有一个 null
- 使用typeof检查一个空值时会返回 "object"
- 使用typeof无法检查空值

未定义(Undefined)
- 当声明一个变量而没有赋值时,它的值就是undefined
- undefined类型的值只有一个就是undefined
- 使用typeof检查一个undefined类型的值时,会返回 "undefined"

符号(Symbol)
- 用来创建一个唯一的标识
- 使用typeof检查符号时会返回 "symbol"

JS中原始值一共有七种
1.Number
2.BigInt
3.String
4.Boolean
5.Null
6.Undefined
7.Symbol
七种原始值是构成各种数据的基石
原始值在JS中是不可变类型,一旦创建就不能修改
*/

let a = Symbol(); // 调用Symbol()创建了一个符号
console.log(typeof a); // symbol

const s1 = Symbol("address");
const s2 = Symbol("height");

const obj = {
name: "ear",
age: 18,
[s1]: "花果山",
[s2]: 1.88
}

/* 获取symbol对应的key */
// 普通属性
console.log(Object.keys(obj)); // ['name', 'age']
// symbol属性
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(address), Symbol(height)]

/* description */
const s3 = Symbol("abc");
console.log(s3.description); // abc
// Symbol函数直接生成的值,都是独一无二的
const s4 = Symbol(s3.description);
console.log(s3 === s4); // false

// 如果相同的key,通过Symbol.for可以生成相同的Symbol值
const s5 = Symbol.for("123");
const s6 = Symbol.for("123");
console.log(s5 === s6); // true

类型转换-字符串

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
/* 
类型转换指将一种数据类型转换为其他类型
将其他类型转换为(字符串、数值和布尔值)

转换为字符串
1.调用toString()方法将其他类型转换为字符串
- 调用xxx的yyy方法
-> xxx.yyy()
- 由于null和undefined没有toString()方法
所以这两个东西调用toString()时会报错
2.调用String()函数将其他类型转换为字符串
- 调用xxx函数
-> xxx()
- 原理:
对于拥有toString()方法的值调用String()函数时,实际上就是在调用toString()方法
对于null,则直接转换为 "null"
对于undefined,直接转换为 "undefined"
*/
let a = 10;
console.log(typeof a, a); // number 10

a = a.toString();
console.log(typeof a, a); // string "10"

let b = 33;
b = undefined;
console.log(typeof b, b); // undefined undefined

b = String(b);
console.log(typeof b, b); // string 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
26
27
28
29
30
31
32
33
34
/* 
将其他类型的数据类型转换为数值
1.使用Number()函数来将其他类型转换为数值
转换的情况:
- 字符串:
- 如果字符串是一个合法的数字,则会自动转换为对应的数字
- 如果字符串不是合法数字,则转换为NaN
- 如果字符串是空串或纯空格的字符串,则转换为0
- 布尔值:
- true转换为1,false转换为0
- null 转换为 0
- undefined 转换为 NaN

2.专门来将字符串转换为数值的两个方法
parseInt() 将一个字符串转换为一个整数,解析一个字符串并返回指定基数的十进制整数
-如果参数不是一个字符串,则将其转换为字符串 (使用 ToString抽象操作)。
字符串开头的空白符将会被忽略。
- 解析时,会自左向右读取一个字符串,直到读取到字符串中所有的有效的整数
parseFloat() 将一个字符串转换为浮点数
- 解析时,会自左向右读取一个字符串,直到读取到字符串中所有的有效的小数
*/
let a = "123";
console.log(typeof a, a); // string "123"

a = Number(a);
console.log(typeof a, a); // number 123

let b = "12.45c456";
console.log(typeof b, b); // string "12.45c456"

c = parseInt(b);
d = parseFloat(b);
console.log(typeof c, c); // number 12
console.log(typeof d, d); // number 12.45

类型转换-布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
1.使用Boolean()函数来将其他类型转换为布尔值
- 转换的情况:
数字:
- 0 和 NaN 转换为false
- 其余是true
字符串:
- 空串 转换为 false
- 其余是true
null和undefined 都转换为false
对象:对象都会转换为true

- 所有标识空性的没有的错误的都会转换为false:0、NaN、空串、null、undefined、false
*/

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
类型转换
- 转换为字符串
显示转换
toString()
String()
隐式转换
+ ""
- 转换为数值
显示转换
Number()
parseInt()
parseFloat()
隐式转换
+
- 转换为布尔值
显示转换
Boolean()
隐式转换
!!
*/

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
25
26
/* 
运算符(操作符)
- 运算符可以用来对一个或多个操作数进行运算
- 算数运算符:
+ - * / 加减乘除
** 幂运算
% 模运算,两个数相除取余数

- 注意:
- 算数运算时,除了字符串的加法,
其他运算的操作数是非数值时,都会转换为数值然后再运算


JS是一门弱类型语言,当进行运算时会通过自动的类型转换来完成运算
a = 10 - "5" // 10 - 5
a = 10 + true // 10 - 1
a = 5 + null // 5 + 0
a = 6 - undefined // 6 - NaN

当任意一个值和字符串做加法时,它会先将其他值转换为字符串,然后再做拼串的操作
可以利用这一特点来完成类型转换
可以通过为 任意类型 + 一个空串 的形式来将其转换为字符串
其原理和String()函数相同,但使用起来更加简洁
*/
a = "a" + null;
console.log(a); // "anull"

赋值运算符

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
      /*
赋值运算符用来将一个值赋给一个变量
= 将符号右侧的值赋值给左侧的变量
??=
- 空赋值
- 只有当变量的值为null或undefined时才会对变量进行赋值
+=
- a += n 等价于 a = a + n
-=
- a -= n 等价于 a = a - n
*=
- a *= n 等价于 a = a * n
/=
- a /= n 等价于 a = a / n
%=
- a %= n 等价于 a = a % n
**=
- a **= n 等价于 a = a ** n
*/
const a = { duration: 50 };

a.speed ??= 25;
console.log(a.speed);
// Expected output: 25

a.duration ??= 10;
console.log(a.duration);
// Expected output: 50

一元的加减

1
2
3
4
5
6
7
8
9
10
11
12
/* 
一元的加减
+ 正号
- 不会改变数值的符号
- 负号
- 可以对数值进行符号位取反

当我们对非数值类型进行正负运算时,会先将其转换为数值然后再运算
*/
let a = "123";
a = +a; // a = Number(a)
console.log(typeof a, a); // number 123

自增和自减

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
/* 
++ 自增运算符
- ++ 使用后会使得原来的变量立刻增加1
- 自增分为前自增(++a)和后自增(a++)
- 无论是++a还是a++都会使原变量立刻增加1
- 不同的是++a和a++所返回的值不同
a++ 是自增前的值 旧值
++a 是自增后的值 新值

-- 自减运算符
- 参考自增
*/
let a = 10;
a++;
console.log(a); // 11

let b = 10;
let c = b++;
console.log("b++ =", c); // b++ = 10

let d = 10;
let e = ++d;
console.log("++d =", e); // ++d = 11

let n = 5;
let result = n++ + ++n + n // 5 + 7 + 7
console.log(result); // 19

逻辑运算符

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
40
41
42
43
/* 
! 逻辑非
- ! 可以用来对一个值进行非运算
- 它可以对一个布尔值进行取反操作
true -> false
false -> true
- 如果对一个非布尔值进行取反,它会先将其转换为布尔值然后再取反
可以利用这个特点将其他类型转换为布尔值

&& 逻辑与
- 可以对两个值进行与运算
- 当&&左右都为true时,返回true,否则返回false
- 与运算时短路的与,如果第一个值为false,则不看第二个值
- 与运算是找false,如果找到false则直接返回,没有false才会返回true
- 对非布尔值进行与运算,它会转换为布尔值然后运算,但是最终会返回原值
- 如果第一个值为false,则直接返回第一个值
- 如果第一个值为true,则返回第二个值

|| 逻辑或
- 可以对两个值进行或运算
- 当||左右有true时,返回true,否则返回false
- 或运算也是短路的或,如果第一个值为true,则不看第二个值
- 或运算是找true,如果找到true则直接返回,没有true才会返回false
- 对非布尔值进行或运算,它会转换为布尔值然后运算,但是最终会返回原值
- 如果第一个值为true,则直接返回第一个值
- 如果第一个值为false,则返回第二个值
*/
let a = 123;
a = !!a;
console.log(typeof a, a); // boolean true

true && alert(123); // 第一个值为true,alert会执行
false && alert(456); // 第一个值为false,alert不会执行

let result = 1 && 2;
console.log(result); // 2
let result2 = 1 && 0;
console.log(result2); // 0
let result3 = 0 && NaN;
console.log(result3); // 0

true || alert(78); // 第一个值为true,alert不会执行
false || alert(90); // 第一个值为false,alert会执行

关系运算符

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
/* 
关系运算符
- 关系运算符用来检查两个值之间的关系是否成立
成立返回true,不成立返回false
- >
- 用来检查左值是否大于右值
- >=
- 用来检查左值是否大于或等于右值
- <
- 用来检查左值是否小于右值
- <=
- 用来检查左值是否小于或等于右值

注意:
当对非数值进行关系运算时,它会先将其转换为数值然后再比较
当关系运算符的两端是两个字符串,它不会将字符串转换为数值,
而是逐位的比较字符的Unicode编码
利用这个特点可以对字符串按照字母排序
注意比较两个字符串格式的数字时一定要进行类型转换
*/

let result = 5 < "10";
console.log(result); // true

let result2 = "1" > false;
console.log(result2); // true

let result3 = "a" < "b";
console.log(result3); // true
let result4 = "abc" < "b";
console.log(result3); // true

let result5 = "12" < "2";
console.log(result3); // true

相等运算符

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
/* 
==
- 相等运算符,用来比较两个值是否相等
- 使用相等运算符比较两个不同类型的值时,
它会将其转换为相同的类型(通常转换为数值)然后再比较
类型转换后值相同也会返回true
- null和undefined进行比较时会返回true
- NaN不和任何值相等,包括它自身
- 不能使用 == 或 === 来检查一个值是否NaN,可以使用isNaN来检查
===
- 全等运算符,用来比较两个值是否全等
- 它不会进行自动的类型转换,如果两个值的类型不同直接返回false

!=
- 不等,用来检查两个值是否不相等
- 会自动的进行类型转换
!==
- 不全等,比较两个值是否不全等
- 不会自动的类型转换
*/
let result = 1 == 1;
console.log(result); // true
let result2 = 1 == "1";
console.log(result2); // true
let result3 = null == undefined;
console.log(result3); // true

条件运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
条件运算符
条件运算符 ? 表达式1 : 表达式2
- 执行顺序:
条件运算符在执行时,会先对条件表达式进行求值判断,
如果结果为true,则执行表达式1
如果结果为false,则执行表达式2
*/
false ? alert(1) : alert(2); // 2

let a = 100;
let b = 10;
a > b ? console.log("a大") : console.log("b大"); // "a大"
let max = a > b ? a : b;
console.log(max); // 100

运算符优先级

1
2
3
4
5
6
7
8
/*
和数学一样,JS中的运算符也有优先级,比如先乘除和加减

可以通过优先级的表格来查询运算符的优先级
- 在表格中位置靠上的优先级越高,优先级越高越先执行,优先级一样自左向右执行
优先级我们不需要记忆
因为()拥有最高的优先级,使用运算符时,如果遇到拿不准的,可以直接通过()来改变优先级即可
*/

空值合并运算符(??)

空值合并运算符??)是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

1
2
3
4
5
6
7
const foo = null ?? 'default string';
console.log(foo);
// Expected output: "default string"

const baz = 0 ?? 42;
console.log(baz);
// Expected output: 0

4.流程控制语句


代码块

1
2
3
4
5
6
7
8
9
10
/*
使用 {} 来创建代码块,代码块可以用来对代码进行分组,
同一个代码块中的代码,就是同一组代码,一个代码块中的代码要么都执行要么都不执行

let 和 var
- 在JS中,使用let声明的变量具有块作用域
在代码块中声明的变量无法在代码块的外部访问

- 使用var声明的变量,不具有块级作用域
*/

if语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
流程控制语句可以用来改变程序执行的顺序
1.条件判断语句
2.条件分支语句
3.循环语句

if语句
- 语法:
if(条件表达式){语句}
- 执行流程
if语句在执行会先对if后的条件表达式进行求值判断
如果结果为true,则执行if后的语句
如果为false则不执行

if语句只会控制紧随其后的那一行代码,如果希望可以控制多行代码,可以使用{}将语句括起来
最佳实践,即使if后只有1行代码,我们也应该编写代码块,这样结构会更加的清晰

如果if后的添加表达式不是布尔值,会转换为布尔值然后再运算

prompt() 可以用来获取用户输入的内容,它会将用户输入的内容以字符串的形式以字符串的形式返回,
可以通过变量来接收
*/

if-else语句

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
/*
if-else语句
- 语法:
if(条件表达式){
语句...
}else{
语句...
}
- 执行流程:
if-else执行时,先对条件表达式进行求值判断,
如果结果为true,则执行if后的语句
如果结果为false,则执行else后的语句

if-else else-if语句:
- 语法:
if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else{
语句...
}
- 执行流程:
会自上向下依次对if后的条件表达式进行求值判断,
如果条件表达式结果为true,则执行当前if后的语句,执行完毕语句结束
如果条件表达式结果为false,则继续向下判断,直到找到true为止
如果所有的条件表达式都是false,则执行else后的语句

注意语句中只会有一个代码块被执行,
一旦有执行的代码块,下边的条件都不会再继续判断了
所以一定要注意,条件的编写顺序
*/

switch语句

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
/*
switch语句
- 语法:
switch(表达式){
case 表达式:
代码...
break;
case 表达式:
代码...
break;
case 表达式:
代码...
break;
default:
代码...
break;
}

- 执行的流程
switch语句在执行时,会依次将switch后的表达式和和case后的表达式进行全等比较
如果比较结果为true,则自当前case处开始执行代码
如果比较结果为false,则继续比较其他case后的表达式,直到找到true为止

- 注意:
当比较结果为true时,会从当前case处开始执行代码
也就是说case是代码执行的起始位置
这就意味着只要是当前case后的代码,都会执行
可以使用break来避免执行其他的case
都没有的话,会执行default的语句

- 总结
switch语句和if语句的功能是重复,switch能做的事if也能做,反之亦然,
它们最大的不同在于,switch在多个全等判断时,结构比较清晰

*/

循环语句

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
/* 
循环语句
- 通过循环语句可以使指定的代码反复执行
- JS中一共有三种循环语句
while语句
do-while语句
for语句
- while语句
- 语法:
while(条件表达式){
语句...
}

- 执行流程:
while语句在执行时,会先对条件表达式进行判断,
如果结果为true,则执行循环体,执行完毕,继续判断
如果为true,则再次执行循环体,执行完毕,继续判断,如此反复
如果知道条件表达式结果为false时,循环结束

- 通常编写一个循环,要有三个条件
1.初始化表达式(初始化变量)
2.条件表达式(设置循环运行的条件)
3.更新表达式(修改初始化变量)
*/
let i = 0;
while (1) {
console.log(i);
i++;
if (i >= 5) {
break;
}
}

do-while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
do-while循环
- 语法:
do{
语句...
}while(条件表达式)

- 执行顺序:
do-while语句在执行时,会先执行do后的循环体
执行完毕后,会对while后的条件表达式进行判断
如果为false,则循环终止
如果为true,则继续执行循环体,以此类推

- 和while的区别:
while语句是先判断再执行
do-while语句是先执行再判断

实质的区别:
do-while语句可以确保循环至少执行一次
*/

for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 
for循环
- for循环和while没有本质区别,都是用来反复执行代码
- 不同点就是语法结构,for循环更加清晰
- 语法:
for(初始化表达式; 条件表达式; 更新表达式){
语句...
}
- 执行流程:
1.执行初始化表达式,初始化变量
2.执行条件表达式,判断循环是否执行(true执行,false终止)
3.判断结果为true,则执行循环体
4.执行更新表达式,对初始化变量进行修改
5.重复第二部,直到判断为false为止
- 初始化表达式,在循环的整个的生命周期中只会执行1次
- for循环中的三个表达式都可以省略,省略后就变为死循环了
- 使用let在for循环的()中声明的变量是局部变量,只能在for循环内部访问
使用var在for循环()中声明的变量可以在for循环的外部访问
- 创建死循环的方式:
while(1){}
for(;;){}
*/

break和continue

1
2
3
4
5
6
7
8
9
10
/*
break和continue
- break
- break用来终止switch和循环语句
- break执行后,当前的switch或循环会立刻停止
- break会终止离他最近的循环

- continue
- continue用来跳过当次循环
*/

5.对象


对象

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
/* 
数据类型:
原始值
1.数值 Number
2.大整数 BigInt
3.字符串 String
4.布尔值 Boolean
5.空值 Null
6.未定义 Undefined
7.符号 Symbol

对象
- 对象是JS中的一种复合数据类型,它相当于一个容器,在对象中可以存储各种不同类型数据
- 对象中可以存储多个各种类型的数据,对象中存储的数据,我们称为属性
- 向对象中添加属性:
对象.属性名 = 属性值
- 读取对象中的属性
对象.属性名
- 如果读取的是一个对象中没有的属性,不会报错而是undefined
- 修改属性
对象.属性名 = 新的属性值
- 删除属性
delete 对象.属性名

原始值只能用来表示一些简单的数据,不能表示复杂数据
比如:现在需要在程序中表示一个人的信息
*/
// 创建对象
let obj = new Object();
let obj2 = Object();

obj.name = "孙悟空";
obj.age = 18;
obj.gender = "男";
console.log(obj); // {name: '孙悟空', age: 18, gender: '男'}
console.log(obj.name); // 孙悟空

对象的属性

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
/* 
属性名
- 通常属性名就是一个字符串,所以属性名可以是任何值,没有什么特殊要求
但是如果你的属性名实在太特殊了,不能直接输入,需要使用[]来设置
如 obj["21432sdaf"],虽然如此,但是我们还是强烈建议属性名也按照标识符的规范命名

- 也可以使用符号(symbol)作为属性名,来添加属性
获取这种属性时,也必须使用symbol
使用symbol添加的属性,通常是那些不希望被外界访问的属性

- 使用[]去操作属性时,可以使用变量

属性值
- 对象的属性值可以是任意的数据类型,也可以是一个对象

使用typeof检查一个对象时,会返回一个object

in 运算符
- 用来检查对象中是否含有某个属性
- 语法 属性名 in obj
- 如果有返回true,没有返回false
*/
let obj = Object();
obj.name = "孙悟空";
obj["21432sdaf"] = "呵呵"; // 不建议

let mySymbol = Symbol();
obj[mySymbol] = "通过symbol添加的属性";

let str = "address";
obj[str] = "花果山"; // 等价于 obj["address"] = "花果山"

console.log("name" in obj); // true
console.log("haha" in obj); // false

对象的字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
对象字面量
- 可以直接用来 {} 来创建对象
- 使用 {} 所创建的对象,可以直接向对象中添加属性
- 语法:
{
属性名:属性值,
["属性名"]:属性值
}
*/
let mySymbol = Symbol();
let str = "address";
let obj = {
name: "孙悟空",
age: 18,
["gender"]: "男",
[mySymbol]: "特殊的属性",
[str]: "花果山"
};
console.log(typeof obj, obj); // object {name: '孙悟空', age: 18, gender: '男', address: '花果山', Symbol(): '特殊的属性'}

对象的枚举

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-in语句
- 语法:
for(let propName in 对象){
语句...
}

- for-in的循环体会执行多次,有几次就会执行几次,
每次执行时,都会将一个属性名赋值给我们所定义的变量

- 注意:并不是所有的属性都可以枚举,比如 使用符号添加的属性
*/
let obj = {
name: "孙悟空",
age: 18,
gender: "男",
[Symbol()]: "测试属性" // 符号添加的属性是不能枚举
};

for (let propName in obj) {
console.log(typeof propName); // string
console.log(propName, obj[propName]);
}

可变类型

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
- 原始值都属于不可变类型,一旦创建就无法修改
- 在内存中不会创建重复的原始值
- 当我们为一个变量重新赋值时,绝对不会影响其他属性

- 对象属于可变类型
- 对象创建完成后,可以任意的添加删除修改对象的属性
- 注意:
- 当对两个对象进行相等或全等比较时,比较的是对象的内存地址
- 如果有两个变量同时指向一个对象,
通过一个变量修改对象时,对另一个变量也会产生影响
当修改一个对象时,所有指向该对象的变量都会受到影响
*/
2

改变量和改对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
修改对象
- 修改对象时,如果有其他变量指向改对象
则所有指向该对象的变量都会受到影响

修改变量
- 修改变量时,只会影响当前的变量

在使用变量存储对象时,很容易因为改变变量指向的对象,提高代码的复杂度
所以通常情况下,声明存储对象的变量时会使用const

注意:
const只是禁止变量被重新赋值,对对象的修改没有任何影响
*/
  • 改变量:修改变量本身的值
1
2
let obj = { a: 1 }; // obj 存的是“这个对象的引用”
obj = 100; // 现在 obj 被重新赋值为 number

你改变的是变量 obj 指向的东西

  • 改对象:修改对象内部的属性,而没有改变变量的引用
1
2
let obj = { a: 1 };
obj.a = 999; // 这是改对象内部的属性

此时没有改变 obj 指向哪儿,只是修改了它指向的那块内存的内容


方法

1
2
3
4
5
/* 
方法(method)
- 当一个对象的属性指向一个函数时,那么我们就称这个函数是该对象的方法,
调用函数就称为调用对象的方法
*/

对象访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const person = {
firstName: "Bill",
lastName: "Gates",
language: "",

get fullName() {
return this.firstName + " " + this.lastName;
},

set lang(lang) {
this.language = lang.toUpperCase();
}
};

console.log(person.fullName); // Bill Gates

// 使用 setter 来设置对象属性:
person.lang = "en";

属性描述符

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*
- 如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符
- 通过属性描述符可以精准的添加或修改对象的属性
- 属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改

- Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有
属性,并返回此对象
- Object.defineProperty(obj, prop, descriptor);
- 可接受三个参数:
- obj要定义属性的对象
- prop要定义或修改的属性的名称或Symbol
- descriptor要定义或修改的属性描述符

- 数据属性描述符:
- [[Configurable]]: 表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false
- [[Enumerable]]: 表示属性是否可以通过for-in或者Object.keys返回该属性
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false
- [[Writable]]: 表示是否可以修改属性的值
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false
- [[value]]: 属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值时undefined
*/

const obj = {
name: "ear", // 默认的 Configurable:true
age: 18
}

Object.defineProperty(obj, "name", {
configurable: false, // 告诉js引擎,obj对象的name属性不可以被删除
enumerable: false, // 告诉js引擎,obj对象的name属性不可枚举(for in/Object.keys)
writable: false, // 告诉js引擎,obj对象的name属性不写入(只读属性 readonly)
value: "earstrive" // 告诉js引擎,返回这个value
});

delete obj.name;
console.log(obj); // {age: 18, name: 'earstrive'}

// 通过Object.defineProperties添加一个新的属性
Object.defineProperty(obj, "address", {});
delete obj.address;
console.log(obj); // {age: 18, name: 'earstrive', address: undefined}

console.log(Object.keys(obj)); // ['age']

obj.name = "kobe";
console.log(obj.name); // earstrive


/*
存取属性描述符
*/
const obj2 = {
name: "ear", // 默认的 Configurable:true
age: 18
}
let _name = obj2.name;

Object.defineProperty(obj2, "name", {
configurable: true,
enumerable: false,
set: function (value) {
console.log("set方法被调用", value);
_name = value;
},
get: function () {
console.log("get方法被调用");
return _name;
}
});

obj2.name = "kobe"; // set方法被调用 kobe
console.log(obj2.name); // get方法被调用


/*
多个属性的属性描述符
*/
const obj3 = {
name: "ear",
age: 18,
height: 1.88
}
Object.defineProperties(obj3, {
name: {
configurable: true,
enumerable: true,
writable: false
},
age: {},
height: {}
});


/*
- 获取对象的属性描述符
- getOwnPropertyDescriptor()
- getOwnPropertyDescriptors()

- 禁止对象扩展新属性:preventExtensions
- 给一个对象添加新的属性会失败(在严格模式下会报错)

- 密封对象,不允许配置和删除属性:seal
- 实际是调用preventExtensions
- 并且将现有属性的configurable: false

- 冻结对象,不允许修改现有属性:freeze
- 实际上调用seal
- 并且将现有属性的writable: false
*/

6.函数


函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
函数(Function)
- 函数也是一个对象
- 它具有其他对象所有的功能
- 函数中可以存储代码,且可以在需要时调用这些代码

- 语法:
function 函数名(){
语句...
}

- 调用函数:
- 调用函数就是执行函数中存储的代码
- 语法:
函数对象()

使用typeof检查函数对象时会返回function
*/

函数的创建方式

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
/* 
函数的定义方式:
1.函数的声明
function 函数名(){
语句...
}

2.函数表达式
const 变量 = function(){
语句...
}

3.箭头函数
const 变量 = () => {
语句...
}
*/

function fn() {
console.log("函数声明所定义的函数");
}

const fn2 = function () {
console.log("函数表达式");
}

const fn3 = () => {
console.log("箭头函数");
}

参数

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
/*
形式参数
- 在定义函数时,可以在函数中指定数量不等的形式参数(形参)
- 在函数中定义形参,就相当于在函数内部声明了对应的变量但是没有赋值

实际参数
- 在调用函数时,可以在函数的()传递数量不等的实参
- 实际参数赋值给对应的形参
- 参数:
1.如果实参和形参数量相同,则对应的实参赋值给对应的形参
2.如果实参多余形参,则多余的实参不会使用
3.如果形参多余形参,则多余的形参为undefined

- 参数的类型
- JS中不会检查参数的类型,可以传递任何类型的值作为参数

1.函数的声明
function 函数名(参数){
语句...
}

2.函数表达式
const 变量 = function(参数){
语句...
}

3.箭头函数
const 变量 = (参数) => {
语句...
}

*/

箭头函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
当箭头函数中只有一个参数时,可以省略()
定义参数时,可以为参数指定默认值
默认值,会在没有对应实参时生效
*/

const fn = a => {
console.log(a);
}
fn(123); // 123

const fn2 = (a = 10, b = 20, c = 30) => {
console.log(a, b, c);
}
fn2(1, 2); // 1 2 30

对象和函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 
对象可以作为参数传递
传递实参时,传递并不是变量本身,而是变量中存储的值

函数每次调用,都会重新创建默认值

在JS中,函数也是一个对象(一等函数)
别的对象能做的事情,函数也可以
*/

let obj = { name: "孙悟空" };

function fn(a) {
console.log("我是fn");
a();
}

function fn2() {
console.log("我是fn2");
}

fn(fn2); // 我是fn 我是fn2

函数的返回值

1
2
3
4
5
6
7
8
9
10
/*
在函数中,可以通过return关键字来指定函数的返回值
返回值就是函数的执行结果,函数调用完毕返回值便会作为结果返回

任何值都可以作为返回值使用(包括对象和函数之类)
如果return后不跟任何值,则相当于返回undefined
如果不写return,那么函数的返回值依然是undefined

return一执行函数立即结束
*/

箭头函数的返回值

1
2
3
4
5
6
7
8
/* 
箭头函数的返回值可以直接写在箭头后
如果直接在箭头后设置对象字面量为返回值时,对象字面量必须使用()括起来
*/
const sum = a => a + 1;
console.log(sum(5)); // 6

const fn = () => ({ name: "孙悟空" });

作用域与作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
作用域(scope)
- 作用域指的是一个变量的可见区域
- 作用域有两种:
全局作用域
- 全局作用域在网页运行时创建,在网页关闭时销毁
- 所有直接编写到script标签中的代码都位于全局作用域中
- 全局作用域中的变量是全局变量,可以在任意位置访问

局部作用域
- 块作用域
- 块作用域是一种局部作用域
- 块作用域在代码块执行时创建,代码块执行完毕它就销毁
- 在块级作用域中声明的变量时局部变量,只能在块内部访问,外部无法访问

- 函数作用域
- 函数作用域也是一种局部作用域
- 函数作用域在函数调用时产生,调用结束后销毁
- 函数每次调用都会产生一个全新的函数作用域
- 在函数中定义的变量时局部变量,只能在函数内部访问,外部无法访问
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 
作用域链
- 当我们使用一个变量时,JS解释器会优先在当前作用域中寻找变量,
如果找到了则直接使用
如果没找到,则去上一层作用域中寻找,找到了则使用
如果没找到,则继续去上一层寻找,以此类推
如果一种到全局作用域都没找到,则报错 xxx is not defined
*/
let a = 10;
{
let a = "第一代码块中的a";
{
let a = "第二代码块中的a";
console.log(a); // "第二代码块中的a"
}
}
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/* 
- 当进入到一个执行上下文时,执行上下文也会关联一个作用域链
- 作用域链是有一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
*/

var message = "globle message";

function fn1() {
console.log(message); // undefined
var message = "fn1 message";
}

fn1();

var obj = {
name: "obj",
bar: function () {
var message = "bar message";
fn1(); // undefined
}
}

obj.bar(); // 作用域链跟调用没有关系,在创建的时候就确定了

/*
面试题
*/
// 1.
var a = 100;
function foo() {
a = 200;
}
foo();

console.log(a); // 200

// 2.
function fn2() {
console.log(b); // undefined
var b = 200;
console.log(b); // 200
}

var b = 100;
fn2();

// 3.
var c = 100;
function fn3() {
console.log(c); // 2.100
}

function fn4() {
var c = 200;
console.log(c); // 1.200
fn3();
}

fn4();
console.log(c); // 3.100

// 4.
var d = 100;

function fn5() {
console.log(d);
return;
var d = 100;
}

fn5(); // undefined

// 5.在开发中可能会出现这样的错误写法
function fn6() {
e = "hello e";
}
fn6();
console.log(e); // hello e

// 6.
function fn7() {
var f = g = 100;
}
fn7();
console.log(f); // f is not defined
console.log(g); // 100

闭包

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
/* 
可以利用函数,来隐藏不希望被外部访问到的变量

闭包:
闭包就是能访问到外部函数作用域中变量的函数
什么时候使用:
当我们需要隐藏一些不希望别人访问的内容时就可以使用闭包
构成闭包的条件:
1.函数的嵌套
2.内部函数要引用外部函数中的变量
3.内部函数要作为返回值返回


函数的作用域,在函数创建时就已经确定了(词法作用域)
和调用的位置无关

闭包利用的就是词法作用域

闭包的生命周期:
1.闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
2.在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)

注意事项:
闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间
相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能)
需要执行次数较少时,使用闭包
需要大量创建实例时,使用类
*/
function outer() {
let num = 0;
return () => {
num++;
console.log(num);
}
}

const newFn = outer();
console.log(newFn());
console.log(outer()());
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
40
41
42
43
44
45
46
47
48
49
50
51
/*
MDN对JavaScript闭包的解释
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的
组合就是闭包
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
- 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
*/

/*
理解和总结:
- 一个普通函数function,如果它一颗访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包
- 从广义的角度来说:JavaScript中的函数都是闭包
- 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包
*/

function fn1() {
var arr = new Array(1024 * 1024).fill(1);

return function () {
console.log(arr);
}
}

var arr = [];

function fn2() {
for (var index = 0; index < 100; index++) {
arr.push(fn1());
}
}

fn2();

function fn3() {
arr = [];
}

fn3(); // 销毁

/*
闭包的内存泄露
- 我们为什么经常会说闭包是由内存泄漏的?
- 在上面的案例中,如果后续我们不再使用add10函数了,那么该函数对象应该被销毁掉,并且其应用着的父作用域AO
也应该被销毁掉
- 但是目前因为在全局作用域下add10变量对0xb00的函数对象有引用,而0xb00的作用域AO(0x200)有引用,所以最终
会造成这些内存是无法被释放掉
- 所以我们经常说的闭包会造成内存泄漏,其实就是刚才的引用链中的所有对象都是无法释放的
- 那么怎么解决这个问题?
- 当将add10设置为null时,就不再对函数对象0xb00有引用,那么对应的AO对象0x200也就不可达了
- 在GC的下一次检测中,它们就会被销毁掉
*/

函数对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
- JavaScript中函数也是一个对象,那么对象就可以有属性和方法
- name属性
一个函数的名字我们可以通过name来访问
- length属性
length属性用于返回函数参数的个数
剩余参数和指定了默认值的参数是不参与参数的个数
*/

function func1() {}

function func2(a, b) {}

console.log(func1.length);
// Expected output: 0

console.log(func2.length);
// Expected output: 2

arguments

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
/*
arguments
- arguments是函数中又一个隐含参数
- arguments是一个类数组对象(伪数组)
和数组类似,可以通过索引来读取元素,也可以通过for循环遍历,但是它不是一个数组对象,不能调用数组方法
- arguments用来存储函数的实参,
无论用户是否定义形参,实参都会存储到arguments对象中
可以通过该对象直接访问实参

... 可变参数,在定义函数时可以将参数指定为可变参数
- 可变参数可以接收任意数量实参,并将他们统一存储到一个数组中返回
- 可变参数的作用和arguments基本是一致,但是也具有一些不同点
1.可变参数的名字可以自己指定
2.可变参数就是一个数组,可以直接使用数组的方法
3.可变参数可以配合其他参数一起使用
*/

function sum() {
let result = 0;

for (const num of arguments) {
result += num;
}

return result;
}

function sum2(...num) {
return num.reduce((a, b => a + b, 0));
}

function fn(a, b, ...args) {
console.log(args); // ['hello', true, 2342]
}
fn(123, 456, "hello", true, 2342);
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
function fn1() {
// arguments是一个伪数组
// 将arguments转换为数组方式一
const arr1 = []
for (const arg of arguments) {
arr1.push(arg);
}
console.log("arr1:", arr1);

// 方式二,es6中的方式
const arr2 = Array.from(arguments);
console.log("arr2:", arr2);

const arr3 = [...arguments];
console.log("arr3:", arr3);

// 方式三,slice方法
const arr4 = [].slice.apply(arguments);
console.log("arr4:", arr4);
}

fn1(1, 2, 3, 4, 5);

/*
箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找
*/

/*
现在推荐使用剩余参数来代理arguments
*/

纯函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
纯函数
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用

- 我们来看一个对数组操作的两个函数
- slice:截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:截取数组,会返回一个新的数组,也会对原数组进行修改

- 为什么纯函数在函数式编程中非常重要呢?
- 因为你可以安心的编写和安心的使用
- 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得
的或者依赖其他的外部变量是否已经发生了修改
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
*/

函数柯里化

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
function fn1(x, y, z) {
console.log(x + y + z);
}

// 自动柯里化函数封装
function hyCurrying(fn) {
function curryFn(...args) {
// 两类操作:
// 第一类操作:继续返回一个新的函数,继续接受参数
// 第二类操作:直接执行fn的函数
if (args.length >= fn.length) { // 执行第二类
return fn.apply(this, args);
} else { // 执行第一类
return function (...nawArgs) {
return curryFn.apply(this, args.concat(nawArgs));
}
}
}
return curryFn;
}


// 对其函数进行柯里化
const fn1Curry = hyCurrying(fn1);
fn1Curry(10)(20)(30);

window对象

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
/* 
window对象
- 在浏览器中,浏览器为我们提供了一个window对象,可以直接访问
- window对象代表的是浏览器窗口,通过该对象可以对窗口进行各种操作
除此之外window对象还负责存储JS中的内置对象和浏览器的宿主对象
- window对象的属性可以通过window对象访问,也可以直接访问
- 函数就可以认为是window对象的方法

向window对象中添加的属性会自动称为全局变量


var 用来声明变量,作用和let相同,但是var不具有块作用域
- 在全局中使用var声明的变量,都会作为window对象的属性保存
- 使用function声明的函数,都会作为window的方法保存
- 使用let声明的变量不会存储在window,而存在一个秘密的小地方
- var虽然没有块作用域,但有函数作用域
- 在局部作用域中,如果没有使用var或let声明变量,则变量会自动成为window对象的属性,也就是全局变量
*/
{
function fn() {
console.log(1);
d = 10; // window.d
}

}
fn(); // 1
window.fn(); // 1
console.log(d); // 10

提升

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
/* 
变量的提升
- 使用var声明的变量,它会在所有代码执行前被声明
所以我们可以在变量声明前就访问变量

函数的提升
- 使用函数声明创建的函数,它会在其他代码执行前被创建
所以我们可以在函数声明前就可以调用函数

let声明的变量实际也会提升,但是在赋值之前解释器禁止对该变量的访问
*/
console.log(a); // undefined
var a = 10;

fn1(); // "我是fn"
function fn1() {
console.log("我是fn");
}

// 变量和函数的提升同样适用于函数作用域
var b = 1;
function fn2() {
console.log(b); // undefined
var b = 2;
console.log(b); // 2
}
fn2();
console.log(b); // 1

立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
在开发中应该尽量减少直接在全局作用域中编写代码

所以我们的代码要尽量编写到局部作用域中

如果使用let声明的变量,可以使用{}来创建块作用域

立即执行函数(IIFE)
- 立即执行函数是一个匿名函数,并且它只会调用一次
- 可以利用立即执行函数来创建一个一次性的函数作用域,避免变量的冲突问题
*/
(function () {
console.log("立即执行函数");
}());

this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
this
- 函数在执行时,JS解释器每次都会传递一个隐含的参数
- 这个参数就叫做this
- this会指向一个对象
- this所指向的对象会根据函数调用方式的不同而不同
1.以函数的形式调用时,this指向的是window
2.以方法的形式调用时,this指向的是调用方法的对象

- 通过this可以在方法中引用调用方法的对象
*/

function fn() {
console.log(this === window);
}
fn(); // true

const obj = { name: "孙悟空" };
obj.test = fn;
obj.test(); // false

默认绑定

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
// 1.普通的函数被独立函数调用,可以理解为函数没有绑定到某个对象上进行调用
function fn1() {
console.log("独立函数调用this-->", this);
}

fn1(); // window

// 2.函数定义在对象中,但是独立调用
const obj = {
name: "ear",
fn2: function () {
console.log("this-->", this);
}
}

const fn3 = obj.fn2;
obj.fn2(); // obj
fn3(); // window

// 3.严格模式下,独立调用的函数中的this指向的是undefined

// 4.高阶函数
function fn4(fn) {
fn();
}

fn4(fn1); // window

隐式绑定

1
2
3
4
5
6
7
8
9
10
function fn1() {
console.log("this-->", this);
}

const obj = {
name: "ear",
fn2: fn1
}

obj.fn2(); // obj

new关键字绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn1() {
this.name = "ear";
console.log("this-->", this);
}

// 使用new关键字
/*
1.创建一个全新的对象
2.这个新对象会被执行prototype链接
3.这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
4.如果函数没有返回其他对象,表达式会返回这个新对象
*/

new fn1(); // fn1对象 此时this指向的是通过new创建的实例对象

显式绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function fn1() {
console.log("this-->", this);
}

const obj = {
name: "ear"
}

// 将this显式绑定到对象中
fn1.call(obj); // obj
fn1.call(123); // Number 包装类
fn1.apply("sunwukong"); // String 包装类
/* 显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则 */
fn1.apply(undefined); // window 没有包装类就是window

// call(this, a, b);
// apply(this, [a, b]);
// bind
const fn2 = fn1.bind(obj);
fn2(); // obj

call和apply

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
/* 
根据函数调用的方式的不同,this的值也不同
1.以函数形式调用,this是window
2.以方法形式调用,this是调用方法的对象
3.构造函数中,this是新建的对象
4.箭头函数没有自己的this,由外层作用域决定
5.通过call和apply调用的函数,它们的第一个参数就是函数的this
6.通过bind返回的函数,this由bind第一个参数决定(无法修改)

调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数
比如,我们可以通过调用函数的call()和apply()来两个方法来调用函数
函数.call()
函数.apply()
- call 和 apply 除了可以调用函数,还可以用来指定函数中的this
- call 和 apply 的第一个参数,将会成为函数的this
- 通过call方法调用函数,函数的实参直接在第一个参数后一个一个的列出来
- 通过apply方法调用函数,函数的实参需要通过一个数组传递
*/

function fn(a, b) {
console.log("函数执行了", this);
console.log(a, b);
}

const obj = { name: "孙悟空" }

fn(); // 函数执行了 Window
fn.call(obj, 1, 2); // 函数执行了 Object
fn.apply(obj, [3, 4]);

bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
bind() 是函数的方法,可以用来创建一个新的函数
- bind可以为新函数绑定this
- bind可以为新函数绑定参数
*/

function fn(a, b, c) {
console.log("fn执行了", this); // fn执行了 {name: '孙悟空'}
console.log(a, b, c); // 10 20 30
}

const obj = { name: "孙悟空" }

const newFn = fn.bind(obj, 10);
newFn(20, 30);

绑定优先级

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
// 1.显式绑定优先级高于隐式绑定
function fn1() {
console.log("this-->", this);
}

const obj = {
name: "ear",
fn1
}

obj.fn1.apply("aaa"); // String

// 1.2 bind绑定
const fn2 = fn1.bind(123);
const obj2 = {
name: "sunwukong",
fn2
}
obj2.fn2(); // Number

// 2.new绑定的优先级高于隐式绑定

// 3.new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
// 3.2 new绑定可以和bind一起使用,new绑定优先级更高

/*
优先级:
1.new
2.bind
3.apply/call
4.隐式绑定
5.默认绑定
*/

箭头函数的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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
箭头函数:
(参数) => 返回值
例子:
无参箭头函数:() => 返回值
一个参数的:a => 返回值
多个参数的:(a, b) => 返回值

只有一个语句的函数:() => 返回值
只返回一个对象的函数:() => ({...})
有多行语句的函数:() => {
...
return 返回值
}

箭头函数没有自己的this,它的this由它的外层作用域决定
箭头函数没有自己的this指向,它的this指向上一级作用域的this
箭头函数的this和它的调用方式无关
箭头函数没有自身的this,它的this由外层作用域决定,也无法通过call apply 和 bind修改它的this
箭头函数没有arguments
*/
function fn() {
console.log(this);
}
const fn2 = () => {
console.log(this);
}
const obj = {
name: "孙悟空",
fn,
fn2,
sayHello() {
console.log(this.name);
function t1() {
console.log("t1-->", this);
const t4 = () => {
console.log("t4-->", this);
}
t4(); // t4--> Window
}
const t2 = () => {
console.log("t2-->", this);
const t3 = () => {
console.log("t3-->", this);
}
t3(); // obj
}
t1(); // t1--> Window
t2(); // t2--> obj
}
}
obj.fn(); // obj
obj.fn2(); // window
obj.sayHello();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
箭头函数的没有自己的this,它的this都是外层作用域的this,使用就近原则来寻找。

作用域:
全局作用域
局部作用域
函数作用域
快作用域

注意:对象没有作用域!!!不要以为有个大括号就有作用域了
*/

const fn = () => {
console.log("this-->", this);
}

const obj = {
name: "ear",
fn
}

fn(); // window
obj.fn(); // window
fn.call(obj); // window

严格模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
JS运行代码的模式有两种:
- 正常模式
- 默认情况下代码都运行在正常模式中,
在正常模式,语法检查并不严格
它的原则是能不报错的地方尽量不报错
- 这种处理方式导致代码的运行性能较差

- 严格模式
- 在严格模式下,语法检查变得严格
1.禁止一些语法
2.更容易报错
3.提升了性能

- 在开发中,应该尽量使用严格模式
这样可以将一些隐藏的问题消灭在萌芽阶段,同时也能提升代码的运行性能
*/
"use strict"

额外知识

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
/* with语句 */
// 扩展一个语句的作用域链
// 不建议使用with语句,因为它可能时混淆错误和兼容性问题的根源
const obj = {
name: "ear"
}

with (obj) {
console.log(name); // ear
}


/* eval函数 */
/*
- eval是一个特殊的函数,它可以将传入的字符串当作JavaScript代码来运行、
- eval会将最后一句执行语句的结果,作为返回值

- 不建议在开发中使用eval
- eval代码的可读性非常的差(代码的可读性时高质量代码的重要原则)
- eval时一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险
- eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化
*/

/*
严格模式的语法限制
1.无法意外的创建全局变量
message = "hello world";
2.严格模式会引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
3.严格模式下试图删除不可删除的属性会抛出异常
4.严格模式不允许函数参数有相同的名称
5.不允许0的八进制语法
6.在严格模式下,不允许使用with
7.在严格模式下,eval不再为上层引用变量
8.严格模式下,this绑定不会默认转换成对象
*/

7.面向对象


面向对象

1
2
3
4
5
6
7
8
9
10
11
/*
面向对象编程(OOP)
1.程序是干嘛的?
- 程序就是对现实世界的抽象
2.对象是干嘛的?
- 一个事物抽象到程序中后就变成了对象
- 在程序的世界中,一切皆对象
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
25
26
27
28
29
30
31
32
33
34
/* 
如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?
- 每个类都可以有一个自己的构造函数(方法),这个方法的名称时固定的constructor
- 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor
- 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常

当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作
1.在内存中创建一个新的对象(空对象)
2.这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性
3.构造函数内部的this,会指向创建出来的新对象
4.执行构造函数的内部代码(函数体代码)
5.如果构造函数没有返回非空对象,则返回创建出来的新对象

使用Object创建对象的问题
1.无法区分出不同类型的对象
2.不方便批量创建对象

在JS中可以通过类(class)来解决这个问题:
1.类是对象模板,可以将对象中的属性和方法直接定义在类中
定义后,就可以直接通过类来创建对象
2.通过同一个类创建的对象,我们称为同类对象
可以使用instanceof来检查一个对象是否是由某一个创建
如果某个对象是某个类所创建,则我们称该对象是这个类的实例

语法:
class 类名{} 类名要使用大驼峰命名
const 类名 = class{}

通过类创建对象
new 类()
*/
class Person { }
const p1 = new Person();
console.log(p1 instanceof Person); // true

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 
类是创建对象的模板,要创建第一件事就是定义类
*/
class Person {
/*
类的代码块,默认就是严格模式
类的代码块是用来设置对象的属性的,不是什么代码都能写
*/
name = "孙悟空"; // Person的实例属性,实例属性只能通过实例访问
age = 18;
static test = "test静态属性"; // 使用static声明的属性,是静态属性(类属性) Person.test
static hh = "静态属性"; // 静态属性只能通过类去访问 Person.hh
}
const p1 = new Person();
console.log(p1); // Person {name: '孙悟空', age: 18}
console.log(p1.name); // 孙悟空
console.log(Person.test); // test静态属性

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
name = "孙悟空";

test1 = function () {
// 添加方法的一种
}

test2() {
// 添加方法(实例方法) 实例方法中的this就是当前实例
console.log(this);
}

static test3() {
// 静态方法(类方法) 通过类来调用,静态方法中的this指向的是当前类
console.log(this);
}
}

const p1 = new Person();
p1.test2();
Person.test3();

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 
当我们在类中直接指定实例属性的值时,意味着我们创建的所有对象的属性都是这个值
*/
class Person {
/*
在类中可以添加一个特殊的方法constructor,该方法我们称为构造函数(构造方法)
构造函数会在我们调用类创建对象时执行
可以在构造函数中,为实例属性进行赋值
在构造函数中,this表示当前所创建的对象
*/
name; // 可以不写
age;
gender;
constructor(name, age, gender) {
console.log("构造函数执行了", name, age, gender);
this.name = name;
this.age = age;
this.gender = gender;
}
}

const p1 = new Person("孙悟空", 18, "男"); // 构造函数执行了 孙悟空 18 男

封装

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/* 
面向对象的特点:
封装、继承和多态

1.封装
- 对象就是一个用来存储不同属性的容器
- 对象不仅负责属性,还要负责数据的安全
- 直接添加到对象的属性,并不安全,因为它们可以被任意的修改
- 如何确保数据的安全:
1.私有化数据
- 将需要保护的数据设置为私有,只能在类内部使用
2.提供setter和getter方法来开放对数据的操作
- 属性设置私有,通过getter setter方法操作带来的好处
1.可以控制属性的读写权限
2.可以在方法中对属性的值进行验证

- 封装主要用来保证数据的安全
- 实现封装的方式:
1.属性私有化 加#
2.通过getter和setter方法来操作属性
get 属性名(){
return this.#属性
}

set 属性名(参数){
this.#属性 = 参数
}

*/

class Person {
/*
实例化使用#开头就变成了私有属性,私有属性只能在类内部访问
*/
#name;
#age;
#gender;

constructor(name, age, gender) {
this.#name = name;
this.#age = age;
this.#gender = gender;
}

sayHello() {
console.log(this.#name);
}

// getter方法,用来读取属性
getName() {
return this.#name;
}

// setter方法,用来设置属性
setName(name) {
// 可以写逻辑
this.#name = name;
}

// 新写法
get age() {
return this.#age;
}

set age(age) {
this.#age = age;
}
}

const p1 = new Person("孙悟空", 18, "男");
p1.setName("猪八戒");
p1.age = 28;
console.log(p1.age); // 28

多态

1
2
3
4
5
/*
多态
- 在JS中不会检查参数的类型,所以这就意味着任何数据都可以作为参数传递
- 要调用某个函数,无需指定类型,只要对象满足某些条件即可
*/

继承

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* 
继承
- 可以通过extends关键字来完成继承
- 当一个类继承另一个类时,就相当于将另一个类中的代码复制到当前类中
- 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
- 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展

封装 --- 安全性
继承 --- 扩展性
多态 --- 灵活性

- 通过继承可以在不修改一个类的情况下对其进行扩展
- OCP 开闭原则
- 程序应该对修改关闭,对扩展开放
*/

class Animal {
constructor(name) {
this.name = name;
}

sayHello() {
console.log("动物在叫");
}
}

class Dog extends Animal {
/*
在子类中,可以通过创建同名方法来重写父类的方法
*/
sayHello() {
console.log("汪汪汪");
}
}

class Cat extends Animal {
/*
重写构造函数
重写构造函数时,构造函数的第一行代码必须为super()
*/
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}

sayHello() {
// 在方法中可以使用super来引用父类的方法
super.sayHello();
console.log("喵喵喵");
}
}

const dog = new Dog("旺财");
const cat = new Cat("汤姆", 3);

dog.sayHello(); // 汪汪汪
cat.sayHello(); // 动物在叫 喵喵喵
console.log(dog.name); // 旺财
console.log(cat.name, cat.age); // 汤姆 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
25
26
27
28
29
/* 
对象中存储属性的区域实际上有两个:
1.对象自身
- 直接通过对象所添加的属性,位于对象自身中 p1.address = "花果山"
- 在类中通过 x = y 的形式添加的属性,位于对象自身中 name = "孙悟空"

2.神秘位置
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__
- 原型对象也负责为对象存储属性
当我们访问对象中的属性时,会优先访问对象自身的属性,
对象自身不包含该属性时,才会去原型对象中寻找
- 会添加到原型对象中的情况:
1.在类中通过xxx(){}方法添加的方法,位于原型中
2.主动向原型中添加的属性或方法
*/
class Person {
name = "孙悟空";
age = 18;
sayHello = "hello";

sayHello() {
console.log("hello");
}
}

const p1 = new Person();

console.log(p1.sayHello); // "hello"
3

原型与原型链

普通对象的原型

1
2
3
4
5
6
7
8
9
10
/*
- JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]], 这个特殊的对象可以指向另外一个对象
- 那么这个对象有什么用呢?
- 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]] 的操作
- 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
- 如果对象中没有该属性,那么会访问对象 [[prototype]] 内置属性指向的对象上的属性
- 获取的方式有两种
- 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)
- 方式二:通过 Object.getPrototypeOf() 方法可以获取到
*/

函数对象的原型

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
40
41
42
const obj = {}
function foo() { }

// 1.将函数看成是一个普通的对象时,它是具备 __proto__ (隐式原型)
// 作用:查找key对应的value时,会找到原型身上
console.log(obj.__proto__);
console.log(foo.__proto__);

// 2.将函数看成时一个函数时,它是具备prototype (显式原型)
// 作用:用来构建对象时,给对象设置隐式原型的
console.log(obj.prototype); // undefined 对象时没有prototype
console.log(foo.prototype);


/*
new关键字的步骤如下
1.在内存中创建一个新的对象(空对象)
2.这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性

那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
*/


function Student(name, age) {
this.name = name;
this.age = age;
}

Student.prototype.running = function () {
console.log(this.name + "running");
}

// 创建三个学生
const stu1 = new Student("ear", 18);
stu1.running(); // earrunning

/*
隐式原型的作用
- 1.stu1的隐式原型时谁? Student.prototype对象
- 2.stu1.running查找:
- 现在自己身上查找,没有找到,然后去原型上去查找
*/

显式原型的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 非常重要的属性:constructor,指向Person函数对象
function Person() { }

// 1.对constructor在prototype上的验证
console.log(Person.prototype);
console.log(Person.prototype.constructor);
console.log(Person.prototype.constructor === Person); // true

console.log(Person.name); // Person
console.log(Person.prototype.constructor.name); // Person

// 2.实例对象
const p = new Person();
console.log(p.__proto__.constructor);
console.log(p.__proto__.constructor.name); // Person

借用构造函数实现继承

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
// 定义Person构造函数(类)
function Person(name, age, height, address) {
this.name = name;
this.age = age;
this.height = height;
this.address = address;
}

Person.prototype.running = function () {
console.log("running");
}

Person.prototype.eating = function () {
console.log("earing");
}

// 定义学生类
function Student(name, age, height, address, sno, score) {
// 重点:借用构造函数
Person.call(this, name, age, height, address);

this.sno = sno;
this.score = score;
}

const s = new Student("zhangsan", 18, 1.88, "花果山", 1, 20);
console.log(s); // Student {name: 'zhangsan', age: 18, height: 1.88, address: '花果山', sno: 1, …}

原型对象

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
40
/* 
访问一个对象的原型对象
1.对象.__proto__
2.Object.getPrototypeOf(对象)

原型对象中的数据:
1.对象中的数据(属性、方法等)
2.constructor(对象的构造函数)

注意:
原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同
p对象的原型链:p对象 -> 原型 -> 原型 -> null
obj对象的原型链:obj对象 -> 原型 -> null

原型链:
- 读取对象属性时,会优先读取对象自身属性,
如果对象中有,则使用,没有则去对象的原型中寻找
如果原型中有,则使用,没有则去原型的原型中寻找
直到找到Object对象的原型(Object的原型没有原型(为null))
如果依然没有找到,则返回undefined

- 作用域链,是找变量的链,找不到会报错
- 原型链,是找属性的链,找不到会返回undefined
*/
class Person {
name = "孙悟空";
age = 18;

sayHello() {
console.log(this.name);
}
}

const p = new Person();

console.log(p); // Person {name: '孙悟空', age: 18}
console.log(p.__proto__); // {constructor: ƒ, sayHello: ƒ}
console.log(Object.getPrototypeOf(p) === p.__proto__); // true
console.log(p.__proto__.__proto__);
console.log(p.__proto__.__proto__.__proto__); // null
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
/* 
所有的同类型对象它们的原型对象都是同一个,
也就意味着,同类型对象的原型链是一样的

原型的作用:
原型就相当于是一个公共的区域,可以被所有该类实例访问,
可以将该类实例中,所有的公共属性(方法)统一存储到原型中
这样我们只需要创建一个属性,即可被所有实例访问

JS中继承就是通过原型来实现的
当我们继承时,子类的原型就是父类的实例

在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己的值,
但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复创建
*/

class Person {
name = "孙悟空"

sayHello() {

}

sayHello2 = () => {

}
}

const p1 = new Person();
const p2 = new Person();

console.log(p1.__proto__ === p2.__proto__); // true
console.log(p1.sayHello === p2.sayHello); // true
console.log(p1.sayHello2 === p2.sayHello2); // false

class Animal { }
class Cat extends Animal { }
const cat = new Cat();
// cat --> Animal实例 --> object --> Object原型 --> null
4

修改原型对象

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
/* 
大部分情况下,我们是不需要修改原型
注意:
千万不要通过类的实例去修改原型
1.通过一个对象影响所有同类对象,这么做不合适
2.修改原型先创建实例,麻烦
3.危险

处理通过__proto__能访问对象的原型外,
还可以通过类的prototype属性,来访问实例的原型
修改原型时,最好通过类去修改
好处:
1.一修改就是修改所有实例的原型
2.无需创建实例即可完成对类的修改

原则:
1.原型尽量不要手动改
2.要改也不要通过实例对象去改
3.通过 类.prototype 属性去修改
4.最好不要直接给prototype去赋值
*/
class Person {

}

const p = new Person();
console.log(Person.prototype === p.__proto__); // true

Person.prototype.fly = () => {
console.log("我在飞");
}
console.log(p.fly()); // 我在飞

instanceof和hasOwn

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
/* 
instanceof 用来检查一个对象是否是一个类的实例
- instanceof检查的是对象的原型链上是否有该类实例
只要原型链上有该类实例,就会返回true
- dog -> Animal的实例 -> Object实例 -> Object原型
- Object是所有对象的原型,所以任何对象和Object进行instanceof运算都会返回true

in
- 使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true

对象.hasOwnProperty(属性名)(不推荐使用)
- 用来检查一个对象的自身是否含有某个属性

Object.hasOwn(对象, 属性名)
- 用来检查一个对象的自身是否含有某个属性
*/
class Animal { }

class Dog extends Animal { }

const dog = new Dog();

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

class Person {
name = "孙悟空";
age = 18;

sayHello() {
console.log(this.name);
}
}

const p = new Person();
console.log(p.hasOwnProperty("sayHello")); // false
console.log(Object.hasOwn(p, "name")); // true

new运算符

1
2
3
4
5
6
7
8
9
10
11
12
/*
new运算符是创建对象时要使用的运算符
- 使用new时,到底发生了哪些事情
- 当使用new去调用一个函数时,这个函数将会作为构造函数调用,
使用new调用函数时,将会发生这些事:
1.创建一个普通的JS对象(Object对象 {}),为了方便,称其为新对象
2.将构造函数的prototype属性设置为新对象的原型
3.使用实参来执行构造函数,并且将新对象设置为函数中的this
4.如果构造函数返回的是一个非原始值,则该值作为new运算的返回值返回(千万不要这么做)
如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回
通常不会为构造函数指定返回值
*/

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
面向对象本质就是,编写代码时所有的操作都是通过对象来进行的。
面向对象的编程的步骤:
1.找对象
2.搞对象

学习对象:
1.明确这个对象代表什么,有什么用
2.如何获取到这个对象
3.如何使用这个对象(对象中的属性和方法)

对象的分类:
内建对象
- 由ES标准所定义的对象
- 比如 Object Function String Number ...

宿主对象
- 由浏览器提供的对象
- BOM、DOM

自定义对象
- 由开发人员自己创建的对象
*/

8.数组


简介

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
/* 
数组(Array)
- 数组也是一种复合数据类型,在数组中可以存储多个不同类型的数据
- 数组中存储的是有序的数据,数组中的每个数据都有一个唯一的索引
可以通过索引来操作数据
- 数据中存储的数据叫做元素
- 索引(index)是一组大于0的整数
- 创建数组
通过Array()来创建数组,也可以通过[]来创建数组

- 向数组中添加元素
语法:
数组[索引] = 元素

- 读取数组中的元素
语法:
数组[索引]
- 如果读取了一个不存在的元素,不好报错而是返回undefined

- length
- 获取数组的长度
- 获取的实际值就是数组的最大索引+1
- 向数组最后添加元素:
数组[数组.length] = 元素
- length是可以修改的
*/

const arr = new Array();
const arr2 = [1, 2, 3, 4, 5];

arr2[100] = 99; // 使用数组时,应该避免非连续数组,因为它性能不好
console.log(arr2); // (101) [1, 2, 3, 4, 5, 空 ×95, 99]

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

console.log(typeof arr); // object

Array() 构造函数

  • 可以通过单个数字参数的构造函数创建数组,数组的长度为传入的参数,该数组不包含任何实际的元素。

    1
    2
    3
    4
    const arrayEmpty = new Array(2);

    console.log(arrayEmpty.length); // 2
    console.log(arrayEmpty[0]); // undefined;实际上是一个空槽

Array.prototype.fill()

  • fill() 方法用一个固定值填充一个数组中从起始索引(默认为 0)到终止索引(默认为 array.length)内的全部元素。它返回修改后的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*
    fill(value)
    fill(value, start)
    fill(value, start, end)
    */
    const array1 = [1, 2, 3, 4];

    // Fill with 0 from position 2 until position 4
    console.log(array1.fill(0, 2, 4));
    // Expected output: Array [1, 2, 0, 0]

    // Fill with 5 from position 1
    console.log(array1.fill(5, 1));
    // Expected output: Array [1, 5, 5, 5]

    console.log(array1.fill(6));
    // Expected output: Array [6, 6, 6, 6]

遍历数组

1
2
3
4
5
6
7
8
9
10
11
12
/* 
任何类型的值都可以成为数组中的元素
创建数组时尽量要确保数组中存储的数据类型是相同

遍历数组
- 遍历数组简单理解,就是获取到数组中的每一个元素
*/

let arr = [1, "hello", true, null, { name: "孙悟空" }, () => { }];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

for-of

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
for-of语句可以用来遍历可迭代对象

语法:
for(变量 of 可迭代的对象){
语句...
}

执行流程:
for-of的循环体会执行多次,数组中由几个元素就会执行几次
每次执行时都会将一个元素赋值给变量
*/

const arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"];

for (const value of arr) {
console.log(value);
}

数组的方法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* 
非破坏性方法
Array.isArray()
- 用来检查一个对象是否时数组

at()
- 可以根据索引获取数组中的指定元素
- at可以接收负索引作为参数

concat()
- 用来连接两个或多个数组
- 非破坏性方法,不会影响数组,而是返回一个新的数组

indexOf()
- 获取元素在数组中第一次出现的索引
- 参数:
1.要查询的元素
2.查询的起始位置
- 返回值:
找到了则返回元素的索引
没找到返回-1

lastIndexOf()
- 获取元素在数组中最后一次出现的位置
- 参数:
1.要查询的元素
2.查询的结束位置
- 返回值:
找到了则返回元素的索引
没找到返回-1

join()
- 将一个数组中的元素连接为一个字符串
- 参数:
指定一个字符串作为连接符

slice()
- 用来截取数组
- 参数:
1.截取的起始位置(包括该位置)
2.截取的结束位置(不包括该位置)
- 第二个参数可以省略不写,如果省略则会一直截取到最后
- 索引可以是负值

如果将两个参数全部省略,则可以对数组进行浅拷贝(浅复制)
*/

const arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "猪八戒"];
const arr2 = ["白骨精", "蜘蛛精", "玉兔精"];

console.log(Array.isArray(arr)); // true

console.log(arr.at(-2)); // '唐僧'

console.log(arr.concat(arr2)); // ['孙悟空', '猪八戒', '沙和尚', '唐僧', '猪八戒', '白骨精', '蜘蛛精', '玉兔精']

console.log(arr.indexOf("猪八戒")); // 1
console.log(arr.indexOf("猪八戒", 3)); // 4

console.log(arr.lastIndexOf("猪八戒", 3)); // 1
console.log(arr.lastIndexOf("猪八")); // -1

console.log(arr.join("@")); // "孙悟空@猪八戒@沙和尚@唐僧@猪八戒"

console.log(arr.slice(0, 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* 
破坏性方法
push()
- 向数组的末尾添加一个或多个元素,并返回新的长度

pop()
- 删除并返回数组的最后一个元素

unshift()
- 向数组的开头添加一个或多个元素,并返回新的长度

shift()
- 删除并返回数组的第一个元素

splice()
- 可以删除、插入、替换数组中的元素
- 参数:
1.删除的起始位置
2.删除的数量
3.要插入的元素

- 返回值:
- 返回被删除的元素

reverse()
- 反转数组,返回值是反转后的数组
*/

const arr = [1, 2, 3, 4];

console.log(arr.push(5, "haha")); // 6
console.log(arr); // [1, 2, 3, 4, 5, 'haha']

console.log(arr.pop()); // "haha"
console.log(arr); // [1, 2, 3, 4, 5]

console.log(arr.unshift("xixi")); // 6
console.log(arr); // ["xixi", 1, 2, 3, 4, 5]

console.log(arr.shift()); // "xixi"
console.log(arr); // [ 1, 2, 3, 4, 5]

const arr2 = [1, 2, 3, 4];

console.log(arr2.splice(1, 2, "haha", "xixi")); // [2, 3]
console.log(arr2); // [1, "haha","xixi", 4]
console.log(arr2.splice(1, 0, "gugu")); // []
console.log(arr2); // [1,"gugu", "haha","xixi", 4]

const arr3 = [5, 4, 3, 2, 1];
console.log(arr3.reverse()); // [1, 2, 3, 4, 5]
console.log(arr3); // [1, 2, 3, 4, 5]
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
sort()
- sort用来对数组进行排序(会改变原数组)
- sort默认会将数组升序排列
注意:sort默认会按照unicode编码进行排序,所以如果直接通过sort对数字进行排序
可能会得到一个不正确的结果
- 参数:
- 可以传递一个回调函数作为参数,通过回调函数来指定排序规则
(a, b) => a - b 升序排列
(a, b) => b - a 降序排序

forEach()
- 用来遍历数组
- 它需要一个回调函数作为参数,这个回调函数会被调用多次
数组中有几个元素,回调函数就会调用几次
每次调用,都会将数组中的数据作为参数传递
- 回调函数中有三个参数:
element 当前的元素
index 当前元素的索引
array 被遍历的数组

filter()
- 将数组中符合条件的元素保存到一个新数组返回
- 需要一个回调函数作为参数,会为每一个元素去调用回调函数,并根据返回值来决定是否将元素添加到新数组中
- 非破坏性方法,不会影响原数组

map()
- 根据当前数组生成一个新的数组
- 需要一个回调函数作为参数,回调函数的返回值会成为新数组中的元素
- 非破坏性方法

reduce()
- 可以用来将一个数组中的所有元素整合为一个值
- 参数:
1.回调函数,通过回调函数来指定合并的规则
2.可选参数,初始值
*/

const arr = [1, 3, 4, 10, 22, 45, 2, 4, 8, 5, 8];
console.log(arr.sort()); // [1, 10, 2, 22, 3, 4, 4, 45, 5, 8, 8]
console.log(arr.sort((a, b) => a - b)); // [1, 2, 3, 4, 4, 5, 8, 8, 10, 22, 45]
console.log(arr.sort((a, b) => b - a)); // [45, 22, 10, 8, 8, 5, 4, 4, 3, 2, 1]

const arr2 = ["a", "b", "c", "d", "e", "f"];
arr2.forEach((element, index, array) => {
console.log(element, index, array);
})

console.log(arr.filter(ele => ele % 2 === 0)); // [22, 10, 8, 8, 4, 4, 2]

console.log(arr2.map(ele => ele + "hello")); // ['ahello', 'bhello', 'chello', 'dhello', 'ehello', 'fhello']

const arr3 = [1, 2, 3, 4, 5, 6];
console.log(arr3.reduce((a, b) => a + b, 10)); // 31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 
flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组合并为一个新数组返回
*/
const arr = [1, 2, 3, 4, 5, [2, 3, 4, 5], [[2, 3], [4, 5]]];
const arr2 = arr.flat(1);
const arr3 = arr.flat(2);
console.log(arr2); // [1, 2, 3, 4, 5, 2, 3, 4, 5, [2, 3], [4, 5]]
console.log(arr3); // [1, 2, 3, 4, 5, 2, 3, 4, 5, 2, 3, 4, 5]

/*
flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
- flatMap是先进行数组的map方法,再做flat的操作
- flatMap中的flat相当于深度为1
*/
const arr4 = arr.flatMap(arr => arr);
console.log(arr4); // [1, 2, 3, 4, 5, 2, 3, 4, 5, [2, 3], [4, 5]]

对象的复制

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
/* 
数组的复制
如何去复制一个对象,复制必须要产生新的对象
当调用slice时,会产生一个新的数组对象,从而完成对数组的复制

...(展开运算符)
- 可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递
- 通过它也可以对数组进行浅复制
*/

const arr = [1, 2, 3, 4];
const arr2 = arr.slice();
const arr3 = [...arr];

console.log(arr2); // [1, 2, 3, 4]
console.log(arr3); // [1, 2, 3, 4]
console.log(arr === arr2); // false
console.log(arr === arr3); // false

/*
对象的复制
- Object.assign(目标对象, 被复制的对象)
- 将被复制对象中的属性复制到目标对象里,并将目标对象返回

- 也可以使用展开运算符对对象进行复制
*/

const obj = { name: "孙悟空", age: 18 }
const obj2 = Object.assign({}, obj);
const obj3 = { ...obj }
console.log(obj2); // {name: '孙悟空', age: 18}
console.log(obj === obj2); // false
console.log(obj3); // {name: '孙悟空', age: 18}
console.log(obj === obj3); // false

浅拷贝和深拷贝

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
40
      /* 
浅拷贝(shallow copy)
- 通常对对象的拷贝都是浅拷贝
- 浅拷贝顾名思义,只对对象的浅层进行复制(只复制一层)
- 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要
- 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或元素)

深拷贝(deep copy)
- 深拷贝指不仅复制对象本身,还复制对象中的属性和元素
- 因为性能问题,通常情况下不太使用深拷贝
*/

const arr = [{ name: "孙悟空" }, { name: "猪八戒" }];
const arr2 = arr.slice(); // 浅拷贝
const arr3 = structuredClone(arr); // 深拷贝

console.log(arr === arr2); // false
console.log(arr[0] === arr2[0]); // true
console.log(arr === arr3); // false
console.log(arr[0] === arr3[0]); // false


/*
对象的拷贝
*/
const obj = {
name: "孙悟空",
friend: {
name: "猪八戒"
}
}

// 浅复制
const obj2 = Object.assign({}, obj);

// 深复制
const obj3 = structuredClone(obj);

// JSON深复制
const obj4 = JSON.parse(JSON.stringify(obj));

排序

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
/* 
这种排序方式,被称为冒泡排序,冒泡排序是最慢的排序方式,数字少还可以凑合用,不适用于数据量较大的排序
*/
const arr = [9, 1, 3, 2, 8, 0, 5, 7, 6, 4];
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
console.log(arr); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

/*
选择排序
*/
const arr2 = [9, 1, 3, 2, 8, 0, 5, 7, 6, 4];
for (let i = 0; i < arr2.length; i++) {
for (let j = i + 1; j < arr2.length; j++) {
if (arr2[i] > arr2[j]) {
let temp = arr2[i];
arr2[i] = arr2[j];
arr2[j] = temp;
}
}
}
console.log(arr2); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

高阶函数

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* 
一个函数的参数也可以是函数,
如果将函数作为参数传递,那么我们就称这个函数为回调函数(callback)

高阶函数
- 如果一个函数的参数或返回值是函数,则这个函数称为高阶函数
- 为什么要将函数作为参数传递?(回调函数有什么作用?)
- 将函数作为参数,意味着可以对另一个函数动态传递参数
*/
class Person {
#name;
#age;

constructor(name, age) {
this.#name = name;
this.#age = age;
}

get name() {
return this.#name;
}

get age() {
return this.#age;
}
}

const personArr = [
new Person("孙悟空", 18),
new Person("猪八戒", 28),
new Person("沙和尚", 38),
new Person("唐僧", 48)
];

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

function filter(arr, cb) {
const newArr = [];

for (let i = 0; i < arr.length; i++) {
if (cb(arr[i])) {
newArr.push(arr[i]);
}
}

return newArr;
}

console.log(filter(personArr, a => a.age > 20));
console.log(filter(arr, a => a % 2 === 0)); // [2, 4, 6, 8]

/*
希望在someFn()函数执行时,可以记录一条日志
在不修改原函数的基础上,为其增加记录日志的功能
可以通过高阶函数,来动态的生成一个新函数
*/
function someFn() {
return "hello";
}

function outer(cb) {
return () => {
console.log("记录日志");
const result = cb();
return result;
}
}

let fn = outer(someFn);
console.log(fn());
console.log(outer(someFn)());

递归

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
/* 
递归
- 调用自身的函数称为递归函数
- 递归的作用和循环是基本一致的
- 递归的核心思想就是将一个大的问题拆分为一个一个的小的问题,小的问题解决了,大的问题也就解决了
- 编写递归函数,一定要包含两个要件:
1.基线条件 -- 递归的终止条件
2.递归条件 -- 如何对问题进行拆分

递归的作用和循环时一致的,不同点在于,递归的思路比较简洁,循环的执行性能比较好
在开发中,一般的问题都可以通过循环解决,也是尽量去使用循环,少用递归
只在一些使用循环解决比较麻烦的场景下,才使用递归
*/

function jieCheng(num) {
// 基线条件
if (num === 1) {
return 1;
}

// 递归条件
return jieCheng(num - 1) * num;
}

console.log(jieCheng(5)); // 120

9.内建对象


结构赋值

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
      /* 
解构赋值
解构数组时,可以使用...来设置获取多余的元素

数组中可以存储任意类型的数据,也可以存数组
如果一个数组中的元素还是数组,则这个数组我们就成为二维数组
*/

const arr = ["孙悟空", "猪八戒", "沙和尚"];

let [a, b, c = 7, d = 10] = arr;
console.log(a, b, c, d); // 孙悟空 猪八戒 沙和尚 10

let [n1, n2, ...n3] = [1, 2, 3, 4, 5, 6];
console.log(n1, n2, n3); // 1 2 [3, 4, 5, 6]

let a1 = 10;
let a2 = 20;
[a1, a2] = [a2, a1];
console.log(a1, a2); // 20 10



/*
对象的解构
*/

const obj = { name: "孙悟空", age: 18, gender: "男" }

let { name, age, gender } = obj;
console.log(name, age, gender); // 孙悟空 18 男

let { address } = obj;
console.log(address); // undefined 没有的属性返回undefined

// 重命名、默认值
let { name: a, age: b, gender: c, address:d = "china" } = obj;
console.log(a, b, c); // 孙悟空 18 男 china

对象的序列化

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
40
41
42
43
44
45
/* 
对象的序列化
- JS中的对象使用时都是存在于计算机的内存中的
- 序列化指将对象转换为一个可以存储的格式
在JS中对象的序列化通常是将一个对象转换为字符串(JSON字符串)
- 序列化的用途(对象转换为字符串有什么用):
- 对象转换为字符串后,可以将字符串在不同的语言之间进行传递
甚至人可以直接对字符串进行读写操作,使得JS对象可以不同的语言之间传递
- 用途:
1.作为数据交换的格式
2.用来编写配置文件
- 如何进行序列化:
- 在JS中有一个工具类 JSON(JavaScript Object Notation) JS对象表示法
- JS对象序列化后会转换为一个字符串,这个字符串我们称为JSON字符串

JSON.stringify() 可以将一个对象转换为JSON字符串
JSON.parse() 可以将一个JSON字符串转换为一个对象

- 也可以手动的编写JSON字符串,在很多程序的配置文件就是使用JSON编写的
- 编写JSON的注意事项:
1.JSON字符串有两种类型:
JSON对象 {}
JSON数组 []
2.JSON字符串的属性名必须使用双引号
3.JSON中可以使用的属性值(元素)
- 数字(Number)
- 字符串(String) 必须使用双引号
- 布尔值(Boolean)
- 空值(Null)
- 对象(Object {})
- 数组(Array [])
4.JSON的格式和JS对象的格式基本上一致的,
注意:JSON字符串如果属性是最后一个,则不要加,
*/

const obj = {
name: "孙悟空",
age: 18
}

const str = JSON.stringify(obj);
console.log(str); // {"name":"孙悟空","age":18}

const obj2 = JSON.parse(str);
console.log(obj2);

Map

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
Map
- Map用来存储键值对结构的数据(key-value)
- Object中存储的数据就可以认为是一种键值对结构
- Map和Object的主要区别
- Object中的属性名只能是字符串或符号,如果传递了一个其他类型的属性名,
JS解释器会自动将其转换为字符串
- Map中任何类型的值都可以称为数据的key

创建:
new Map()

属性和方法:
map.size() 获取map中键值对的数量
map.set(key, value) 向map中添加键值对
map.get(key) 根据key获取值
map.delete(key) 删除指定数据
map.has(key) 检查map中是否包含指定键
map.clear() 删除全部的键值对
map.keys() 获取map的所有的key
map.value() 获取map的所有的value
*/

const map = new Map();
const obj = { name: "孙悟空" }

map.set(obj, "haha");
map.set(NaN, "xixi");
console.log(map.get(NaN)); // "xixi"
map.delete(NaN);
console.log(map.has(obj)); // true
map.clear();

const map2 = new Map();
map2.set("name", "孙悟空");
map2.set({}, "hehe");
// 将map转换为数组
const arr = Array.from(map2);
const arr2 = [...map2];
console.log(arr, arr2);

/*
- 和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的

那么和Map有什么区别呢?
- 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key
- 区别二:WeakMap的key对对象的引用是弱引用,如果没有其他的引用来引用这个对象,那么GC可以回收该对象

- WeakMap常见的方法有4个:
- set(key,value):在Map中添加key、value,并且返回整个Map对象
- get(key):根据key获取Map中的value
- has(key):判断是否包括某一个key,返回Boolean类型
- delete(key):根据key删除一个键值对,返回Boolean类型
*/

Set

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
Set
- Set用来创建一个集合
- 它的功能和数组类似,不同点在于set中不能存储重复的数据
- 可以用Set来去重

- 使用方式:
创建
- new Set()
- new Set([...])

Set常见的属性
size:返回Set中元素的个数

Set常用的方法:
add(value):添加某个元素,返回Set对象本身
delete(value):从set中删除和这个值相等的元素,返回boolean类型
has(value):判断set中是否存在某个元素,返回boolean类型
clear():清空set中所有的元素,没有返回值
forEach():通过forEach遍历set
*/

// 创建一个Set
const set = new Set();

// 向set中添加数据
set.add(10);
set.add("孙悟空");
set.add(10);
console.log(set); // {10, '孙悟空'}

// 使用set对数组进行去重
const num = [1, 2, 3, 4, 5, 6, 2, 3, 4, 3, 2];
const newNumSet = new Set(num);
const newNum = Array.from(newNumSet); // Array.from-将可迭代或类数组对象创建一个新的浅拷贝的数组实例。
console.log(newNum); // [1, 2, 3, 4, 5, 6]

/*
- 和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构
- 那么和Set有什么区别呢?
- 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型
- 区别二:WeakSet对元素的引用是弱引用,如果没有其他类型引用对某个对象进行引用,那么GC可以对该对象进行回收
- WeakSet常见的方法
- add(value):添加某个元素,返回WeakSet对象本身
- delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型
- has(value):判断WeakSet中是否存在某个元素,返回值boolean类型
- WeakSet不能遍历
- 因为WeakSet只能对对象的弱引用,如果我们遍历获取到其中的元素,那么可能造成对象不能正常销毁
- 所以存储到WeakSet中的对象是没办法获取的
*/

const obj = {}
const weakSet = new WeakSet();
weakSet.add(obj);

Math

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
Math
- Math是一个工具类
- Math中为我们提供了数学运算相关的常量和方法
- 常量:
Math.PI
- 方法;
Math.abs() 求一个数的绝对值
Math.min() 求多个值中的最小值
Math.max() 求多个值中的最大值
Math.pow() 求x的y次幂 等价于**
Math.sqrt() 求一个数的平方根

Math.floor() 向下取整
Math.ceil() 向上取整
Math.round() 四舍五入取整
Math.trunc() 直接去除小数位

Math.random() 生成随机数[0,1)
*/

Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
Date
- 在JS中所有的和时间相关的数据都由Date对象来表示
- 对象的方法:
getFullYear() 获取4位年份
getMonth() 返回当前日期的月份(0-11)
getDate() 返回当前今天是几日
getDay() 返回当前日期是周几(0-6) 0 表示周日
getTime() 返回当前日期对象的时间戳
时间戳:自1970年1月1日0时0分0秒到当前时间所经历的毫秒数
计算机底层存储时间时,使用的是时间戳
Date.now() 获取当前的时间戳
*/

let d = new Date(); // 直接通过new Date()创建时间对象时,它创建的是当前的时间的对象
console.log(d);
console.log(d.getFullYear());
console.log(d.getMonth());
console.log(d.getDate());
console.log(d.getDay());
console.log(d.getTime());

日期的格式化

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
Date
- 在JS中所有的和时间相关的数据都由Date对象来表示
- 对象的方法:
getFullYear() 获取4位年份
getMonth() 返回当前日期的月份(0-11)
getDate() 返回当前是几日
getDay() 返回当前日期是周几(0-6) 0表示周日
.....

getTime() 返回当前日期对象的时间戳
时间戳:自1970年1月1日0时0分0秒到当前时间所经历的毫秒数
计算机底层存储时间时,使用都是时间戳
Data.now() 获取当前的时间戳

toLocaleString()
- 可以将一个日期转换为本地时间格式的字符串
- 参数:
1.描述语言和国家信息的字符串
zh-CN 中文中国
zh-HK 中文香港
en-US 英文美国
2.需要一个对象作为参数,在对象中可以通过对象的属性来对日期的格式进行配置
dateStyle 日期的风格
timeStyle 时间的风格
full
long
medium
short
hour12 是否采用12小时制
true
false
weekday 星期的显示方式
long
short
narrow
year
numeric
2-dagit
*/

const d = new Date();
let result = d.toLocaleDateString();
let result2 = d.toLocaleTimeString();
let result3 = d.toLocaleString()
console.log(result); // 2022/12/19
console.log(result2); // 10:25:29
console.log(result3); // 2022/12/19 10:25:29
let result4 = d.toLocaleString("zh-CN", {
year: "numeric",
month: "long",
day: "2-digit",
weekday: "short"
});
console.log(result4); // 2022年12月19日周一

包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
在JS中,除了直接创建原始值外,也可以创建原始值的对象
通过 new String() 可以创建String类型的对象
通过 new Number() 可以创建Number类型的对象
通过 new Boolean() 可以创建Boolean类型的对象
- 但是千万不要这么用

包装类:
JS中一共有5个包装类
String -- 字符串包装为String对象
Number -- 数值包装为Number对象
Boolean -- 布尔值包装为Boolean对象
BigInt -- 大整数包装为BigInt对象
Symbol -- 符号包装为Symbol对象
- 通过包装类可以将一个原始值包装为一个对象
当我们对一个原始值调用方法或属性时,JS解释器会临时将原始值包装为对应的对象
然后调用这个对象的属性或方法

- 由于原始值会被临时转换为对应的对象,这就意味着对象中的方法都可以直接通过原始值来调用
*/

let num = 11;
num = num.toString();
console.log(typeof num); // string

字符串方法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* 
字符串:
- 字符串其本质就是一个字符数组
- "hello" -- ["h","e","l","l","o"]
- 字符串的很多方法都和数组是非常类似的
- 属性和方法:
length 获取字符串的长度
字符串[索引] 获取指定位置的字符
str.at() (实验方法)
- 根据索引获取字符,可以接受负索引
str.chatAt()
- 根据索引获取字符
str.concat()
- 用来连接两个或多个字符串
str.includes("内容",查询起始位置)
- 用来检查字符串中是否含某个内容
有返回true
没有返回false
str.indexOf()
str.lastIndexOf()
- 查询字符串中是否包含某个类容
str.startsWith()
- 检查一个字符串是否以指定内容开头
str.endsWith()
- 检查一个字符串是否以指定内容结尾
str.padStart()
str.padEnd()
- 通过添加指定的内容,使字符串保持某个长度
str.replace()
- 使用一个新的字符串替换一个指定内容
str.replaceAll()
- 使用一个新字符串替换所有指定内容
str.slice()
- 对字符串进行切片
str.substring()
- 截取字符串
str.split()
- 用来将一个字符串拆分为一个数组
str.toLowerCase()
- 将字符串转换为小写
str.toUpperCase()
- 将字符串转换为大写
str.trim()
- 去除前后空格
str.trimStart()
- 去除开始空格
str.trimEnd()
- 去除结束空格
*/

let str = "hello";

// length
console.log(str.length); // 5

// at()
console.log(str.at(-1)); // o

// includes()
let str2 = "hello how are you";
console.log(str2.includes("how", 2)); // true

// padStart()
let str3 = "1";
console.log(str3.padStart(5, "0")); // 00001

// replace()
let str4 = "hello hello how are you";
console.log(str4.replace("hello", "happy")); // happy hello how are you

// slice()
console.log(str4.slice(0, 5)); // hello

// split()
let str5 = "sadf@kjh@hkj@hkefj@hfd";
console.log(str5.split("@")); // ['sadf', 'kjh', 'hkj', 'hkefj', 'hfd']

正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 
正则表达式
- 正则表达式用来定义一个规制
- 通过这个规制计算机可以检查一个字符串是否符合规制
或者字符串中符合规制的内容提取出来
- 正则表达式也是JS中的一个对象,所以要使用正则表达式,需要先创建正则表达式的对象

通过构造函数来创建一个正则表达式的对象
new RegExp() 可以接收两个参数(字符串) 1.正则表达式 2.匹配模式
使用字面量来创建正则表达式: /正则/匹配模式

/a/ 表示,检查一个字符串是否有a
*/

let reg = new RegExp("a", "i"); // 通过构造函数来创建一个正则表达式的对象
let reg2 = /a/i;
console.log(reg, reg2);
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* 
1.在正则表达式中大部分字符串都可以直接写
2. | 在正则表达式中表示或
3.[] 表示或(字符集)
[a-z] 任意的小写字母
[A-Z] 任意的大写字母
[a-zA-Z] 任意的字母
[0-9] 任意数字
4.[^] 表示除了
[^x] 除了x
5. . 表示除了换行符外的任意字符
6.在正则表达式中使用\作为转义字符
7.其他的字符集
\w 任意单词字符 [A-Za-z0-9_]
\W 除了单词字符 [^A-Za-z0-9_]
\d 任意数字 [0-9]
\D 除了数字 [^0-9]
\s 空格
\S 除了空格
\b 单词边界
\B 除了单词边界
8.开头和结尾
^ 表示字符串的开头
$ 表示字符串的结尾

i 匹配模式i表示忽略大小写

量词
{m} 正好m个
{m,} 至少m个
{m,n} m-n个
+ 一个以上,相当于{1,}
* 任意数量,有没有都行
? 0-1次,相当于{0,1}
*/

let reg = /ab/;
console.log(reg.test("abc")); // true

let reg2 = /a|b/;
console.log(reg2.test("ac")); // true

let reg3 = /[a-z]/;
console.log(reg3.test("ac")); // true

let reg4 = /[^a-z]/;
console.log(reg4.test("ac")); // false

let reg5 = /\w/;
console.log(reg5.test("ac")); // true

let reg6 = /^a/;
console.log(reg6.test("ba")); // false

let reg7 = /a$/;
console.log(reg7.test("ba")); // true

let reg8 = /a{3}/;
console.log(reg8.test("aaa")); // true

let reg9 = /^[a-z]{1,4}$/;
console.log(reg9.test("aaajjj")); // false

/*
exec()
- 获取字符串中符合正则表达式的内容

g 表示全局匹配
*/

let str = "abcaecafcacc";
let re = /a[a-z]c/ig;
console.log(re.exec(str));
console.log(re.exec(str));


/*
asgia15345678911ogdsjgoegri18745632111ndsothh14158296311aegrjesg

用自己的语言来描述出来
1 [3-9] 任意数字*9
*/

let str2 = "asgia15345678911ogdsjgoegri18745632111ndsothh14158296311aegrjesg";
// let re2 = /1[3-9]\d{9}/g;
let re2 = /(1[3-9]\d)\d{4}(\d{4})/g;

let result;
while (result = re2.exec(str2)) {
console.log(result[0], result[1], result[2]);
console.log(result[1] + "****" + result[2]);
}

字符串的正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
split()
- 可以根据正则表达式来对一个字符串进行拆分
search()
- 可以去搜索符合正则表达式的内容第一次在字符串出现的位置
replace()
- 根据正则表达式替换字符串中指定内容
match()
- 根据正则表达式去匹配字符串中符合要求的内容
matchAll()
- 根据正则表达式去匹配字符串中符合要求的内容(必须设置g 全局配置)
- 它返回的是一个迭代器
- 和exec() 一样

*/

let str = "孙悟空abc猪八戒adc沙和尚"
console.log(str.split(/a[bd]c/)); // ['孙悟空', '猪八戒', '沙和尚']

let str2 = "asgia15345678911ogdsjgoegri18745632111ndsothh14158296311aegrjesg";
console.log(str2.search(/1[3-9]\d{9}/)); // 5
console.log(str2.replace(/1[3-9]\d{9}/g, "哈哈哈")); // asgia哈哈哈ogdsjgoegri哈哈哈ndsothh哈哈哈aegrjesg
console.log(str2.match(/1[3-9]\d{9}/g)); // ['15345678911', '18745632111', '14158296311']

垃圾回收

1
2
3
4
5
6
7
8
9
10
/*
垃圾回收(Garbage collection)
- 和生活一样,生活时间长了以后会产生生活垃圾
程序运行一段时间后也会产生垃圾
- 在程序的世界中,什么是垃圾?
- 如果一个对象没有任何的变量对其进行引用,那么这个对象就是一个垃圾
- 垃圾对象的存在,会严重的影响程序的性能
- 在JS中有自动的垃圾回收机制,这些垃圾对象会被解释器自动回收,我们无需手动处理
- 对于垃圾回收来说,我们唯一能做的事情就是将不再使用的变量设置为null
*/

Proxy

监听对象属性操作(Vue2原理)

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 需求:监听对象属性的所有操作
// 1.针对一个属性
const obj = {
name: "ear",
age: 18
}

let _name = obj.name;
Object.defineProperty(obj, "name", {
set: function (newValue) {
console.log("监听:给name设置了新的值:", newValue);
_name = newValue;
},
get: function () {
console.log("监听:获取name值");
return _name;
}
});

console.log(obj.name); // 监听:获取name值 ear
obj.name = "sunwukong"; // 监听:给name设置了新的值: sunwukong
console.log(obj.name); // 监听:获取name值 sunwukong

console.log("------------------------------");


// 2.监听所有的属性,遍历所有的属性,对每一个属性使用defineProperty
const obj2 = {
name: "ear",
age: 18
}
const keys = Object.keys(obj2);
keys.forEach(ele => {
let value = obj2[ele];
Object.defineProperty(obj2, ele, {
set: function (newValue) {
console.log(`监听:给${ele}设置了新的值:`, newValue);
value = newValue;
},
get: function () {
console.log(`监听:获取${ele}值`);
return value;
}
})
});
obj2.name = "sunwukong"; // 监听:给name设置了新的值: sunwukong
console.log(obj2.name); // 监听:获取name值 sunwukong
obj2.age = 16; // 监听:给name设置了新的值: 16


/*
使用Object.dedefineProperty有什么缺点呢?
首先,Object.dedefineProperty设计的初衷,不是为了去监听一个对象中的所有的属性的
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.dedefineProperty是无能为力的
*/

监听对象属性操作(Vue3原理)

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
40
41
/* 
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的
- 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以创建一个代理对象(Proxy对象)
- 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

我们将以前通过Object.defineProperty的案例来实现一次
- 首先,我们需要new Proxy对象,并且传入一个侦听的对象以及一个处理对象,可以称之为handler
const p = new Proxy(target,handler)
- 其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听

如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap)
- set函数有四个参数:
target:目标对象(侦听的对象)
property:将被设置的属性key
value:新属性值
receiver:调用的代理对象
- get函数有三个参数
target:目标对象(侦听的对象)
property:被获取的属性key
receiver:调用的代理对象
*/

const obj = {
name: "ear",
age: 18
}

const objProxy = new Proxy(obj, {
get(target, key) {
console.log(`监听${key}的获取`);
return target[key];
},
set(target, key, newValue) {
console.log(`监听${key}的设置`);
target[key] = newValue;
}
});

console.log(objProxy.name); // 监听name的获取 ear
objProxy.age = 17; // 监听age的设置
console.log(objProxy.age); // 监听age的设置 17

其他捕获器

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* 
13个捕获器
- handler.getPrototypeOf()
Object.getPrototypeOf 方法的捕获器
- handler.setPrototypeOf()
Object.setPrototypeOf() 方法的捕获器
- handler.isExtensible()
Object.isExtensible 方法的捕获器(判断是否可以新增属性)
- handler.preventExtensions()
Object.preventExtensions() 方法的捕获器
- handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 方法的捕获器
- handler.defineProperty()
Object.defineProperty 方法的捕获器
- handler.ownKeys()
Object.getOwnPropertyNames 方法和Object.getOwnPropertySymbols 方法的捕获器
- handler.has()
in 操作符的捕获器
- handler.get()
属性读取操作的捕获器
- handler.set()
属性设置操作的捕获器
- handler。deleteProperty()
delete操作符的捕获器
- handler.apply()
函数调用操作的捕获器
- handler.construct()
new 操作符的捕获器
*/
const obj = {
name: "ear",
age: 18
}

const objProxy = new Proxy(obj, {
get(target, key) {
console.log(`监听${key}的获取`);
return target[key];
},
set(target, key, newValue) {
console.log(`监听${key}的设置`);
target[key] = newValue;
},
deleteProperty(target, key) {
console.log(`监听${key}的删除`);
delete target[key];
},
has(target, key) {
console.log(`监听in判断${key}属性`);
return key in target;
}
});

delete objProxy.age; // 监听age的删除
console.log(objProxy); // Proxy(Object) {name: 'ear'}
console.log("name" in objProxy); // 监听in判断name属性 true

Reflect

Reflect和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
/* 
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射
那么这个Reflect有什么用呢?
- 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
- 比如Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf()
- 比如Reflect.defineProperty(target,propertyKey,attributes)类似于Object.defineProperty()
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
- 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以这些API放到了Object上面
- 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适
- 另外还包含一些类似于in、delete操作符,让JS看起来是会有些奇怪的
- 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上
- 另外在使用Proxy时,可以做到不操作原对象
*/
const obj = {
name: "ear",
age: 18
}

Object.defineProperty(obj, "name", {
configurable: false
});
// Reflect.defineProperty

// 1.用以前的方式进行操作
// delete obj.name; // 严格模式下还会报错
// if (obj.name) {
// console.log("name没有删除成功");
// } else {
// console.log("name删除成功");
// }

// 2.Reflect
if (Reflect.deleteProperty(obj, "name")) {
console.log("name删除成功");
} else {
console.log("name没有删除成功");
}

Reflect和Proxy共同完成代理

1
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
40
41
42
43
44
/* 
Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个
*/

const obj = {
_address: "China",
name: "why",
age: 18,
get address() {
return this._address;
},
set address(newValue) {
/*
如果传receiver,就是Proxy(Object) {_address: 'China', name: 'jiang', age: 18}
不传,就是{_address: 'China', name: 'jiang', age: 18}
*/
console.log("this:", this);
this._address = newValue;
}
}

const objProxy = new Proxy(obj, {
set(target, key, newValue, receiver) {
// target[key] = newValue // 直接操作原对象,不推荐

// 1.好处一:代理对象的目的,不再直接操作原对象
// 2.好处二:Reflect.set方法有返回Boolean值,可以判断本次操作是否成功
/*
3.好处三:
- receiver就是Proxy对象
- Reflect.set/get最后一个参数,可以决定对象访问器setter/getter的this指向
*/
console.log("proxy中设置方法被调用");
const isSuccess = Reflect.set(target, key, newValue, receiver);
if (!isSuccess) throw new Error(`set ${key} failure`);
},
get(target, key, receiver) {
console.log("proxy中获取方法被调用");
return Reflect.get(target, key, receiver);
}
});

objProxy.address = "USA"; // proxy中设置方法被调用 * 2
console.log(objProxy.address); // proxy中设置方法被调用 * 2 USA

Promise

传统解决异步和Promise解决异步的区别

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
40
41
42
43
44
45
46
47
48
49
// 使用回调函数来处理异步,没有统一的规范
function execCode(counter, successCallBack, failureCallBack) {
setTimeout(() => {
if (counter > 0) {
let total = 0;
for (let i = 0; i < counter; i++) {
total += i;
}

successCallBack(total);
} else {
failureCallBack(`${counter}值有问题`);
}
}, 3000);
}

execCode(100, value => {
console.log("执行成功:", value);
}, (err) => {
console.log("执行失败:", err);
});


// 使用Promise来处理异步
function execCode(counter) {
return new Promise((resolve, reject) => {
// 异步任务
setTimeout(() => {
if (counter > 0) {
let total = 0;
for (let i = 0; i < counter; i++) {
total += i;
}

// 成功的回调
resolve(total);
} else {
// 失败的回调
reject(`${counter}有问题`);
}
}, 3000);
});
}

execCode(100).then(res => {
console.log("成功:", res);
}).catch(err => {
console.log("失败:", err);
});

Promise的各种状态区分

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
/* 
在Promise使用过程中,我们可以将它划分成三个状态
- 待定(pending)初始状态,既没有被兑现,也没有被拒绝
当执行executor中的代码时,处于该状态
- 已兑现(fulfilled):意味着操作成功完成
执行resolve时,处于该状态,Promise已经被兑现
- 已拒绝(rejected):意味着操作失败
执行reject时,处于该状态,Promise已经被拒绝
*/

const promise = new Promise((resolve, reject) => {
// Exector是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数

// 一旦状态被确定下来,Promise的状态会被锁死,该Promise的状态是不可更改的

// 1.待定状态pending
console.log("待定状态");

// 2.兑现状态 fulfilled
resolve();

// 3.拒绝状态 rejected
reject();
});

promise.then(value => {
console.log("成功的回调");
}).catch(err => {
console.log("失败的回调");
});

Promise中resolve的值

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
const p = new Promise(resolve => {
setTimeout(resolve("p的resolve"), 2000);
});

const promise = new Promise((resolve, reject) => {
// 1.情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数
resolve(123);
resolve(false);
resolve({ name: "ear" });

// 2.情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态
resolve(p); // then中拿到的结果: p的resolve

// 3.情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态
resolve({
name: "ear",
then(resolve) {
resolve("对象中的then-resolve"); // then中拿到的结果: p的resolve
}
})
});

promise.then(res => {
console.log("then中拿到的结果:", res);
})

Promise中then和catch的调度

1
2
3
4
5
6
7
8
const promise = new Promise((resolve, reject) => {
reject("failure");
});

// promise.then(res => console.log(res)); // 会报错 Uncaught (in promise) failure

promise.then(res => console.log(res))
.catch(err => console.log(err)); // 最好then和catch一起写上

Promise中reject和error

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
40
41
42
43
44
45
const promise1 = new Promise((resolve, reject) => {
throw new Error("错误,Error");
console.log(123);
});

promise1.then(res => console.log(res))
.catch(err => console.log(err)); // 错误,reject


const promise2 = new Promise((resolve, reject) => {
reject("错误,reject");
console.log(123);
})

promise2.then(res => console.log(res))
.catch(err => console.log(err)); // 123 错误,reject

/*
键区别在于 throw 语句会立即中断执行函数的后续代码,而显式调用 reject 不会中断代码的执行。

让我们逐步分析两个代码片段的行为:

第一段代码:
const a = new Promise((resolve, reject) => {
throw new Error("错误,Error"); // 抛出异常,代码执行立即中断
console.log(123); // 不会执行
});
当遇到 throw 时,会立即中断执行函数的后续代码,相当于函数立刻返回。
console.log(123) 永远不会被执行。
Promise 状态直接变为 rejected,catch 捕获错误。

第二段代码:
const a = new Promise((resolve, reject) => {
reject("错误,reject"); // 调用 reject,不会中断代码执行
console.log(123); // 会继续执行
});
reject 是一个普通函数调用,它会将 Promise 状态变为 rejected,但不会影响函数后续代码的执行。
因此,console.log(123) 会被正常执行。

总结:
throw 是语法级中断:
抛出异常后,函数的执行立即停止,后续代码不会运行。
reject 是一个函数调用:
不会中断当前代码的执行流。
*/

Promise中then的返回值

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
const promise = new Promise((resolve, reject) => {
resolve("1111111");
});

const newPromise = new Promise((resolve, reject) => {
setTimeout(resolve("新的Promise"), 3000);
});

// 1.then方法是返回一个新的Promise,这个新Promise的决议是等到then方法传入的回调函数有返回值时,才进行决议
promise.then(res => {
console.log("第一个then方法:", res); // 第一个then方法: 1111111
return "2222222";
}).then(res => {
console.log("第二个then方法:", res); // 第二个then方法: 2222222
// 普通值
return "普通值";
}).then(res => {
console.log("第三个then方法:", res); // 第三个then方法: 普通值
// 新的Promise
return newPromise;
}).then(res => {
console.log("第四个then方法", res); // 第四个then方法 新的Promise
// thenable值
return {
then(resolve) {
resolve("thenable值");
}
}
}).then(res => console.log("第五个then方法", res)); // 第五个then方法 thenable值

Promise中catch的返回值

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
const promise = new Promise((resovle, reject) => {
reject("1111111");
});

// 1.catch方法也会返回一个新的Promise
promise.then(res => {
console.log("第一个then的回调", res); // 没有执行
}).catch(err => {
console.log("catch回调", err); // catch回调 1111111
return "2222222";
}).then(res => {
console.log("then的第一个回调", res); // then的第一个回调 2222222
});


const promise2 = new Promise((resolve, reject) => {
resolve("aaaaaaa");
});

// 2.catch方法的执行时机
promise2.then(res => {
console.log("then的第一次回调", res); // then的第一个回调 2222222
throw new Error("第一个then的异常error");
return "bbbbbbb";
}).then(res => {
console.log("then的第二次回调", res); // 没有执行
throw new Error("第二个then的异常error");
return "ccccccc"
}).catch(err => {
console.log("catch回调执行", err); // catch回调执行 Error: 第一个then的异常error
});

/*
中断函数继续执行:
1.retrue
2.throw new Error()
3.yield 暂停(暂时性的中断)
*/

Promise中finally的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise = new Promise((resolve, reject) => {
// reslove("111111");
reject("2222222");
});

promise.then(res => {
console.log("then的回调", res);
}).catch(err => {
console.log("catch的回调", err);
}).finally(() => {
/*
finally方法是不接受参数的,因为无论前面是fulfilled状态,还是rejected状态,它都会执行
*/
console.log("finally的回调"); // resolve和reject都会执行
});

Promise的类方法

1
2
3
4
5
6
7
8
9
10
11
12
/* resolve */
const promise = Promise.resolve("1111111");
// 相当于
// new Promise(resolve =>resolve("1111111"));
promise.then(res => console.log("then的结果:", res)); // then的结果: 1111111


/* reject */
const promise2 = Promise.reject("2222222");
// 相当于
// new Promise((_,reject)=>reject("2222222"));
promise2.catch(err => console.log("err的结果:", err)); // err的结果: 2222222
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve("p1 resolve");
reject("p1 reject");
}, 2000);
});

const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p1 resolve");
}, 3000);
});

const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p1 resolve");
}, 10000);
});

/*
Promise.all
- 将多个Promise包裹在一起形成一个新的Promise
- 新的Promise状态由包裹的所有Promise共同决定
- 当所有的Promise状态变为fulfilled状态时,新的Promise状态为fulfilled,并且将所有Promise的返回值组成一个数组
- 当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数,马上返回,不会等待其他Promise
*/
Promise.all([p1, p2, p3]).then(res => {
console.log("all promise res:", res); // all promise res: ['p1 resolve', 'p1 resolve', 'p1 resolve']
}).catch(err => {
console.log("all promise err:", err); // all promise err: p1 reject
});


/*
Promise.allSettled
- 该方法会在所有的Promise都有结果,无论时fulfilled,还是rejected时,才会有最终的状态,通过一个对象数组返回,这个对象中包含status状态,以及对应的value值
- 这个Promise的结果一定时fulfilled
*/
Promise.allSettled([p1, p2, p3]).then(res => {
console.log("allSettled promise res:", res);
}).catch(err => {
console.log("allSettled promise err:", err);
});


/*
Promise.race
- 如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race
- 表示多个Promise相互竞争,谁有了结果,那么就使用谁的结果
*/
Promise.race([p1, p2, p3]).then(res => {
console.log("race promise res:", res);
}).catch(err => {
console.log("rece promise err:", err);
});


/*
Promise.any
- any方法会等到一个fulfilled状态,才会决定新Promise的状态,race是任何结果,无论fulfilled还是rejected
- 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态
- 如果所有的Promise都是reject的,那么会报一个AggregateError的错误
*/
Promise.any([p1, p2, p3]).then(res => {
console.log("any promise res:", res);
}).catch(err => {
console.log("any promise err:", err);
});

Iterator和Generator

迭代器

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
/* 
在JavaScript中,迭代器是一个具体的对象,这个对象需要符合迭代器协议
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
- 在JavaScript中这个标准就是一个特定的next方法

next方法如下的要求
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象
- done(Boolean)
- 如果迭代器可以生成序列中的下一个值,则为false(这等价于没有指定done这个属性)
- 如果迭代器已将序列迭代完毕,则为true,这种情况下,value是可选的,如果它依然存在,即为迭代结束之后的默认返回值
- value
- 迭代器返回的任何JavaScript值,done为true时可以省略
*/

const names = ["abc", "bca", "cab"];

const createArrayIterator = arr => {
let index = 0;
return {
next() {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done: true }
}
}
}
}

const namesInerator = createArrayIterator(names); // 这个namesInerator就是一个迭代器
console.log(namesInerator.next()); // {done: false, value: 'abc'}
console.log(namesInerator.next()); // {done: false, value: 'bca'}
console.log(namesInerator.next()); // {done: false, value: 'cab'}
console.log(namesInerator.next()); // {done: true}

可迭代对象-创建可迭代对象

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* 
可迭代对象
1.必须实现一个特定的函数:[Symbol.iterator]
2.这个函数需要返回一个迭代器(这个迭代器用于迭代当前的对象)
*/
const infos = {
friends: ["kobe", "james", "curry"],
[Symbol.iterator]: function () {
let index = 0;
const infosIterator = {
next() {
if (index < infos.friends.length) {
return { done: false, value: infos.friends[index++] }
} else {
return { done: true }
}
}
}

return infosIterator;
}
}

// 可迭代对象必然具备下面的特点
const iterator = infos[Symbol.iterator]();
console.log(iterator.next()); // {done: false, value: 'kobe'}
console.log(iterator.next()); // {done: false, value: 'james'}
console.log(iterator.next()); // {done: false, value: 'curry'}
console.log(iterator.next()); // {done: true}

// 可迭代对象可以进行 for of 操作
for (const item of infos) {
console.log(item); // kobe james curry
}

// 可迭代对象必然有一个[Symbol.iterator]函数
// 数组是一个可迭代对象
const students = ["张三", "李四", "王五"];
const studentIterator = students[Symbol.iterator]();
console.log(studentIterator.next()); // {value: '张三', done: false}
console.log(studentIterator.next()); // {value: '张三', done: false}
console.log(studentIterator.next()); // {value: '张三', done: false}
console.log(studentIterator.next()); // {value: undefined, done: true}


/*
优化可迭代对象
*/
const infos = {
friends: ["kobe", "james", "curry"],
[Symbol.iterator]: function () {
let index = 0;
const infosIterator = {
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}

return infosIterator;
}
}

可迭代对象的应用

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
/* 
JavaScript中语法:for...of 、 展开语法(spread syntax) 、 yield* 、 结构赋值
创建一些对象时:new Map(Iterable) 、new WeakMap(Iterable) 、new Set(Iterable) 、new WeakSet(iterable)
一些方法的调用:Promise.all(Iterable)、Promise.race(Iterable),Array.from(Iterable)
*/
const info = {
name: "why",
age: 18,
height: 1.88,
[Symbol.iterator]: function () {
const values = Object.values(this);
let index = 0;
const iterator = {
next: () => {
if (index < values.length) {
return { done: false, value: values[index++] }
} else {
return { done: true }
}
}
}

return iterator;
}
}

function foo(arg1, arg2, arg3) {
console.log(arg1, arg2, arg3);
}

foo(...info); // why 18 1.88


const set = new Set(["aaa", "bbb", "ccc"]);
const set2 = new Set("abc");
console.log(set); // Set(3) {'aaa', 'bbb', 'ccc'}
console.log(set2); // Set(3) {'a', 'b', 'c'}

自定义类的迭代

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
class Person {
constructor(name, age, height, friend) {
this.name = name;
this.age = age;
this.height = height;
this.friend = friend;
}

[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.friend.length) {
return { done: false, value: this.friend[index++] }
} else {
return { done: true }
}
}
}
}
}

const p1 = new Person("ear", 18, 1.88, ["a", "b", "c"]);

for (const item of p1) {
console.log(item); // a b c
}

迭代器的中断

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
class Person {
constructor(name, age, height, friend) {
this.name = name;
this.age = age;
this.height = height;
this.friend = friend;
}

[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.friend.length) {
return { done: false, value: this.friend[index++] }
} else {
return { done: true }
}
},

return: () => {
console.log("监听到迭代器中断了");
return { done: true }
}
}
}
}

const p1 = new Person("ear", 18, 1.88, ["a", "b", "c"]);

for (const item of p1) {
console.log(item);
// 迭代器在遍历过程中通过break、return、throw中断了循环操作会中断
if (item === "b") break;
}

生成器函数的基本使用

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/* 
生成器函数也是一个函数,但是和普通的函数有一些区别
首先,生成器函数需要在function的后边加一个符号:*
其次,生成器函数可以通过yield关键字来控制函数的执行流程
最后,生成器函数的返回值是一个Generator(生成器)
生成器事实上是一种特殊的迭代器
*/

/*
生成器函数:
1.function后边会跟上符号:*
2.代码的执行可以被yield控制
3.生成器函数默认在执行时,返回一个生成器对象
- 想要执行函数内部的代码,需要生成器对象,调用它的next操作
- 当遇到yield时,就会中断执行
*/

// 1.定义一个生成器函数
function* foo(name1) {
console.log("执行内部代码:1111", name1); // 执行内部代码:1111 next1
console.log("执行内部代码:2222", name1); // 执行内部代码:2222 next1
const name2 = yield "aaaa";
console.log("执行内部代码:3333", name2); // 执行内部代码:3333 next2
console.log("执行内部代码:4444", name2); // 执行内部代码:4444 next2
const name3 = yield "bbbb";
// return "中间用return中断";
console.log("执行内部代码:5555", name3); // 执行内部代码:5555 next3
console.log("执行内部代码:6666", name3); // 执行内部代码:6666 next3
}

// 2.调用生成器函数,返回一个生成器对象
const generator = foo("next1");
// 调用next方法
console.log(generator.next()); // {value: 'aaaa', done: false}
console.log(generator.next("next2")); // {value: 'bbbb', done: false}
console.log(generator.next("next3")); // {value: undefined, done: true}

// 如果在中间直接return中断
// console.log(generator.next()); // {value: 'aaaa', done: false}
// console.log(generator.next()); // {value: 'bbbb', done: true}
// console.log(generator.next()); // {value: undefined, done: true}
// console.log(generator.next()); // {value: undefined, done: true}


/*
生成器代替迭代器
*/
// 1.对之前的代码进行重构(用生成器函数)
const names = ["abc", "cab", "nba"];

function* createArrayIterator(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}

const namesIterator = createArrayIterator(names);
console.log(namesIterator.next()); // {value: 'abc', done: false}
console.log(namesIterator.next()); // {value: 'cab', done: false}
console.log(namesIterator.next()); // {value: 'nba', done: false}
console.log(namesIterator.next()); // {value: undefined, done: true}

// 2.生成器函数,生成一个范围内的值
function* createRangeGenerator(start, end) {
for (let i = start; i < end; i++) {
yield i;
}
}

const rangeGen = createRangeGenerator(3, 5);
console.log(rangeGen.next()); // {value: 3, done: false}
console.log(rangeGen.next()); // {value: 4, done: false}
console.log(rangeGen.next()); // {value: undefined, done: true}

生成器yield的语法糖

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
// 1.使用yield*替换之前的方案
const names = ["abc", "cba", "nba"];

function* createArrayIterator(arr) {
/*
使用yield*来生产一个可迭代对象
- 这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值
*/
yield* arr;
}

const nameIterator = createArrayIterator(names);
console.log(nameIterator.next()); // {value: 'abc', done: false}
console.log(nameIterator.next()); // {value: 'cba', done: false}
console.log(nameIterator.next()); // {value: 'nba', done: false}
console.log(nameIterator.next()); // {value: undefined, done: true}

// 2.yield替换类中的实现
class Person {
constructor(name, age, height, friends) {
this.name = name;
this.age = age;
this.height = height;
this.friends = friends;
}

// 实现方法
*[Symbol.iterator]() {
yield* this.friends;
}
}

const p = new Person("ear", 18, 1.88, ["kobe", "james", "curry"]);
for (const item of p) {
console.log(item); // kobe james curry
}

异步函数async-await

异步函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function foo() {
// 1.返回一个普通值
return 123; // Promise.resolve(123);

// 2.返回一个Promise
// return new Promise((resolve,reject)=>{
// setTimeout(() => {
// resolve("返回一个Promise");
// }, 2000);
// });

// 3.返回一个thenable值
// return {
// then(resolve,reject){
// resolve("返回thenable值");
// }
// }
}

foo().then(res => {
console.log("res:", res);
});

异步函数的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 什么情况下异步函数的结果是rejected
async function foo() {
// 1.通过返回Promise的时候设置reject值
// return new Promise((_, reject) => {
// reject("err reject");
// });

// 2.如果异步函数中抛出异常(产生了错误),这个异常不会被立即处理,而是进行 Promise.reject(err)
throw new Error("ear async function error");
}

foo().then(res => {
console.log("res:", res);
}).catch(err => {
console.log("ear err", err);
});

await的使用

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
/* 
await关键字有什么特点
- 通常使用await后边会跟上一个表达式,这个表达式会返回一个Promise
- 那么这个await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数
*/

function bar() {
return new Promise(resolve => {
setTimeout(() => {
resolve(123);
}, 2000);
});
}

async function foo() {
try {
const res1 = await bar();
console.log("res1:", res1);
const res2 = await bar();
console.log("res2:", res2);
} catch (error) {
console.log("err:", error);
}
}

foo();

异步代码如何执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式
线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
*/


/*
浏览器是一个进程吗?它里面只有一个线程吗?
- 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止
一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
- 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程

JavaScript的代码执行是在一个单独的线程中执行的
- 这就意味着JavaScript的代码,在同一时刻只能做一件事
- 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

所以有些耗时的操作,实际上并不是由JavaScript线程在执行的
- 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作
- 比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可
*/

事件循环


微任务和宏任务的区别

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
/* 
事件循环中并非只维护着一个队列,事实上是有两个队列
- 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
- 微任务队列(microtask queue):Promise的then回调、Mutation Observer API、queueMicrotask()等

事件循环对于两个队列的优先级是怎么样的呢?
1.main script 中的代码先执行(编写的顶层script代码)
2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的
- 如果不为空,那么就优先执行微任务队列中的任务(回调)
*/

console.log("11111");
new Promise(resolve => {
// 这里的代码正常依次执行
console.log("22222");
resolve();
console.log("33333");
}).then(res => {
// Promise中then的回调会被添加到微任务队列中
console.log("44444");
});

console.log("55555");

/*
执行结果
11111
22222
33333
55555
44444
*/

代码执行顺序

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
 /*
console.log("script start");

function requestData(url) {
return new Promise(resolve => {
setTimeout(() => {
console.log("setTimeout");
resolve(url);
}, 2000);
});
}

function getData() {
console.log("getData start");
requestData("why").then(res => {
console.log("then1-res:", res);
});
console.log("getData end");
}

getData();

console.log("script end");
*/

/*
script start
getData start
getData end
script end
setTimeout
then1-res:why
*/


/*
console.log("script start");

function requestData(url) {
return new Promise(resolve => {
setTimeout(() => {
console.log("setTimeout");
resolve(url);
}, 2000);
});
}

async function getData() {
console.log("getData start");
const res = await requestData("why");
console.log("then1-res:", res);
console.log("getData end");
}

getData();

console.log("script end");
*/

/*
script start
getData start
script end
setTimeout
then1-res:why
getData end
*/


/*
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}

async function async2() {
console.log("async2");
}

console.log("script start");

setTimeout(() => {
console.log("setTimeout");
});

async1();

new Promise(resolve => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});

console.log("script end");
*/

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

防抖和节流

防抖函数

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
40
41
42
43
function eardebounce(fn, delay, immediate = false) {
// 1.用于记录上一次事件触发的timer
let timer = null;
let isInvoke = false;

// 2.触发事件时执行的函数
const _debounce = function (...args) {
return new Promise((resolve, reject) => {
try {
// 2.1 如果有再次触发(更多次触发)事件,那么取消上一次的事件
if (timer) clearTimeout(timer);

// 第一次操作不需要延迟
let res = undefined;
if (immediate && !isInvoke) {
res = fn.apply(this, args);
resolve(res);
isInvoke = true;
return;
}

// 2.2 延迟去执行对应的fn函数(传入的回调函数)
timer = setTimeout(() => {
res = fn.apply(this, args);
resolve(res);
timer = null; // 执行过函数之后,将timer重置为null
isInvoke = false;
}, delay);
} catch (error) {
reject(error);
}
});
}

_debounce.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
isInvoke = false;
}

// 返回一个新的函数
return _debounce;
}

节流函数

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
40
41
42
43
44
45
46
47
48
49
function earthrottle(fn, interval, { leading = true, trailing = true } = {}) {
let startTime = 0;
let timer = null;

const _throttle = function (...args) {
return new Promise((resolve, reject) => {
try {
// 1.获取当前时间
const nowTime = new Date().getTime();

// 对立即执行进行控制
if (!leading && startTime === 0) {
startTime = nowTime;
}

// 2.计算需要等待的时间执行函数
const waitTime = interval - (nowTime - startTime);
if (waitTime <= 0) {
if (timer) clearTimeout(timer);
const res = fn.apply(this, args);
resolve(res);
startTime = nowTime;
timer = null;
return;
}

// 3.判断是否需要执行尾部
if (trailing && !timer) {
timer = setTimeout(() => {
const res = fn.apply(this, args);
resolve(res);
startTime = new Date().getTime();
timer = null;
}, waitTime);
}
} catch (error) {
reject(error);
}
})
}

_throttle.cancel = function () {
if (timer) clearTimeout(timer);
startTime = 0;
timer = null;
}

return _throttle;
}

深拷贝

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/* 
JSON.parse可以来实现深拷贝
- 这种深拷贝的方式其实对于函数、Symbol等是无法处理的
- 并且如果存在对象的循环引用,也会报错的
*/

// 判断是否为对象
const isObject = value => {
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function");
}

// 深拷贝函数
const deepCopy = (originValue, map = new Map()) => {
// 0.如果值是Symbol
if (typeof originValue === "symbol") {
return Symbol(originValue.description);
}

// 1.如果是原始类型,直接返回
if (!isObject(originValue)) return originValue;

// 2.特殊类型 Set
if (originValue instanceof Set) {
const newSet = new Set();
for (const setItem of originValue) {
newSet.add(deepCopy(setItem, map));
}
return newSet;
}

// 3.特殊类型 Map
if (originValue instanceof Map) {
const newMap = new Map();
for (const [key, value] of originValue) {
newMap.set(key, deepCopy(value, map));
}
return newMap;
}

// 4.如果是函数function类型,不需要进行深拷贝
if (typeof originValue === "function") return originValue;

// 5.如果是对象类型,才需要创建对象
if (map.get(originValue)) return map.get(originValue);
const newObj = Array.isArray(originValue) ? [] : {};
map.set(originValue, newObj);
// 遍历普通的key
for (const key in originValue) {
newObj[key] = deepCopy(originValue[key], map);
}
// 单独遍历symbol
const symbolKeys = Object.getOwnPropertySymbols(originValue);
for (const symbolKey of symbolKeys) {
newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey], map);
}

return newObj;
}

const info = {
name: "ear",
age: 18,
friend: {
name: "kobe",
address: {
name: "洛杉矶",
detail: "斯坦普斯中心"
}
},
// 1.特殊类型 Set
set: new Set(["abc", "cba", "bac"]),

// 2.值的特殊类型:Symbol
symbolKey: Symbol("abc"),

// 3.key是symbol
[Symbol("s1")]: "aaa",
[Symbol("s2")]: "bbb"

// 4.方法,不需要拷贝
}

info.info = info;

const newObj = deepCopy(info);
console.log(newObj);
console.log(newObj === newObj.info);

EventBus

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class EarEventBus {
eventMap = {}

on(eventName, eventFn) {
let eventFns = this.eventMap[eventName];
if (!eventFns) {
eventFns = [];
this.eventMap[eventName] = eventFns;
}
eventFns.push(eventFn);
}

off(eventName, eventFn) {
let eventFns = this.eventMap[eventName];
if (!eventFns) return;
// eventFns.some((fn, i) => {
// if (fn === eventFn) {
// eventFns.splice(i, 1);
// return true;
// }
// });
this.eventMap[eventName] = eventFns.filter(fn => fn !== eventFn);

// 如果eventFns已经清空了
if (eventFns.length === 0) delete this.eventMap[eventName];
}

emit(eventName, ...args) {
let eventFns = this.eventMap[eventName];
if (!eventFns) return;
eventFns.forEach(fn => {
fn(...args);
});
}
}


const eventBus = new EarEventBus();

eventBus.on("navClick", (name, age, height) => {
console.log("navClick listener 01", name, age, height);
});

const click = () => {
console.log("navClick listener 02");
}
eventBus.on("navClick", click);

setTimeout(() => {
eventBus.off("navClick", click);
}, 5000);

const navBtnEl = document.querySelector(".nav-btn");
navBtnEl.onclick = () => {
eventBus.emit("navClick", "ear", 18, 1.88);
}

XHR-Fetch

XHR-XHR的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.创建XMLHttpRequest请求
const xhr = new XMLHttpRequest();

// 2.监听状态的改变(宏任务)
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;

// 将字符串转成JSON对象(js对象)
const resJSON = JSON.parse(xhr.response);
console.log(resJSON);
}

// 3.配置请求open
xhr.open("get", "http://123.207.32.32:8000/home/multidata");

// 4.发送请求(浏览器帮助发送对应请求)
xhr.send();

XHR-XML的响应数据与类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.创建XMLHttpRequest请求
const xhr = new XMLHttpRequest();

// 2.监听状态的改变(宏任务)
xhr.onload = () => {
console.log(xhr.response);
}

// 最常用,还有text、xml
xhr.responseType = "json";

// 3.配置请求open
xhr.open("get", "http://123.207.32.32:8000/home/multidata");

// 4.发送请求(浏览器帮助发送对应请求)
xhr.send();
  • 响应状态码:
6_请求方式

XHR-GET-POST请求传参

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
const formEl = document.querySelector(".info");
const sendBtn = document.querySelector(".send");
sendBtn.onclick = () => {
// 创建xhr对象
const xhr = new XMLHttpRequest();

// 监听数据响应
xhr.onload = () => {
console.log(xhr.response);
}

// 配置请求
xhr.responseType = "json";

// 1.方式一:get -> query
// xhr.open("get", "http://123.207.32.32:1888/02_param/get?name=ear&age=18");
// xhr.send();

// 2.方式二:post -> urlencoded
// xhr.open("post", "http://123.207.32.32:1888/02_param/posturl");
// xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// xhr.send("name=ear&age=18");

// 3.方式三:post -> formdata
// xhr.open("post", "http://123.207.32.32:1888/02_param/postform");
// // formElement对象转成FormData对象 可以不用设置Content-type
// const formData = new FormData(formEl);
// xhr.send(formData);

// 4.方式四:post -> json
xhr.open("post", "http://123.207.32.32:1888/02_param/postjson");
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify({ name: "ear", age: 18 }));
}
  • 常见请求方式
6_请求方式
  • content-type
6_请求方式
  • 数据常见传递方式
6_请求方式

XHR-AJAX网络请求封装Promise

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
40
41
42
43
44
45
46
47
48
49
const earAjax = ({
url,
method = "get",
data = {},
success,
failure
} = {}) => {
return new Promise((resolve, reject) => {
// 1.创建对象
const xhr = new XMLHttpRequest();

// 2.监听数据
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({ status: xhr.status, message: xhr.statusText });
}
}

// 3.设置类别
xhr.responseType = "json";

// 4.open方法
if (method.toUpperCase() === "GET") {
const queryStrings = [];
for (const key in data) {
queryStrings.push(`${key}=${data[key]}`);
}
url = url + "?" + queryStrings.join("&");
xhr.open(method, url);
xhr.send();
} else {
xhr.open(method, url);
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify(data));
}
});
}

earAjax({
url: "http://123.207.32.32:1888/02_param/get",
method: "GET",
data: {
name: "why",
age: 18
}
}).then(res => console.log("res:", res))
.catch(err => console.log("err:", err));

Fetch-Fetch的基本使用

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
const formData = new FormData();
formData.append("name", "ear");
formData.append("age", 18);
fetch("http://123.207.32.32:1888/02_param/postform", {
method: "post",
body: formData
}).then(res => res.json()).then(res => console.log(res));

fetch("http://123.207.32.32:1888/02_param/postjson", {
method: "post",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
name: "ear",
age: 18
})
}).then(res => res.json()).then(res => console.log(res));

fetch("http://123.207.32.32:1888/02_param/posturl", {
method: "post",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
body: "name=ear&age=18"
}).then(res => res.json()).then(res => console.log(res));

文件上传

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
const uploadBtn = document.querySelector(".upload");
// xhr
uploadBtn.onclick = () => {
const xhr = new XMLHttpRequest();

xhr.onload = () => {
console.log(xhr.response);
}

xhr.responseType = "json";
xhr.open("post", "http://123.207.32.32:1888/02_param/upload");

const fileEl = document.querySelector(".file");
const file = fileEl.files[0];

const formData = new FormData();
formData.append("avatar", file);

xhr.send(formData);
}

// fetch
uploadBtn.onclick = () => {
const fileEl = document.querySelector(".file");
const file = fileEl.files[0];

const formData = new FormData();
formData.append("avatar", file);

fetch("http://123.207.32.32:1888/02_param/upload", {
method: "post",
body: formData
}).then(res => res.json()).then(res => console.log(res));

ES6其他规范以及其他补充知识

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* padStart和padEnd */
const min = "15".padStart(2, "0");
const sec = "6".padStart(2, "0");
console.log(min + ":" + sec); // 15:06


/* FinalizationRegistry */
/*
FinalizationRegistry对象可以让你在对象被垃圾回收时请求一个回调
- FinalizationRegistry提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在
某个时间点上调用一个清理回调
- 可以通过调用register方法,注册任何你想要清除回调的对象,传入该对象和所含的值
*/
let obj = { name: "ear" }
const registey = new FinalizationRegistry(value => {
console.log("对象被销毁了", value);
});

registey.register(obj, "obj"); // 对象被销毁了 obj

obj = null;


/* 逻辑赋值运算符 */
function foo(message) {
// 1.||逻辑赋值运算符
// message = message || "默认值";
// message ||= "默认值";

// 2.??逻辑赋值运算符
// message = message ?? "默认值";
message ??= "默认值";

console.log(message);
}

foo("abc"); // abc
foo(); // 默认值


/* replaceAll */
const message = "my name is ear, ear age is 18";
const newMessage = message.replace("ear", "kobe");
const newMessage2 = message.replaceAll("ear", "kobe");
console.log(newMessage); // my name is kobe, ear age is 18
console.log(newMessage2); // my name is kobe, kobe age is 18


/* Object.hasOwn */
// 如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn() 返回 true。如果属性是继承的或者不存在,该方法返回 false。
const obj2 = {
name: "ear",
age: 18,
__proto__: {
address: "广安市"
}
}

console.log(obj2.hasOwnProperty("name")); // true
console.log(obj2.hasOwnProperty("address")); // false

// 推荐用这个
console.log(Object.hasOwn(obj2, "name")); // true
console.log(Object.hasOwn(obj2, "address")); // false

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
throw抛出一个异常
1.函数中的代码遇到throw之后,后续的代码都不会执行
2.throw抛出一个具体的错误信息
*/
function foo() {
console.log("foo function1");
// 1.number/string/boolean
// throw "这是一个错误"

// 2.抛出一个对象
// throw { errMessage:"我是错误信息", errCode:-1001}

// 3.Error类:错误函数的调用栈以及位置信息
throw new Error("我是错误信息");

console.log("foo function2");
}

foo();

Storage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
WebStorage主要提供了一种机制,可以让浏览器提供一种比cookie更直观的key、value存储方法:
localStorage:本地存储,提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留
sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除
*/

/*
Storage有如下的属性和方法:
属性:
Storage.length:只读属性
方法:
Storage.key(index):返回存储中的第n个key名称
Storage.getItem(key):返回key对应的value
Storage.setItem(key,value):会把key和value添加到存储中
Storage.removeItem(key):把该key从存储中删除
Storage.clear():清空存储中的所有key
*/

10.DOM


简介

1
2
3
4
5
6
7
/* 
要使用DOM来操作网页,我们需要浏览器至少得先给我一个对象
才能完成各种操作

所以浏览器已经为我们提供了一个document对象,它是一个全局变量可以直接使用
document代表得是整个的网页
*/

document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
document对象
- document对象表示的是整个网页
- document对象的原型链
HTMLDocument -> Document -> Node -> EventTarget -> Object.prototype -> null
- 凡是在原型链上存在的对象和属性和方法都可以哦通过Document去调用
- 部分属性:
document.documentElement -> html根元素
document.head -> head元素
document.title -> title元素
document.body -> body元素
document.links -> 获取页面中所有的超链接
...
*/

元素节点

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
/* 
元素节点对象(element)
- 在网页中,每一个标签都是一个元素节点
- 如何获取元素节点对象?
1.通过document对象来获取元素节点
2.通过document对象来创建元素节点
- 通过document来获取已有的元素节点:
document.getElementById()
- 根据id获取一个元素节点对象
document.getElementsByClassName()
- 根据元素的class属性值获取一组元素节点对象
- 返回的是一个类数组对象
- 该方法返回的结果是一个实时更新的集合
document.getElementsByTagName()
- 根据标签名获取一组元素节点对象
- document.getElementByTagName("*") 获取页面中所有的元素
document.getElementsByName()
- 根据name属性获取一组元素节点对象
- 返回一个实时更新的集合
- 主要用于表单项
document.querySelectorAll()
- 根据选择器去页面中查询元素
- 会返回一个类数组(不会实时更新)
document.querySelector()
- 根据选择器去页面中查询第一个符合条件的元素

- 创建一个元素节点
document.createElement()
- 根据标签名创建一个元素节点对象
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 
div元素的原型链
HTMLDivElement -> HTMLElement -> ELement -> Node -> ...

通过元素节点对象获取其他节点的方法
element.childNodes 获取当前元素的子节点(会包含空白的子节点)
element.children 获取当前元素的子元素
element.firstElementChild 获取当前元素的第一个子元素
element.lastElementChild 获取当前元素的最后一个子元素
element.nextElementSibling 获取当前元素的下一个兄弟元素
element.previousElementSibling 获取当前元素的前一个兄弟元素
element.parentNode 获取当前元素的父节点
element.tagName 获取当前元素的标签名
*/
const box1 = document.getElementById("box1");

const spans = box1.getElementsByTagName("span"); // 范围不同

文本节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
在DOM中,网页中所有的文本内容都是文本节点对象
可以通过元素来获取其中的文本节点对象,但是我们通常不会这么做

我们可以直接通过元素去修改其中的文本
修改文本的三个属性
element.textContent 获取或修改元素中的文本内容
- 获取的是标签中的内容,不会考虑css样式

element.innerText 获取或修改元素中的文本内容
- innerText获取内容时,会考虑css样式
- 通过innerText去读取CSS样式,会触发网页的重排(计算CSS样式)
- 当字符串中有标签时,会自动对标签进行转义
- <li> -> &lt;li&gt

element.innerHTML 获取或修改元素中的html代码
- 可以直接向元素中添加html代码
- innerHTML插入内容时,有被xss注入的风险
*/

const box1 = document.getElementById("box1");

console.log(1, box1.textContent); // 1 '\n 我是box1\n
console.log(2, box1.innerText); // 2 ''

属性节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 
属性节点(Attr)
- 在DOM中也是一个对象,通常不需要获取对象而直接通过元素即可完成对其的各种操作
- 如何操作属性节点:
方式一:
读取:元素.属性名(注意:class属性需要使用className来读取)
读取一个布尔值时,会返回true或false
修改:元素.元素名 = 属性值

方式二:
读取:元素.getAttribute(属性名)

修改:元素.setAttribute(属性名, 属性值)

删除:元素.removeAttribute(属性名)
*/

// <input type="text" name="username" value="admin" class="a" disabled>
const input = document.getElementsByName("username")[0];
const input2 = document.querySelector("[name=username]");
console.log(input.disabled); // true
console.log(input2.className); // a

事件

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
<!-- <button id="btn" onclick="alert('点我')">点我一下</button> -->
<button id="btn">点我一下</button>
<button id="btn2">点我一下2</button>
<script>
/*
事件(event)
- 事件就是用户和页面之间发生的交互行为
比如:点击按钮、鼠标移动、双击按钮、敲击键盘、松开按键...
- 可以通过为事件绑定响应函数(回调函数),来完成和用户之间的交互
- 绑定响应函数的方式:
1.可以直接在元素的属性中设置
2.可以通过元素的指定函数设置回调函数的形式来绑定事件(一个事件只能绑定一个响应函数)
3.可以通过元素addEventListener()方法来绑定事件
*/

// 获取到按钮对象
const btn = document.getElementById("btn");
// 为按钮对象的事件属性设置响应函数
btn.onclick = function () {
alert("点我了");
}

const btn2 = document.getElementById("btn2");
btn2.addEventListener("click", function () {
alert("点我了了");
});
</script>

文档的加载

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
/* 
网页是自上向下加载的,如果将js代码编写到网页的上边,
js代码在执行时,网页还没有加载完毕,这时会出现无法获取到DOM对象的情况

window.onload 事件会在窗口中的内容加载完毕后才触发
document的DOMContentLoaded事件会在当前文档加载完毕之后触发

如何解决这个问题:
1.将script标签编写到body的最后
2.将代码编写到window.onload的回调函数中
3.将代码编写到document对象的DOMContentLoaded的回调函数中(执行时机更早)
4.将代码编写到外部的js文件,然后以defer的形式引入(执行时机更早,早于DOMContentLoaded)
*/

window.onload = function () {

}

window.addEventListener("load", function () {

});

document.addEventListener("DOMContentLoaded", function () {

});

DOM的修改

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
       // 创建一个li
const li = document.createElement("li");
// 向li中添加文本
li.textContent = "唐僧";
// 给li添加属性
li.id = "ts"

// appendChild() 用于给一个节点添加子节点
list.appendChild(li);

/*
insertAdjacentElement() 可以向元素的任意位置添加元素
两个参数:
1.要添加的位置
beforeend 标签的最后
afterbegin 标签的开始
beforebegin 在元素的前边插入元素(兄弟元素)
afterend 在元素的后边插入元素(兄弟元素)
2.要添加的元素
*/
// list.insertAdjacentElement("beforeend", li);
list.insertAdjacentHTML("beforeend", "<li id='bgj'>白骨精</li>");



// 获取孙悟空
const swk = document.getElementById("swk");

/*
replaceWith() 使用一个元素替换当前元素
remove() 方法用来删除当前元素
*/
swk.replaceWith(li);


/*
只要点击超链接就会触发页面的跳转,事件中可以通过取消默认行为来阻止超链接的跳转
return false来取消默认行为,只在xxx.xxx = function(){} 这种形式绑定的事件才适
*/

节点的复制

1
2
3
4
5
6
7
const newL1 = l1.cloneNode(true); // 用来对节点的复制

/*
使用cloneNode() 方法对节点进行复制时,它会复制节点的所有特点包括各种属性
这个方法默认只会复制当前节点,而不会复制节点的子节点
可以传递一个true作为参数,这样该方法也会将元素的子节点一起复制
*/

11.浏览器知识


回流和重绘

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
40
41
42
43
44
45
/*
回流:
第一次确定节点的大小和位置,称之为布局(layout),之后对节点的大小、位置修改重新计算称之为回流

什么情况下引起回流呢?
1.DOM结构发生改变(添加新的节点或者移除节点)
2.改变了布局(修改了width、height、padding、font-size等值)
3.浏览器窗口resize(修改了窗口的尺寸等)
4.调用getComputedStyle方法获取尺寸、位置信息
*/

/*
重绘:
第一次渲染内容称之为绘制(paint),之后重新渲染称之为重绘。

什么情况下会引起重绘呢?
比如修改背景色、文字颜色、边框颜色、样式等
*/

/*
回流一定会引起重绘,所以回流是一件消耗性能的事情。
所以在开发中要尽量避免发生回流:
1.修改样式时尽量一次性修改
2.尽量避免频繁的操作DOM
3.尽量避免通过getComputedStyle获取尺寸、位置等信息
4.对某些元素使用position的absolute或者fixed。
并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响。
*/

/*
- 绘制的过程,可以将布局后的元素绘制到多个合成图层中,这是浏览器的一种优化手段
- 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的
- 而一些特殊的属性,会创建一个新的合成层(CompositingLayer),并且新的图层可以利用GPU来加上绘制
- 因为每个合成层都是单独渲染的

那么那些属性可以形成新的合层层呢?常见的一些属性:
1.3D transforms
2.video、canvas、iframe
3.opacity动画转换时
4.position:fixed
5.will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化
6.animation或transition设置了opacity、transform

分层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用
*/

defer和async属性

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
40
41
42
43
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!--
可以在添加defer的js中随意操作dom,不用担心dom是否加载完成,因为其中的js
-->
<script defer src="./js/test.js"></script>
</head>

<body>
<!--
- defer属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。
- 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程;
- 如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前执行defer中的代码;
- 另外多个带defer的脚本是可以保持正确的顺序执行的
- 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中
-->
<div>1111</div>
<div>2222</div>
<div>3333</div>

<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded");
});
</script>

<!--
- async特性与defer有些类似,它也能够让脚本不阻塞页面
- async是让一个脚本完全独立的:
- 浏览器不会因async脚本而阻塞(与defer类似)
- async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
- async不会保证在DOMContentLoaded之前或者之后执行
- defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件顺序要求的
- async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的
-->
</body>

</html>

JS执行原理

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
/*
初始化全局对象(GO)
- js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object
- 该对象所有的作用域(scope)都可以访问
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
- 其中还有一个window属性指向自己
*/

/*
执行上下文(ECS)
- js引擎内部有一个执行上下文栈,它是用于执行代码的调用栈
- 那么现在它要执行谁呢?执行的是全局的代码块
- 全局的代码块为了执行会构建一个 Global Execution Context(GEC)
- GEC 会被放入到ECS中执行
- GEC被放入到ECS中里面包含两部分内容:
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
这个过程也称之为变量的作用域提升
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数
*/

/*
VO(Variable Object)对象
- 每一个执行上下文会关联一个VO,变量和函数声明会被添加到这个VO对象中
- 当全局代码被执行的时候,VO就是GO对象了
*/

/*
函数是如何被执行的呢?
- 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(FEC),并且压入到EC Stack中
- 因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?
- 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
- 这个AO对象会作为执行上下文的VO来存放变量的初始化
*/