JavaScript高级

1、作用域

1.1 局部作用域

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,

作用域分为:

(1)局部作用域

(2)全局作用域

局部作用域分为函数作用域和块作用域。

(1) 函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

1
2
3
4
5
6
总结:
1. 函数内部声明的变量,在函数外部无法被访问
2. 函数的参数也是函数内部的局部变量
3. 不同函数内部声明的变量无法互相访问
4. 函数执行完毕后,函数内部的变量实际被清空了

(2)块级作用域

在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问

1
2
3
4
5
6
总结:
1. let 声明的变量会产生块作用域,var 不会产生块作用域
2. const 声明的常量也会产生块作用域
3. 不同代码块之间的变量无法互相访问
4. 推荐使用 let 或 const

1.2 全局作用域

<script> 标签 和.js文件 的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

全局作用域中声明的变量,任何其它作用域都可以被访问

1
2
3
4
5
注意:
1. 为 window 对象动态添加的属性默认也是全局的,不推荐!
2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
3. 尽可能少的声明全局变量,防止全局变量被污染

1.3 作用域链

下面代码的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// 全局作用域
let a = 1;
let b = 2;
// 局部作用域
function f(){
lets a = 1;
// 局部作用域
function g(){
a = 2;
console.log(a);
}
g();
}
f();
</script>
1
2
3
4
作用域链本质上是底层的变量查找机制。
在函数被执行时,会优先查找当前函数作用域中查找变量
如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域

1
2
3
4
5
总结:
1. 嵌套关系的作用域串联起来形成了作用域链
2. 相同作用域链中按着从小到大的规则查找变量
3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

1
2
3
4
作用域链查找的规则是什么?
会优先查找当前函数作用域中查找变量
查找不到则会依次逐级查找父级作用域直到全局作用域

1.4 闭包

概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域

简单理解:闭包 = 内层函数 + 外层函数的变量

先看个简单的代码:

1
2
3
4
5
6
7
8
function outer(){
const a = 1
function f(){
console.log(a);
}
f()
}
outer()
1
2
3
4
5
6
7
8
9
10
// 常见的闭包的形式   外部可以访问使用 函数内部的变量
function outer() {
let a = 100
function fn() {
console.log(a)
}
return fn
}
const fun = outer()
fun() // 外层函数使用内部函数的变量

以上代码,经常被简化成如下的形式:

1
2
3
4
5
6
7
8
function outer() {
let a = 100
return function () {
console.log(a)
}
}
const fun = outer()
fun() // 调用函数

闭包应用:实现数据的私有

比如,我们要做个统计函数调用次数,函数调用一次,就++

大家可能想到的就是通过全局变量来实现

1
2
3
4
5
6
7
let i = 1
function fn() {
i++
console.log(`函数被调用了${i}次`)
}
fn()
fn()

但是,这个i是一个全局变量,很容易被修改。

1
2
3
4
5
6
7
8
9
10
11
function count() {
let i = 0
function fn() {
i++
console.log(`函数被调用了${i}次`)
}
return fn
}
const fun = count()
fun()

这样通过闭包的形式实现了数据的私有,无法直接修改count

1.5 变量提升

变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)

1
2
3
4
5
6
7
注意:
1. 变量在未声明即被访问时会报语法错误
2. 变量在var声明之前即被访问,变量的值为 undefined
3. let/const 声明的变量不存在变量提升
4. 变量提升出现在相同作用域当中
5. 实际开发中推荐先声明再访问变量

1
2
3
4
5
6
7
8
9
10
<script>
// 访问变量 str
console.log(str +"world")
// 声明变量
var str = "hello"
//-------------------------------------
var str;
console.log(str +"world")
str = "hello"
<script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 1. 把所有var声明的变量提升到 当前作用域的最前面
// 2. 只提升声明, 不提升赋值
// var num
// console.log(num + '件')
// num = 10
// console.log(num)

function fn() {
console.log(num)
var num = 10
}
fn()
</script>

2、函数进阶

2.1 函数提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
var fun
// 1. 会把所有函数声明提升到当前作用域的最前面
// 2. 只提升函数声明,不提升函数调用
// fn()
// function fn() {
// console.log('函数提升')
// }
// 下面的会出错
// fun()
// var fun = function () {
// console.log('函数表达式')
// }
// 函数表达式 必须先声明和赋值, 后调用 否则 报错
/// var fun;
// fun()

</script>

2.2 函数参数

2.2.1 动态参数

产品需求: 写一个求和函数

不管用户传入几个实参,都要把和求出来

实参:

问题是形参应该怎样写?

arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
function getSum() {
// arguments 动态参数 只存在于 函数里面
// 是伪数组 里面存储的是传递过来的实参
// console.log(arguments) [2,3,4]
let sum = 0
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
console.log(sum)
}
getSum(2, 3, 4)
getSum(1, 2, 3, 4, 2, 2, 3, 4)
</script>
1
2
3
4
5
总结:
arguments 是一个伪数组,只存在于函数中
arguments 的作用是动态获取函数的实参
可以通过for循环依次得到传递过来的实参

2.2.2 剩余参数

产品需求: 写一个求和函数

不管用户传入几个实参,都要把和求出来

实参:

形参应该怎样写?

剩余参数允许我们将一个不定数量的参数表示为一个数组

1
2
3
4
5
6
7
<script>
function getSum(a, b, ...arr) {
console.log(arr) // 使用的时候不需要写 ...
}
getSum(2, 3)
getSum(1, 2, 3, 4, 5)
</script>

问题是与arguments有什么不同?

1
2
3
1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
2. 借助 ... 获取的剩余实参,是个真数组

开发中提倡多使用剩余参数

2.2.3 展开运算符

与剩余参数数书写类似的是展开运算符。

展开运算符(…),将一个数组进行展开

典型运用场景: 求数组最大值**(最小值)**、合并数组等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
const arr1 = [1, 2, 3]
// 展开运算符 可以展开数组
// console.log(...arr) 1,2,3

// console.log(Math.max(1, 2, 3)) // 不能给max函数传递数组
// ...arr1 === 1,2,3
// 1 求数组最大值
console.log(Math.max(...arr1)) // 3
console.log(Math.min(...arr1)) // 1
// 2. 合并数组
const arr2 = [3, 4, 5]
const arr = [...arr1, ...arr2]
console.log(arr)

</script>

说明:展开运算符不会修改原有的数组

2.3 箭头函数

2.3.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
<script>
// const fn = function () {
// console.log(123)
// }
// 1. 箭头函数 基本语法
// const fn = () => {
// console.log(123)
// }
// fn()


// const fn = (x) => {
// console.log(x)
// }
// fn(1)


// 2. 只有一个形参的时候,可以省略小括号
// const fn = x => {
// console.log(x)
// }
// fn(1)
// // 3. 只有一行代码的时候,我们可以省略大括号
// const fn = x => console.log(x)
// fn(1)
// 4. 只有一行代码的时候,可以省略return
// const fn = x => x + x
// console.log(fn(1))
// 5. 箭头函数可以直接返回一个对象

// const fn = (username)=>{
// return {username}
// }
// 这里对上面的代码做了简化,省略了return ,所以需要添加小括号
// const fn = (uname) => ({ uname: uname })
// console.log(fn('刘德华'))

</script>

2.3.2 箭头函数参数

1.普通函数有arguments 动态参数

2.箭头函数没有 arguments 动态参数,但是有 剩余参数 ..args

1
2
3
4
5
6
7
8
9
10
11
12
<script>
// 1. 利用箭头函数来求和
const getSum = (...arr) => {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}
const result = getSum(2, 3, 4)
console.log(result) // 9
</script>

2.3.3 箭头函数this

引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁

箭头函数更适用于那些本来需要匿名函数的地方

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值, 非常令人讨厌。

