JavaScript -- 08. 数组介绍

news2024/11/23 14:54:07

文章目录

  • 数组
    • 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

image-20221202193123811

如果不按顺序加

但是使用数组的时候,应该避免非连续数组,因为性能不好

arr[0] = 10
arr[1] = 22
arr[2] = 44
arr[3] = 88
arr[100] = 99

image-20221202193231610

除此之外,也可以在定义的时候就指定初始化元素

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)
}
// 

image-20221202194513039

for (let value of "hello") {
    console.log(value)
}

image-20221202194517759

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()可以接收负索引作为参数

image-20221202195124807

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)

看结果可以发现原素组并未被改变,并且按照传入的顺序拼接到一起

image-20221202195427905

3.4 indexOf()

  • 获取元素在数组中第一次出现的索引
  • 参数:
    1. 要查询的元素
    2. 查询的起始位置
  • 返回值:
    • 找到了则返回元素的索引,
    • 没有找到返回-1
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.indexOf("沙和尚"))
console.log(arr.indexOf("沙和尚", 3))

image-20221202195725247

3.5 lastIndexOf()

  • 获取元素在数组中最后一次出现的位置

  • 返回值:

    • 找到了则返回元素的索引,
    • 没有找到返回-1
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.lastIndexOf("沙和尚"))

image-20221202195828246

3.6 join()

  • 将一个数组中的元素连接为一个字符串
  • 参数:
    • 指定一个字符串作为连接符
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.join())// 默认使用 , 分割
console.log(arr.join("@-@"))
console.log(arr.join(" "))

image-20221202200259301

3.7 slice()

  • 用来截取数组(非破坏性方法)

  • 参数(前开后闭):

    1. 截取的起始位置(包括该位置
    2. 截取的结束位置(不包括该位置
    • 第二个参数可以省略不写,如果省略则会一直截取到最后
    • 索引可以是负值
  • 如果将两个参数全都省略,则可以对数组进行浅拷贝(浅复制)

arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.slice(0, 2))
console.log(arr.slice(1, 3))
console.log(arr.slice(1, -1))
console.log(arr.slice())

image-20221202200248498

3.8 push()

向数组的末尾添加一个或多个元素,并返回新的长度

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.push("bb"))
console.log(arr)

image-20221202221330901

3.9 pop()

删除并返回数组的最后一个元素

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.pop())
console.log(arr)

image-20221202221621022

3.10 unshift()

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

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.unshift("bb"))
console.log(arr)

image-20221202221633061

3.11 shift()

删除并返回数组的第一个元素

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.shift())
console.log(arr)

image-20221202221650266

3.12 splice()

  • 可以删除、插入、替换数组中的元素

  • 参数:

    1. 删除的起始位置
    2. 删除的数量
    3. 要插入的元素
  • 返回值:

    • 返回被删除的元素

删除

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 2))
console.log(arr)

image-20221202221924900

插入

插入的话就是删除0个,然后第一个参数指定开始位置,后面的参数指定要插入的元素

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 0, "猪", "猴"))
console.log(arr)

image-20221202222022256

修改

修改的话需要将第二个参数设置成和要插入的参数一样长即可

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.splice(1, 2, "猪", "猴"))
console.log(arr)

image-20221202222346816

3.13 reverse()

反转数组,返回翻转后的数组

arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.reverse())
console.log(arr)

image-20221202222414083

3.14 sort()

  • sort用来对数组进行排序(会对改变原数组)

  • sort默认会将数组升序排列

    • 注意:sort默认会按照Unicode编码进行排序,所以如果直接通过sort对数字进行排序可能会得到一个不正确的结果

      let arr = [2, 3, 1, 9, 0, 4, 5, 7, 8, 6, 10]
      arr.sort()
      

      image-20221203005444915

  • 参数:

    • 可以传递一个回调函数作为参数,通过回调函数来指定排序规则
      • (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)

升序排列

image-20221203005509650

降序排列

image-20221203005532830

3.15 forEach()

  • 用来遍历数组
  • 它需要一个回调函数作为参数,这个回调函数会被调用多次
    • 数组中有几个元素,回调函数就会调用几次
    • 每次调用,都会将数组中的数据作为参数传递
  • 回调函数中有三个参数:
    • element 当前的元素
    • index 当前元素的索引
    • array 被遍历的数组
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]

