文章目录
- 数组
- 1 数组简介
- 1.1 创数组
- 1.2 向数组添加元素
- 1.3 读取数组中的元素
- 1.4 获取数组长度
- 2 数组的遍历
- 2.1 使用for循环遍历
- 2.2 for-of遍历
- 3 数组的方法
- 3.1 Array.isArray()
- 3.2 at()
- 3.3 concat()
- 3.4 indexOf()
- 3.5 lastIndexOf()
- 3.6 join()
- 3.7 slice()
- 3.8 push()
- 3.9 pop()
- 3.10 unshift()
- 3.11 shift()
- 3.12 splice()
- 3.13 reverse()
- 3.14 sort()
- 3.15 forEach()
- 3.16 filter()
- 3.17 map()
- 3.18 reduce()
- 4 数组的拷贝
- 4.1 slice()拷贝
- 4.2 new Array()拷贝
- 4.3 浅拷贝和深拷贝
- 4.4 展开运算符
- 4.5 对象的复制
- 4.6 使用JSON进行深拷贝
- 5 数组去重
- 6 数组排序
- 6.1 冒泡排序
- 例子:
- 6.2 选择排序
- 7 封装函数
- 7.1 封装排序函数
- 7.2 封装过滤函数
- 8 高阶函数
- 9 闭包
- 9.1 引入闭包
- 9.2 闭包的原理
- 9.3 注意事项
- 10 递归
- 11 可变参数
- 11.1 arguments
- 11.2 可变参数
- 12 call和apply
- 13 bind
数组
数组
这部分内容讲解了JS中的数组,除了数组外还在这部分内容中对对象和函数的部分知识进行了补充,所以视频中的知识并不全都是数组的知识。其中包括:数组简介、遍历数组、for-of语句、数组的方法、对象的复制、冒泡排序、选择排序、高阶函数、闭包、递归、可变参数、call和apply、bind。
1 数组简介
数组(Array)
- 数组也是一种复合数据类型,在数组可以存储多个不同类型的数据
- 数组中存储的是有序的数据,数组中的每个数据都有一个唯一的索引,可以通过索引来操作获取数据
- 数组中存储的数据叫做元素
typeof
返回的也是Object
1.1 创数组
创建数组:通过new Array()
来创建数组,也可以通过[]
来创建数组
const arr = new Array()
const arr2 = [1, 2, 3, 4, 5] // 数组字面量,推荐使用这种方式
1.2 向数组添加元素
数组[索引] = 元素
索引(index)是一组大于等于0的整数
arr[0] = 10
arr[1] = 22
arr[2] = 44
arr[3] = 88
如果不按顺序加
但是使用数组的时候,应该避免非连续数组,因为性能不好
arr[0] = 10
arr[1] = 22
arr[2] = 44
arr[3] = 88
arr[100] = 99
除此之外,也可以在定义的时候就指定初始化元素
const arr = [1, 2, 3, 4]
1.3 读取数组中的元素
数组[索引]
如果读取了一个不存在的元素,不好报错而是返回undefined
console.log(arr[4])
console.log(arr[200]) // undefined
1.4 获取数组长度
length
- 获取数组的长度
- 获取的实际值就是数组的最大索引 + 1
- 向数组最后添加元素:
数组[数组.length] = 元素
- length是可以修改的
- 改大的话会扩张数组,增加新元素
- 改小的话会删除多余元素
2 数组的遍历
任何值都可以称为数组中的元素
let arr = [1, "hello", true, null, { name: "孙悟空" }, () => {}]
但是在创建数组时尽量要确保数组中存储的数据的类型是相同
arr = ["孙悟空", "猪八戒", "沙和尚"]
2.1 使用for循环遍历
遍历数组简单理解,就是获取到数组中的每一个元素
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "白骨精"]
// 正序遍历
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
// 倒序遍历
for (let i = arr.length - 1; i >= 0; i--) {
console.log(arr[i])
}
案例:
定义一个Person类,类中有两个属性name和age,然后创建几个Person对象,将其添加到一个数组中,最后遍历数组,并打印未成年人的信息
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
const personArr = [
new Person("孙悟空", 18),
new Person("沙和尚", 38),
new Person("红孩儿", 8),
]
for (let i = 0; i < personArr.length; i++) {
if (personArr[i].age < 18) {
console.log(personArr[i])
}
}
2.2 for-of遍历
for-of语句可以用来遍历可迭代对象
语法:
for(变量 of 可迭代的对象){
语句...
}
执行流程:
- for-of的循环体会执行多次,数组中有几个元素就会执行几次,
- 每次执行时都会将一个元素赋值给变量
示例:
const arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]
for (let value of arr) {
console.log(value)
}
//
for (let value of "hello") {
console.log(value)
}
3 数组的方法
Array参考文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
3.1 Array.isArray()
- 用来检查一个对象是否是数组
// 下面的都返回true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
Array.isArray(new Array("a", "b", "c", "d"));
Array.isArray(new Array(3));
//Array.prototype 本身也是一个array
Array.isArray(Array.prototype);
// 下面的都返回false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray("Array");
Array.isArray(true);
Array.isArray(false);
Array.isArray(new Uint8Array(32));
3.2 at()
- 可以根据索引获取数组中的指定元素
at()
可以接收负索引作为参数
3.3 concat()
用来连接两个或多个数组
非破坏性方法,不会影响原数组,而是返回一个新的数组
const arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]
const arr2 = ["白骨精", "蜘蛛精", "玉兔精"]
let result1 = arr.concat(arr2) // 连接一个数组
let result2 = arr.concat(arr2, ["牛魔王","铁扇公主"]) // 连接两个数组
console.log(result1)
console.log(result2)
console.log(arr)
console.log(arr2)
看结果可以发现原素组并未被改变,并且按照传入的顺序拼接到一起
3.4 indexOf()
- 获取元素在数组中第一次出现的索引
- 参数:
- 要查询的元素
- 查询的起始位置
- 返回值:
- 找到了则返回元素的索引,
- 没有找到返回-1
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.indexOf("沙和尚"))
console.log(arr.indexOf("沙和尚", 3))
3.5 lastIndexOf()
-
获取元素在数组中最后一次出现的位置
-
返回值:
- 找到了则返回元素的索引,
- 没有找到返回-1
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.lastIndexOf("沙和尚"))
3.6 join()
- 将一个数组中的元素连接为一个字符串
- 参数:
- 指定一个字符串作为连接符
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.join())// 默认使用 , 分割
console.log(arr.join("@-@"))
console.log(arr.join(" "))
3.7 slice()
-
用来截取数组(非破坏性方法)
-
参数(前开后闭):
- 截取的起始位置(包括该位置)
- 截取的结束位置(不包括该位置)
- 第二个参数可以省略不写,如果省略则会一直截取到最后
- 索引可以是负值
-
如果将两个参数全都省略,则可以对数组进行浅拷贝(浅复制)
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.slice(0, 2))
console.log(arr.slice(1, 3))
console.log(arr.slice(1, -1))
console.log(arr.slice())
3.8 push()
向数组的末尾添加一个或多个元素,并返回新的长度
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.push("bb"))
console.log(arr)
3.9 pop()
删除并返回数组的最后一个元素
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.pop())
console.log(arr)
3.10 unshift()
向数组的开头添加一个或多个元素,并返回新的长度
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.unshift("bb"))
console.log(arr)
3.11 shift()
删除并返回数组的第一个元素
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.shift())
console.log(arr)
3.12 splice()
-
可以删除、插入、替换数组中的元素
-
参数:
- 删除的起始位置
- 删除的数量
- 要插入的元素
-
返回值:
- 返回被删除的元素
删除
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 2))
console.log(arr)
插入
插入的话就是删除0个,然后第一个参数指定开始位置,后面的参数指定要插入的元素
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 0, "猪", "猴"))
console.log(arr)
修改
修改的话需要将第二个参数设置成和要插入的参数一样长即可
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 2, "猪", "猴"))
console.log(arr)
3.13 reverse()
反转数组,返回翻转后的数组
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.reverse())
console.log(arr)
3.14 sort()
-
sort用来对数组进行排序(会对改变原数组)
-
sort默认会将数组升序排列
-
注意:sort默认会按照Unicode编码进行排序,所以如果直接通过sort对数字进行排序可能会得到一个不正确的结果
let arr = [2, 3, 1, 9, 0, 4, 5, 7, 8, 6, 10] arr.sort()
-
-
参数:
- 可以传递一个回调函数作为参数,通过回调函数来指定排序规则
- (a, b) => a - b 升序排列
- (a, b) => b - a 降序排列
- 可以传递一个回调函数作为参数,通过回调函数来指定排序规则
let arr = [2, 3, 1, 9, 0, 4, 5, 7, 8, 6, 10]
arr.sort((a, b) => a - b)
arr.sort((a, b) => b - a)
升序排列
降序排列
3.15 forEach()
- 用来遍历数组
- 它需要一个回调函数作为参数,这个回调函数会被调用多次
- 数组中有几个元素,回调函数就会调用几次
- 每次调用,都会将数组中的数据作为参数传递
- 回调函数中有三个参数:
- element 当前的元素
- index 当前元素的索引
- array 被遍历的数组
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]
arr.forEach((element, index, array) => {
console.log(element, index, array)
})
3.16 filter()
- 将数组中符合条件的元素保存到一个新数组中返回
- 需要一个回调函数作为参数,会为每一个元素去调用回调函数,并根据返回值来决定是否将元素添加到新数组中
- 非破坏性方法,不会影响原数组
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.filter((ele) => ele > 5)
不做任何过滤
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.filter((ele) => true)
3.17 map()
https://www.bilibili.com/video/BV1mG411h7aD?t=1349.8&p=111
- 根据当前数组生成一个新数组
- 需要一个回调函数作为参数,回调函数的返回值会成为新数组中的元素
- 非破坏性方法不会影响原数组
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.map((ele) => ele * 2)
3.18 reduce()
https://www.bilibili.com/video/BV1mG411h7aD?t=1750.5&p=111
- 可以用来将一个数组中的所有元素整合为一个值
- 参数:
- 回调函数,通过回调函数来指定合并的规则
- 可选参数,初始值
arr = [1, 2, 3, 4, 5, 6, 7, 8]
result = arr.reduce((a, b) => {
console.log(a, b)
return a + b
})
4 数组的拷贝
复制是复制出来一个新的对象,二者指向的是不同的对象,直接采用等号赋值的话二者指向的是同一个对象
也可以修改某个元素,看看另一个数组中的元素会不会被修改掉
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr2 = arr // 不是复制,二者指向的是同一个对象
console.log(arr === arr2) // true
这种情况内存图如下
4.1 slice()拷贝
当调用slice
时,会产生一个新的数组对象,从而完成对数组的复制
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr3 = arr.slice()
console.log(arr === arr3) // false
4.2 new Array()拷贝
也可以使用new Array()
产生新对象
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr4 = new Array(arr)
console.log(arr === arr4) // false
4.3 浅拷贝和深拷贝
https://www.bilibili.com/video/BV1mG411h7aD?p=95
浅拷贝(shallow copy)
- 通常对对象的拷贝都是浅拷贝
- 浅拷贝顾名思义,只对对象的浅层进行复制(只复制一层)
- 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要
- 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或元素)
- 使用
slice()
或者new Array(arr)
进行浅拷贝
深拷贝(deep copy)
- 深拷贝指不仅复制对象本身,还复制对象中的属性和元素
- 因为性能问题,通常情况不太使用深拷贝
- 使用
structuredClone(arr)
进行深拷贝
例如定义如下数组,其在内存图中情况如图:
使用slice()
进行浅拷贝,从图中看出来只复制了第一层,并没有复制元素中的属性
在此时,通过代码进行验证,可以看出两个数组指向不一样,但是其中的对象是共享的,所以修改一个另一个也会发生改变:
深拷贝内存情况如图,不止复制了元素,也复制了元素里面的属性:
对于如下代码和结果也可以验证结果:
const arr = [{name:"孙悟空"}, {name:"猪八戒"}]
const arr2 = arr.slice() // 浅拷贝
const arr3 = structuredClone(arr) // 专门用来深拷贝的方法
console.log(arr)
console.log(arr3)
4.4 展开运算符
展开运算符:...
- 可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递
- 通过它也可以对数组进行浅复制
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr3 = [...arr]
arr === arr3 // false
这种方式得到的也是浅拷贝
除此之外,还可以把参数展开
function sum(a, b, c) {
return a + b + c
}
const arr4 = [10, 20, 30]
let result1 = sum(arr4[0], arr4[1], arr4[2])
let result2 = sum(...arr4)
4.5 对象的复制
-
Object.assign(目标对象, 被复制的对象)
-
将被复制对象中的属性复制到目标对象里,并将目标对象返回
-
也可以使用展开运算符对对象进行复制
const obj = { name: "孙悟空", age: 18 }
// 方法一:复制到空对象后获取返回值
const obj2 = Object.assign({}, obj)
console.log(obj2)
// 方式二:直接复制到一个对象中,不会改变原有属性
const obj3 = { address: "花果山", age: 28 }
Object.assign(obj3, obj)
console.log(obj3)
也可以使用展开运算符
const obj3 = { address: "高老庄", ...obj, age: 48 } // 将obj中的属性在新对象中展开
4.6 使用JSON进行深拷贝
const obj = {
name: "孙悟空",
friend: {
name: "猪八戒",
},
}
// 对obj进行浅复制
const obj2 = Object.assign({}, obj)
// 对obj进行深复制
const obj3 = structuredClone(obj)
// 利用JSON来完成深复制
const str = JSON.stringify(obj)
const obj4 = JSON.parse(str)
5 数组去重
有如下一个数组arr = [1,2,1,3,2,4,5,5,6,7]
,编写代码,去除数组中重复的元素 --> [1,2,3,4,5,6,7]
方法一:遍历到每个元素,从这个元素后面看有没有和这个元素一样的,如果有就删除
const arr = [1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]
// 分别获取数组中的元素
for (let i = 0; i < arr.length; i++) {
// 获取当前值后边的所有值
for (let j = i + 1; j < arr.length; j++) {
// 判断两个数是否相等
if (arr[i] === arr[j]) {
// 出现了重复元素,删除后边的元素
arr.splice(j, 1)
/*
当arr[i] 和 arr[j]相同时,它会自动的删除j位置的元素,然后j+1位置的元素,会变成j位置的元素
而j位置已经比较过了,不会重复比较,所以会出现漏比较的情况
解决办法,当删除一个元素后,需要将该位置的元素在比较一遍
*/
j--
}
}
}
console.log(arr)
方法二:使用indexOf()
函数查找后面有没有和当前元素一样的元素,如果有就删除
const arr = [1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]
for (let i = 0; i < arr.length; i++) {
const index = arr.indexOf(arr[i], i + 1)
if (index !== -1) {
// 出现重复内容
arr.splice(index, 1)
i--
}
}
console.log(arr)
方法三:创建一个新数组,遍历旧数组中的每个元素,如果该元素没有在新元素出现过就加入到新数组中
const newArr = []
for (let ele of arr) {
if (newArr.indexOf(ele) === -1) {
newArr.push(ele)
}
}
console.log(newArr)
6 数组排序
有一个数组:[9,1,3,2,8,0,5,7,6,4]
,编写代码对数组进行排序 --> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
6.1 冒泡排序
-
比较相邻的两个元素,然后根据大小来决定是否交换它们的位置
-
例子:
- 第一次排序:1, 3, 2, 8, 0, 5, 7, 6, 4, 9
- 第二次排序:1, 2, 3, 0, 5, 7, 6, 4, 8, 9
- 第三次排序:1, 2, 0, 3, 5, 6, 4, 7, 8, 9
- …
- 倒数第二次 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
-
这种排序方式,被称为冒泡排序,冒泡排序是最慢的排序方式,数字少还可以凑合用,不适用于数据量较大的排序
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)
const arr = [9, 1, 3, 2, 8, 0, 5, 7, 6, 4]
for (let j = 0; j < arr.length - 1; j++) {
for (let i = 0; i < arr.length - 1; i++) {
// arr[i] 前边的元素 arr[i+1] 后边元素
if (arr[i] < arr[i + 1]) {
// 大数在前,小数在后,需要交换两个元素的位置
let temp = arr[i] // 临时变量用来存储arr[i]的值
arr[i] = arr[i + 1] // 将arr[i+1]的值赋给arr[i]
arr[i + 1] = temp // 修改arr[i+1]的值
}
}
}
console.log(arr)
6.2 选择排序
取出一个元素,然后将其他元素和该元素进行比较,如果其他元素比该元素小则交换两个元素的位置
时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 对于:9, 1, 3, 2, 8, 0, 5, 7, 6, 4
- 1, 3, 2, 8, 0, 5, 7, 6, 4, 9
- 1, 2, 3, 0, 5, 7, 6, 4, 8, 9
- 1, 2, 0, 3, 5, 6, 4, 7, 8, 9
- 1, 0, 2, 3, 5, 4, 6, 7, 8, 9
const arr = [9, 1, 3, 2, 8, 0, 5, 7, 6, 4]
for(let i=0; i<arr.length; i++){
for(let j=i+1; j<arr.length; j++){
if(arr[i] > arr[j]){
// 交换两个元素的位置
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
console.log(arr)
7 封装函数
7.1 封装排序函数
封装排序函数,且不会破坏原数组
function sort(array) {
const arr = [...array]
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
// 交换两个元素的位置
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
return arr
}
const arr = [9, 1, 3, 2, 8, 0, 5, 7, 6, 4]
const arr2 = [9, 8, 7, 6, 5, 4, 3, 2, 1]
console.log(sort(arr))
console.log(sort(arr2))
7.2 封装过滤函数
案例:过滤得到所有未成年的Person
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
const personArr = [
new Person("孙悟空", 18),
new Person("沙和尚", 38),
new Person("红孩儿", 8),
new Person("白骨精", 16),
]
// filter()函数用来对数组进行过滤
function filter(arr) {
const newArr = []
for (let i = 0; i < arr.length; i++) {
if (arr[i].age < 18) {
newArr.push(arr[i])
}
}
return newArr
}
result = filter(personArr)
console.log(result)
但是这样做有个问题,就是对于比较函数已经定死了,只能针对于这种特定的案例,等需求变了之后就需要将filter()
函数改掉,不符合封装的定义
对于比较来说,不同的类有不同的比较方式,所以可以将比较函数也当做一个参数传进来,然后每次调用比较函数,判断是否满足条件,如果满足条件就加到新数组中
一个函数的参数也可以是函数,如果将函数作为参数传递,那么我们就称这个函数为回调函数(callback)
所以可以修改如下列形式:
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
const personArr = [
new Person("孙悟空", 18),
new Person("沙和尚", 38),
new Person("红孩儿", 8),
new Person("白骨精", 16),
]
function filter(arr, compartor) {
const newArr = []
for (let i = 0; i < arr.length; i++) {
if (compartor(arr[i])) {
newArr.push(arr[i])
}
}
return newArr
}
function compartor(a){
return a.name === "孙悟空"
}
// 过滤未成年
result = filter(personArr, item => item.age < 18)
console.log(result)
// 过滤名字为孙悟空的人
result = filter(personArr, fn)
console.log(result)
8 高阶函数
在上面写的fn()
一般不会这么写,一般都是使用匿名函数来处理,非常的便捷
类似于filter()
这种函数我们称之为高阶函数
- 如果一个函数的参数或返回值是函数,则这个函数就称为高阶函数
- 为什么要将函数作为参数传递?(回调函数有什么作用?)
- 将函数作为参数,意味着可以对另一个函数动态的传递代码
案例:希望在执行某个函数的时候,记录一条日志(在不修改原函数的基础上),可以通过高阶函数,来动态的生成一个新函数
function someFn() {
return "hello"
}
function outer(cb){
return () => {
console.log("记录日志~~~~~")
const result = cb()
return result
}
}
let result = outer(someFn)
consolt.log(result())
function test(){
console.log("test~~~~")
return "test"
}
let newTest = outer(test)
newTest()
9 闭包
案例:创建一个函数,第一次调用时打印1,第二次调用打印2,以此类推
let num = 0
function fn(){
num++
console.log(num)
}
fn()
但是这样有个问题,使用全局变量很容易被别人修改,就会导致在获取中的值出问题
9.1 引入闭包
因此可以放到一个封闭的环境中(例如函数),可以确保外界无法修改它,这时候就又用到了高阶函数的东西,如下代码:
function outer() {
let num = 0 // 位于函数作用域中
return () => {
num++
console.log(num)
}
}
const newFn = outer()
// console.log(newFn)
newFn()
newFn()
newFn()
像outer()
这样的东西就叫做闭包,闭包就是能访问到外部函数作用域中变量的函数
什么时候使用闭包?
当我们需要隐藏一些不希望被别人访问的内容时就可以使用闭包,构成闭包的要件:
- 函数的嵌套
- 内部函数要引用外部函数中的变量
- 内部函数要作为返回值返回
9.2 闭包的原理
主要就是用到作用域链的东西
let a = "全局变量a"
// 示例一:
function fn(){
console.log(a)
}
function fn2(){
let a = "fn2中的a"
console.log(a)
}
fn() // 全局变量a
fn2() // fn2中的a
// 示例2:
function fn3(){
let a = "fn3中的a"
fn()//
}
fn3() // 全局变量a
// 示例3
function fn3(){
let a = "fn3中的a"
function fn4(){
console.log(a)
}
return fn4
}
fn3() // fn3中的a
// 示例4
function fn3(){
let a = "fn3中的a"
function fn4(){
console.log(a)
}
return fn4
}
let fn4 = fn3()
fn4() // fn3中的a
具体打印谁看的不是在哪里调用,而是看在哪定义的,如果自己没有变量a就去自己的外部也就是全局变量去找
也就是在定义的时候就已经确定值了,在定义的外部作用于去找
函数在作用域,在函数创建时就已经确定的(词法作用域)和调用的位置无关
闭包利用的就是词法作用域
闭包的生命周期:
- 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
- 在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)
function outer2(){
let num = 0
return () => {
num++
console.log(num)
}
}
let fn1 = outer2() // 独立闭包
let fn2 = outer2() // 独立闭包
fn1() // 1
fn1() // 2
fn2() // 1
// 销毁闭包
fn1 = null
fn2 = null
9.3 注意事项
闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间
相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能),
- 需要执行次数较少时,使用闭包
- 需要大量创建实例时,使用类
10 递归
- 调用自身的函数称为递归函数
- 递归的作用和循环是基本一直
编写递归函数,一定要包含两个要件:
- 基线条件 —— 递归的终止条件
- 递归条件 —— 如何对问题进行拆分
递归的作用和循环是一致的,不同点在于,递归思路的比较清晰简洁,循环的执行性能比较好,在开发中,一般的问题都可以通过循环解决,也是尽量去使用循环,少用递归,只在一些使用循环解决比较麻烦的场景下,才使用递归
案例:求任意数的阶乘
- for循环思路:
- 1! 1
- 2! 1 x 2 = 2
- 3! 1 x 2 x 3 = 6
- …
- 10! 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 = xxx
// for循环表示
function jieCheng(num){
// 创建一个变量用了记录结果
let result = 1
for(let i=2; i<=num; i++){
result *= i
}
return result
}
let result = jieCheng(3)
console.log(result)
- 递归思路
- 求5的阶乘
- 5! = 4! x 5
- 4! = 3! x 4
- 3! = 2! x 3
- 2! = 1! x 2
- 1! = 1
- 递归的核心思想就是将一个大的问题拆分为一个一个小的问题,小的问题解决了,大的问题也就解决了
// 递归表示
function jieCheng2(num){
// 基线条件
if(num === 1){
return 1
}
// 递归条件
// num! = (num-1)! * num
return jieCheng2(num-1) * num
}
result = jieCheng2(5)
/*
jieCheng2(5)
- return jieCheng2(4) * 5
- return jieCheng2(3) * 4
- return jieCheng2(2) * 3
- return jieCheng2(1) * 2
- return 1
*/
console.log(result)
练习:求斐波那契数列中的第i个数字
// 求斐波那契数列中的第n个数
function fib(n) {
// 确定基线条件
if (n < 3) {
return 1
}
// 设置递归条件
// 第n个数 = 第n-1个数 + 第n-2个数
return fib(n - 1) + fib(n - 2)
}
let result = fib(10)
console.log(result)
11 可变参数
11.1 arguments
- arguments是函数中又一个隐含参数
- arguments是一个类数组对象(伪数组)
- 和数组相似,可以通过索引来读取元素,也可以通过for循环变量,但是它不是一个数组对象,不能调用数组的方法
- arguments用来存储函数的实参,无论用户是否定义形参,实参都会存储到arguments对象中,可以通过该对象直接访问实参
function fn() {
console.log(Array.isArray(arguments)) // false
for(let v of arguments){
console.log(v)
}
// rguments.forEach((ele) => console.log(ele)) // 报错
}
fn(1, 10, 33)
案例:定义一个函数,可以求任意个数值的和
function sum() {
// 通过arguments,可以不受参数数量的限制更加灵活的创建函数
let result = 0
for (let num of arguments) {
result += num
}
return result
}
sum(1) // 1
sum(1, 2) // 3
sum(1, 2, 3)// 6
11.2 可变参数
可变参数,在定义函数时可以将参数指定为可变参数
- 可变参数可以接收任意数量实参,并将他们统一存储到一个数组中返回
- 可变参数的作用和arguments基本是一致,但是也具有一些不同点:
- 可变参数的名字可以自己指定
- 可变参数就是一个数组,可以直接使用数组的方法
- 可变参数可以配合其他参数一起使用
function fn2(...abc) {
console.log(abc)
}
fn2(1, 2, 3) //
function sum(...num) {
return num.reduce((a, b) => a + b, 0)
}
sum(1) // 1
sum(1, 2) // 3
sum(1, 2, 3)// 6
当可变参数和普通参数一起使用时,需要将可变参数写到最后
function fn3(a, b, ...args) {
// for (let v of arguments) {
// console.log(v)
// }
console.log(arguments)
console.log(a)
console.log(b)
console.log(args)
}
fn3(123, 456, "hello", true, "1111")
12 call和apply
根据函数调用方式的不同,this的值也不同:
- 以函数形式调用,this是window
- 以方法形式调用,this是调用方法的对象
- 构造函数中,this是新建的对象
- 箭头函数没有自己的this,由外层作用域决定
- 通过
call
和apply
调用的函数,它们的第一个参数就是函数的this - 通过bind返回的函数,this由bind第一个参数决定(无法修改)
调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数,比如,我们可以通过调用函数的call()和apply()来个方法来调用函数
- call 和 apply除了可以调用函数,还可以用来指定函数中的this
- call和apply的第一个参数,将会成为函数的this
function fn() {
console.log("函数执行了~", this)
}
const obj = { name: "孙悟空", fn }
fn.call(obj)
fn.apply(console)
- 通过call方法调用函数,函数的实参直接在第一个参数后一个一个的列出来
- 通过apply方法调用函数,函数的实参需要通过一个数组传递
function fn2(a, b) {
console.log("a =", a, "b =", b, this)
}
const obj = {name: "孙悟空", fn}
fn2.call(obj, "hello", true)
fn2.apply(obj, ["hello", true])
13 bind
bind()
是函数的方法,可以用来创建一个新的函数
function fn(){
console.log("fn执行了")
}
const newFn = fn.bind()
newFn()
- bind可以为新函数绑定this
- bind可以为新函数绑定参数
function fn(a, b, c) {
console.log("fn执行了~~~~", this) // 正常情况下这个this应该指向window
console.log(a, b, c)
}
const obj = {name:"孙悟空"}
const newFn = fn.bind(obj, 10, 20, 30)
newFn()
箭头函数没有自身的this,它的this由外层作用域决定,也无法通过call apply 和 bind修改它的this
箭头函数中没有arguments
const arrowFn = () => {
console.log(this)
}
// 三种方法都是打印dewindow
arrowFn()
arrowFn.call(obj)
const newArrowFn = arrowFn.bind(obj)
newArrowFn()
class MyClass {
fn = () => {
console.log(this)
}
}
const mc = new MyClass()
mc.fn.call(window)