JavaScript 进阶 - 第1天
学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。
- 理解作用域对程序执行的影响
- 能够分析程序执行的作用域范围
- 理解闭包本质,利用闭包创建隔离作用域
- 了解什么变量提升及函数提升
- 掌握箭头函数、解析剩余参数等简洁语法
作用域
了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
1
2
3
4
5
6
7
8
9
10
11
12
|
<script>
// 声明 counter 函数
function counter(x, y) {
// 函数内部声明的变量
const s = x + y
console.log(s) // 18
}
// 设用 counter 函数
counter(10, 8)
// 访问变量 s
console.log(s)// 报错
</script>
|
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 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
|
<script>
{
// age 只能在该代码块中被访问
let age = 18;
console.log(age); // 正常
}
// 超出了 age 的作用域
console.log(age) // 报错
let flag = true;
if(flag) {
// str 只能在该代码块中被访问
let str = 'hello world!'
console.log(str); // 正常
}
// 超出了 age 的作用域
console.log(str); // 报错
for(let t = 1; t <= 6; t++) {
// t 只能在该代码块中被访问
console.log(t); // 正常
}
// 超出了 t 的作用域
console.log(t); // 报错
</script>
|
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<script>
// 必须要有值
const version = '1.0.0';
// 不能重新赋值
// version = '1.0.1';
// 常量值为对象类型
const user = {
name: '小明',
age: 18
}
// 不能重新赋值
user = {};
// 属性和方法允许被修改
user.name = '小小明';
user.gender = '男';
</script>
|
总结:
let 声明的变量会产生块作用域,var 不会产生块作用域
const 声明的常量也会产生块作用域
- 不同代码块之间的变量无法互相访问
- 推荐使用
let 或 const
注:开发中 let 和 const 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。
全局作用域
<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
1
2
3
4
5
6
7
8
9
|
<script>
// 此处是全局
function sayHi() {
// 此处为局部
}
// 此处为全局
</script>
|
全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<script>
// 全局变量 name
const name = '小明'
// 函数作用域中访问全局
function sayHi() {
// 此处为局部
console.log('你好' + name)
}
// 全局变量 flag 和 x
const flag = true
let x = 10
// 块作用域中访问全局
if(flag) {
let y = 5
console.log(x + y) // x 是全局的
}
</script>
|
总结:
- 为
window 对象动态添加的属性默认也是全局的,不推荐!
- 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
作用域链
在解释什么是作用域链前先来看一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// 局部作用域
function g() {
let d = 'yo'
}
}
</script>
|
函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
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
|
<script>
// 全局作用域
let a = 1
let b = 2
// 局部作用域
function f() {
let c
// let a = 10;
console.log(a) // 1 或 10
console.log(d) // 报错
// 局部作用域
function g() {
let d = 'yo'
// let b = 20;
console.log(b) // 2 或 20
}
// 调用 g 函数
g()
}
console.log(c) // 报错
console.log(d) // 报错
f();
</script>
|
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
JS垃圾回收机制
什么是垃圾回收机制?
垃圾回收机制(Garbage Collection) 简称 GC
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况
不再用到的内存,没有及时释放,就叫做内存泄漏
内存的生命周期
JS环境中分配的内存, 一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
- 说明:
Ø 全局变量一般不会回收(关闭页面回收);
Ø 一般情况下局部变量的值, 不用了, 会被自动回收掉
堆栈空间分配区别:
- 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
- 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
下面介绍两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法
l 引用计数
IE采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象
算法:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
- 如果减少一个引用就减1 –
- 如果引用次数是0 ,则释放内存
但它却存在一个致命的问题:嵌套引用(循环引用)
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
l 标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在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
|
<body>
<script>
// 1. 闭包 : 内层函数 + 外层函数变量
// function outer() {
// const a = 1
// function f() {
// console.log(a)
// }
// f()
// }
// outer()
// 2. 闭包的应用: 实现数据的私有。统计函数的调用次数
// let count = 1
// function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
// 3. 闭包的写法 统计函数的调用次数
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer() // 被保存到全局变量中,不会被回收
// const re = function fn() {
// count++
// console.log(`函数被调用${count}次`)
// }
re() // 函数被调用2次
re() // 函数被调用3次
// const fn = function() { } 函数表达式
// 4. 闭包存在的问题: 可能会造成内存泄漏
</script>
</body>
|
总结:
1.怎么理解闭包?
2.闭包的作用?
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
3.闭包可能引起的问题?
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,
1
2
3
4
5
6
7
|
<script>
// 访问变量 str
console.log(str + 'world!');
// 声明变量 str
var str = 'hello ';
</script>
|
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let 声明的变量不存在变量提升,推荐使用 let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。
函数
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<script>
// 调用函数
foo()
// 声明函数
function foo() {
console.log('声明之前即被调用...')
}
// 不存在提升现象
bar() // 错误
var bar = function () {
console.log('函数表达式不存在提升现象...')
}
</script>
|
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
默认值
1
2
3
4
5
6
7
8
9
10
|
<script>
// 设置参数默认值
function sayHi(name="小明", age=18) {
document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
}
// 调用函数
sayHi();
sayHi('小红');
sayHi('小刚', 21);
</script>
|
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
动态参数
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<script>
// 求生函数,计算所有参数的和
function sum() {
// console.log(arguments)
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
// 调用求和函数
sum(5, 10)// 两个参数
sum(1, 2, 4) // 两个参数
</script>
|
总结:
arguments 是一个伪数组
arguments 的作用是动态获取函数的实参
剩余参数(推荐使用这个)
1
2
3
4
5
6
7
8
|
<script>
function config(baseURL, ...other) {
console.log(baseURL) // 得到 'http://baidu.com'
console.log(other) // other 得到 ['get', 'json']
}
// 调用函数
config('http://baidu.com', 'get', 'json');
</script>
|
总结:
... 是语法符号,置于最末函数形参之前,用于获取多余的实参
- 借助
... 获取的剩余实参,是个真数组
展开运算符
展开运算符(…),将一个数组进行展开
1
2
|
const arr = [1, 2, 3, 4, 5]
console.log(...arr) // 1 2 3 4 5
|
说明:不会修改原数组
典型运用场景: 求数组最大值(最小值)、合并数组等
1
2
3
4
5
6
7
8
9
|
// 求数组最大(最小)值
const arr1 = [1, 2, 3, 4, 5]
console.log(Math.max(...arr1)) // 5
console.log(Math.min(...arr1)) // 1
// 合并数组
const arr2 = [6, 7, 8]
const arr3 = [...arr1, ...arr2]
console.log(arr3) //
|
箭头函数
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
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
|
<body>
<script>
const fn = function () {
console.log(123)
}
// 1. 箭头函数 基本语法
const fn = () => {
console.log(123)
}
fn() // 123
const fn = (x) => {
console.log(x)
}
fn(1) // 1
// 2. 只有一个形参的时候,可以省略小括号
const fn = x => {
console.log(x)
}
fn(1) // 1
// 3. 只有一行代码的时候,我们可以省略大括号
const fn = x => console.log(x)
fn(1) // 1
// 4. 只有一行代码的时候,可以省略return
const fn = x => x + x
console.log(fn(1)) // 2
// 5. 箭头函数可以直接返回一个对象
const fn = (uname) => ({ uname: uname })
console.log(fn('刘德华')) // {uname: '刘德华'}
</script>
</body>
|
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{},并自动做为返回值被返回
箭头函数参数
箭头函数中没有 arguments,只能使用 ... 动态获取实参
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<body>
<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>
|
箭头函数 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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<script>
// 以前this的指向: 谁调用的这个函数,this 就指向谁
// console.log(this) // window
// // 普通函数
// function fn() {
// console.log(this) // window
// }
// window.fn()
// // 对象方法里面的this
// const obj = {
// name: 'andy',
// sayHi: function () {
// console.log(this) // obj
// }
// }
// obj.sayHi()
// 2. 箭头函数的this 是上一层作用域的this 指向
// const fn = () => {
// console.log(this) // window
// }
// fn()
// 对象方法箭头函数 this
// const obj = {
// uname: 'pink老师',
// sayHi: () => {
// console.log(this) // this 指向谁? window
// }
// }
// obj.sayHi()
const obj = {
uname: 'pink老师',
sayHi: function () {
console.log(this) // obj
let i = 10
const count = () => {
console.log(this) // obj
}
count()
}
}
obj.sayHi()
</script>
|
解构赋值
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
1
2
3
4
5
6
7
8
9
10
|
<script>
// 普通的数组
let arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
</script>
|
总结:
- 赋值运算符
= 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
- 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
... 获取剩余单元值,但只能置于最末位
- 允许初始化变量的默认值,且只有单元值为
undefined 时默认值才会生效
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
典型应用交互2个变量
1
2
3
4
|
let a = 1
let b = 2; // 注:这里的分号必须加
[b, a] = [a, b]
console.log(a, b) // 2 1
|
两种要加分号的情况
1.立即执行函数
1
2
3
4
5
6
7
8
9
|
// 不加分号
console.log(1) // 1
(function fn() {console.log(2)})() // Uncaught TypeError: console.log(...) is not a function
// 不加分号时,上述代码在电脑看来如下,当然会报错
console.log(1)(function fn() {console.log(2)})()
// 加分号
console.log(1); // 1
(function fn() {console.log(2)})() // 2
|
2.数组开头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 不加分号
let a, b
console.log(1) // 1
[a, b] = [1, 2] // Uncaught TypeError: Cannot set properties of undefined (setting 'undefined')
console.log(a, b)
// 与立即执行函数同理,不加分号时,上述代码在电脑看来如下,当然会报错
let a, b
console.log(1)[a, b] = [1, 2]
console.log(a, b)
// 加分号
let a, b
console.log(1); // 1
[a, b] = [1, 2]
console.log(a, b) // 1 2
|
数组解构的几种情况
利用剩余参数解决变量少 单元值多的情况:
剩余参数返回的还是一个数组
防止有undefined传递单元值的情况,可以设置默认值:
允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效
按需导入,忽略某些返回值:
支持多维数组的结构:
对象解构
基本语法
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<script>
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user
console.log(name) // 小明
console.log(age) // 18
</script>
|
总结:
- 赋值运算符
= 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
- 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined 时默认值才会生效
注:支持多维解构赋值
给新的变量名赋值
可以从一个对象中提取变量并同时修改新的变量名
1
2
3
4
|
const {uname: name, age} = {uname: '张三', age: 18}
console.log(name) // 张三
console.log(age) // 18
// 此时打印uname会提示undefined
|
冒号表示“什么值:赋值给谁”
数组对象结构
1
2
3
4
5
6
7
8
9
|
const pig = [
{
uname: '佩奇',
age: 5
}
]
const [{uname:name, age}] = pig
console.log(name) // 佩奇
console.log(age) // 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
|
// 多级对象结构
const pig = {
name: '佩奇',
family: {
mom: 'pigMom',
dad: 'pigDad'
},
age: 5
}
const {name, family:{mom, dad}, age} = pig
console.log(name) // 佩奇
console.log(mom) // pigMom
console.log(dad) // pigDad
console.log(age) // 5
// 多级数组对象结构
const pig = [
{
name: '佩奇',
family: {
mom: 'pigMom',
dad: 'pigDad'
},
age: 5
}
]
const [{name, family:{mom, dad}, age}] = pig
console.log(name) // 佩奇
console.log(mom) // pigMom
console.log(dad) // pigDad
console.log(age) // 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
|
<body>
<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选出当做参数传递给 函数
// const { data } = msg
// msg 虽然很多属性,但是我们利用解构只要 data值
function render({ data }) {
// const { data } = arr
// 我们只要 data 数据
// 内部处理
console.log(data)
}
render(msg)
// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)
}
render(msg)
</script>
|
综合案例
forEach遍历数组
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
注意:
1.forEach 主要是遍历数组
2.参数当前数组元素是必须要写的, 索引号可选。
1
2
3
4
5
6
7
8
9
10
11
|
<body>
<script>
// forEach 就是遍历 加强版的for循环 适合于遍历数组对象
const arr = ['red', 'green', 'pink']
const result = arr.forEach(function (item, index) {
console.log(item) // 数组元素 red green pink
console.log(index) // 索引号
})
// console.log(result) // undefined
</script>
</body>
|
渲染商品列表案例
核心思路:有多少条数据,就渲染多少模块,然后 生成对应的 html结构标签, 赋值给 list标签即可
①:利用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
|
<!DOCTYPE html>
<html lang="zh-CN">
<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',
},
]
// 声明一个字符串变量
let str = ''
// 遍历数组
goodsList.forEach(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>
`
})
document.querySelector('.list').innerHTML = str
</script>
</body>
</html>
|
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
l filter() 筛选数组
l 返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
l 参数:currentValue 必须写, index 可选
l 因为返回新数组,所以不会影响原数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<body>
<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>
</body>
|
商品列表价格筛选
需求:
①:渲染数据列表
②:根据选择不同条件显示不同商品
分析:
①:渲染页面 利用forEach 遍历数据里面的 数据,并渲染数据列表
②:根据 filter 选择不同条件显示不同商品
步骤:
①:渲染页面模块
(1) 初始化需要渲染页面,同时,点击不同的需求,还会重新渲染页面,所以渲染做成一个函数
(2) 做法基本跟前面案例雷同,就是封装到了一个函数里面
②:点击不同需求,显示不同页面内容
(1) 点击采取事件委托方式 .filter
(2) 利用过滤函数 filter 筛选出符合条件的数据,因为生成的是一个数组,传递给渲染函数即可
(3) 筛选条件是根据点击的 data-index 来判断
(4) 可以使用对象解构,把 事件对象 解构
(5) 因为 全部区间不需要筛选,直接 把goodList渲染即可
效果图:

代码:
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
196
|
<!DOCTYPE html>
<html lang="zh-CN">
<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>
// 初始化数据
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',
},
]
// 封装渲染函数
function render(arr = []) {
// 声明空字符串
let str = ''
// 遍历数组
arr.forEach(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>
`
})
// 把内容追加给list
document.querySelector('.list').innerHTML = str
}
// 页面一打开就要渲染
render(goodsList)
// 过滤筛选
// 事件委托
document.querySelector('.filter').addEventListener('click', e => {
// 解构对象
const {tagName, dataset} = e.target
if(tagName === 'A') {
let arr = goodsList
if (dataset.index === '1') {
// 箭头函数,参数只有一个时可以省略形参的小括号,代码只有一行时,可以省略大括号并作为返回值返回
// filter函数,此时返回的是price在0~100间的item
arr = goodsList.filter(item => item.price >= 0 && item.price <= 100)
}
if (dataset.index === '2') {
arr = goodsList.filter(item => item.price >= 100 && item.price <= 300)
}
if (dataset.index === '3') {
arr = goodsList.filter(item => item.price >= 300)
}
// 无论点了谁,都要渲染页面,并且因为一开始将goodsList赋值给了arr
// 所以如果点全部区间选项时,data-index不满足上面的判断,等同于直接渲染goodsList
render(arr)
}
})
</script>
</body>
</html>
|
JavaScript 进阶 - 第2天
了解面向对象编程的基础概念及构造函数的作用,体会 JavaScript 一切皆对象的语言特征,掌握常见的对象属性和方法的使用。
- 了解面向对象编程中的一般概念
- 能够基于构造函数创建对象
- 理解 JavaScript 中一切皆对象的语言特征
- 理解引用对象类型值存储的的特征
- 掌握包装类型对象常见方法的使用
深入对象
了解面向对象的基础概念,能够利用构造函数创建对象。
构造函数
构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<script>
// 定义函数
function foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new foo;
// 例
function Pig(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
// 创建对象
const Peppa = new Pig('佩奇', 6, '男')
const George = new Pig('乔治', 3, '男')
</script>
|
总结:
- 命名以大写字母开头
- 使用
new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数时可以省略
()
- 构造函数的返回值即为新创建的对象
- 构造函数内部的
return 返回的值无效!
注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<script>
// 构造函数
function Person() {
// 构造函数内部的 this 就是实例对象
// 实例对象中动态添加属性
this.name = '小明'
// 实例对象动态添加方法
this.sayHi = function () {
console.log('大家好~')
}
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法
</script>
|
总结:
- 构造函数内部
this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
- 为构造函数传入参数,动态创建结构相同但值不同的对象
注:构造函数创建的实例对象彼此独立互不影响。
静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<script>
// 构造函数
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
console.log('^_^人都会走路...')
// this 指向 Person
console.log(this.eyes)
}
</script>
|
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的
this 指向构造函数本身
内置构造函数
掌握各引用类型和包装类型对象属性和方法的使用。
在 JavaScript 中最主要的数据类型有 6 种,分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。
在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date 就是内置的构造函数。
1
2
3
4
5
6
7
|
<script>
// 实例化
let date = new Date();
// date 即为实例对象
console.log(date);
</script>
|
甚至字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。
1
2
3
4
5
|
const str = 'pink'
// js底层会将上面的代码包装成下方代码
const str = new String('pink')
// 所以才能调用方法
console.log(str.length)
|
Object
Object 是内置的构造函数,用于创建普通对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<script>
// 通过构造函数创建普通对象
const user = new Object({name: '小明', age: 15})
// 这种方式声明的变量称为【字面量】
let student = {name: '杜子腾', age: 21}
// 对象语法简写
let name = '小红';
let people = {
// 相当于 name: name
name,
// 相当于 walk: function () {}
walk () {
console.log('人都要走路...');
}
}
console.log(student.constructor);
console.log(user.constructor);
console.log(student instanceof Object);
</script>
|
总结:
-
推荐使用字面量方式声明对象,而不是 Object 构造函数
-
Object.assign 静态方法创建新的对象
1
2
3
4
5
6
7
8
|
// 拷贝对象 Object.assign(新对象, 被拷贝对象)
const obj = { name: 'pink', age: 18}
const ept = {}
Object.assign(ept, obj)
console.log(ept) // {name: 'pink', age: 18}
// 可用于新增属性
Object.assign(ept, {gender: '男'})
console.log(ept) // {name: 'pink', age: 18, gender: '男'}
|
-
Object.keys 静态方法获取对象中所有属性(返回数组)
-
Object.values 表态方法获取对象中所有属性值(返回数组)
1
2
3
|
const obj = { name: 'pink', age: 18}
console.log(Object.keys(obj)) // ['name', 'age']
console.log(Object.values(obj)) // ['pink', 18]
|
Array
Array 是内置的构造函数,用于创建数组。
1
2
3
4
5
6
7
8
|
<script>
// 构造函数创建数组
let arr = new Array(5, 7, 8);
// 字面量方式创建数组
let list = ['html', 'css', 'javascript']
</script>
|
数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。
总结:
-
推荐使用字面量方式声明数组,而不是 Array 构造函数
-
实例方法 forEach 用于遍历数组,替代 for 循环 (重点)
-
实例方法 filter 过滤数组单元值,生成新数组(重点)
-
实例方法 map 迭代原数组,生成新数组(重点)
-
实例方法 join 数组元素拼接为字符串,返回字符串(重点)
-
实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)
使用场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const arr = [
{
name: '小米',
price: 4999
},
{
name: '华为',
price: 6999
},
{
name: '苹果',
price: 12999
}
]
const mi = arr.find(item => item.name === '小米')
console.log(mi) // {name: '小米', price: 4999}
|
- 实例方法
every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)
使用场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const arr = [
{
name: '小米',
price: 4999
},
{
name: '华为',
price: 6999
},
{
name: '苹果',
price: 12999
}
]
console.log(arr.every(item => item.price >= 2999)) // true
console.log(arr.every(item => item.price >= 6999)) // false
|
-
实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false
-
实例方法 concat 合并两个数组,返回生成新数组
-
实例方法 sort 对原数组单元值排序
-
实例方法 splice 删除或替换原数组单元
-
实例方法 reverse 反转数组
-
实例方法 findIndex 查找元素的索引值
-
实例方法reduce 返回函数累计处理的结果,经常用于求和等
基本语法:数组.reduce(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
30
31
32
33
34
35
36
37
|
// 数组reduce方法
const arr = [1, 2, 3]
// 没有初始值时
const total = arr.reduce(function(prev, current) {
return prev + current
})
console.log(total) // 6
// 有初始值时
const total = arr.reduce(function(prev, current) {
return prev + current
}, 10)
console.log(total) // 16
// 箭头函数的写法
const total = arr.reduce((prev, current) => prev + current, 10)
console.log(total) // 16
// 执行过程
// 1. 如果没有*初始值*,则*上一次值*为数组的第一个元素的值
// 2. 每一次循环,把返回值作为下一次循环的*上一次值*
// 3. 如果有*初始值*,则初始值作为*上一次值*
// 上面代码无初始值时的执行过程演示
// 上一次值 当前值 返回值 (第一次循环)
// 1 2 3
// 上一次值 当前值 返回值 (第二次循环)
// 3 3 6
// 上面代码无初始值时的执行过程演示
// 上一次值 当前值 返回值 (第一次循环)
// 10 1 11
// 上一次值 当前值 返回值 (第二次循环)
// 11 2 13
// 上一次值 当前值 返回值 (第三次循环)
// 13 3 16
|
- 静态方法
Array.from 将伪数组转换为真数组
1
2
3
4
5
6
7
|
const lisFake = document.querySelectorAll('ul li')
console.log(lisFake) // NodeList(3) [li, li, li]
// lisFake.pop() // 报错,伪数组无法调用数组方法
const lisReal = Array.from(lisFake)
console.log(lisReal) // (3) [li, li, li]
lisReal.pop() // pop()删除数组最后一个元素
console.log(lisReal) // (2) [li, li]
|
小练习:计算薪资
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
const arr = [{
name: '张三',
salary: 10240
}, {
name: '李四',
salary: 20480
}, {
name: '王五',
salary: 4096
},
]
// 需求1:求出总薪资
// 因为设置了初始值,所以第一次循环的prev为0
const total = arr.reduce((prev, current) => prev + current.salary, 0)
console.log(total) // 34816
// 需求2:每个人涨薪30%,求出总薪资
const total = arr.reduce((prev, current) => prev + current.salary * 1.3, 0)
console.log(total) // 45260.8
|
小练习:将对象内的属性值转换为字符串,属性值中间用符号隔开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 现有:
const spec = {size: '40cm*40cm', color: '黑色'}
// 要求:在页面显示40cm*40cm/黑色
// 1. 将对象的属性值存成数组
const arr = Object.values(spec)
// console.log(arr) // ['40cm*40cm', '黑色']
// 2. 用join()将数组转换成字符串,并用想要的分割符隔开
const str = arr.join('/')
// console.log(str) // 40cm*40cm/黑色
// 3. 将得到的字符串传给div
document.querySelector('div').innerHTML = str
|
包装类型
在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:
1
2
3
4
5
6
7
8
9
10
11
|
<script>
// 字符串类型
const str = 'hello world!'
// 统计字符的长度(字符数量)
console.log(str.length)
// 数值类型
const price = 12.345
// 保留两位小数
price.toFixed(2) // 12.34
</script>
|
之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。
String
String 是内置的构造函数,用于创建字符串。
1
2
3
4
5
6
7
8
9
10
11
|
<script>
// 使用构造函数创建字符串
let str = new String('hello world!');
// 字面量创建字符串
let str2 = '你好,世界!';
// 检测是否属于同一个构造函数
console.log(str.constructor === str2.constructor); // true
console.log(str instanceof String); // false
</script>
|
总结:
-
实例属性 length 用来获取字符串的度长(重点)
-
实例方法 split('分隔符') 用来将字符串拆分成数组(重点)
1
2
3
4
5
6
7
8
|
// split()和join()类似,不过一个是字符串转数组,一个是数组转字符串
const str1 = 'pink, red'
const arr1 = str1.split(',')
console.log(arr1) // ['pink', ' red']
const str2 = '2024-11-10'
const arr2 = str2.split('-')
console.log(arr2) // ['2024', '11', '10']
|
- 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)
1
2
3
4
5
6
7
8
9
|
// 省略结束的索引号,默认取到最后
const str = '0123456789'
const arr = str.substring(4)
console.log(arr) // 456789
// 有结束索引号时,取到结束索引号前一个
const str = '0123456789'
const arr = str.substring(4, 8)
console.log(arr) // 4567
|
- 实例方法
startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点)
1
2
3
4
5
6
7
|
// startsWith()判断是否以某字符开头
const str = '0123456789'
console.log(str.startsWith('0')) // true
console.log(str.startsWith('0123')) // true
console.log(str.startsWith('123')) // false
console.log(str.startsWith('123', 1)) // true
console.log(str.startsWith('56789', 5)) // true
|
- 实例方法
includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)
1
2
3
4
5
6
|
// includes()判断某字符是否包含在一个字符串内
const str = '0123456789'
console.log(str.includes('123')) // true
console.log(str.includes('0123')) // true
console.log(str.includes('0123', 0)) // true
console.log(str.includes('0123', 1)) // false
|
-
实例方法 toUpperCase 用于将字母转换成大写
-
实例方法 toLowerCase 用于将就转换成小写
-
实例方法 indexOf 检测是否包含某字符
-
实例方法 endsWith 检测是否以某字符结尾
-
实例方法 replace 用于替换字符串,支持正则匹配
-
实例方法 match 用于查找字符串,支持正则匹配
-
实例方法toString 将其他数据类型转换为字符串
-
静态方法String(变量) 将其他数据类型转换为字符串
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
小练习:显示赠品
请将下面字符串渲染到准备好的 p标签内部,结构必须如左下图所示,展示效果如图所示:
const gift = ‘50g茶叶,清洗球’