arr.forEach((element, index, array) => {
    console.log(element, index, array)
})

image-20221203005657069

3.16 filter()

  • 将数组中符合条件的元素保存到一个新数组中返回
  • 需要一个回调函数作为参数,会为每一个元素去调用回调函数,并根据返回值来决定是否将元素添加到新数组中
  • 非破坏性方法,不会影响原数组
arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.filter((ele) => ele > 5)

image-20221203005800782

不做任何过滤

arr = [1, 2, 3, 4, 5, 6, 7, 8]
arr.filter((ele) => true)

image-20221203010052431

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)

image-20221203005834212

3.18 reduce()

https://www.bilibili.com/video/BV1mG411h7aD?t=1750.5&p=111

  • 可以用来将一个数组中的所有元素整合为一个值
  • 参数:
    1. 回调函数,通过回调函数来指定合并的规则
    2. 可选参数,初始值
arr = [1, 2, 3, 4, 5, 6, 7, 8]
result = arr.reduce((a, b) => {
    console.log(a, b)
    return a + b
})

image-20221203010000793

4 数组的拷贝

复制是复制出来一个新的对象,二者指向的是不同的对象,直接采用等号赋值的话二者指向的是同一个对象

也可以修改某个元素,看看另一个数组中的元素会不会被修改掉

const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr2 = arr  // 不是复制,二者指向的是同一个对象

console.log(arr === arr2)	// true

这种情况内存图如下

image-20221202200648484

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)进行深拷贝

例如定义如下数组,其在内存图中情况如图:

image-20221202201252027

使用slice()进行浅拷贝,从图中看出来只复制了第一层,并没有复制元素中的属性

image-20221202201355793

在此时,通过代码进行验证,可以看出两个数组指向不一样,但是其中的对象是共享的,所以修改一个另一个也会发生改变:

image-20221202201607828

深拷贝内存情况如图,不止复制了元素,也复制了元素里面的属性:

image-20221202201819232

对于如下代码和结果也可以验证结果:

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

const arr3 = structuredClone(arr) // 专门用来深拷贝的方法

console.log(arr)
console.log(arr3)

image-20221202215845329

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)

image-20221202220805245

也可以使用展开运算符

const obj3 = { address: "高老庄", ...obj, age: 48 } // 将obj中的属性在新对象中展开

image-20221202221047064

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()

但是这样有个问题,使用全局变量很容易被别人修改,就会导致在获取中的值出问题

image-20221203002941361

9.1 引入闭包

因此可以放到一个封闭的环境中(例如函数),可以确保外界无法修改它,这时候就又用到了高阶函数的东西,如下代码:

function outer() {
    let num = 0 // 位于函数作用域中

    return () => {
        num++
        console.log(num)
    }
}

const newFn = outer()

// console.log(newFn)
newFn()
newFn()
newFn()

outer()这样的东西就叫做闭包,闭包就是能访问到外部函数作用域中变量的函数

什么时候使用闭包?

当我们需要隐藏一些不希望被别人访问的内容时就可以使用闭包,构成闭包的要件:

  1. 函数的嵌套
  2. 内部函数要引用外部函数中的变量
  3. 内部函数要作为返回值返回

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就去自己的外部也就是全局变量去找

也就是在定义的时候就已经确定值了,在定义的外部作用于去找

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

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

闭包的生命周期

  1. 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
  2. 在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)
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 递归

  • 调用自身的函数称为递归函数
  • 递归的作用和循环是基本一直

编写递归函数,一定要包含两个要件:

  • 基线条件 —— 递归的终止条件
  • 递归条件 —— 如何对问题进行拆分

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

案例:求任意数的阶乘

  1. 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)
  1. 递归思路
  • 求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基本是一致,但是也具有一些不同点:
    1. 可变参数的名字可以自己指定
    2. 可变参数就是一个数组,可以直接使用数组的方法
    3. 可变参数可以配合其他参数一起使用
function fn2(...abc) {
    console.log(abc)
}

fn2(1, 2, 3) //

image-20221203104648057

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")

image-20221203104911207

