严格模式:use strict

<script>
	"use strict"    
</script>

声明区别

推荐使用const,定时器和循环要用let

  1. var 没有块级作用域,会变量提升,可以重复赋值。声明的变量拥有全局作用域或局部作用域
  2. let const 有块级作用域,没有变量提升,不可以重复声明
  3. let 声明变量。const声明只读的常量,常量的值不允许更改(只针对基本数据类型)

★数组常用方法

数组map方法
  • return是用来终止函数的
  • for…in最好用来遍历对象

数组常见遍历

  1. for循环
  2. forEach循环,不需要返回,只是单纯的遍历(推荐),一旦开始,无法停止
  3. 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 的当前数组。

返回值:

  • 返回一个判断后的布尔值,需要有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 的当前数组

返回值:

  • 返回一个判断后的布尔值,需要有return
  • 只要都符合要求才为true
const arr = [1,2,3,4,5]
const result = arr.every(item => {
return item < 6 }) console.log(result) // true
数组filter方法

参数:

  • callback
    1. item当前项,index索引值,arr调用 filter 的当前数组
  • thisArg

返回值:

  • 返回符合条件的组成的新数组,需要有return
const arr = [1,2,3,4]
const res = arr.filter(item => {
return item > 2 }) console.log(res) // [3,4]
累加器reduce

参数:

  • callbackFn


    1. pre 累加和
    2. item当前项
    3. index索引值
    4. 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指向

箭头函数传参

  1. 只有一个参数,可以省略 ( )
let fn = x => {
return x * x } console.log(fn(10)) // 100

返回体

  1. 函数体只有一条语句,可以省略 { } 和 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() 函数生成

  • 对象的属性名现在可以有两种类型


    1. 原来就有的字符串类型 ‘uname’ : ‘张三’
    2. 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的遍历
  1. keys() 返回键名的一个遍历器

  2. 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}
  1. entries() 返回键值对映射的遍历器
const s = new Set([1,5,9,4])
// 3. entries()   返回键值对映射的遍历器
console.log(s.entries())  // SetIterator {1 => 1, 5 => 5, 9 => 9, 4 => 4}
  1. 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 }
  1. 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(要监听的对象名,新增属性名/对象原有的属性名,{
配置项})

配置项里面有多个属性:

  1. value 数据被劫持后,修改后的新值,默认 undefined
  2. 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}
  1. get() get方法,只要获取对象中的属性/方法,就会调用这个方法。默认undefined

  2. 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() 里面也可以接收参数,就是请求失败后返回的提示信息
const p = new Promise((resolve,reject) => {
setTimeout(() => {
// resolve('请求成功') reject('请求失败') },2000) }).then(res => {
console.log(res) // 请求成功 }).catch(err => {
console.log(err) // 请求失败 }) console.log(p)
promise调用状态
  1. fulfilled 表成功
  2. pending 表进行中 可以过渡到成功或者失败的状态
  3. 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>

模块引入

  1. 整体引入
import * as 导入名 from '....'
  1. 结构赋值形式
import { 属性名[, 方法名...]} from '...'
  1. 默认导出的引入
import { default as 导入名 } from '...'
  1. 简便型式,针对默认暴露
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 19get() {
// return 'get方法被调用了' // return this // this 指向 obj 22return this.uname }, // set set方法,只要对象中的属性/方法修改,就会调用这个方法。默认undefined // set() 会有一个默认参数,就是修改后的新值 set(newVal) {
// set() 劫持到修改后的新值newVal以后,要赋值给原先的属性名吧 28this.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

最后修改:2023 年 10 月 30 日
如果觉得我的文章对你有用,请随意赞赏