KilgourNote

箭头函数中的this指的是定义箭头函数时的上下文

例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const btn = document.querySelector("#p");
btn.addEventListener("click", f);

function f() {
let obj2 = {
userName: "zhangsan",
sayHi: () => {
console.log(this);
// 这个this指的就是 <a id="p">我的博客网站</a>
// sayHi变量定义了箭头函数,sayHi变量又属于obj作用域,obj属于函数f,因此this指向的是创建f的btn
},
};
obj2.sayHi();
}

例二

1
2
3
4
5
6
7
8
9
10
11
12
13
function outer() {
return () => {
console.log(this);
};
}

let obj = {
method: outer,
};

let arrowFunction = obj.method();
arrowFunction(); // 这里的this指的是obj,因为箭头函数是在obj的method方法中创建的
// 在return这里创建了箭头函数,return属于outer,因此this指向创建了outer的obj变量

例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function traditionalFunction() {// 正常函数
console.log(this);
}

const arrowFunction = () => {// 箭头函数
console.log(this);
};

traditionalFunction(); // 正常函数:this 指的是调用函数时的上下文window
arrowFunction(); // 箭头函数:this 这里指的是window(只有一层上级,只能指向window)

const obj = {
traditionalMethod: traditionalFunction,
arrowMethod: arrowFunction,
};

obj.traditionalMethod(); // this 指向调用函数时的上下文obj
obj.arrowMethod(); // arrowMethod创建了箭头函数,arrowMethod属于变量obj,因此this指向的是创建了obj变量的window

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<script>
// 以前this的指向: 谁调用的这个函数,this 就指向谁
// console.log(this) // window
// // 普通函数
// function fn() {
// console.log(this) // window
// }
// window.fn()
// // 对象方法里面的this
// const obj = {
// name: 'zhangsan',
// sayHi: function () {
// console.log(this) // obj
// }
// }
// obj.sayHi()

// 2. 箭头函数的this 是上一层作用域的this 指向
// const fn = () => {
// console.log(this) // window
// }
// fn()
// 对象方法箭头函数 this
// const obj = {
// uname: '张三',
// sayHi: () => {
// console.log(this) // this 指向谁? window
// }
// }
// obj.sayHi()

const obj = {
uname: '张三',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()

</script>

在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数为了简便,还是不太推荐使用箭头函数

3、解构赋值

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值。

分为:

数组解构

对象解构

3.1 数组解构

1
2
3
4
基本语法:
1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
2. 变量的顺序对应数组单元值的位置依次进行赋值操作

以上要么不好记忆,要么书写麻烦,此时可以使用解构赋值的方法让代码更简洁

1
2
3
4
5
// 交换2个变量的值
let a = 1
let b = 2; // 注意这里一定要添加分号
[b, a] = [a, b]
console.log(a, b)

变量多 单元值少的情况:

1
2
变量的数量大于单元值数量时,多余的变量将被赋值为  undefined

变量少 单元值多的情况:

利用剩余参数解决变量少 单元值多的情况:

剩余参数返回的还是一个数组

防止有undefined传递单元值的情况,可以设置默认值:

按需导入,忽略某些返回值:

3.2 对象解构

1.基本语法:

1
2
3
4
5
1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
2. 对象属性的值将被赋值给与属性名相同的变量
3. 注意解构的变量名不要和外面的变量名冲突否则报错
4.对象中找不到与变量名一致的属性时变量值为 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象解构
const obj = {
uname: '张三',
age: 18
}
// obj.uname
// obj.age

// 解构的语法
const { uname, age } = {age: 18, uname: '张老师' }
// // 等价于 const uname = obj.uname
// // 要求属性名和变量名必须一直才可以
console.log(uname)
console.log(age)

可以从一个对象中提取变量并同时修改新的变量名

1
2
3
4
5
6
// 1. 对象解构的变量名 可以重新改名  旧变量名: 新变量名
// 把原来的uname重新命名为username
const { uname: username, age } = { uname: '张三', age: 18 }

console.log(username)
console.log(age)

数组对象解构

1
2
3
4
5
6
7
8
9
10
// 2. 解构数组对象
const pig = [
{
uname: '佩奇',
age: 6
}
]
const [{ uname, age }] = pig
console.log(uname)
console.log(age)

多级对象解构

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
<script>
// const pig = {
// name: '佩奇',
// family: {
// mother: '猪妈妈',
// father: '猪爸爸',
// sister: '乔治'
// },
// age: 6
// }
// // 多级对象解构
// const { name, family: { mother, father, sister } } = pig
// console.log(name)
// console.log(mother)
// console.log(father)
// console.log(sister)

const person = [
{
name: '佩奇',
family: {
mother: '猪妈妈',
father: '猪爸爸',
sister: '乔治'
},
age: 6
}
]
const [{ name, family: { mother, father, sister } }] = person
console.log(name)
console.log(mother)
console.log(father)
console.log(sister)
</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
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
<script>
// 1. 这是后台传递过来的数据
const msg = {
"code": 200,
"msg": "获取新闻列表成功",
"data": [
{
"id": 1,
"title": "5G商用自己,三大运用商收入下降",
"count": 58
},
{
"id": 2,
"title": "国际媒体头条速览",
"count": 56
},
{
"id": 3,
"title": "乌克兰和俄罗斯持续冲突",
"count": 1669
},

]
}

// 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面
// const { data } = msg
// console.log(data)
// 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
// msg 虽然很多属性,但是我们利用解构只要 data值
function render({ data }) {

// 我们只要 data 数据
// 内部处理
console.log(data)

}
render(msg)


// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)

}
render(msg)

</script>

3.3 遍历数组 forEach 方法

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数

主要使用场景: 遍历数组的每个元素

语法:

1
2
3
4
5
6
7
8
9
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'black']
const result = arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green black
console.log(index) // 索引号
})
// console.log(result)
</script>
1
2
3
注意:  
forEach 主要是遍历数组
参数当前数组元素是必须要写的, 索引号可选

商品列表案例

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品渲染</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.list {
width: 990px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
padding-top: 100px;
}

.item {
width: 240px;
margin-left: 10px;
padding: 20px 30px;
transition: all .5s;
margin-bottom: 20px;
}

.item:nth-child(4n) {
margin-left: 0;
}

.item:hover {
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
transform: translate3d(0, -4px, 0);
cursor: pointer;
}

.item img {
width: 100%;
}

.item .name {
font-size: 18px;
margin-bottom: 10px;
color: #666;
}

.item .price {
font-size: 22px;
color: firebrick;
}

.item .price::before {
content: "¥";
font-size: 14px;
}
</style>
</head>

<body>
<div class="list">
<!-- <div class="item">
<img src="" alt="">
<p class="name"></p>
<p class="price"></p>
</div> -->
</div>
<script>
const goodsList = [
{
id: '4001172',
name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
price: '289.00',
picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
},
{
id: '4001594',
name: '日式黑陶功夫茶组双侧把茶具礼盒装',
price: '288.00',
picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg',
},
{
id: '4001009',
name: '竹制干泡茶盘正方形沥水茶台品茶盘',
price: '109.00',
picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
},
{
id: '4001874',
name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
price: '488.00',
picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
},
{
id: '4001649',
name: '大师监制龙泉青瓷茶叶罐',
price: '139.00',
picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
},
{
id: '3997185',
name: '与众不同的口感汝瓷白酒杯套组1壶4杯',
price: '108.00',
picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg',
},
{
id: '3997403',
name: '手工吹制更厚实白酒杯壶套装6壶6杯',
price: '99.00',
picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg',
},
{
id: '3998274',
name: '德国百年工艺高端水晶玻璃红酒杯2支装',
price: '139.00',
picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg',
},
]