思路:
①:把字符串拆分为数组,这样两个赠品就拆分开了 用split(’,')
②:利用map遍历数组,同时把数组元素生成到span里面,并且返回
③:因为返回的是数组,所以需要 转换为字符串, 用join(’')
④:p的innerHTML 存放刚才的返回值
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
const gift = '50g茶叶,清洗球'
// 以,为分隔符,将字符串拆分成数组
const arr = gift.split(',')
console.log(arr) // ['50g茶叶', '清洗球']
// 利用map遍历数组,同时把数组元素生成到span里面
const newArr = arr.map(function(item) {
return `<span>【赠品】${item}</span><br>`
})
console.log(newArr) // ['<span>【赠品】50g茶叶</span><br>', '<span>【赠品】清洗球</span><br>']
// 将新数组转换成字符串,中间无需空格,所以用join('')
const str = newArr.join('')
console.log(str) // <span>【赠品】50g茶叶</span><br><span>【赠品】清洗球</span><br>
// 将生成的字符串放入div中
document.querySelector('div').innerHTML = str
// 上述代码可以简写为一行
document.querySelector('div').innerHTML = gift.split(',').map(item => `<span>【赠品】${item}</span><br>`).join('')
|
Number
Number 是内置的构造函数,用于创建数值。
1
2
3
4
5
6
7
8
9
|
<script>
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
</script>
|
总结:
- 推荐使用字面量方式声明数值,而不是
Number 构造函数
- 实例方法
toFixed 用于设置保留小数位的长度,会四舍五入
综合案例:购物车展示
需求:
根据后台提供的数据,渲染购物车页面

代码:
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<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>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.list {
width: 990px;
margin: 100px auto 0;
}
.item {
padding: 15px;
transition: all .5s;
display: flex;
border-top: 1px solid #e4e4e4;
}
.item:nth-child(4n) {
margin-left: 0;
}
.item:hover {
cursor: pointer;
background-color: #f5f5f5;
}
.item img {
width: 80px;
height: 80px;
margin-right: 10px;
}
.item .name {
font-size: 18px;
margin-right: 10px;
color: #333;
flex: 2;
}
.item .name .tag {
display: block;
padding: 2px;
font-size: 12px;
color: #999;
}
.item .price,
.item .sub-total {
font-size: 18px;
color: firebrick;
flex: 1;
}
.item .price::before,
.item .sub-total::before,
.amount::before {
content: "¥";
font-size: 12px;
}
.item .spec {
flex: 2;
color: #888;
font-size: 14px;
}
.item .count {
flex: 1;
color: #aaa;
}
.total {
width: 990px;
margin: 0 auto;
display: flex;
justify-content: flex-end;
border-top: 1px solid #e4e4e4;
padding: 20px;
}
.total .amount {
font-size: 18px;
color: firebrick;
font-weight: bold;
margin-right: 50px;
}
</style>
</head>
<body>
<div class="list">
<!-- <div class="item">
<img src="https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg" alt="">
<p class="name">称心如意手摇咖啡磨豆机咖啡豆研磨机 <span class="tag">【赠品】10优惠券</span></p>
<p class="spec">白色/10寸</p>
<p class="price">289.90</p>
<p class="count">x2</p>
<p class="sub-total">579.80</p>
</div> -->
</div>
<div class="total">
<div>合计:<span class="amount">1000.00</span></div>
</div>
<script>
const goodsList = [
{
id: '4001172',
name: '称心如意手摇咖啡磨豆机咖啡豆研磨机',
price: 289.9,
picture: 'https://yanxuan-item.nosdn.127.net/84a59ff9c58a77032564e61f716846d6.jpg',
count: 2,
spec: { color: '白色' }
},
{
id: '4001009',
name: '竹制干泡茶盘正方形沥水茶台品茶盘',
price: 109.8,
picture: 'https://yanxuan-item.nosdn.127.net/2d942d6bc94f1e230763e1a5a3b379e1.png',
count: 3,
spec: { size: '40cm*40cm', color: '黑色' }
},
{
id: '4001874',
name: '古法温酒汝瓷酒具套装白酒杯莲花温酒器',
price: 488,
picture: 'https://yanxuan-item.nosdn.127.net/44e51622800e4fceb6bee8e616da85fd.png',
count: 1,
spec: { color: '青色', sum: '一大四小' }
},
{
id: '4001649',
name: '大师监制龙泉青瓷茶叶罐',
price: 139,
picture: 'https://yanxuan-item.nosdn.127.net/4356c9fc150753775fe56b465314f1eb.png',
count: 1,
spec: { size: '小号', color: '紫色' },
gift: '50g茶叶,清洗球'
}
]
// ***************自己写****************
// 根据数据渲染页面
document.querySelector('.list').innerHTML = goodsList.map(item => {
// 解构对象
const {picture, name, count, price, spec, gift} = item
// 规格文字模块
// Object.values()返回的是数组,再用join()把数组拼接成字符串
const text = Object.values(spec).join('/')
// 小计模块,保留两位小数,所以乘100就能化为整数
// 乘100再除100的原因:为了防止精度bug,先用整数计算,之后再除回来
// 精度bug:console.log(0.1 + 0.2) // 0.30000000000000004
const total = ((price * 100 * count ) / 100).toFixed(2)
// 赠品模块 '50g茶叶,清洗球' ps:赠品模块详细写法可以见上方笔记的 小练习:显示赠品
// 并不是所有商品都有赠品,所以加一个判断
const str = gift ? gift.split(',').map(item => `<span class="tag">【赠品】${item}</span>`).join('') : ''
return `
<div class="item">
<img src="${picture}" alt="">
<p class="name">${name} ${str}</p>
<p class="spec">${text}</p>
<p class="price">${price.toFixed(2)}</p>
<p class="count">x${count}</p>
<p class="sub-total">${total}</p>
</div>
`
}).join('')
// 合计价格模块
const amount = document.querySelector('.amount')
const total = goodsList.reduce((prev, item) => prev + (item.price * 100 * item.count ) / 100, 0)
amount.innerHTML = total.toFixed(2)
// ***************自己写****************
</script>
</body>
</html>
|
JavaScript 进阶 - 第3天笔记
了解构造函数原型对象的语法特征,掌握 JavaScript 中面向对象编程的实现方式,基于面向对象编程思想实现 DOM 操作的封装。
- 了解面向对象编程的一般特征
- 掌握基于构造函数原型对象的逻辑封装
- 掌握基于原型对象实现的继承
- 理解什么原型链及其作用
- 能够处理程序异常提升程序执行的健壮性
编程思想
学习 JavaScript 中基于原型的面向对象编程序的语法实现,理解面向对象编程的特征。
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次
调用就可以了。
举个栗子:蛋炒饭

面向对象
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:
构造函数
对比以下通过面向对象的构造函数实现的封装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<script>
function Person() {
this.name = '佚名'
// 设置名字
this.setName = function (name) {
this.name = name
}
// 读取名字
this.getName = () => {
console.log(this.name)
}
}
// 实例对像,获得了构造函数中封装的所有逻辑
let p1 = new Person()
p1.setName('小明')
console.log(p1.name)
// 实例对象
let p2 = new Person()
console.log(p2.name)
</script>
|
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之
间是彼此不影响的
总结:
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题
原型对象
构造函数通过原型分配的函数是所有对象所共享的。
prototype 属性
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
- 构造函数和原型对象中的this 都指向 实例化的对象
一般在构造函数放公共的属性,原型对象内放公共的方法
1
2
3
4
5
6
7
8
|
<script>
function Person() {
}
// 每个函数都有 prototype 属性
console.log(Person.prototype)
</script>
|
构造函数内方法与原型对象内方法的优先级
了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<script>
function Person() {
// 此处未定义任何方法
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}
// 实例化
let p1 = new Person();
p1.sayHi(); // 输出结果为 Hi~
</script>
|
构造函数 Person 中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi,接下来改动一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<script>
function Person() {
// 此处定义同名方法 sayHi
this.sayHi = function () {
console.log('嗨!');
}
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}
let p1 = new Person();
p1.sayHi(); // 输出结果为 嗨!
</script>
|
构造函数 Person 中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi。
通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<script>
function Person() {
// 此处定义同名方法 sayHi
this.sayHi = function () {
console.log('嗨!' + this.name)
}
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~' + this.name)
}
// 在构造函数的原型对象上添加属性
Person.prototype.name = '小明'
let p1 = new Person()
p1.sayHi(); // 输出结果为 嗨!
let p2 = new Person()
p2.sayHi()
</script>
|
原型对象也可以节约内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function Fn(name) {
this.name = name
this.say = function() {console.log('hi')}
}
const temp1 = new Fn(1)
const temp2 = new Fn(2)
console.log(temp1.say === temp2.say) // false
// 所以一般把公共的属性写进构造函数内
function Star(name, age) {
this.name = name
this.age = age
}
// 把公共的方法写进原型对象内
Star.prototype.sing = function() {
console.log('唱歌')
}
const ldh = new Star('刘德华', 18)
const zxy = new Star('张学友', 19)
console.log(ldh.sing === zxy.sing) // true
|
原型对象的this指向
构造函数和原型对象中的this 都指向实例化的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 构造函数的this指向
let that
function Person(name) {
this.name = name
that = this
}
const temp = new Person()
console.log(that === temp) // true,说明构造函数的this指向实例对象
// 原型对象的this指向
let that
function Person(name) {
this.name = name
}
Person.prototype.sing = function() {
that = this
}
const temp = new Person()
temp.sing() // 要先调用一次原型对象,将this赋值给that
console.log(temp === that) // true,说明原型对象的this也指向实例对象
|
总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。
小练习:给数组扩展方法
需求:
①:给数组扩展求最大值方法和求和方法
比如: 以前学过
const arr = [1,2,3]
arr.reverse() 结果是 [3,2,1]
扩展完毕之后:
arr.sum() 返回的结果是 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const arr = [1, 2, 3]
// 求最大值方法
Array.prototype.max = function() {
// 利用展开运算符 console.log(...arr) // 1 2 3
// 但如果写形参...arr的话后续还需要传参,为了省事直接使用this
// 因为this指向实例对象,所以就能省了传参的步骤
return Math.max(...this)
}
console.log(arr.max()) // 3
// 求和方法
Array.prototype.sum = function() {
return this.reduce((prev, item) => prev + item, 0)
}
console.log(arr.sum()) // 6
|
constructor 属性
在哪里? 每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子
1
2
3
|
function Person() {}
console.log(Person.prototype.constructor) // Person() {}
console.log(Person.prototype.constructor === Person) // true
|
使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
1
2
3
4
5
6
7
8
|
function Person() {}
console.log(Person.prototype) // 输出结果展开有constructor
// 一次次给prototype赋值很不方便,所以要存多个时一般存成对象
Person.prototype = {
sing: () => {console.log('唱歌')},
dance: () => {console.log('跳舞')},
}
console.log(Person.prototype) // 发现输出结果展开没有constructor
|
输出结果:
此时constructor就派上用场了
1
2
3
4
5
6
7
8
9
|
function Person() {}
console.log(Person.prototype)
Person.prototype = {
// 重新指回创造这个原型对象的构造函数
constructor: Person,
sing: () => {console.log('唱歌')},
dance: () => {console.log('跳舞')},
}
console.log(Person.prototype) // 此时就能从结果中看见constructor了
|
输出结果:
对象原型
对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype
原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。
注意:
__proto__ 是JS非标准属性
- [[prototype]]和
__proto__意义相同
- 用来表明当前实例对象指向哪个原型对象prototype
__proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
1
2
3
4
5
6
|
function Person() {}
const temp = new Person()
// 对象原型 __proto__ 指向该构造函数的原型对象
console.log(temp.__proto__ === Person.prototype) // true
// 对象原型里也有一个constructor指向该实例对象的构造函数
console.log(temp.__proto__.constructor === Person) // true
|
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,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
34
35
36
37
38
|
<body>
<script>
// 构造函数 new 出来的对象 结构一样,但是对象不一样
function Person() {
this.eyes = 2
this.head = 1
}
// console.log(new Person() === new Person()) // false
// 女人 构造函数 继承 想要 继承 Person
function Woman() {
}
// Woman 通过原型来继承 Person
// 父构造函数(父类) 子构造函数(子类)
// 子类的原型 = new 父类
// 右边必须写成new Person(),若写成Person,则后续若给Woman添加prototype方法时,会同步到Man身上,反之亦然,因为每次new Person()生成的都是不同的对象
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 pink = new Man()
console.log(pink)
</script>
</body>
|
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对
象的链状结构关系称为原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<body>
<script>
// function Objetc() {}
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>
</body>
|
原型链的查找规则
① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
⑥ 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
总结
- 所有对象里面都有对象原型
__proto__,并且指向原型对象
- 所有原型对象里面都有constructor,指向创造该原型对象的构造函数
综合案例
需求:
分析需求:
- 定义模态框 Modal 构造函数,用来创建对象
- 模态框具备 打开功能 open 方法 (按钮点击可以打开模态框)
- 模态框 具备关闭功能 close 方法
问:
open 和 close 方法 写到哪里?
构造函数的原型对象上,共享方法
所以可以分为三个模块, 构造函数, open方法, close方法
代码:
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>面向对象封装消息提示</title>
<style>
.modal {
width: 300px;
min-height: 100px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 4px;
position: fixed;
z-index: 999;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
background-color: #fff;
}
.modal .header {
line-height: 40px;
padding: 0 10px;
position: relative;
font-size: 20px;
}
.modal .header i {
font-style: normal;
color: #999;
position: absolute;
right: 15px;
top: -2px;
cursor: pointer;
}
.modal .body {
text-align: center;
padding: 10px;
}
.modal .footer {
display: flex;
justify-content: flex-end;
padding: 10px;
}
.modal .footer a {
padding: 3px 8px;
background: #ccc;
text-decoration: none;
color: #fff;
border-radius: 2px;
margin-right: 10px;
font-size: 14px;
}
.modal .footer a.submit {
background-color: #369;
}
</style>
</head>
<body>
<button id="delete">删除</button>
<button id="login">登录</button>
<!-- <div class="modal">
<div class="header">温馨提示 <i>x</i></div>
<div class="body">您没有删除权限操作</div>
</div> -->
<script>
// 定义构造函数Modal
function Modal(title = '', message = '') {
// 创建一个modalBox,并在其中创建一个div标签
this.modalBox = document.createElement('div')
// 给div标签添加modal类
this.modalBox.className = 'modal'
this.modalBox.innerHTML = `
<div class="header">${title} <i>x</i></div>
<div class="body">${message}</div>
`
}
// 给构造函数的原型对象添加open方法
Modal.prototype.open = function() {
// 解决多次点击出现多个盒子的bug
// 先获取类名为modal的盒子
const box = document.querySelector('.modal')
// 利用逻辑与的中断,若box为空,则不执行box.remove(),若box不为空,则会执行
box && box.remove()
// 把刚才创建的modalBox显示到页面的body中
document.body.append(this.modalBox)
// 绑定关闭的点击事件的前提是盒子已经出现在页面中
// 调用了open方法,盒子就已经被添加到页面中了,所以写在open方法下面比较稳妥
this.modalBox.querySelector('.modal i').addEventListener('click', () => {
// 这里需要使用箭头函数,因为箭头函数内没有this
// 所以下方的this指向的是实例对象,而不是i标签
this.close()
})
}
// 给构造函数的原型对象添加close方法
Modal.prototype.close = function() {
// 移除实例对象的modalBox
this.modalBox.remove()
}
// 给删除按钮绑定事件
document.querySelector('#delete').addEventListener('click', () => {
// 先实例化Modal构造函数
const del = new Modal('温馨提示', '您没有删除权限')
// 调用实例对象的open方法
del.open()
})
// 给登录按钮绑定事件
document.querySelector('#login').addEventListener('click', () => {
// 先实例化Modal构造函数
const log = new Modal('暖心提示', '您还没有注册')
// 调用实例对象的open方法
log.open()
})
</script>
</body>
</html>
|
JavaScript 进阶 - 第4天
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…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 obj = {
a: 10,
b: 20
}
const o = {...obj}
o.b = 25
console.log(o) // {a: 10, b: 25}
console.log(obj) // {a: 10, b: 20}
const o = {}
Object.assign(o, obj)
o.b = 25
console.log(o) // {a: 10, b: 25}
console.log(obj) // {a: 10, b: 20}
// 缺点
const obj = {
a: 10,
b: 20,
c: {
d: 'hi'
}
}
const o = {}
Object.assign(o, obj)
o.c.d = '你好'
console.log(o) // {a: 10, b: 20, c: {d: '你好'}}
console.log(obj) // {a: 10, b: 20, c: {d: '你好'}}
|
- 拷贝数组:Array.prototype.concat() 或者 […arr]
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
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
|
<body>
<script>
// 简易版深拷贝,处理不了太复杂的情况
const obj = {
uname: 'pink',
age: 18,
hobby: ['唱歌','跳舞'],
family: {
baby: '小pink'
}
}
const o = {}
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 最先处理数组的问题,必须先处理数组再处理对象
// 数组也属于对象,若先处理对象,会把数组当对象处理
// 所以先处理数组,把数组筛掉后,再处理对象
if (oldObj[k] instanceof Array) { // 判断是否是数组
// 数组特殊处理
// 已知此轮要存放数组,先声明空数组用于存放
newObj[k] = []
// 递归,进入数组内部复制
deepCopy(newObj[k], oldObj[k])
}
// 处理对象的问题
else if (oldObj[k] instanceof Object) {
// 对象特殊处理,与数组类似
// 已知此轮要存放对象,先声明空对象用于存放
newObj[k] = {}
// 递归,进入对象内部复制
deepCopy(newObj[k], oldObj[k])
}
// 普通简单数据类型处理
else {
// k表示每轮的属性名,k是变量,所以必须用newObj[k]的形式,而不是newObj.k
// newObj[k] === 属性名.属性值
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, obj)
// 测试
o.uname = 'red'
o.hobby[0] = '篮球'
o.family.baby = '老pink'
console.log(o)
console.log(obj)
</script>
</body>
|
输出结果:
js库lodash里面cloneDeep内部实现了深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<body>
<!-- 先引用 -->
<!-- <script src="./lodash.min.js"></script> -->
<!--没安装也可以用在线地址-->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = _.cloneDeep(obj)
console.log(o)
o.family.baby = '老pink'
console.log(obj)
</script>
</body>
|
JSON序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
// 把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
// 再把 JSON 字符串转换为对象
// console.log(JSON.parse(JSON.stringify(obj)))
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby = '123'
console.log(obj)
</script>
</body>
|
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<script>
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()
</script>
|
总结:
throw 抛出异常信息,程序也会终止执行
throw 后面跟的是错误提示信息
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
|
<script>
function foo() {
try {
// 将可能出错的代码写到try中
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息,但是不会中断程序执行
console.log(error.message)
// 利用return终止代码继续执行
return
// 不写return也可以用throw中断程序
// throw new Error('不写return时也能用throw中断程序')
}
finally {
// 不论程序对不对,都会执行
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()
</script>
|
总结:
try...catch 用于捕获错误信息
- 将预估可能发生错误的代码写在
try 代码段中
- 如果
try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
debugger
相当于断点调试,执行到该语句时相当于遇到了断点调试

处理this
了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。
this 是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。
普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<script>
// 普通函数
function sayHi() {
console.log(this)
}
// 函数表达式
const sayHello = function () {
console.log(this)
}
// 函数的调用方式决定了 this 的值
sayHi() // window
window.sayHi()
// 普通对象
const user = {
name: '小明',
walk: function () {
console.log(this)
}
}
// 动态为 user 添加方法
user.sayHi = sayHi
uesr.sayHello = sayHello
// 函数调用方式,决定了 this 的值
user.sayHi()
user.sayHello()
</script>
|
注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。
-
箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
-
箭头函数中的this引用的就是最近作用域中的this
-
向外层作用域中,一层一层查找this,直到有this的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
<script>
console.log(this) // 此处为 window
// 箭头函数
const sayHi = function() {
console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
}
// 普通对象
const user = {
name: '小明',
// 该箭头函数中的 this 为函数声明环境中 this 一致
walk: () => {
console.log(this)
},
sleep: function () {
let str = 'hello'
console.log(this)
let fn = () => {
console.log(str)
console.log(this) // 该箭头函数中的 this 与 sleep 中的 this 一致
}
// 调用箭头函数
fn();
}
}
// 动态添加方法
user.sayHi = sayHi
// 函数调用
user.sayHi()
user.sleep()
user.walk()
</script>
|
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
<script>
// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
console.log(this)
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
console.log(this)
})
</script>
|
同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
|
<script>
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()
</script>
|
改变this指向
以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:
call
使用 call 方法调用函数,同时指定函数中 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
|
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student
// 求和函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
</script>
|
总结:
call 方法能够在调用函数的同时指定 this 的值
- 使用
call 方法调用函数时,第1个参数为 this 指定的值
call 方法的其余参数会依次自动传入函数做为函数的参数
apply
使用 call 方法调用函数,同时指定函数中 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
|
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.apply(user) // this 值为 user
sayHi.apply(student) // this 值为 student
// 求和函数
function counter(x, y) {
return x + y
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10])
console.log(result)
// 求数组最大值
const arr = [123, 321, 111]
const max = Math.max.apply(Math, arr)
console.log(max) // 321
</script>
|
总结:
apply 方法能够在调用函数的同时指定 this 的值
- 使用
apply 方法调用函数时,第1个参数为 this 指定的值
apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
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
|
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
// 调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
// 调用使用 bind 创建的新函数
sayHello()
// 需求:有一个按钮,点击后就禁用,2秒后重新启用
const btn = document.querySelector('button')
btn.addEventListener('click', function() {
// 此处this指向btn
this.disabled = true
window.setTimeout(function() {
this.disabled = false
}.bind(this), 2000) // 将定时器中的函数中的this指向外部函数的this,即btn
})
</script>
|
注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。
call、apply、bind的总结
防抖节流
防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间

案例:利用防抖处理鼠标移动事件
要求:鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会+1
效果图:
实现方式有两种:
①利用lodash提供的防抖函数来处理
_.debounce(func, [wait=0], [options=])
官方说明:创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。 debounced(防抖动)函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。 可以提供一个 options(选项) 对象决定如何调用 func 方法,options.leading 与|或 options.trailing 决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。 func 调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。 后续调用的 debounced(防抖动)函数返回是最后一次 func 调用的结果。
参数
func (Function): 要防抖动的函数。
[wait=0] (number): 需要延迟的毫秒数。
[options=] (Object): 选项对象。
[options.leading=false] (boolean): 指定在延迟开始前调用。
[options.maxWait] (number): 设置 func 允许被延迟的最大值。
[options.trailing=true] (boolean): 指定在延迟结束后调用。
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 在线引入lodash库 -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<style>
.box {
height: 300px;
width: 300px;
margin: auto;
border: 2px solid black;
background-color: #ccc;
font-size: 80px;
line-height: 300px;
text-align: center;
}
</style>
</head>
<body>
<div class="box">0</div>
<script>
// 利用防抖实现性能优化
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
}
box.addEventListener('mousemove', _.debounce(mouseMove, 500))
</script>
</body>
</html>
|
②自己手写一个防抖函数来处理
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
.box {
height: 300px;
width: 300px;
margin: auto;
border: 2px solid black;
background-color: #ccc;
font-size: 80px;
line-height: 300px;
text-align: center;
}
</style>
</head>
<body>
<div class="box">0</div>
<script>
// 利用防抖实现性能优化
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
}
box.addEventListener('mousemove', debounce(mouseMove, 500))
// 定义一个防抖函数
function debounce(fn, t) {
let timer
// 因为要传入参数,所以debounce必须带小括号
// 而回调函数(函数作为参数传入时)不能带小括号,带上小括号就会立即执行,之后就不再执行
// 所以在debounce内部再返回一个函数,相当于直接把这个函数写在了原本调用debounce()的位置
// 为什么要多此一举,不直接写这个函数?因为要清除定时器,而要清除定时器必须要保证timer在函数执行后不被清除,所以利用了闭包的方法
return function() {
if (timer) clearTimeout(timer)
timer = setTimeout(function() {
fn() // 调用传入的函数
}, t) // t秒后调用
}
}
</script>
</body>
</html>
|
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数