12 call和apply

根据函数调用方式的不同,this的值也不同:

  1. 以函数形式调用,this是window
  2. 以方法形式调用,this是调用方法的对象
  3. 构造函数中,this是新建的对象
  4. 箭头函数没有自己的this,由外层作用域决定
  5. 通过callapply调用的函数,它们的第一个参数就是函数的this
  6. 通过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)

image-20221203112106857

  • 通过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])

image-20221203112300690

13 bind

bind() 是函数的方法,可以用来创建一个新的函数

function fn(){
    console.log("fn执行了")
}

const newFn = fn.bind()
newFn()

image-20221203112554145

  • 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()

image-20221203112704738

箭头函数没有自身的this,它的this由外层作用域决定,也无法通过call apply 和 bind修改它的this

箭头函数中没有arguments

const arrowFn = () => {
    console.log(this)
}

// 三种方法都是打印dewindow
arrowFn()
arrowFn.call(obj)

const newArrowFn = arrowFn.bind(obj)
newArrowFn()

image-20221203113109286

class MyClass {
    fn = () => {
        console.log(this)
    }
}

const mc = new MyClass()

mc.fn.call(window)

image-20221203113241703

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/58649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android Jetpack-Compose相关

Android Jetpack-Compose相关 一、什么是Compose&#xff1f; Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API&#xff0c;可以帮助您简化并加快 Android 界面开发&#xff0c;打造生动而精彩的应用。它可让您更快…

Go学习之路-环境搭建

默认运行设备系统&#xff1a;MacOS 安装 安装包下载地址&#xff08;下面3个都可以&#xff09;&#xff1a; https://studygolang.com/dl https://golang.google.cn/dl/ https://golang.org/dl/ 我这里选择 pkg包、一键安装、默认的安装路径 /usr/local/go 环境 设置go语言…

[附源码]计算机毕业设计springboot在线图书销售系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

mmap 创建共享内存映射

所谓内存映射指的是 让一个磁盘文件与内存中的一个缓冲区相映射&#xff0c;进程访问这块内存时&#xff0c;就等同于访问文件对应映射部分&#xff0c;不必再调用 read / write 。 我们可以使用mmap函数来建立内存和文件某一部分的映射关系。 目录 一、共享内存映射的创建 /…

代码审计-3 文件包含漏洞

文章目录代码审计-文件包含漏洞文件包含漏洞种类当检测到目标存在文件包含漏洞时可以怎么利用文件包含中php可使用的协议PHPCMS V9.6.3后台文件包含漏洞后台路由分析漏洞寻找代码审计-文件包含漏洞 文件包含漏洞种类 当检测到目标存在文件包含漏洞时可以怎么利用 文件包含中php…

npm run dev和npm run serve

npm run dev和npm run serve目录概述需求&#xff1a;设计思路实现思路分析1.npm install命令2.package.json3.npm run serve参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make …

redis基础4——RDB持久化、AOF持久化全面深入解读

文章目录一、redis持久化机制1.1 持久化的背景1.2 两种持久化概念1.2.1 快照方式&#xff08;RDB&#xff09;1.2.2 文件追加方式&#xff08;AOF&#xff09;1.3 rdb持久化&#xff08;Redis Database&#xff09;1.3.1 快照原理1.3.2 触发机制1.3.2.1 手动触发1.3.2.1.1 save…

最常用的python开发工具

有哪些值得推荐的 Python 开发工具 推荐5个非常适合Python小白的开发工具&#xff1a;1、Python TutorPython Tutor是由Philip Guo开发的一个免费教育工具&#xff0c;可帮助开发者攻克编程学习中的基础障碍&#xff0c;理解每一行源代码在程序执行时在计算机中的过程。 通过…

CentOS8克隆虚拟机修改IP,错误:未知的连接 “ens160“

关于CentOS8该如何克隆与修改IP&#xff0c;并设置成静态IP的方法&#xff0c;可以参考我的另一篇文章&#xff0c;我也是一直这么做的。 CentOS8拷贝虚拟机、修改ip、主机 但是最近我再使用我的这篇文章克隆虚拟机的时候&#xff0c;竟然报错了&#xff0c;本来想删除掉克隆…