// 1. 声明一个字符串变量
let str = ''
// 2. 遍历数据
goodsList.forEach(item => {
// console.log(item) // 可以得到每一个数组元素 对象 {id: '4001172'}
// const {id} = item 对象解构
const { name, price, picture } = item
str += `
<div class="item">
<img src=${picture} alt="">
<p class="name">${name}</p>
<p class="price">${price}</p>
</div>
`
})
// 3.生成的 字符串 添加给 list
document.querySelector('.list').innerHTML = str
</script>
</body>

</html>

3.4 筛选数组 filter 方法

KilgourNote:(数组方法总结)

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
<script>
const arr = [1, 2, 3, 4, 5];
// map方法(返回数组):用于创建一个新数组
const a = arr.map((it) => it > 2);
console.log(a); //false, false, true, true, true]

const b = arr.map((it) => it * 2);
console.log(b); //[2, 4, 6, 8, 10]

// find方法(返回arr中的item):找到符合条件的元素
const c = arr.find((it) => it == 3);
console.log(c); // 3(多用于根据对象中属性的值找具体对象)

// join方法(返回字符串):join()无参数默认使用逗号连接
const d = arr.join("_");
console.log(d);

// reduce方法(返回数值):累计函数(遍历计算值),第二个参数表示sum的起始值
const e = arr.reduce((sum, it) => sum + it, 0);
console.log(e);

// forEach方法(无返回值):遍历方法
// 复制对象方法Object.assign(obj)【浅拷贝】
// 复制对象方法.silce【深拷贝】
// const f = arr.forEach((num,index,array) => array[index] = num + 1);
// const array = Object.assign(arr);
const array = arr.slice();
array.forEach((num, index, array) => (array[index] = num + 1));
// num : 当前正在处理的元素的值。
// index : 当前正在处理的元素的索引。
// array : 调用 forEach 方法的数组本身。
array.forEach(function (item, index, array) {
array[index] = item + 1;
});
console.log(array); // [3, 4, 5, 6, 7]

// filter方法():
const g = arr.filter((item) => item > 2);
console.log(g); // [3, 4, 5]
</script>

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

语法:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
const arr = [10, 20, 30]
// const newArr = arr.filter(function (item, index) {
// // console.log(item)
// // console.log(index)
// return item >= 20
// })
// 返回的符合条件的新数组

const newArr = arr.filter(item => item >= 20)
console.log(newArr)
</script>

说明:

1
2
3
返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
参数:currentValue 必须写, index 可选
因为返回新数组,所以不会影响原数组

价格筛选案例:

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品渲染</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.list {
width: 990px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
}

.item {
width: 240px;
margin-left: 10px;
padding: 20px 30px;
transition: all .5s;
margin-bottom: 20px;
}

.item:nth-child(4n) {
margin-left: 0;
}

.item:hover {
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
transform: translate3d(0, -4px, 0);
cursor: pointer;
}

.item img {
width: 100%;
}

.item .name {
font-size: 18px;
margin-bottom: 10px;
color: #666;
}

.item .price {
font-size: 22px;
color: firebrick;
}

.item .price::before {
content: "¥";
font-size: 14px;
}

.filter {
display: flex;
width: 990px;
margin: 0 auto;
padding: 50px 30px;
}

.filter a {
padding: 10px 20px;
background: #f5f5f5;
color: #666;
text-decoration: none;
margin-right: 20px;
}

.filter a:active,
.filter a:focus {
background: #05943c;
color: #fff;
}
</style>
</head>

<body>
<div class="filter">
<a data-index="1" href="javascript:;">0-100元</a>
<a data-index="2" href="javascript:;">100-300元</a>
<a data-index="3" href="javascript:;">300元以上</a>
<a href="javascript:;">全部区间</a>
</div>
<div class="list">
<!-- <div class="item">
<img src="" alt="">
<p class="name"></p>
<p class="price"></p>
</div> -->
</div>
<script>
// 2. 初始化数据
const goodsList = [
{
id: '4001172',
name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
price: '289.00',
picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
},
{
id: '4001594',
name: '日式黑陶功夫茶组双侧把茶具礼盒装',
price: '288.00',
picture: 'https://yanxuan-item.nosdn.127.net/3346b7b92f9563c7a7e24c7ead883f18.jpg',
},
{
id: '4001009',
name: '竹制干泡茶盘正方形沥水茶台品茶盘',
price: '109.00',
picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
},
{
id: '4001874',
name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
price: '488.00',
picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
},
{
id: '4001649',
name: '大师监制龙泉青瓷茶叶罐',
price: '139.00',
picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
},
{
id: '3997185',
name: '与众不同的口感汝瓷白酒杯套组1壶4杯',
price: '108.00',
picture: 'https://yanxuan-item.nosdn.127.net/8e21c794dfd3a4e8573273ddae50bce2.jpg',
},
{
id: '3997403',
name: '手工吹制更厚实白酒杯壶套装6壶6杯',
price: '100.00',
picture: 'https://yanxuan-item.nosdn.127.net/af2371a65f60bce152a61fc22745ff3f.jpg',
},
{
id: '3998274',
name: '德国百年工艺高端水晶玻璃红酒杯2支装',
price: '139.00',
picture: 'https://yanxuan-item.nosdn.127.net/8896b897b3ec6639bbd1134d66b9715c.jpg',
},
]

// 1. 渲染函数 封装
function render(arr) {
// 声明空字符串
let str = ''
// 遍历数组
arr.forEach(item => {
// 解构
const { name, picture, price } = item
str += `
<div class="item">
<img src=${picture} alt="">
<p class="name">${name}</p>
<p class="price">${price}</p>
</div>
`
})
// 追加给list
document.querySelector('.list').innerHTML = str
}
render(goodsList) // 页面一打开就需要渲染

// 2. 过滤筛选
document.querySelector('.filter').addEventListener('click', e => {
// e.target.dataset.index e.target.tagName
const { tagName, dataset } = e.target
// 判断
if (tagName === 'A') {
// console.log(11)
// arr 返回的新数组
let arr = goodsList // 如果不单击价格按钮,默认展示原有goodsList数组中的内容
if (dataset.index === '1') {
arr = goodsList.filter(item => item.price > 0 && item.price <= 100)
} else if (dataset.index === '2') {
arr = goodsList.filter(item => item.price >= 100 && item.price <= 300)
} else if (dataset.index === '3') {
arr = goodsList.filter(item => item.price >= 300)
}
// 渲染函数
render(arr)
}
})
</script>
</body>

</html>

4、对象深入

4.1 创建对象三种方式

1
2
3
4
const person = {
username:'zhangsan'
}

2.利用 new Object 创建对象

1
2
3
4
5
// const obj = new Object()
// obj.uname = '张三'
// console.log(obj)
const obj = new Object({ uname: '张三' })
console.log(obj)

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
// 创建一个猪 构造函数 
function Pig(uname, age) {
this.uname = uname
this.age = age
}

// console.log(new Pig('佩奇', 6))
// console.log(new Pig('乔治', 3))
const p = new Pig('佩奇', 6)
console.log(p)

// const pepa = { uname: '佩奇', age: 6 }


function Goods(name, price, count) {
this.name = name
this.price = price
this.count = count
this.sayhi = function () { }
}
const mi = new Goods('小米', 1999, 20)
console.log(mi)
const hw = new Goods('华为', 3999, 59)
console.log(hw)
console.log(mi === hw)
mi.name = 'vivo'
console.log(mi)
console.log(hw)
// const date = new Date('2022-4-8')
// console.log(date)
1
2
3
4
5
构造函数在技术上是常规函数。
不过有两个约定:
它们的命名以大写字母开头。
它们只能由 "new" 操作符来执行。

补充说明:

1
2
3
4
5
6
7
说明:
1. 使用 new 关键字调用函数的行为被称为实例化
2. 实例化构造函数时没有参数时可以省略 ()
3. 构造函数内部无需写return,返回值即为新创建的对象
4. 构造函数内部的 return 返回的值无效,所以不要写return
5. new Object() new Date() 也是实例化构造函数

4.2 实例成员&静态成员

实例成员

通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。

静态成员:

构造函数的属性和方法被称为静态成员

1
2
3
4
5
说明:
1. 构造函数的属性和方法被称为静态成员
2. 一般公共特征的属性或方法静态成员设置为静态成员
3. 静态成员方法中的 this 指向构造函数本身

4.3 内置构造函数

4.3.1 Object

1
2
3
4
5
在 JavaScript 中最主要的数据类型有 6 种:
基本数据类型:
字符串、数值、布尔、undefined、null 
引用类型:
对象

但是,我们会发现有些特殊情况:

1
2
const str = "zhangsan"
console.log(str.length); // 只有对象采用属性,这里的字符串基本类型怎么可以调用属性呢?

其实字符串、数值、布尔、等基本类型也都有专门的构造函数,这些我们称为包装类型。

JS中几乎所有的数据都可以基于构成函数创建。

1
2
3
4
5
6
7
8
9
<script>
// const str = 'zhangsan'
// console.log(str.length)
// const num = 12
// console.log(num.toFixed(2))
// const str = 'zhangsan'
// js 底层完成, 把简单数据类型包装为了引用数据类型
// const str = new String('zhangsan')
</script>
1
2
3
4
5
引用类型
Object,Array,RegExp,Date 等
包装类型
String,Number,Boolean

Object 是内置的构造函数,用于创建普通对象。

1
2
const obj = new Object({ uname: '张三' })
console.log(obj)

但是这种方式不常用

但是在Object中有三个常用的静态方法。(静态方法就是只有构造函数Object可以调用的)

Object.keys 静态方法获取对象中所有属性(键)

1
2
3
4
5
6
const o = { name:'zhangsan',age:18 }
// 想要获取对象中的属性名和属性值,应该怎样做?
for(let k in o ){
console.log(k) // name,age
console.log(o[k]) // zhangsan,18
}

以前是通过for..in来实现,但是现在有了更加简单的方法,就是通过Object.keys方法和Object.values 方法

现在先看一下Object.keys方法

1
2
3
4
const o = { name:'zhangsan',age:18 }
// 获取对象的所有键,并且返回的是一个数组
const arr = Object.keys(o)
console.log(arr) // ['name','age'] // 注意返回的是一个数组

Object.values 静态方法获取对象中所有属性值

1
2
3
4
const o = { name:'zhangsan',age:18 }
// 获取对象的所有键,并且返回的是一个数组
const arr = Object.values(o)
console.log(arr) //['zhangsan',18] // 注意返回的是一个数组

Object. assign 静态方法常用于对象拷贝

1
2
3
4
const o = { name:'zhangsan',age:18 }
const obj = {}
Object.assign(obj,o)
console.log(obj) // {name:'zhangsan',age:18}

使用:经常使用的场景给对象添加属性

1
2
3
const o = { name:'zhangsan',age:18 }
Object.assign(o,{address:'北京'})
console.log(o) // { name:'zhangsan',age:18,address:'北京' }

4.3.2 Array

Array 是内置的构造函数,用于创建数组

1
2
3
const arr1 = [];//经常使用
const arr = new Array(3,5) // 不常用
console.log(arr) // [3,5]

这种方式不常用

数组常见实例方法-核心方法

作用:reduce 返回函数累计处理的结果,经常用于求和等

基本语法

1
arr.reduce(function(累计值, 当前元素){}, 起始值)

起始值可以省略,如果写就作为第一次累计的起始值

1
2
3
4
5
6
const arr = [1, 2, 3]
const result = arr.reduce(function (prev, item) {
console.log(prev)
return prev + item
}, 0) // 这里的起始值是0
console.log("result =",result);
1
2
3
4
arr.reduce(function (prev, item) { 
console.log(prev)
return prev + item
}) // 这里省略了起始值

可以简化成箭头函数的形式:

1
2
3
const arr = [1, 2, 3]
const re = arr.reduce((prev, item) => prev + item)
console.log(re)
1
2
3
4
5
关于累计值参数说明:
1. 如果有起始值,则以起始值为准开始累计, 累计值 = 起始值
2. 如果没有起始值, 则累计值以数组的第一个数组元素作为起始值开始累计
3. 后面每次遍历就会用后面的数组元素 累计到 累计值 里面 (类似求和里面的 sum )

使用场景:求和运算

案例:员工涨薪计算成本

需求:

(1):给员工每人涨薪 30%

(2):然后计算需要多支出的费用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<script>
const arr = [{
name: '张三',
salary: 10000
}, {
name: '李四',
salary: 10000
}, {
name: '王五',
salary: 20000
},
]
// 涨薪的钱数 10000 * 0.3
// const money = arr.reduce(function (prev, item) {
// return prev + item.salary * 0.3
// }, 0)
const money = arr.reduce((prev, item) => prev + item.salary * 0.3, 0)
console.log(money)
</script>
</body>

数组中常见的其他方法

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
<script>

const arr = [
{
name: '小米',
price: 1999
},
{
name: '华为',
price: 3999
},
]
// 找华为 这个对象,并且返回这个对象
// const mi = arr.find(function (item) {
// // console.log(item) //
// // console.log(item.name) //
// console.log(111)
// return item.name === '华为'
// })
// 1. find 查找
// const mi = arr.find(item => item.name === '小米')
// console.log(mi)
// 2. every 每一个是否都符合条件,如果都符合返回 true ,否则返回false
const arr1 = [10, 20, 30]
const flag = arr1.every(item => item >= 20)
console.log(flag)
</script>

4.3.3 String

常见实例方法

案例:

案例数据:

最终展示效果:

1
2
3
4
5
6
思路:
①:把字符串拆分为数组,这样两个赠品就拆分开了 用那个方法?
②:利用map遍历数组,同时把数组元素生成到span里面,并且返回
③:因为返回的是数组,所以需要 转换为字符串, 用那个方法?
④:p的innerHTML 存放刚才的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<div></div>
<script>
const gift = '50g的茶叶,清洗球'
// 1. 把字符串拆分为数组
// 2. 根据数组元素的个数,生成 对应 span标签
// const str = gift.split(',').map(function (item) {
// return `<span>【赠品】 ${item}</span> <br>`
// }).join('')

// // console.log(str)
// document.querySelector('div').innerHTML = str
document.querySelector('div').innerHTML = gift.split(',').map(item => `<span>【赠品】 ${item}</span> <br>`).join('')
</script>
</body>

4.4 原型

KilgourNote:

JavaScript中将一些对象中的公共元素提取出来单独放置于一个对象中作为公共对象继承。这个公共对象在JavaScript中被称为原型对象。

继承原型对象的对象通过.prototype引用公共对象,公共对象中又一个constructor属性指向继承公共对象的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Person() {
this.eyes = 2;
this.head = 1;
}
function Woman() {}

Woman.prototype = new Person(); // {eyes: 2, head: 1}
Woman.prototype.constructor = Woman;

Woman.prototype.baby = function () {
console.log("宝贝");
};

function Man() {}
Man.prototype = new Person();
Man.prototype.constructor = Man;
Man.prototype.Somking = function () {
console.log("抽烟");
};
const wm = new Woman();// 单独为wm创建一个对象空间
console.log(wm);
//Woman {}
//[[Prototype]]: Person
//baby: ƒ ()
//constructor: ƒ Woman()
//eyes: 2
//head: 1
//[[Prototype]]: Object
console.log(wm.__proto__);//指的是wm继承的对象原型 4.4.5对象原型图
// Person {eyes: 2, head: 1, baby: ƒ}
// baby:ƒ ()
// constructor:ƒ Woman()
// eyes:2
// head:1
// [[Prototype]]:Object
const m = new Man();
console.log(m);