案例:利用节流处理鼠标移动事件
要求:鼠标在盒子上移动,不管移动多少次,每隔1500ms才+1
利用lodash的效果图:(不知道为什么触发好像有点问题)
利用自己写的函数的效果图:
实现方式有两种:
①利用lodash提供的节流函数来处理
_.throttle(func, [wait=0], [options=])
官方说明:创建一个节流函数,在 wait 秒内最多执行 func 一次的函数。 该函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。 可以提供一个 options 对象决定如何调用 func 方法, options.leading 与|或 options.trailing 决定 wait 前后如何触发。 func 会传入最后一次传入的参数给这个函数。 随后调用的函数返回是最后一次 func 调用的结果。
参数
func (Function): 要节流的函数。
[wait=0] (number): 需要节流的毫秒。
[options=] (Object): 选项对象。
[options.leading=true] (boolean): 指定调用在节流开始前。
[options.trailing=true] (boolean): 指定调用在节流结束后。
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 在线引入lodash库 -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<style>
.box {
height: 300px;
width: 300px;
margin: auto;
border: 2px solid black;
background-color: #ccc;
font-size: 80px;
line-height: 300px;
text-align: center;
}
</style>
</head>
<body>
<div class="box">0</div>
<script>
// 利用节流实现性能优化
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
}
box.addEventListener('mousemove', _.throttle(mouseMove, 1500))
</script>
</body>
</html>
|
②自己手写一个节流函数来处理
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
.box {
height: 300px;
width: 300px;
margin: auto;
border: 2px solid black;
background-color: #ccc;
font-size: 80px;
line-height: 300px;
text-align: center;
}
</style>
</head>
<body>
<div class="box">0</div>
<script>
// 利用节流实现性能优化
const box = document.querySelector('.box')
let i = 1
function mouseMove() {
box.innerHTML = i++
}
box.addEventListener('mousemove', throttle(mouseMove, 1500))
// 自己写的节流函数
function throttle(fn, t) {
let timer = null
return function() {
// 没有定时器时才开启定时器
if (!timer) {
// 先调用,再清空
fn()
timer = setTimeout(function() {
// 清空定时器,在定时器内无法使用clearTimeout()
timer = null
}, t)
}
}
}
</script>
</body>
</html>
|
防抖和节流总结