2023年天津农学院专升本专业课报名、确认缴费及准考证打印流程

天津农学院2023年高职升本科专业课考试 报名、确认缴费及准考证打印操作流程 一、报名操作流程 1. 阅读报名注意事项 请考生于2022年12月5日9点—12月10日12点登录报名系统 http://gzsb.tjau.edu.cn&#xff0c;请认真阅读报名注意事项。2.报名登录 点击“报名”菜单后进入报名…

LeetCode 374. 猜数字大小

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 374. 猜数字大小&#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 一、题目名称 LeetCode 374.…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java高校辅导员工作管理系统82aub

对于即将毕业或者即将做课设的同学而言&#xff0c;由于经验的欠缺&#xff0c;面临的第一个难题就是选题&#xff0c;确定好题目之后便是开题报告&#xff0c;如果选题首先看自己学习那些技术&#xff0c;不同技术适合做不同的产品&#xff0c;比如自己会些简单的Java语言&…

【圣诞文】用python带你体验多重花样圣诞树

前言 大家早好、午好、晚好吖 ❤ ~ 在圣诞要来临的前夕&#xff0c;我们来出一起圣诞特辑吧 &#xff08;虽说可能有一丢丢早&#xff0c;但是等要来了在准备就来不及了呀~&#xff09; 圣诞节要素 1、圣诞袜&#xff0c;最早以前是一对红色的大袜子&#xff0c;大小不拘。 …

Spring异步任务async介绍与案例实战

关于spring异步任务 简单地说&#xff0c;用Async注释bean的方法将使其在单独的线程中执行。换句话说&#xff0c;调用者不会等待被调用方法的完成。利用spring提供的注解即可简单轻松的实现异步任务处理。 默认线程池问题 Spring 异步任务默认使用 Spring 内部线程池 Simpl…

网页自动点赞

进入网页版QQ空间 ,在开发者模式下创建脚本 var x5,y10;function autoClick(){yy5;var zandocument.getElementsByClassName(item qz_like_btn_v3);for(var i0;i<zan.length;i){if(zan[i].attributes[6].valuelike){zan[i].firstChild.click();}};window.scrollBy(x,y);}win…

LC118原厂直流驱动芯片 SOP-8

直流驱动芯片 LC118 SOP-8 电机驱动电路 马达驱动IC 马达驱动IC LC118的描述&#xff1a; LC118 是为专门为低电压下工作的系统而设计的单通道玩具直流电机驱动集成电路。它具有 H桥驱动器, 采用导通电阻非常小的P-MOS和N-MOS功率晶体管作为开关管&#xff0c;可在瞬间提供大电…

CMake中target_link_libraries的使用

CMake中的target_link_libraries命令用于指定链接给定目标和/或其依赖项时要使用的库或标志。来自链接库目标的使用要求将被传播(propagated)。目标依赖项的使用要求会影响其自身源代码的编译。其格式如下&#xff1a; target_link_libraries(<target> ... <item>…

用Rust写一个链表,非常详细,一遍看懂

前言 在Rust里写一个链表可不是一件容易的事&#xff0c;涉及到很多的知识点&#xff0c;需要熟练掌握之后才能写出一个不错的链表。这篇文章主要介绍了如何写一个Rust链表&#xff0c;并且补充了涉及到的很多的额外知识点&#xff0c;尤其是所有权问题。 首先&#xff0c;你需…

人工智能内容生成元年—AI绘画原理解析

AIGC体验生成一、背景 2022年AIGC&#xff08;AI生成内容&#xff09;焕发出了勃勃生机&#xff0c;大有元年之势&#xff0c;技术与应用迭代都扎堆呈现。在各种新闻媒体处可以看到诸多关于学术前沿研究&#xff0c;以及相应落地的商用案例。可谓出现了现象级的学术-商业共振。…

phpbugs代码审计第三题多重加密答案错误

phpbugs多重加密 这题官方给的答案是错的&#xff0c;网上给的也是错的&#xff0c;麻烦别直接拿给的答案来抄好吗&#xff1f;就想纠正一下别误导别人了 源码如下: <?phpinclude common.php;$requset array_merge($_GET, $_POST, $_SESSION, $_COOKIE);//把一个或多个…