封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。

前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题

1
2
3
4
5
6
7
8
9
10
function Star(uname, age) {
this.uname = uname
this.age = age
this.sing = function() {
console.log('我会唱歌')
}
}
const ldh = new Star('刘德华', 18)
const zxy = new Star('张学友', 19)

sing方法是公共的。

我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?

通过原型来实现,原型本质就是一个对象,我们也称prototype为原型对象

1
2
3
4
5
6
1、构造函数通过原型分配的函数是所有对象所 共享的。
2、JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
3、这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
4、我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
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
<script>
// 构造函数 公共的属性和方法 封装到 Star 构造函数里面了
// 1.公共的属性写到 构造函数里面
function Star(uname, age) {
this.uname = uname
this.age = age
// this.sing = function () {
// console.log('唱歌')
// }
}
// 2. 公共的方法写到原型对象身上 节约了内存
Star.prototype.sing = function () {
console.log('唱歌')
}
const ldh = new Star('刘德华', 55)
const zxy = new Star('张学友', 58)
ldh.sing() //调用
zxy.sing() //调用
// console.log(ldh === zxy) // false
console.log(ldh.sing === zxy.sing)


</script>

4.4.1 this指向

构造函数和原型对象中的this 都指向 实例化的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
let that
function Star(uname) {
this.uname = uname
}
// 原型对象里面的函数this指向的还是 实例对象 ldh
Star.prototype.sing = function () {
that = this
console.log('唱歌')
}
// 实例对象 ldh
// 构造函数里面的 this 就是 实例对象 ldh
const ldh = new Star('刘德华')
ldh.sing()
console.log(that === ldh)
</script>

构造函数和原型里面的this指向谁?

指向的是实例化的对象

4.4.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
<script>
// 自己定义 数组扩展方法 求和 和 最大值
// 1. 我们定义的这个方法,任何一个数组实例对象都可以使用
// 2. 自定义的方法写到 数组.prototype 身上
// 1. 最大值
const arr = [1, 2, 3]
Array.prototype.max = function () {
// 展开运算符
return Math.max(...this)
// 原型函数里面的this 指向谁? 实例对象 arr
}
// 2. 最小值
Array.prototype.min = function () {
// 展开运算符
return Math.min(...this)
// 原型函数里面的this 指向谁? 实例对象 arr
}
console.log(arr.max())
console.log([2, 5, 9].max())
console.log(arr.min())
// const arr = new Array(1, 2)
// console.log(arr)
// 3. 求和 方法
Array.prototype.sum = function () {
return this.reduce((prev, item) => prev + item, 0)
}
console.log([1, 2, 3].sum())
console.log([11, 21, 31].sum())
</script>

4.4.3 constructor 属性

每个原型对象里面都有个constructor 属性(constructor 构造函数)

作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子

1
2
3
4
使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  <script>
// constructor 单词 构造函数

// Star.prototype.sing = function () {
// console.log('唱歌')
// }
// Star.prototype.dance = function () {
// console.log('跳舞')
// }
function Star() {
}
// console.log(Star.prototype)
// 如果有多个公共方法,我们可以给原型对象采取对象的形式进行赋值
Star.prototype = {
// 从新指回创造这个原型对象的 构造函数
constructor: Star,
sing: function () {
console.log('唱歌')
},
dance: function () {
console.log('跳舞')
},
}
console.log(Star.prototype)
// console.log(Star.prototype.constructor)

// const ldh = new Star()
// console.log(Star.prototype.constructor === Star)
</script>

4.4.4 思考

构造函数可以创建实例对象,构造函数还有一个原型对象,一些公共的属性或者方法放到这个原型对象身上

但是 为啥实例对象可以访问原型对象里面的属性和方法呢?

4.4.5 对象原型

对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function Star() {

}
const ldh = new Star()
// 对象原型__proto__ 指向 改构造函数的原型对象
console.log(ldh.__proto__)
// console.log(ldh.__proto__ === Star.prototype)
// 对象原型里面有constructor 指向 构造函数 Star
console.log(ldh.__proto__.constructor === Star)

</script>

__proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数

1
2
3
4
5
6
7
8
注意:
__proto__ 是JS非标准属性
[[prototype]]和__proto__意义相同
用来表明当前实例对象指向哪个原型对象prototype
__proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数



4.4.6 原型继承

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。

先看一段初始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Man(){
this.head = 1
this.eyes = 2
this.say = function(){}
this.eat = function(){}
}
function Woman(){
this.head = 1
this.eyes = 2
this.say = function(){}
this.eat = function(){}
this.baby = function(){}
}

以上构造函数中存在大量的重复代码。所以下面抽取公共部分的代码

把男人和女人公共的部分抽取出来放到人类里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 人类
const People = {
head:1,
eyes:2,
eat:function(){

}
say:function(){}
}
// 男人
function Man(){}
// 女人
function Woman(){
this.baby = function(){}
}

实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 人类(包含了公共属性和方法)
const People = {
head:1,
eyes:2,
eat:function(){

},
say:function(){}
}
// 男人
function Man(){}
// 把公共的属性和方法赋值给Man原型对象,这样就可以共享了
Man.prototype = People
// 注意:让原型对象里面的constructor重新指回Man自己的构造函数(上图所示)
Man.prototype.constructor = Man
// 女人
function Woman(){
this.baby = function(){}
}
Woman.prototype = People
Woman.prototype.constructor = Woman

下面进行测试

1
2
const zs = new Man()
console.log(zs)
1
2
3
4
让男人和女人都能继承人类的一些属性和方法
把男人女人公共的属性和方法抽取出来 People
然后赋值给Man的原型对象,可以共享这些属性和方法
注意让constructor指回Man这个构造函数

但是,以上的写法是有问题的,例如:如果我们给男人添加了一个吸烟的方法,发现女人自动也添加这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 人类(包含了公共属性和方法)
const People = {
head:1,
eyes:2,
eat:function(){

},
say:function(){}
}
// 男人
function Man(){}
// 把公共的属性和方法赋值给Man原型对象,这样就可以共享了
Man.prototype = People
// 注意:让原型对象里面的constructor重新指回Man自己的构造函数
Man.prototype.constructor = Man
Man.prototype.smoking = function(){}
// 女人
function Woman(){
this.baby = function(){}
}
Woman.prototype = People
Woman.prototype.constructor = Woman

测试:

1
2
const woman = new Woman()
console.log(woman)

原因是:男人和女人都同时使用了同一个对象,根据引用类型的特点,他们指向同一个对象,修改一个就会都影响

怎样进行解决呢?

需求:男人和女人不要使用同一个对象,但是不同对象里面包含相同的属性和方法

答案:构造函数

new 每次都会创建一个新的对象

下面继续完善代码

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
<script>
// 继续抽取 公共的部分放到原型上
// const Person1 = {
// eyes: 2,
// head: 1
// }
// const Person2 = {
// eyes: 2,
// head: 1
// }
// 构造函数 new 出来的对象 结构一样,但是对象不一样
function Person() {
this.eyes = 2
this.head = 1
}

// 女人 构造函数 继承 想要 继承 Person
function Woman() {

}
// Woman 通过原型来继承 Person
// 父构造函数(父类) 子构造函数(子类)
// --------------------子类的原型 = new 父类
Woman.prototype = new Person() // {eyes: 2, head: 1}
// 指回原来的构造函数
Woman.prototype.constructor = Woman

