说明:

一、数组简介

  • 数组也是一种复合数据类型,在数组可以存储多个不同类型的数据,数组中存储的是有序的数据,数组中的每个数据都有一个唯一的索引。
  • 可以通过索引来操作获取数据
  • 数组中存储的数据叫做元素,索引(index)是一组大于等于0的整数
1
2
3
4
5
6
7
8
//创建数组(可以通过Array()来创建数组,也可以通过[]来创建数组)
const arr = new Array()
const arr2 = [1,2,3,4,5]
//添加元素
arr[0] = 1
//读取数组中的元素
console.log(arr2[0])
arr[arr.length] = 33 //向数组最后添加元素
  • length

    获取数组的长度,获取的实际值就是数组的最大索引 + 1

    利用此,可以向数组最后添加元素:
    数组[数组.length] = 元素

    length是可以修改的

二、遍历数组

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

1
2
3
4
5
6
7
8
9
10
11
let arr = ["张三","李四","王麻子","店小二"]
//利用for循环
for(let i=0; i<arrrr.length; i++){
if(arr[i].age < 18){
console.log(personArr[i])
}
}
//利用for-of语句
for(let value of arr){
console.log(value)
}

三、数组的方法

1、Array.isArray(参数)

检查一个对象是否是数组

1
2
3
4
let arr = [1,2]
let x = 2
console.log(Array.isArray(arr)); //true
console.log(Array.isArray(a)); //false

2、at(索引)

  • 可以根据索引获取数组中的指定元素
  • at可以接收负索引作为参数
1
2
3
let arr = [1,2,3,4]
let result = arr.at(-1) //返回4
console.log(result)

3、concat()

  • 用来连接两个或多个数组
  • 非破坏性方法,不会影响原数组,而是返回一个新的数组
1
2
3
let arr1 = [1,2,3]
let result = arr.([4,5],[7,8]) //可传入一个或以上参数
console.log(result) //打印 [1,2,3,4,5,6,7,8]

4、indexOf() 与 lastIndexOf()

indexOf()

  • 获取元素在数组中第一次出现的索引

  • 参数:

    1. 要查询的元素
    2. 查询的起始位置

lastIndexOf()

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

  • 参数:

    1. 要查询的元素
    2. 查询的起始位置
  • 返回值:
    找到了则返回元素的索引,
    ​ 没有找到返回-1

1
2
3
4
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
let result = arr.indexOf("沙和尚", 3) //返回3
result = arr.lastIndexOf("沙和尚", 3) //返回3
result = arr.indexOf("白骨精") // 返回-1

5、join()

  • 将一个数组中的元素连接为一个字符串
  • 参数:
    ​ 指定一个字符串作为连接符,若不指定则默认用英文逗号连接
1
2
3
result = arr.join()  //用英文逗号连接
result = arr.join("123") //用 123 连接
result = arr.join("") //不使用连接符,即效果为,两个元素间没有连接符

6、slice()

  • 用来截取数组(非破坏性方法)
  • 参数:
    1. 截取的起始位置(包括该位置)
    2. 截取的结束位置(不包括该位置)
  • 第二个参数可以省略不写,如果省略则会一直截取到最后
1
2
let arr = [1, 2, 3, 4]
result = arr.slice(0, 2) //[0,1]

7、push()

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

1
2
let arr = [1,2,3]
let result = arr.push(4,5)

8、pop()

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

1
2
let arr = [1,2,3]
let result = arr.pop()

9、unshift()

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

1
2
let arr = [1,2,3]
let result = arr.[-1,0]

10、shift()

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

1
2
let arr = [1,2,3]
let result = arr.shift()

11、reverse()

反转数组

1
2
let arr = [1,2,3]
arr.reverse()

12、splice()

  • 可以删除、插入、替换数组中的元素
  • 参数:
    1、删除的起始位置
    2、删除的数量
    3、要插入的元素
  • 返回值:
    返回被删除的元素
1
2
let arr = [1,2,3]
arr.splice(0,2,9)//表示从索引0开始删除两个元素,同时添加元素9

说明:后面的方法中需要用到一些回调函数的知识,可查看第六章进行了解

