数组
这部分内容讲解了JS中的数组,除了数组外还在这部分内容中对对象和函数的部分知识进行了补充,所以视频中的知识并不全都是数组的知识。其中包括:数组简介、遍历数组、for-of语句、数组的方法、对象的复制、冒泡排序、选择排序、高阶函数、闭包、递归、可变参数、call和apply、bind。
数组(Array)
typeof
返回的也是Object创建数组:通过new Array()
来创建数组,也可以通过[]
来创建数组
const arr = new Array()
const arr2 = [1, 2, 3, 4, 5] // 数组字面量,推荐使用这种方式
数组[索引] = 元素
索引(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]
数组[索引]
如果读取了一个不存在的元素,不好报错而是返回undefined
console.log(arr[4])
console.log(arr[200]) // undefined
length
数组[数组.length] = 元素
任何值都可以称为数组中的元素
let arr = [1, "hello", true, null, { name: "孙悟空" }, () => {}]
但是在创建数组时尽量要确保数组中存储的数据的类型是相同
arr = ["孙悟空", "猪八戒", "沙和尚"]
遍历数组简单理解,就是获取到数组中的每一个元素
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])
}
}
for-of语句可以用来遍历可迭代对象
语法:
for(变量 of 可迭代的对象){
语句...
}
执行流程:
示例:
const arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]
for (let value of arr) {
console.log(value)
}
//
for (let value of "hello") {
console.log(value)
}
Array参考文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
- 用来检查一个对象是否是数组
// 下面的都返回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));
at()
可以接收负索引作为参数用来连接两个或多个数组
非破坏性方法,不会影响原数组,而是返回一个新的数组
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)
看结果可以发现原素组并未被改变,并且按照传入的顺序拼接到一起
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.indexOf("沙和尚"))
console.log(arr.indexOf("沙和尚", 3))
获取元素在数组中最后一次出现的位置
返回值:
let arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.lastIndexOf("沙和尚"))
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.join())// 默认使用 , 分割
console.log(arr.join("@-@"))
console.log(arr.join(" "))
用来截取数组(非破坏性方法)
参数(前开后闭):
如果将两个参数全都省略,则可以对数组进行浅拷贝(浅复制)
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧", "沙和尚"]
console.log(arr.slice(0, 2))
console.log(arr.slice(1, 3))
console.log(arr.slice(1, -1))
console.log(arr.slice())
向数组的末尾添加一个或多个元素,并返回新的长度
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.push("bb"))
console.log(arr)
删除并返回数组的最后一个元素
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.pop())
console.log(arr)
向数组的开头添加一个或多个元素,并返回新的长度
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.unshift("bb"))
console.log(arr)
删除并返回数组的第一个元素
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.shift())
console.log(arr)
可以删除、插入、替换数组中的元素
参数:
返回值:
删除
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)
反转数组,返回翻转后的数组
arr = ["孙悟空", "猪八戒", "沙和尚"]
console.log(arr.reverse())
console.log(arr)
sort用来对数组进行排序(会对改变原数组)
sort默认会将数组升序排列
参数:
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)
升序排列
降序排列
arr = ["孙悟空", "猪八戒", "沙和尚", "唐僧"]
arr.forEach((element, index, array) => {
console.log(element, index, array)
})
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)
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)
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
})
复制是复制出来一个新的对象,二者指向的是不同的对象,直接采用等号赋值的话二者指向的是同一个对象
也可以修改某个元素,看看另一个数组中的元素会不会被修改掉
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr2 = arr // 不是复制,二者指向的是同一个对象
console.log(arr === arr2) // true
这种情况内存图如下
当调用slice
时,会产生一个新的数组对象,从而完成对数组的复制
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr3 = arr.slice()
console.log(arr === arr3) // false
也可以使用new Array()
产生新对象
const arr = ["孙悟空", "猪八戒", "沙和尚"]
const arr4 = new Array(arr)
console.log(arr === arr4) // false
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)
展开运算符:...
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)
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中的属性在新对象中展开
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)
有如下一个数组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)
有一个数组:[9,1,3,2,8,0,5,7,6,4]
,编写代码对数组进行排序 --> 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)
取出一个元素,然后将其他元素和该元素进行比较,如果其他元素比该元素小则交换两个元素的位置
时间复杂度: O ( n 2 ) O(n^2) O(n2)
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)
封装排序函数,且不会破坏原数组
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))
案例:过滤得到所有未成年的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)
在上面写的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()
案例:创建一个函数,第一次调用时打印1,第二次调用打印2,以此类推
let num = 0
function fn(){
num++
console.log(num)
}
fn()
但是这样有个问题,使用全局变量很容易被别人修改,就会导致在获取中的值出问题
因此可以放到一个封闭的环境中(例如函数),可以确保外界无法修改它,这时候就又用到了高阶函数的东西,如下代码:
function outer() {
let num = 0 // 位于函数作用域中
return () => {
num++
console.log(num)
}
}
const newFn = outer()
// console.log(newFn)
newFn()
newFn()
newFn()
像outer()
这样的东西就叫做闭包,闭包就是能访问到外部函数作用域中变量的函数
什么时候使用闭包?
当我们需要隐藏一些不希望被别人访问的内容时就可以使用闭包,构成闭包的要件:
主要就是用到作用域链的东西
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
闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间
相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能),
编写递归函数,一定要包含两个要件:
递归的作用和循环是一致的,不同点在于,递归思路的比较清晰简洁,循环的执行性能比较好,在开发中,一般的问题都可以通过循环解决,也是尽量去使用循环,少用递归,只在一些使用循环解决比较麻烦的场景下,才使用递归
案例:求任意数的阶乘
// 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)
// 递归表示
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)
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
可变参数,在定义函数时可以将参数指定为可变参数
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")
根据函数调用方式的不同,this的值也不同:
call
和apply
调用的函数,它们的第一个参数就是函数的this调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数,比如,我们可以通过调用函数的call()和apply()来个方法来调用函数
function fn() {
console.log("函数执行了~", this)
}
const obj = { name: "孙悟空", fn }
fn.call(obj)
fn.apply(console)
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])
bind()
是函数的方法,可以用来创建一个新的函数
function fn(){
console.log("fn执行了")
}
const newFn = fn.bind()
newFn()
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)