// 给女人添加一个方法 生孩子
Woman.prototype.baby = function () {
console.log('宝贝')
}
const red = new Woman()
console.log(red)
// console.log(Woman.prototype)
// 男人 构造函数 继承 想要 继承 Person
function Man() {

}
// 通过 原型继承 Person
Man.prototype = new Person()
Man.prototype.constructor = Man
const zs = new Man()
console.log(zs)
</script>
1
2
3
4
5
6
7

通过以上的
Woman.prototype = new Person()
重新创建了一个Person对象赋值给了 Woman.prototype 原型对象
Man.prototype = new Person()
重新创建了一个Person对象赋值给了 Man.prototype 原型对象
两次new Person创建了两个不同的Person对象,但是都可以该对象中的公共的属性和方法

4.4.7 原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链

1
2
3
4
5
6
7
1、当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
2、如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
3、如果还没有就查找原型对象的原型(Object的原型对象)
4、依此类推一直找到 Object 为止(null)
5、__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
6、可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>

console.log(Object.prototype)
console.log(Object.prototype.__proto__)

function Person() {

}
const ldh = new Person()
// console.log(ldh.__proto__ === Person.prototype)
// console.log(Person.prototype.__proto__ === Object.prototype)
console.log(ldh instanceof Person)
console.log(ldh instanceof Object)
console.log(ldh instanceof Array)
console.log([1, 2, 3] instanceof Array)
console.log(Array instanceof Object)
</script>

5、高级技巧

5.1 深浅拷贝

开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
const obj = {
uname: 'zhangsan',
age: 18
}
const o = obj
console.log(o)
o.age = 20
console.log(o)
console.log(obj)
</script>

浅拷贝

浅拷贝:拷贝的是地址

1
2
3
1. 拷贝对象:Object.assgin()  / 展开运算符  {...obj}  拷贝对象
2.拷贝数组:Array.prototype.concat() 或者 [...arr]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
const obj = {
uname: 'zhangsan',
age: 18,
family: {
baby: '小zhang'
}
}
// 浅拷贝
// const o = { ...obj }
// console.log(o.age)
// o.age = 20
// console.log(o)
// console.log(obj)
const o = {}
Object.assign(o, obj)
o.age = 20
o.family.baby = 'zhangxiao'
console.log(o)
console.log(obj)
</script>

如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)

问题:

1
2
3
4
5
6
直接赋值和浅拷贝有什么区别?
直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响
浅拷贝怎么理解?
拷贝对象之后,里面的属性值是简单数据类型直接拷贝值
如果属性值是引用数据类型则拷贝的是地址

5.2 深拷贝

深拷贝:拷贝的是对象,不是地址

1
2
3
4
5
常见方法:
通过递归实现深拷贝
lodash/cloneDeep
通过JSON.stringify()实现

1
2
3
4
5
6
7
通过递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
简单理解:函数内部自己调用自己, 这个函数就是递归函数
递归函数的作用和循环效果类似
由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return

1
2
3
4
5
6
7
8
9
10
11
12
<script>
let i = 1
function fn() {
console.log(`这是第${i}次`)
if (i >= 6) {
return
}
i++
fn()
}
fn()
</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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 <script>
const obj = {
uname: 'zhangsan',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小zhang'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = [] // o{ hobby:[] }
// newObj[k] 接收 [] hobby
// oldObj[k] ['乒乓球', '足球']
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
}
else {
// k 属性名 uname age oldObj[k] 属性值 18
// newObj[k] === o.uname 给新对象添加属性
newObj[k] = oldObj[k]
// o{ unmae:"zhangsan",age:18}
}
}
}
deepCopy(o, obj) // 函数调用 两个参数 o 新对象 obj 旧对象

o.age = 20
o.hobby[0] = '篮球'
o.family.baby = 'laozhang'

console.log(o)
console.log(obj)
console.log([1, 23] instanceof Object)

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<!-- 先引用 -->
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'zhangsan',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小zhang'
}
}
const o = _.cloneDeep(obj)
// console.log(o)
o.family.baby = '老zhang'
console.log(o)
console.log(obj)
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
const obj = {
uname: 'zhangsan',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小zhang'
}
}
// 把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj))
// console.log(o)
o.family.baby = '123'
console.log(o)
console.log(obj)
</script>

5.3 异常处理

throw 抛异常

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function fn(x, y) {
if (!x || !y) {
// throw '没有参数传递进来'
throw new Error('没有参数传递过来')
}

return x + y
}
console.log(fn())
</script>

1
2
3
4
5
总结:
1. throw 抛出异常信息,程序也会终止执行
2. throw 后面跟的是错误提示信息
3. Error 对象配合 throw 使用,能够设置更详细的错误信息

try /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
<body>
<p>123</p>
<script>
function fn() {
try {
// 可能发送错误的代码 要写到 try
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (err) {
// 拦截错误,提示浏览器提供的错误信息,但是不中断程序的执行
console.log( err.message)
// throw new Error('你看看,选择器错误了吧')
// 需要加return 中断程序
return
}
finally {
// 不管你程序对不对,一定会执行的代码
alert('弹出对话框')
}
console.log(11)
}
fn()
</script>
</body>

1
2
3
4
5
6
总结:
1. try...catch 用于捕获错误信息
2. 将预估可能发生错误的代码写在 try 代码段中
3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
4. finally 不管是否有错误,都会执行

5.4 this指向

5.4.1 普通函数this指向

普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<button>点击</button>
<script>
// 普通函数: 谁调用我,this就指向谁
console.log(this) // window
function fn() {
console.log(this) // window
}
window.fn()
window.setTimeout(function () {
console.log(this) // window
}, 1000)
document.querySelector('button').addEventListener('click', function () {
console.log(this) // 指向 button
})
const obj = {
sayHi: function () {
console.log(this) // 指向 obj
}
}
obj.sayHi()
</script>

5.4.2 箭头函数this指向

箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this

  1. 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的

2.箭头函数中的this引用的就是最近作用域中的this

3.向外层作用域中,一层一层查找this,直到有this的定义

注意情况1

在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window

因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数

注意情况2*:

同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数

5.5 改变this指向

JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向

1
2
3
4
call()
apply()
bind()

**1. call()了解 **

使用 call 方法调用函数,同时指定被调用函数中 this 的值

语法

1
fun.call(thisArg, arg1, arg2, ...) 
1
2
3
4
thisArg:在 fun 函数运行时指定的 this 值
arg1,arg2:传递的其他参数
返回值就是函数的返回值,因为它就是调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<script>
const obj = {
uname: 'zs'
}
function fn(x, y) {
console.log(this)
console.log(x + y)
}
// 1. 调用函数
// 2. 改变 this 指向
fn.call(obj, 1, 2)
</script>
</body>

总结

1
2
3
4
5
call的作用是?
调用函数,并可以改变被调用函数里面的this指向
2. call 里面第一个参数是 指定this, 其余是实参,传递的参数
整体做个了解,后期用的很少

2 apply()-理解*

语法:

1
2
fun.apply(thisArg, [argsArray])

(1)thisArg:在fun函数运行时指定的 this 值

(2) argsArray:传递的值,必须包含在数组里面

(3) 返回值就是函数的返回值,因为它就是调用函数

(4)因此 apply 主要跟数组有关系,比如使用Math.max()求数组的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
const obj = {
age: 18
}
function fn(x, y) {
console.log(this) // {age: 18}
console.log(x + y)
}
// 1. 调用函数
// 2. 改变this指向
// fn.apply(this指向谁, 数组参数)
fn.apply(obj, [1, 2])
// 3. 返回值 本身就是在调用函数,所以返回值就是函数的返回值

// 使用场景: 求数组最大值
// const max = Math.max(1, 2, 3)
// console.log(max)
const arr = [100, 44, 77]
const max = Math.max.apply(Math, arr)
const min = Math.min.apply(null, arr)
console.log(max, min)
// 使用场景: 求数组最大值
console.log(Math.max(...arr))
</script>
1
2
3
4
call和apply的区别是?
都是调用函数,都能改变this指向
参数不一样,apply传递的必须是数组

**3. bind()-**重点

语法:

1
2
fun.bind(thisArg, arg1, arg2, ...) 

1
2
3
4
5
thisArg:在 fun 函数运行时指定的 this 值
arg1,arg2:传递的其他参数
返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数)
因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的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
<body>
<button>发送短信</button>
<script>
const obj = {
age: 18
}
function fn() {
console.log(this)
}