13、sort()

  • sort用来对数组进行排序(会对改变原数组)
  • sort默认会将数组升序排列
    ​ 注意:sort默认会按照Unicode编码进行排序,所以如果直接通过sort对数字进行排序
    ​ 可能会得到一个不正确的结果
  • 参数:
    ​ 可以传递一个回调函数作为参数,通过回调函数来指定排序规则
    ​ (a, b) => a - b 升序排列
    ​ (a, b) => b - a 降序排列
1
2
let arr = [2, 3, 1, 9, 0, 4, 5, 7, 8, 6, 10]
arr.sort((a, b) => a - b) //将原数组按升序排列

14、forEach()

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

15、fliter()

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

16、map()

  • 根据当前数组生成一个新数组
  • 需要一个回调函数作为参数,回调函数的返回值会成为新数组中的元素
  • 非破坏性方法不会影响原数组
1
2
3
arr = [1, 2, 3, 4, 5, 6, 7, 8]
result = arr.map((ele) => ele * 2)
//result就变成了 [2,4,6,8,10,12,14,18]

17、reduce()

  • 可以用来将一个数组中的所有元素整合为一个值

  • 参数:

    1. 回调函数,通过回调函数来指定合并的规则

    2. 可选参数,初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
arr = [1, 2, 3, 4, 5]  
result = arr.reduce((a, b) => {
console.log(a, b)
return a + b
})
console.log(result) //结果:15
//函数会调用多次
/* a b
第一次:1,2
第二次:3,3 相当于把前一次次相加的结果返回给a
... 6,4
10,5
*/

四、深拷贝与浅拷贝

1、概念

如何去复制一个对象 复制必须要产生新的对象

1
2
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr2 = arr // 不是复制

浅拷贝(shallow copy)

  • 通常对对象的拷贝都是浅拷贝
  • 浅拷贝顾名思义,只对对象的浅层进行复制(只复制一层)
  • 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要
  • 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或元素)

深拷贝(deep copy)

  • 深拷贝指不仅复制对象本身,还复制对象中的属性和元素
  • 因为性能问题,通常情况不太使用深拷贝
1
2
3
4
5
const arr = [{name:"孙悟空"}, {name:"猪八戒"}]
const arr2 = arr.slice() // 浅拷贝 当调用slice时,会产生一个新的数组对象,从而完成对数组的复制
const arr3 = structuredClone(arr) // 专门用来深拷贝的方法
console.log(arr)
console.log(arr3)

2、展开运算符(…)

可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递

通过它也可以对数组进行浅复制

1
2
3
4
5
6
7
8
const arr = ["李四""王麻子"]
const arr2 = [...arr] //进行浅复制
const arr3 = ["张三", ...arr, "李四"] //也可添加其他元素或数组
//作为函数参数
function sum(a, b, c) {
return a + b + c
}
let result = sum(...arr4)

3、对象的复制

  • Object.assign(目标对象, 被复制的对象)

    将被复制对象中的属性复制到目标对象里,并将目标对象返回

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

1
2
3
const obj = { name: "张三", age: 18 }
const obj2 = Object.assign({}, obj)
const obj3 = { address: "高老庄", ...obj, age: 48 } // 将obj中的属性在新对象中展开

五、练习:数组去重与排序

1、数组去重

1.1 最基本的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
1.2、方法二
1
2
3
4
5
6
7
8
9
10
11
// 获取数组中的元素
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)
1.3、方法三
1
2
3
4
5
6
7
8
9
10
const arr = [1, 2, 1, 3, 2, 2, 4, 5, 5, 6, 7]
const newArr = []
for(let ele of arr){
//不存在ele元素,indexOF会返回-1
if(newArr.indexOf(ele) === -1){
//将元素添加到新数组中
newArr.push(ele)
}
}
console.log(newArr)

2、冒泡排序与选择排序

2.1、冒泡排序
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
2.2、选择排序
1
2
3
4
5
6
7
8
9
10
11
12
console.log(arr)
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)

六、回调函数与高阶函数

  • 回调函数:如果将函数作为参数传递,那么我们就称这个函数为回调函数

  • 高阶函数:高阶函数是一个 接收函数作为参数或将函数作为输出返回 的函数

1、情景

创建一个函数对数组进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
const personArr =[
new Person("张三", 18),
new Person("李四", 27),
new Person("王麻子", 8),
new Person("店小二", 16),
]
function fliter(arr){
const newArr = []
for(let i = 0; i < arr.length; i++){
if(arr[i].age >= 18){
newArr.push(arr[i]) //向新数组中添加
}
}
return newArr
}
let result = fliter(personArr)
console.log(result)//即可得到一个过滤后的数组