综合案例:页面打开,可以记录上一次的视频播放位置
效果图:

分析:
两个事件:
①:ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发
②:onloadeddata 事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发
谁需要节流?ontimeupdate , 触发频次太高了,我们可以设定 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
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
|
<!DOCTYPE html>
<html lang="zh-CN">
<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" />
<meta name="referrer" content="never" />
<title>页面打开,可以记录上一次的视频播放位置</title>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.container {
width: 1200px;
margin: 0 auto;
}
.video video {
width: 100%;
padding: 20px 0;
}
.elevator {
position: fixed;
top: 280px;
right: 20px;
z-index: 999;
background: #fff;
border: 1px solid #e4e4e4;
width: 60px;
}
.elevator a {
display: block;
padding: 10px;
text-decoration: none;
text-align: center;
color: #999;
}
.elevator a.active {
color: #1286ff;
}
.outline {
padding-bottom: 300px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="http://pip.itcast.cn">
<img src="https://pip.itcast.cn/img/logo_v3.29b9ba72.png" alt="" />
</a>
</div>
<div class="video">
<video src="https://v.itheima.net/LapADhV6.mp4" controls></video>
</div>
<div class="elevator">
<a href="javascript:;" data-ref="video">视频介绍</a>
<a href="javascript:;" data-ref="intro">课程简介</a>
<a href="javascript:;" data-ref="outline">评论列表</a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
const video = document.querySelector('video')
// 添加视频播放事件
video.addEventListener('timeupdate', _.throttle(() => {
// 把当前视频的播放时间存储到本地
localStorage.setItem('videoCurrentTime', video.currentTime)
}, 1000)) // 利用节流,一秒执行一次
// 添加视频加载完成事件
video.addEventListener('loadeddata', () => {
// 利用逻辑或的中断,让本地没有数据时就取0,一般浏览器在没有本地数据时即便不加这段也会取0
video.currentTime = localStorage.getItem('videoCurrentTime') || 0
})
</script>
</body>
</html>
|