// 1. bind 不会调用函数
// 2. 能改变this指向
// 3. 返回值是个函数, 但是这个函数里面的this是更改过的obj
const fun = fn.bind(obj)
// console.log(fun)
fun()

// 需求,有一个按钮,点击里面就禁用,2秒钟之后开启
document.querySelector('button').addEventListener('click', function () {
// 禁用按钮
this.disabled = true
window.setTimeout(function () {
// 在这个普通函数里面,我们要this由原来的window 改为 btn
this.disabled = false
}.bind(this), 2000) // 这里的this 和 btn 一样
})
</script>
</body>

call apply bind 总结

1
2
3
4
5
6
7
8
9
10
11
相同点:  
都可以改变函数内部的this指向.
区别点:
call 和 apply 会调用函数, 并且改变函数内部this指向.
call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2..形式 apply 必须数组形式[arg]
bind 不会调用函数, 可以改变函数内部this指向.
主要应用场景:
call 调用函数并且可以传递参数
apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.

6、模块化

1、模块化简介

随着前端的代码量越来越多,那么需要对代码进行管理,而模块化就是代码组织管理的最主流的一种方式,它可以按照不同的功能,将代码划分为不同的功能模块。

模块化只是一种思想

2、模块化演变过程

在最早期,JavaScript的模块化就是根据文件划分方式来实现的,将不同的功能以及相关的数据存储到不同的文件中,我们约定每一个文件就是一个独立的模块

我们使用这个模块,就是把这个模块引入到页面中。一个script标签对应一个模块

1
2
3
4
5
<script src="a.js"></script>
<script>
method1();
username="zhangsan"
</script>

在使用的时候,直接在代码中调用全局成员,这些全局成员有可能是一个变量,也有可能是一个函数。

这种方式的缺点也非常明显,所有的模块都是在全局的范围内工作,并没有独立的私有空间,这样导致模块内的成员会在模块外被任意的访问或者修改。而且模块多了以后,很容易出现命名冲突的问题,同时也无法管理模块之间的依赖关系。

所以说,早期的模块化完全依靠的是约定,而这种情况在项目增大以后,就会出现各种问题。

下面进入说一下第二阶段:命名空间方式

我们约定每个模块只暴露一个全局的对象,模块的所有成员都挂载到这个对象的下面。在第一个阶段基础上(也是单独创建一个文件),把每个模块包裹成一个全局对象的方式来实现。

1
2
3
4
5
6
var moduleA={
name:'zhangsan',
method1:function(){
console.log('username:',this.name)
}
}

使用的方式

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
</head>
<body>
<script src="a.js"></script>
<script>
moduleA.method1()
//模块成员可以被修改
moduleA.name="lisi"
</script>
</body>
</html>

在使用的时候,需要通过全局对象的名称来获取内部成员,这就类似于命名空间的方式。

但是,这种方式仍然没有私有空间,所以说模块内的成员依然可以在模块外被访问修改,另外模块的依赖关系也没有得到解决。

第三阶段:立即执行函数的方式

通过立即执行函数的方式为模块提供私有空间。具体做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
;(function($){
var name="zhangsan" // 该变量只能在当前立即函数中有效
function method1(){
cosnole.log(this.name)
}
function method2(){
console.log('method2')
}
window.moduleA={
method1:method1,
method2:method2
}
})(jQuery)

将模块中的每个成员都放到函数所提供的私有作用域内,对需要暴露给外部的成员,将其挂载到全局对象window中。这种方式实现了私有成员的概念,在外部无法访问模块内的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
</head>
<body>
<script src="a.js"></script>
<script>
moduleA.method1()
moduleA.method2()
//模块私有成员无法访问
console.log(moduleA.name)//undefined
</script>
</body>
</html>

这种方式,也解决了模块的依赖关系,例如,我们定义的模块需要jQuery,这里就可以直接传递jQuery.

以上方式,就是早期在没有工具和规范的情况下,对模块化的落地方式。

3、模块化规范的出现

前面介绍的模块化方式,都是通过约定的方式来实现的,这样在实际的应用中会出现相应的一些差异,所以这里就需要有模块化的规范与标准。

同时,前面定义的模块,在使用的时候都是通过srcipt标签的手动方式进行引入的。

1
<script src='a.js'></script>

这也就是说,模块的加载,不受代码的控制。这样维护起来也非常的麻烦,而且还容易出现问题。

例如,在html代码中忘记引入模块,或者是一个模块我们不需要了,但是在html代码中,却还引入该模块,这样都会出问题。

所以,我们需要通过一些代码来帮我们加载模块。

所以说,现在我们需要:

模块化的标准与模块加载的库。

说到模块化规范:首先我们想到的就是CommonJS规范,它是node.js中提到的一个规范。也就是说我们在写Node.js的时候,必须符合CommonJS的规范。

该规范要求:

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块

如果,我们在浏览端使用CommonJS规范,就会出现问题。而且CommonJS是以同步模式加载模块,因为Node的执行机制是在启动的时候加载模块,执行过程中是不需要加载模块的,所以这种方式在Node中不会出现问题。如果我们在浏览器端使用CommonJS规范,必然会导致效率低下,因为每次页面的加载,都会导致大量的同步请求出现,这样严重的降低了浏览器加载的效率。

所以后来浏览器端没有使用CommonJS规范,而是结合浏览器端的特性,重新设计了一套规范。

这个规范就是AMD(Asynchronous Module Definition),翻译过来就是异步的模块定义规范。

同期还出现了一个比较著名的库Require.js,它实现了AMD的规范。

它定义模块的方式:

1
2
3
4
5
6
7
8
define('module1',['jquery','./module2'],function($,module2){
return {
start:funciton(){
$('body').animate({margin:'200px'})
module2()
}
}
})

通过define来定义模块,第一个参数为模块的名字,第二个参数是一个数组,用来声明我们创建的模块所依赖的项。第三个参数是一个函数,函数的参数与前面依赖项一一对应,$对应jquery,module2对应的是./module2.

这个函数的作用就是为当前模块提供一个私有的空间,如果我们需要模块向外部导出一些成员,可以使用return.

同时Require.js中还提供了一个载入模块的方法:require

1
2
3
require(['./module1'],funciton(module1){
module1.start
})

目前绝大多数第三方库都支持AMD规范。但是,AMD使用起来相对复杂。因为除了编写业务代码,还需要写大量的define函数与require函数。

另外,项目中模块划分的过于细致的化,那么在同一个文件中JS文件请求频繁,从而导致页面的效率比较低下。

所以说AMD不能是前端模块化最终的实现方案,仅仅是一个过渡的方案。

4、模块化标准规范

随着技术的不断发展,模块化的标准越来越规范,现在大家对模块化的最佳实践方式都统一了。

在浏览器端使用ES Modules,在Node.js中使用CommonJS

现在最新的浏览器都开始支持ES Module模块化的方式了。所以,我们可以浏览器端直接使用ES Module来实现模块化。

5、ES Modules 语法

下面我们来看一下它的基本使用

通过给script标签添加type=module的属性,就可以以ES Module的标准执行其中的js代码了。

1
2
3
<script type='module'>
console.log('hello world')
</script>

每个ES Module都是运行在单独的私有作用域中的。