问题:

  • 目前我们的函数只能过滤出数组中age属性小于18的对象,我们希望过滤更加灵活,在不同条件下进行过滤

  • 这时候就需要学习一点新东西了

    事实上,一个函数的参数也可以是函数,如果将函数作为参数传递,那么我们就称这个函数为回调函数(callback)

  • 好处:将函数作为参数,意味着可以对另一个函数动态的传递代码

2、优化(动态传递代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function filter(arr, cb) {
const newArr = []
for (let i = 0; i < arr.length; i++) {
if (cb(arr[i])) {
newArr.push(arr[i])
}
}

return newArr
}
result = filter(personArr, a => a.name === "孙悟空") //会返回判断的结果(true或false),然后if根据此决定是否执行
result = filter(personArr, a => a.age >= 18)


//温习一下箭头函数的知识
/*
无参箭头函数:() => 返回值
一个参数的:a => 返回值
多个参数的:(a, b) => 返回值

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

3 高阶函数练习

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*  希望在someFn()函数执行时,可以记录一条日志
在不修改原函数的基础上,为其增加记录日志的功能
方法:可以通过高阶函数,来动态的生成一个新函数 */
function someFn() {
return "hello"
}
function outer(cb){
return () => {
console.log("记录日志~")
const result = cb()
return result
}
}
let result = outer(someFn)
console.log(result()) //结果:记录日志~ hello

七、闭包

1、情景

1
2
3
4
5
6
7
8
/* 
创建一个函数,第一次调用时打印1,第二次调用打印2,以此类推 */
let num = 0
function fn(){
num++
console.log(num)
}
fn()
  • 问题:创建的变量num,我们不希望被别人访问到,否则容易被修改,出问题。这时我们就要了解一下闭包的概念了

    闭包就是能访问到外部函数作用域中变量的函数

  • 什么时候使用:
    ​ 当我们需要隐藏一些不希望被别人访问的内容时就可以使用闭包

  • 构成闭包的要件:
    1、函数的嵌套
    2、内部函数要引用外部函数中的变量
    3、内部函数要作为返回值返回

2、使用闭包

1
2
3
4
5
6
7
8
9
10
11
function outer(){
let num = 0 // 位于函数作用域中
return () => {
num++
console.log(num)
}
}
const newFn = outer()
newFn() //1
newFn() //2
newFn() //3

3、作用域问题

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

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

1
2
3
4
5
6
7
8
9
let a = "全局变量中的a"
function fn(){
console.log(a)
}
function fn2(){
let a = "fn2中的a"
fn()
}
fn2() //返回的是全局变量中的a

4、闭包的注意事项

  • 闭包的生命周期:
  1. 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包

  2. 在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)

  • 注意事项:

1、闭包主要用来隐藏一些不希望被外部访问的内容, 这就意味着闭包需要占用一定的内存空间
2、相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能),

  • 需要执行次数较少时,使用闭包
  • 需要大量创建实例时,使用类
1
2
3
4
5
6
7
8
9
10
11
12
13
function outer2(){
let num = 0
return () => {
num++
console.log(num)
}
}
let fn1 = outer2() // 独立闭包
let fn2 = outer2() // 独立闭包,二者互不影响
fn1() //打印结果:1
fn2() //打印结果:1
fn1 = null
fn2 = null //内部函数丢失,闭包生命周期结束

八、递归

递归的作用和循环是一致的,不同点在于,递归思路的比较清晰简洁,循环的执行性能比较好

在实际开发中,一般的问题都可以通过循环解决,尽量去使用循环,少用递归

只在一些使用循环解决比较麻烦的场景下,才使用递归

1、如何编写递归函数

把握两个条件: 1.基线条件 —— 递归的终止条件 2.递归条件 —— 如何对问题进行拆分

1
2
3
4
5
6
7
8
9
10
 function fact(num){
// 基线条件
if(num === 1){
return 1
}
// 递归条件
// num! = (num-1)! * num
return fact(num-1) * num
}
result = fact(5)

2、递归练习之兔子数列

兔子数列(斐波那契数列)

