严格模式:use strict
<script>
"use strict"
</script>
声明区别
推荐使用const,定时器和循环要用let
- var 没有块级作用域,会变量提升,可以重复赋值。声明的变量拥有全局作用域或局部作用域
- let const 有块级作用域,没有变量提升,不可以重复声明
- let 声明变量。const声明只读的常量,常量的值不允许更改(只针对基本数据类型)
★数组常用方法
数组map方法
数组常见遍历
- for循环
- forEach循环,不需要返回,只是单纯的遍历(推荐),一旦开始,无法停止
- map循环,必须有返回值,返回的是一个新数组
- 执行的操作是把原数组中的每个值按照指定的规则映射到新数组中。通过return返回操作后的新数组
const newArr = arr.map(item => {
return item * item
})
console.log(newArr) // [81, 25, 36, 64, 5929, 4, 9]
数组some方法
参数:
- item,index,arr
- item当前项,index索引值,arr调用
some
的当前数组。
- item当前项,index索引值,arr调用
返回值:
- 返回一个判断后的布尔值,需要有return
- 只要有一个符合要求就为true
const arr = [1,2,3,4,5]
const res = arr.some(item => {
item > 2
})
console.log(res) // true
数组every方法
参数:
- item,index,arr
- item当前项,index索引值,arr调用
every
的当前数组
- item当前项,index索引值,arr调用
返回值:
- 返回一个判断后的布尔值,需要有return
- 只要都符合要求才为true
const arr = [1,2,3,4,5]
const result = arr.every(item => {
return item < 6
})
console.log(result) // true
数组filter方法
参数:
- callback
- item当前项,index索引值,arr调用
filter
的当前数组
- item当前项,index索引值,arr调用
- thisArg
返回值:
- 返回符合条件的组成的新数组,需要有return
const arr = [1,2,3,4]
const res = arr.filter(item => {
return item > 2
})
console.log(res) // [3,4]
累加器reduce
参数:
callbackFn
- pre 累加和
- item当前项
- index索引值
- arr调用
reduce
的当前数组
initialValue 初始值
返回值:
- 返回一个累加后的和,需要有return
const arr = [1,2,3,45]
const res = arr.reduce((pre,item) => {
return pre += item
},0)
console.log(res) // 51
其他方法
- findIndex 返回元素对应的索引
- findLastIndex 返回元素对应的最后的索引
★解构
字符串
const str = 'hello'
const [a,b,c,d,e,f] = str
console.log(a) // h
console.log(f) // undefined
// 结合扩展运算符使用
const str1 = 'helloworld'
const [a,b,..c] = str1
console.log(a) // h
console.log(c) // ['l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
- 扩展运算符可以快速实现字符串分割
const [...s] = 'helloworld'
console.log(s) // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
s.reverse().join('')
console.log(s) // ['d', 'l', 'r', 'o', 'w', 'o', 'l', 'l', 'e', 'h']
数组
- 通过索引值来解构的
扩展运算符
- 可以实现数组的合并
const arr1 = [1,2,3]
const arr2 = [4,5]
const arr = [...arr1,...arr2]
console.log(arr) // [1,2,3,4,5]
可以实现拷贝数组
- 如果是一维,相当于深拷贝,不会影响原数组
const arr = [1,2,3,4] const newArr = [...arr] console.log(newArr) // [1,2,3,4] newArr[0] = 100 console.log(newArr) // [100,2,3,4] console.log(arr) // [1,2,3,4]
- 如果是多维,相当于浅拷贝,修改引用类型,会影响原数组
const arr1 = [1,2,[3,4]] const arr2 = [...arr1] arr2[2][0] = 99 console.log(arr2) // [1, 2, Array(2)] console.log(arr1) // [1, 2, Array(2)]
对象
通过键来映射的
解构出来对象中的属性和方法
- 可以通过 :重命名
const obj = {
uname: '张三',
age: 24,
say() {
console.log('会说话')
}
}
// 解构对象的属性和方法
const {
uname } = obj
console.log(uname)
const {
age: newAge } = obj
console.log(newAge)
const {
say } = obj
say()
- 解构整个对象,利用扩展运算符,做的是深拷贝,不影响原对象
// 解构整个对象,利用扩展运算符
const o = {
...obj }
console.log(o)
★箭头函数
- 箭头函数没有this指向,它的this指向就是上一层作用域的this指向
箭头函数传参
- 只有一个参数,可以省略 ( )
let fn = x => {
return x * x
}
console.log(fn(10)) // 100
返回体
- 函数体只有一条语句,可以省略 { } 和 return
let fn = x => x * x
console.log(fn(10)) // 100
★rest参数
- 又叫剩余参数 …args 不确定多少参数的情况下可以使用
- 会将多余的变量合成一个数组
- 参数也可以指定默认值,防止不传入参数
function fn(x = 0,y = 0,...args) {
console.log(x) // 1
console.log(args) // [3,6,9]
}
fn(1,2,3,6,9)
- 也可以为对象赋默认值
const {
uname,age = 24 } = {
uname: 'zs' }
console.log(uname) // zs
console.log(age) // 24
新增的数据解构
Symbol
为了解决对象属性名冲突而诞生的新的原始数据类型
代表独一无二的值
Symbol值通过 Symbol() 函数生成
对象的属性名现在可以有两种类型
- 原来就有的字符串类型 ‘uname’ : ‘张三’
- Symbol类型,可以保障不会与其他属性名产生冲突
Symbol() 里面可以接收一个字符串参数。也就是属性名,用于区分
应用场景:
- 对对象进行扩展
- 如果添加一个,可以直接写在对象中
- 访问:对象名[Symbol]
- 如果多个,就只能再外面声明好,再追加
- 访问:对象名[属性名]
- 如果添加一个,可以直接写在对象中
// 如果添加一个,可以直接写在对象中
const obj = {
// uname : '张三',
[Symbol] : '张三'
}
console.log(obj[Symbol]) // 张三
// 如果多个,就只能在外面声明好,再追加
const age = Symbol('age')
const gender = Symbol('gender')
obj[age] = 24
obj[gender] = '男'
console.log(obj)
console.log(obj[age]) // 24
Set
很少用,了解
不允许出现重复的值,也是一个构造函数,需要new进行使用
接收数组类型的参数,如果不是,会尝试进行转换为数组,转换不成功,则报错
可以进行解构出来
格式:set(size长度) {
元素...}
const s = new Set([1, 2, 3])
console.log(s) // set(3) {1,2,3}
console.log(s[0]) // undefined
console.log(...s) // 1,2,3
常用方法
add() 进行添加
- 一次只能添加一个,如果想添加多个,可以放到数组或者对象里
s.add(4,5)
console.log(s) // Set(4) {1, 2, 3, 4}
// 一次只能添加一个,如果想添加多个,可以放到数组或者对象里
s.add([6,7,8])
console.log(s) // Set(5) {1, 2, 3, 4, Array(3)}
s.add({
uname: 'zs',age: 24})
console.log(s) // Set(6) {1, 2, 3, 4, Array(3), …}
delete() 进行删除
- 返回一个布尔值,表删除成功/失败
- 如果删除对象,需要用到 delete 操作符(具体怎么删除Set里的对象,待定)
const s = new Set([1, 2, 3])
// delete删除某个值,返回一个布尔值,删除成功/失败
console.log(s.delete(1)) // true
console.log(s) // Set(2) {2, 3}
has() 是否包含某个值
- 返回一个布尔值,判断是否存在某个值
const s = new Set([1, 2, 3])
// has判断是否有某个值
console.log(s.has(9)) // false
console.log(s.has(4)) // true
clear()
- 清空全部
const s = new Set([1, 2, 3])
s.clear()
console.log(s) // Set(0) {size: 0}
Set的遍历
keys() 返回键名的一个遍历器
values() 返回值的一个遍历器
Set的键和值是同一个值,所以 keys() 和 values() 返回的结果是一样的
const s = new Set([1,5,9,4])
console.log(s) // Set(4) {1, 5, 9, 4}
// 1. keys() 返回键名的遍历器
console.log(s.keys()) // SetIterator {1, 5, 9, 4}
// 2. values() 返回值的遍历器
console.log(s.values()) // SetIterator {1, 5, 9, 4}
- entries() 返回键值对映射的遍历器
const s = new Set([1,5,9,4])
// 3. entries() 返回键值对映射的遍历器
console.log(s.entries()) // SetIterator {1 => 1, 5 => 5, 9 => 9, 4 => 4}
- for…of 进行遍历,因为 Set 内置了
iterator
接口(只要有iterator接口,就可以使用for…of)
- Array,String,Map,Set,NodeList,TypeArray,函数的arguments 都内置了iterator
- 其他类型想要使用iterator,则需要进行配置,才能使用for…of
const s = new Set([1,5,9,4])
// 4. for...of 遍历
for (const iterator of s) {
console.log(iterator) // 1 5 9 4
}
- Set 自带的forEach进行循环遍历
const s = new Set([1,5,9,4])
// 5. 用Set自带的forEach循环遍历
s.forEach(item => {
console.log(item) // 1 5 9 4
})
应用场景
数组去重:
const arr = [1, 2, 6, 6, 6, 9]
let res = new Set(arr)
console.log(res) // Set(4) {1, 2, 6, 9}
// 此时res还是Set数据类型,要把Set转为数组
// 1. Array.from(res)
console.log(Array.from(res)) // [1, 2, 6, 9]
// 2. 扩展运算符
console.log([...res]) // [1, 2, 6, 9]
Map
很少用,了解
也是对对象进行扩展的,对对象进行了升级(原来对象的键只能用字符串,这个的键可以是任何类型)
常用方法
set() 进行添加
- 使用方法前,要区分简单数据类型和复杂数据类型
- 存引用数据类型时,最好用一个变/常量接收引用类型,然后直接存入这个变/常量(方便后面取)
格式: Map(size长度) {
元素...}
const m = new Map()
console.log(m) // Map(0) {size: 0}
// 1. set() 添加
// 简单数据类型时,会覆盖,因为存放在栈里
m.set(1,'yi')
m.set(1,'one')
console.log(m) // Map(1) {1 => 'one'}
// 复杂数据类型时,因为内存地址不同,所以会添加两个
m.set([1],'yi')
const arr = [1]
m.set(arr,'one')
console.log(m) // Map(3) {1 => 'one', Array(1) => 'yi', Array(1) => 'one'}
get() 进行获取
const m = new Map()
m.set(1,'one')
// 复杂数据类型时,因为内存地址不同,所以会添加两个
m.set([1],'yi')
const arr = [1]
m.set(arr,'one')
console.log(m) // Map(3) {1 => 'one', Array(1) => 'yi', Array(1) => 'one'}
// 2. get() 获取
console.log(m.get(1)) // one
// 引用类型不能直接用定义的键名获取
console.log(m.get([1])) // undefined
// 要提前给引用类型的数据赋值变量,然后把变量存进去,取的时候也用变量名取
console.log(m.get(arr)) // one
delete() 删除
const m = new Map()
m.set(1,'one')
// 复杂数据类型时,因为内存地址不同,所以会添加两个
m.set([1],'yi')
const arr = [1]
m.set(arr,'one')
// 3. delete() 删除
m.delete(1)
console.log(m) // Map(2) {Array(1) => 'yi', Array(1) => 'one'}
m.delete(arr)
console.log(m) // Map(1) {Array(1) => 'yi'}
clear() 清空
const m = new Map()
console.log(m) // Map(0) {size: 0}
m.set(1,'one')
// 4. clear() 清空
m.clear()
console.log(m) // Map(0) {size: 0}
Map的遍历
遍历和Set一样,也可以通过 keys()
values()
entries()
for...of
forEach
★数据劫持
- 又叫数据拦截,数据代理,数据绑定
数据双向绑定原理
vue2 Object.defindeProperty()
vue3 proxy()
区别:
- Object.defindeProperty() 一次只能监听一个对象的一个属性
- proxy() 可以监听整个对象
Object.defindeProperty()
语法:Object.defindeProperty(要监听的对象名,新增属性名/对象原有的属性名,{
配置项})
配置项里面有多个属性:
- value 数据被劫持后,修改后的新值,默认 undefined
- writable 布尔值,是否允许再被修改,默认 false
// vue2 响应式原理是 Object.defindeProperty()
const obj = {
uname: '张三',
age: 23
}
// defineProperty 有3个参数 要监听的对象名 新增属性名/对象有的属性名 {配置项}
Object.defineProperty(obj,'uname',{
// value 劫持属性名后要修改的值
value: '王二小',
// writable 布尔值,是否允许被重写
writable: false,
})
// 因为配置项里不允许再被修改,所以这里修改了也没用
obj.uname = '王五'
console.log(obj) // {uname: '王二小', age: 23}
get() get方法,只要获取对象中的属性/方法,就会调用这个方法。默认undefined
set() set方法,只要对象中的属性/方法修改,就会调用这个方法。默认undefined
- set() 存在一个默认参数,就是修改后的新值
注意:get() set() 方法 与 value writable冲突,二选一,不能同时出现
const obj = {
uname: '张三',
age: 23
}
// 定义一个过渡值,就可以解决栈内存溢出的问题
let newName = ''
Object.defineProperty(obj, 'uname', {
// get get方法,只要获取对象中的属性/方法,就会调用这个方法。默认undefined
get() {
// return 'get方法被调用了'
// return this // this 指向 obj
// return this.uname
return newName
},
// set set方法,只要对象中的属性/方法修改,就会调用这个方法。默认undefined
// set() 会有一个默认参数,就是修改后的新值
set(newVal) {
// set() 劫持到修改后的新值newVal以后,要赋值给原先的属性名吧
// this.uname = newVal
// 把newVal赋值给过渡值
newName = newVal
}
})
// 修改uanme 会被set方法劫持到,会触发set()
obj.uname = '王五'
// 查看uname的值,会触发get方法
console.log(obj.uname)
- 如果要监视对象,则需要把defindeProperty的 第二个参数(新增属性名/对象原有的属性名) 替换为 键名(kes()获取)
- 然后把代码放入到循环中,每个属性都要有新的中间过渡值
Proxy
- 对整个对象进行数据代理,是个构造函数,需要new来使用
- 通过 Proxy() 代理对象后,不能再操作原对象了,而是操作实例化对象
- 如果配置项不进行任何操作,则相当于直接操作原对象
const obj = {
uname: '王五',
age: 23
}
// Proxy() 有两个参数,第一个是要监视的对象名,第二个是配置项(get&set)
const p = new Proxy(obj,{
// 获取属性的时候触发,有3个参数
// target:目标对象 propKey:目标对象的属性 recevier:可选 实例化对象
get: function(target, propKey, recevier) {
// console.log(target)
// console.log(propKey)
return target[propKey]
},
// 设置属性的时候触发,有4个属性,比get多了一个value
// value:设置的新值
set: function(target, propKey, value, recevier) {
target[propKey] = value
}
})
// 原对象调用是不会触发get和set的,因为原对象已经交给了Proxy()进行代理
// console.log(obj.uname) // 王五
// 所以应该使用Proxy()进行调用
p.age = 24
console.log(p.age) // 24
p.uname = '罗翔'
console.log(p.uname) // 罗翔
★Promise对象
- Promise也是一个构造函数,需要new来使用
- 实例化的Promise(),里面接收一个函数 new Promise(() => {…})
- 函数可以接收两个参数,一个是成功回调(resolve()),一个是失败回调(reject(),可选)
- 成功回调(resolve()) 会自动调用then方法
- resolve() 里面也可以接收参数,就是请求成功后返回的数据
- 失败回调(reject()) 会自动调用catch方法
- reject() 里面也可以接收参数,就是请求失败后返回的提示信息
- 成功回调(resolve()) 会自动调用then方法
const p = new Promise((resolve,reject) => {
setTimeout(() => {
// resolve('请求成功')
reject('请求失败')
},2000)
}).then(res => {
console.log(res) // 请求成功
}).catch(err => {
console.log(err) // 请求失败
})
console.log(p)
promise调用状态
- fulfilled 表成功
- pending 表进行中 可以过渡到成功或者失败的状态
- reject 表失败
- 一旦状态确定后,就不再改变了
Promise.all()与Promise.race()
promise.all()
- 全部异步操作都成功,则组合成数组。只要有一个失败,则返回失败的回调
- 应用场景: 网页必须等待所有异步请求都完成后才能展示
- 接收参数:promise组成的数组
<body>
<script>
// 模拟几个异步请求
const p1 = new Promise((resolve,reject) => {
setTimeout(() => {
// resolve('调用成功')
reject('调用失败')
},1000)
})
// console.log(p1)
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('调用成功')
}, 2000)
})
// console.log(p2)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('调用成功')
// reject('调用失败')
}, 3000)
})
// console.log(p3)
// Promise.all() 全部成功回调后,合成成功的回调数组。有一个失败的,就返回失败的
const p = Promise.all([p1,p2,p3])
console.log(p)
// Promise.race() 不管成功失败,返回最先返回的回调状态
const pp = Promise.race([p1, p2, p3])
console.log(pp)
</script>
</body>
面试题:Promise.all()与Promise.race() 的 异同
同:
- 接收的参数都是 promise 组成的数组
异:
- promise.all() 全部异步操作都成功,则组合成数组。只要有一个失败,则返回失败的回调
- 而promise.race() 则是不管成功还是失败,只要那个异步操作先完成,则返回这个异步操作的状态(成功/失败)
★async与await
- es7推出的回调地狱的解决方案
- async与await成双成对出现,await写在async修饰的函数里
- await 代替了then的工作
- async修饰的函数,里面的异常只能通过 try-catch 捕获
<body>
<button>get请求</button>
<button class="post">post请求</button>
<script>
// get
document.querySelector('button').addEventListener('click',async function() {
const {
data: res } = await axios({
method:'get',
url: 'https://autumnfish.cn/api/joke',
params: {
num: 1
}
})
console.log(res)
})
// post
document.querySelector('.post').addEventListener('click',async function() {
try {
const {
data: res } = await axios({
method: 'post',
url: 'https://autumnfish.cn/api/joke',
data: {
num: 1
}
})
console.log(res)
} catch (error) {
console.log(error)
}
})
</script>
</body>
★类class
- 私有属性要写到constructor构造器里面
- 共有方法/属性直接在在外面,会挂载到原型对象prototype上
- 如果不写constructor构造器,则会默认给添加一个空的构造器
class Father {
// 私有属性
constructor(uname,age) {
this.uname = uname
this.age = age
}
// 共有属性/方法
gender = '男'
say() {
console.log('总是训斥儿子')
}
}
const f = new Father('王五',55)
console.log(f) // Father {gender: '男', uname: '王五', age: 55}
f.say() // 总是训斥儿子
console.log(f.gender) // 男
继承
- 使用extends进行继承,可以继承父类的所有属性和方法
- 子类要有自己的私有属性,必须调用super()
- 子类也可以重写父类的方法
- 子类也可以添加自己特有的方法
// 创建一个子类,继承父类,可以使用父类的方法和属性
class Son extends Father {
constructor(uname,age,gender) {
// 子类要有自己的私有属性,必须调用super()
super(uname,age)
this.gender = gender
}
// 子类可以添加自己特有的方法
study() {
console.log('儿子努力学习')
}
// 子类也可以重写父类的方法
say() {
console.log('儿子希望父亲能和蔼一点')
}
}
const s = new Son('张三',24,'男')
console.log(s) // Son {gender: '男', uname: '张三', age: 24}
s.say() // 儿子希望父亲能和蔼一点
s.study() // 儿子努力学习
静态方法
- static修饰的方法,不会被继承,它只属于当前类,只有当前类才能调用
- 被static修饰的共有属性,也是不会被继承的,只有当前类才能调用
- 注意:是当前类,不是类的实例化
class Person {
constructor(uname,age) {
this.uname = uname
this.age = age
}
// static 修饰的方法和属性,都不会被继承,只能通过此类调用
static gender = '男'
static say() {
console.log('会说话')
}
}
console.log(Person.gender) // 男
Person.say() // 会说话
class zs extends Person {
constructor(uname,age) {
super(uname, age)
}
}
const z = new zs('张三',24)
console.log(z) // // zs {uname: '张三', age: 24}
iterator接口
Array,String,Map,Set,NodeList,TypeArray,函数的arguments 都内置了iterator
只要有iterator接口,就可以使用 for…of 方法进行循环
其他类型想要使用iterator,则需要进行配置,才能使用for…of
// Array,String,Map,Set,NodeList,TypeArray,函数的arguments 都内置了iterator
// 可以直接使用 for...of 进行循环
// const arr = [1,5,9,10]
// for (const iterator of arr) {
// console.log(iterator)
// }
// const str = 'helloworld'
// for (const iterator of str) {
// console.log(iterator)
// }
// console.log(arr)
// 其他类型想要使用iterator,则需要进行配置,才能使用for...of
const obj = {
uname: '张三',
age: 24,
// 进行iterator配置
[Symbol.iterator]() {
const that = this
// console.log(that) // 此时that就是obj
let index = 0
const data = Object.keys(that) // 返回一个键组成的新数组
// console.log(data)
return {
next() {
if (index < data.length) {
return {
value: data[index++],
done: false
}
}
return {
value: undefined, done: true }
}
}
}
}
console.log(obj)
for (const k of obj) {
// 键
console.log(k)
// 值
console.log(obj[k])
}
★模块化
模块,就是一个独立的js文件
模块导出 export 模块导入import
script标签的type属性,可以定义模块。定义模块后,浏览器会将其识别为一个模块
<script type="module">...</script>
分别导出
- 可以解构导入,也可以全部导入
.js文件(模块)
export let hero = '维克托'
export function work() {
console.log('发明家');
}
.html文件
<script type="module">
// 可以解构引入
// import { hero,work } from './module/01-分别暴露.js'
// console.log(hero) // 维克托
// work() // 发明家
// 也可以全部引入
import * as h from './module/01-分别暴露.js'
console.log(h) // Module {Symbol(Symbol.toStringTag): 'Module'}
h.work() // 发明家
</script>
统一导出又叫批量导出
- export { … } 里面一定不要加括号
- 可以使用解构导入
.js文件(模块)
const num = 10
function fn() {
console.log('77777')
}
export { num, fn }
.html文件
<script type="module">
// 解构导入
import { num,fn } from './module/03-批量导出.js'
console.log(num) // 10
fn() // 77777
</script>
默认导出
- 一个模块只能有唯一一个默认的导出模块
- export default { … } 不能使用解构来导入,也不能使用全部导入
- 只能使用
import 导入名 from '...'
导入
.js文件(模块)
// 一个模块只能有一个默认导出
export default {
uname: '张三',
age: 23,
say() {
console.log('会普法')
},
}
.html文件
<script type="module">
// 引入默认模块,不需要解构,也不能全部导入,只能通过 import 导入名 from '...' 导入
import person from './02-默认导出.js'
console.log(person) // {uname: '张三', age: 23, say: ƒ}
person.say() // 会普法
</script>
模块引入
- 整体引入
import * as 导入名 from '....'
- 结构赋值形式
import { 属性名[, 方法名...]} from '...'
- 默认导出的引入
import { default as 导入名 } from '...'
- 简便型式,针对默认暴露
import 导入名 from '...'
补充
对象简写
const uname = '张三'
const age = 23
const obj = {
uname: uname,
// 1. 键名和值名都相同的情况下,可以简写成一个
age,
// 2. 函数简写
// 老版
sing: function() {
console.log('唱')
},
// 简写
dance() {
console.log('跳')
}
}
console.log(obj) // {uname: '张三', age: 23, sing: ƒ, dance: ƒ}
obj.sing() // 唱
obj.dance() // 跳
栈内存溢出报错
Uncaught RangeError: Maximum call stack size exceeded
栗子
<body>
<script>
const obj = {
uname: '张三',
age: 23
}
Object.defineProperty(obj, 'uname', {
// get get方法,只要获取对象中的属性/方法,就会调用这个方法。默认undefined
19行 get() {
// return 'get方法被调用了'
// return this // this 指向 obj
22行 return this.uname
},
// set set方法,只要对象中的属性/方法修改,就会调用这个方法。默认undefined
// set() 会有一个默认参数,就是修改后的新值
set(newVal) {
// set() 劫持到修改后的新值newVal以后,要赋值给原先的属性名吧
28行 this.uname = newVal
}
})
// 修改uanme 会被get方法劫持到
obj.uname = '王五'
33行 console.log(obj.uname)
// 为什么22行+28行的this写法会造成栈内存溢出?
// 因为:代码执行到33行时,要查看obj的uname属性,所以会调用19行的get方法
// 而19行的get方法,返回值又是 obj.uname 又是要查看obj的uname属性
// 所以会接着调用19行的get方法,反复调用,然后就陷入了死循环
</script>
</body>
原文链接:https://blog.csdn.net/qq_52845451/article/details/126933741
此处评论已关闭