1
2
3
4
5
6
7
<script type="module">
var foo=100
console.log(foo)
</script>
<script type="module">
console.log(foo) //这里会出现foo未定义的错误
</script>

6、模块导出

ES Modules的核心功能是模块的导出与导入

1
2
3
4
5
6
// ./module.js
const foo='es modules'
export {foo}
// ./app.js
import {foo} from './module.js'
console.log(foo)

下面,我们演示一下基本的导入与导出。

在目录下面创建app.jsindex.html,module.js三个文件。

module.js文件中的代码如下:

1
export var name='lisi'

name这个变量进行导出,这样在其他的模块中才能访问该变量。

app.js文件的代码如下:

1
2
import {name} from './module.js';
console.log(name);

进行了导入,并且输出name变量的值

index.html文件中的代码

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ES Module 导出与导入</title>
</head>
<body>
<script type='module' src="app.js"></script>
</body>
</html>

可以通过Live Server启动,来查看运行的效果

在上面的案例中,我们通过export导出了变量,当然也可以导出函数

1
2
3
4
export var name='lisi'
export function hello(){
console.log('hello world')
}

如果以上导出的方式,书写起来比较麻烦,可以采用如下的方式

1
2
3
4
5
6
var name='lisi'
function hello(){
console.log('hello world')
}

export {name,hello}

最后通过export统一进行导出。

这种方式是使用最多的导出方式,因为这种方式可以让我们更加直观的感受到当前模块到底导出了哪些成员。

当然,通过以上的方式,还可以对导出的成员进行重命名。

1
2
3
4
5
6
7
 var name='lisi'
function hello(){
console.log('hello world')
}

// export {name,hello,Person}
export { name as username}

在上面的代码中,我们在导出name的时候通过as关键字将其重命名为username,那么在进行导入的时候就需要使用username

1
2
import {username} from './module.js';
console.log(username);

如果我们给export后面添加了default关键字,表示的是默认的导出。

1
2
3
4
5
6
7
8
var name='lisi'
function hello(){
console.log('hello world')
}

// export {name,hello,Person}
// export { name as username}
export default name

在上面的代码中,默认导出了name属性。

这时导入的方式如下:

1
2
import username from './module.js';
console.log(username);

在导入的时候,不需要添加大括号,同时导入的名称是可以随意命名的,例如导出的变量名叫name,但是导入的时候修改成了username

7、导入与导出的注意事项

1
2
3
var name='zhangsan'
var age=18
export {name,age}

在进行导出的时候,注意语法,例如上面我们导出了nameage这两个变量,在这里必须将其放到大括号中,这时它的语法要求,不要认为这里导出的是一个对象。

如果,这里确定需要导出一个对象,可以使用export default的方式

1
export default {name,age}

下面将module.js文件中的代码修改成如下形式:

1
2
3
var name='zhangsan'
var age=18
export default {name,age}

如果app.js文件中的导入是如下写法,而这种写法是错误的。

1
2
import {name,age} from './module.js';
console.log(name,age);

这里,不要把导入的写法理解成解构。这其实是固定的写法。

正确的导入方式如下:

1
2
import obj from './module.js';
console.log(obj.name,obj.age);

下面我们再来强调要注意的第二点内容:

es module中,通过export对成员进行导出的时候,注意导出的是该成员的引用。

也就是说,在模块内部导出的成员与其它模块中导入的成员是指向了同一个内存的地址。

下面,演示一下

module.js文件中的代码如下:

1
2
3
4
5
6
7
var name='zhangsan'
var age=18
// export default {name,age}
export {name,age}
setTimeout(function(){
name='lisi'
},1000)

在导出nameage成员后,停顿了1秒钟,修改name成员的值。

app.js文件中的代码如下:

1
2
3
4
5
import {name,age} from './module.js';
console.log(name,age);
setTimeout(function() {
console.log(name,age);
},1500)

导入nameage成员,然后输出打印,停顿1500毫秒后,重新打印nameage的值,

app.js文件中,我们没有修改name属性的值,但是在1500毫秒以后,name的值变成了lisi,而对name属性值的修改是在module.js文件中完成的。通过这个例子,我们可以得出的结论就是:通过export对成员进行导出的时候,导出的是该成员的引用,并不是将值进行了拷贝。

第三点:要注意的时,导出的成员是只读的。也就是说,如果我们现在在app.js文件中导入了module.js模块中的name

age成员,然后在app.js文件中去修改这两个成员的值的时候,会出现错误。也就说,导入的成员是一个只读的成员

1
2
import {name,age} from './module.js';
name="ssss"

以上就是导入与导出要注意的事项。

8、import用法

import中的from后面跟的是一个字符串,表示的是文件的路径,该路径必须是一个完整的路径。

1
import {name} from './module'

以上的写法是错误的,必须添加js后缀。

1
import {name} from './module.js'

如果某个目录下面有index.js文件,在进行导入的时候,也需要写完整。例如,如下写法错误

1
import {age} from './utils'

正确的写法如下:

1
import {age} from './utils/index.js'

当我们后面使用一些打包工具,来打包模块的时候,就可以省略扩展名和默认文件的情况。

下面还要注意的就是相对路径中的.不能省略,如下写法是错误的

1
import {name} from 'module.js'

以上写法是错误的,这种写法默认的是加载第三方模块。

当然也可以使用绝对路径

1
import {name,age} from '/module.js';

也可以使用完整的url的方式,这样就可以引用cdn上的模块。

以上就是在导入模块的时候,关于路径的问题。

如果,我们只是需要执行某个模块,而不需要提取这个模块中的成员,可以采用如下的写法:

1
import {} from './module.js'

以上表明只是执行module这个模块,并不提取这个模块中的成员。

也可以采用如下简写的方式

1
import './module.js'

下面,我们再来看第三种情况,如果某个模块中导出的成员比较多,而且现在我们都需要这些导出的成员,那么再进行导入的时候,为了书写的简单方便,可以采用如下的方式:

1
2
import * as mod from './module.js'
console.log(mod.name)

首先import后面跟了一个星号,然后在星号后面跟了一个关键字as,将导入的全部成员放到一个对象中。这样导出的成员都会作为这个对象的属性而存在。

下面,我们看一下第四点情况。

当我们通过import进行模块导入的时候,必须明确指定导入模块的路径。

但是,在有的情况下,我们所导入的模块的路径是在运行的情况下才确定的,那么你可能会想到使用如下的方式进行导入:

1
2
3
var modulePath='./module.js'
import {name} from modulePath
console.log(name)

以上的写法是错误的,也就是说我们不能通过importfrom后面跟着一个变量。

还有一种情况,就是我们想在某个判断条件成立后,才去导入某个模块,所以你会想到如下的写法:

1
2
3
if(true){
import {name} from './module.js'
}

以上的写法也是错误的,因为import这个关键字只能出现在最顶层,并不能嵌套在if或者是函数中。

如果,现在你遇到了以上两种情况,那么可以通过动态导入模块的方式来进行解决。写法如下:

1
import('./module.js')

在以上的写法中,将import作为一个函数来使用,这样可以在任何的位置去调用该函数来导入相应的模块。而且import函数返回的是一个promise,而且我们知道模块加载是一个异步的过程,这时当模块加载完成后,会执行后面的then函数。

1
2
3
import('./module.js').then(funciton(module){
console.log(module)
})

下面我们来看第五点: 就是在导出模块的时候,我们导出了一个命名的成员,又导出了一个默认的成员。

如下代码所示:

1
2
3
4
var name='zhangsan'
var age=18
export {name,age}
export default 'hello world'

在进行导入的时候,可以采用如下的写法:

1
2
import title,{name,age} from './module.js';
console.log(title,age,name);

import后面跟的title是起了一个别名(这个名字可以随便起),用来接收默认的导入的成员,大括号中的内容,用来接收命名的导出的成员。