一对兔子出生后两个月后每个月都能生一对小兔子
例子:1 1 2 3 5 8 13 21 34 …

题目:计算n个月后兔子数量

1
2
3
4
5
6
7
8
9
10
11
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)

九、可变参数与arguments

但有的时候我们并不能确定到底要传入多少数量的参数,这时可以利用可变参数,可以不受参数数量的限制,从而更加灵活地创建函数

1、函数中隐含的参数——arguments

  • arguments是函数中又一个隐含参数
  • arguments是一个类数组对象(伪数组)
    ​ 和数组相似,可以通过索引来读取元素,也可以通过for循环变量,但是它不是一个数组对象,不能调用数组的方法
  • arguments用来存储函数的实参,
    ​ 无论用户是否定义形参,实参都会存储到arguments对象中
    ​ 另外也可以通过该对象直接访问实参
1
2
3
4
5
6
7
8
9
//定义一个函数可以求任意数量的数值的和
function sum() {
// 通过arguments,可以不受参数数量的限制更加灵活的创建函数
let result = 0
for (let num of arguments) {
result += num
}
return result
}

2、使用可变参数

  • 可变参数可以接收任意数量实参,并将他们统一存储到一个数组中返回

  • 可变参数的作用和arguments基本是一致,但是也具有一些不同点:

                 1. 可变参数的名字可以自己指定
                 2. 可变参数就是一个数组,可以直接使用数组的方法
                 3. 可变参数可以配合其他参数一起使用
           
    
  • 补充:… (展开运算符)

    ​ - 可以将一个数组中的元素展开到另一个数组中或者作为函数的参数传递

1
2
3
4
5
6
7
function fn2(...abc) {
console.log(abc) //打印结果:[]
}
function sum2(...num) { //以数组形式传入,这样就可以调用数组的方法
return num.reduce((a, b) => a + b, 0)
}
let result = sum2(1,2,3,4,5) //进行求和
  • 普通参数与可变参数配合使用
  • 注意:此时需要将可变参数写到最后
1
2
3
4
5
6
7
function fn3(a, b, ...args) {
// for (let v of arguments) {
// console.log(v)
// }
console.log(args)
}
fn3(123, 456, "hello", true, "1111") //此时a=123,b=456,其他的会存到args数组中

十、改变函数的this对象

复习函数的this

  • 函数在执行时,JS解析器每次都会传递进一个隐含的参数
    这个参数就叫做 this,this会指向一个对象

  • this所指向的对象会根据函数调用方式的不同而不同

    1.以函数形式调用时,this指向的是window

    2.以方法的形式调用时,this指向的是调用方法的对象

    3.构造函数中,this是新建的对象

    4.箭头函数没有自己的this,由外层作用域决定

    5.通过call和apply调用的函数,它们的第一个参数就是函数的this

  • 通过this可以在方法中引用调用方法的对象

箭头函数的this

箭头函数没有自身的this,它的this由外层作用域决定,

也无法通过call apply 和 bind修改它的this

箭头函数中没有arguments

1、调用函数的方式

调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数
比如,我们可以通过调用函数的call()和apply()来个方法来调用函数
函数.call() 与 函数.apply()

  • call 和 apply除了可以调用函数,还可以用来指定函数中的this
  • call和apply的第一个参数,将会成为函数的this
  • 通过call方法调用函数,函数的实参直接在第一个参数后一个一个的列出来
  • 通过apply方法调用函数,函数的实参需要通过一个数组传递
1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
console.log("函数执行了~", this)
}
const obj = { name: "张三", fn }
fn.call(obj) //指定this
fn.apply(console)
//二者的不同点:通过apply方法调用函数,函数的实参需要通过一个数组传递
function fn2(a, b) {
console.log("a =", a, "b =", b, this)
}
// fn2.call(obj, "hello", true)
fn2.apply(obj, ["hello", true])

2、bind()方法

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

  • bind可以为新函数绑定this

  • bind还可以为新函数绑定参数

1
2
3
4
5
6
7
8
9
10
11
12
function fn(a, b, c) {
console.log("fn执行了~~~~", this)
console.log(a, b, c)
}
const obj = {name:"张三"}
const newFn = fn.bind(obj, 10, 20, 30)
newFn() //调用新函数
/*
结果:
fn执行了 {name:'张三'}
10 20 30
*/