2022.10.31
一.万物皆对象
/*
万物皆对象
+ 私人: 任何非空数据结构都可以看做一个 对象数据结构
回忆:
+ 对象 / 数组 / 函数 / 时间 / 正则 / ...
=> 本身就是一个复杂数据类型, 是一个 "盒子"
+ 数值 / 字符串 / 布尔
=> 是基本数据类型, 但是同时也是包装数据数据类型
=> 在使用的时候, 会瞬间转换成复杂数据类型的样子让你使用
=> 使用完毕会转换回基本数据类型的样子存储
+ 任何非空数据结构都可以把它当做一个 对象数据结构 使用
=> 任何一个数据, 都是一类内容中一个真实的个体
=> 例子:
{} 对象 这一类数据的一个真实个体
[] 数组 这一类数据的一个真实个体
10 数字 这一类数据的一个真实个体
'1' 字符串 这一类数据的一个真实个体
true 布尔 这一类数据的一个真实个体
function () {} 函数 这一类数据的一个真实个体
/^a$/ 正则 这一类数据的一个真实个体
每一个数据都可以被叫做 实例对象
-> 每一个数据都满足对象的特点
+ 特殊的数据结构(函数, 一等公民)
=> 普通函数, 可以封装一段代码, 执行一段代码
=> 构造函数, 主要关心书写的 this.xxx 的代码, 使用的时候 和 new 关键字连用, 可以创建对象
=> 对象结构, 可以当做一个对象使用, 存储一些数据
+ 每个数据所属的类
=> 数值 所属的类 Number
=> {} 所属的类 Object
=> [] 所属的类 Array
=> /^$/ 所属的类 RegExp
=> 时间对象 所属的类 Date
=> 函数 所属的类 Function
=> '' 所属的类 String
=> true 所属的类 Boolean
*/
二.原型链
/*
原型链
+ 面试题: 你给我解释一下什么是 原型 和 原型链 ?
+ 原型: 每一个构造函数天生自带的 prototype, 是一个对象数据结构, 讲方法书写在这里, 提供给所有实例对象使用
+ 原型链: 使用 __proto__ 串联起来的对象链状结构, 为了是对象访问机制服务
概念:
1. 每一个函数天生自带一个属性, 叫做 prototype, 是一个对象数据结构
2. 每一个对象天生自带一个属性, 叫做 __proto__, 指向所属构造函数的 prototype
3. 每一个没有构造函数的对象数据结构, 所属的构造函数是, 内置构造函数 Object
对象访问机制:
+ 当你需要访问一个对象的成员的时候
+ 首先在自己身上查找, 如果有, 直接使用, 停止查找
+ 如果没有, 会自动去自己的 __proto__ 查找, 如果有, 直接使用, 停止查找
+ 如果还没有, 就再去 __proto__ 查找
+ 直到 Object.prototype 都没有, 那么返回 undefined
问题1: 实例对象 p1 的 __proto__ 指向谁 ?
+ 因为 p1 是由 Person 创造出来的对象
+ 所以 p1 所属的构造函数是 Person
+ 所以 p1.__proto__ === Person.prototype
问题2: 自定义构造函数 Person 的 __proto__ 指向谁 ?
+ 自定义构造函数也是一个构造函数, 构造函数也是一个函数
+ 在 JS 内, 只要是函数, 就是属于 Function 类
+ Person 所属的构造函数就是 内置构造函数 Function
+ Person.__proto__ === Function.prototype
问题3: Person.prototype 的 __proto__ 指向谁 ?
+ 因为 Person.prototype 是一个标准的对象数据结构
+ 所以 Person.prototype 所属的构造函数就是内置构造函数 Object
+ Person.prototype.__proto__ === Object.prototype
问题4: 内置构造函数 Function 的 __proto__ 指向谁 ?
+ 内置构造函数也是一个构造函数, 构造函数也是一个函数
+ 在 JS 内, 只要是函数, 就是属于 Function 类
+ 内置构造函数 Function, 是 JS 内的顶级函数
=> 自己是自己的构造函数, 自己是自己的实例对象
+ 内置构造函数所属的构造函数就是 Function
+ Function.__proto__ === Function.prototype
问题5: Function.prototype 的 __proto__ 指向谁 ?
+ 因为 Function.prototype 是一个标准的对象数据结构
+ 所以 Function.prototype 所属的构造函数就是内置构造函数 Object
+ Function.prototype.__proto__ === Object.prototype
问题6: 内置构造函数 Object 的 __proto__ 指向谁 ?
+ 内置构造函数也是一个构造函数, 构造函数也是一个函数
+ 在 JS 内, 只要是函数, 就是属于 Function 类
+ 内置构造函数 Object 所属的构造函数 Function
+ Object.__proto__ === Function.prototype
问题7: Object.prototype 的 __proto__ 指向谁 ?
+ Object.prototype.__proto__ === null
+ 因为 JS 内 Object.prototype 叫做 顶级原型
*/
三.检测数据类型
检测数据类型
1. typeof
+ 语法:
=> typeof 要检测的数据结构
=> typeof(要检测的数据结构)
+ 特点: 只能准确的检测基本数据类型
2. constructor
+ 语法: 数据结构.constructor
+ 返回: 该数据结构所属的构造函数
+ 缺点: 不能判断 null 和 undefined 类型
=> 因为我们只有 非空 数据类型才可以使用 点语法
=> null 和 undefined 不能用
3. instanceof
+ 语法: 数据结构 instanceof 类
+ 返回值是一个布尔值
=> true: 说明 该数据结构是从属于该 类
=> false: 说明 该数据结构不从属于该 类
+ 特点: 不能判断 null 和 undefined 类型
4. Object.prototype.toString.call()
+ 语法: Object.prototype.toString.call(你要判断的数据结构)
+ 返回值: '[object 数据类型]'
+ 特点: 可以判断所有数据结构
+ 详见解释1
/* 解释1:
+ 在 Object.prototype 上有一个叫做 toString 的方法
=> 目的是为了给所有对象使用的
=> 作用: 只要是对象数据结构, 就转换成字符串数据类型
=> 结果: {} -> '[object Object]'
+ 为了避免所有的数据结构都能使用这个方法
=> 给每一个数据结构上都添加了 toString 方法
=> Array.prototype.toString = function () {} // 数组的就是去括号
=> String.prototype.toString = function () {} // 数值 字符串 布尔的可以转换进制转字符串
=> Object.prototype.toString = function () {} // 还原数据结构
+ 就想直接调用 Object 原型上存储的 toString 方法
=> 书写 Object.prototype.toString()
=> 原理就是把 this 还原数据结构
=> 如果这个函数内的 this 指向对象, 还原对象
=> 如果这个函数内的 this 指向数组, 还原数组
=> 如果这个函数内的 this 指向字符串, 还原字符串
=> Object.prototype.toString() 调用的时候
-> toString 函数内的 this 是谁 ? Object.prototype
=> 利用 call 方法改变函数内的 this 指向
-> Object.prototype.toString.call({}) 调用的时候
-> toString 函数内的 this 指向 {}
-> Object.prototype.toString.call([]) 调用的时候
-> toString 函数内的 this 指向 []
*/
四.对象的 toString 方法
/*
对象的 toString 方法
+ 在 Object.prototype 上是有这个方法的
+ 任何一个对象数据结构都可以调用
+ 当发生隐式转换的时候, 会自动调用该方法
=> 例子:
-> 用一个对象和 字符串 进行加法运算
-> '1' + {}
-> 以上表达式会进行字符串拼接
-> 左边是一个字符串, 但是右边不是
-> 会自动把右边的 {} 转换成 字符串类型, 在进行运算
-> 就触发了隐式转换, 就会自动调用 toString 方法
*/
// 改写一下 toString 方法
// Object.prototype.toString = function () {
// console.log('hello world')
// return '123'
// }
// const res = '1' + {}
// 左边是 : '1'
// 右边是 : '[object Object]'
// console.log(res)
// 任何一个对象数据结构, 访问成员的时候
// 在自己身上查找, 如果有直接使用
// 如果没有, 自动去到自己的 __proto__ 查找
// const o = {}
// 访问 o 的 toString 成员
// 自己没有, 去到自己的 __proto__ 查找
// o.__proto__ 是 Object.protoytype
// 访问并执行的就是 Object.prototype 上的 toString 方法
// o.toString()
// 面试题: 让下面等式成立
// const o = ?
// console.log(o == 1 && o == 2 && o == 3)
// ? 位置应该书写什么 ?
// o 的三次比较都是和 数值类型进行比较
// 在三次比较的时候
// 如果 o 本身就是一个数值类型, 那么没有关系
// 只要 o 不是数值类型, 就会触发三次 隐式转换, 转换成数值类型去比较
// o 会触发三次隐式转换
// 因为只要触发隐式转换就会触发 toString 方法
// 要求 o 必须是一个可以调用 toString 方法的数据
// 把 o 设计为一个对象数据结构
// const o = {
// }
// console.log(o == 1 && o == 2 && o == 3)
// 思考:
// 如果我让 o 可以调用 toString 方法
// 方案1: 修改 Object.prototype.toString 方法
// 方案2: 给 o 自己加一个 toString 方法
// const o = {
// toString () {
// console.log('我是给 o 自己家的一个 toString 方法')
// return 1
// }
// }
// console.log(o == 1 && o == 2 && o == 3)
// 因为这个 toString 方法会被调用三次
// 我需要第一次返回的是 1
// 我需要第二次返回的是 2
// 我需要第三次返回的是 3
const o = {
count: 0,
toString () {
// this 是谁 ? o
// this.count ? 0
// ++this.count ? 1, 并且把 o 内的 count 修改为 1
return ++this.count
}
}
console.log(o == 1 && o == 2 && o == 3 && o == 4)
/*
o == 1 && o == 2 && o == 3
第一步: o == 1
=> 发现 o 不是数值类型, 需要把 o 转换成数值类型
1-1. o.toString()
-> 会把 o.count 修改为 1
-> return 1
1-2. 用执行完毕的结果和 1 进行比较
-> 1 == 1
-> true
第二步: o == 2
=> 发现 o 不是数值类型, 需要把 o 转换成数值类型
2-1. o.toString()
-> 会把 o.count 修改为 2
-> return 2
2-2. 用执行完毕的结果和 2 进行比较
-> 2 == 2
-> true
第三部: o == 3
=> 发现 o 不是数值类型, 需要把 o 转换成数值类型
3-1. o.toString()
-> 会把 o.count 修改为 3
-> return 3
3-2. 用执行完毕的结果和 3 进行比较
-> 3 == 3
-> true
*/
2022.11.01
一.复习函数
/*
函数
+ 是一个 JS 内的数据结构, 是一个复杂数据类型
+ 私人: 一个承载一段代码的 "盒子"
+ 函数必然有两个阶段
=> 函数定义阶段: 把代码装进 "盒子" 里面
=> 函数调用阶段: 把 "盒子" 内的代码执行一遍
函数两个阶段做的事情(熟读并背诵全文)
+ 函数定义阶段
1. 在堆内存开辟一段函数存储空间
2. 把函数体内的代码一模一样的存储在该空间内(不进行函数体内的变量解析)
3. 把空间地址赋值给变量
+ 函数调用阶段
1. 按照变量内的地址去查找一个函数存储空间
=> 如果变量不存在, 直接报错: xxx is not defined
=> 如果变量保存的不是函数地址, 直接报错: xxx is not a function
2. 在 调用栈 内开辟一段函数执行空间
3. 在 执行空间 内进行形参赋值
4. 在 执行空间 内进行预解析
5. 在 执行空间 内把函数体内的代码复制过来从头到尾执行一遍
6. 代码执行完毕, 执行空间销毁
*/
// 函数定义阶段
// let num = 100
// function fn() {
// // 假设在定义函数的时候就已经解析了变量, 那么应该存储起来的是 console.log(100)
// // 假设没有在定义函数的时候解析变量, 那么存储起来的是 console.log(num)
// console.log(num)
// }
// // 修改 num 的值
// num = 200
// 调用函数
// fn()
/*
先修改的 num 才调用的 fn 函数
+ 如果定义阶段就已经解析了变量, 那么 后期 num = 200 不会影响结果, 打印出来依旧是 100
+ 如果定义阶段没有解析变量, 那么后期 num = 200 会影响结果, 打印出来就是 200
*/
// 函数调用阶段
let num = 100
// 函数调用阶段
/*
如果函数调用的时候,
先进行形参赋值, 后预解析
1. 把 a 赋值为 10
2. 预解析的时候, 解析 a 变量发现已经存在, 但是要赋值为一个函数
打印 a 变量的时候, 得到的应该是一个函数
先预解析, 后进行形参赋值
1. 声明 a 变量, 并且赋值为一个函数
2. 给 a 变量赋值为 10
打印 a 变量的时候, 得到的应该是一个 10
*/
function fn(a) {
console.log(a)
function a() {
console.log('你好 世界')
}
}
fn(10)
二.不会销毁的空间
/*
不会销毁的空间
+ 当函数内返回了一个复杂数据类型
+ 并且外部有变量接受该数据的时候
+ 该空间不会被销毁
再次想让他销毁
+ 只要让外部变量不在存储该地址即可
*/
function fn() {
const obj = {
name: 'Jack'
}
return obj
}
let res = fn()
console.log(res)
三.初识闭包
初识闭包
+ 概念: 函数里的函数
+ 私人:
1. 一个不会销毁的空间
2. 在函数内 直接 或者 间接 返回一个函数
3. 内部函数需要使用着外部函数的私有数据
特点:
1. 可以在函数外部利用闭包来操作函数的私有变量
2. 延长了私有变量的生命周期
// 我们管 inner 叫做 outer 的闭包函数
function outer() {
// num 是 outer 的私有数据
const num = 100
function inner() {
return num
}
return inner
}
// res 接受的就是 outer 函数内定义的那个 inner 函数
// 我们就说 inner 是 outer 的闭包函数
// 因为 inner 被返回以后赋值给了 res
// 所以也说 res 是 outer 的闭包函数
const res = outer()
// 在调用 res 函数, 其实也就是在调用 outer 内的 inner 函数
// n 得到的是 inner 函数内 return num 结果
// inner 函数自己有 num 变量吗 ? 没有, 要去到外部作用域查找
// 也就是 outer 函数的私有变量 num
const n = res()
console.log(n)
四.间接返回一个函数( 沙箱模式 )
/*
间接返回一个函数( 沙箱模式 )
*/
function outer() {
let num = 100
let str = 'hello world'
let boo = true
const obj = {
getNum: function () { return num },
getStr: function () { return str },
setNum: function (val) { num = val },
}
return obj
}
// res 接受到的就是 outer 函数内的 obj 对象
// 内部存储了两个函数
const res = outer()
// 当我需要使用 outer 函数内的 num 变量的值的时候
// 代用的 getNum 函数, return 的是 outer 的私有变量 num 的值
console.log(res.getNum())
res.setNum(200)
// 再一次获取 outer 内的私有变量 num 的值
console.log(res.getNum())
const res2 = outer()
console.log(res2.getNum()) // 100
五.闭包语法糖
/*
res === {
getNum: 函数,
setNum: 函数
}
得到这个对象的目的:
+ 对该 fn 函数形成一个 "包裹"
+ 为了 获取 和 设置 fn 函数内的 私有变量 num
+ 一旦 "包裹" 内的 getNum 执行, 就会获取到 私有变量num
+ 一旦 "包裹" 内的 setNum 执行, 就会设置 私有变量num 的值
针对以上目的:
+ 考虑该语法, 发现不那么符合语境
+ 研究了一个 getter 获取器 和 setter 设置器的语法
+ 目的: 只是为了让 闭包 的语法更贴合语境
思考:
+ res 对象
+ 如果我把 res 真的想象成一个对象
+ 对于对象的操作语法
=> 对象名.xxx 这样来操作
*/
function fn() {
let num = 100
const obj = {
// 原先的 getNum 目的获取 num 的值, 我们语法糖使用 getter 获取器来书写
// 语法: get 名字 () {}
// getter 获取器会把 getNum 制作为一个 obj 对象内的成员, 值就是该函数的返回值
get getNum () { return num },
// 原先的 setNum 目的是设置 num 的值, 我们语法糖使用 setter 设置器来书写
// 语法: set 名字 () {}
// 当你按照对象的语法, 给 setNum 赋值的时候, 就会触发该函数
set setNum (val) { num = val }
}
return obj
}
// res 得到的就是 fn 函数内的 obj 对象
const res = fn()
console.log(res)
// getter 获取器的使用
// 当 getter 获取器把该内容直接制作成对象内的成员了
// 我们就按照对象的语法操作即可
console.log(res.getNum)
// setter 设置器的使用
res.setNum = 200
六.函数 柯理化
函数柯理化:一种对封装函数的处理方式
// 封装方案3: 利用闭包
// 步骤1: 书写一个 验证函数的 制造函数
function createRegTest(reg) {
function inner(str) {
return reg.test(str)
}
return inner
}
// 利用 createRegTest 函数来制造出正则验证函数
// testUsername 得到的是 inner 函数
const testUsername = createRegTest(/\w{5,11}/)
const testPassword = createRegTest(/\d{6,12}/)
// console.log(testUsername);
// console.log(testPassword);
// 开始验证
// 验证用户名
const r1 = testUsername('admin')
console.log(r1)
// 验证用户名第二次
const r2 = testUsername('guoxiang')
console.log(r2)
// 验证密码第一次
const r3 = testPassword('12345a')
console.log(r3)
// 验证用户名第三次
const r4 = testUsername('aaa')
console.log(r4)
七.复杂一些的柯理化函数 - 01
// 负责记录初次保留的内容
function create() {
// 需要把你传递的参数都保留下来
// 利用一个信息, arguments
// 函数内天生自带的信息, 表示该函数有多少个实参
// console.log(arguments)
// 创建一个数组, 把信息保留下来
const arr = [ ...arguments ]
// inner 函数需要做的事情, 就是接受四个参数, 把它们组装起来
function inner() {
// 把本次的参数传递到 arr 内继续保留下来
const data = [ ...arr, ...arguments ]
console.log(data)
// 判断, 如果 data 内不够 4 个, 我需要把目前的 若干个(2, 3, 1) 保留下来
// 如果 data 内够 4 个, 处理逻辑
if (data.length < 4) {
// console.log('应该继续收集')
// 把目前的 data 内的内容作为初始内容保留, 继续去收集
return create(...data)
} else {
// 把 data 内的数据求和, 给出结果
const res = data.reduce((prev, item) => prev + item, 0)
return res
}
}
return inner
}
// f1 接受了一个闭包. 闭包里面存储了一个 arr 数组, 里面有一个数据
// f1 得到了一个 闭包函数, 保留了一个数据结合, 里面有一个数据 [10]
const f1 = create(10)
// r 得到了一个 闭包函数, 保留了一个数据集合, 里面有三个数据 [10, 20, 30]
const r = f1(20, 30)
const res = r(40)
console.log(res)
八.复杂一些的柯理化函数 - 02
/*
复杂一些的柯理化函数
https://www.xhl.com:8080/index.html
https://www.xhl.com:8080/login.html
https://www.xhl.com:8081/index.html
https://www.gx.com:8080/index.html
http://www.xhl.com:8080/index.html
+ 以上内容包含了 四段 基本信息
=> 传输协议 http 或者 https
=> 域名 www.xhl.com 或者 www.gx.com 或者 ...
=> 端口号 8080 8081 0 ~ 65535
=> 地址 /index.html 或者 /login.html
*/
// 函数的柯理化处理
// 把一个正常逻辑的函数, 改变成 柯理化逻辑的 函数
// 正常逻辑的函数
// function resolveUrl(a, b, c, d) {
// return a + '://' + b + ':' + c + d
// }
// // 将来使用的时候
// const url1 = resolveUrl('http', 'www.xhl.com', 8080, '/index.html')
// const url2 = resolveUrl('http', 'www.xhl.com', 8080, '/login.html')
// console.log(url1)
// console.log(url2)
function resolveUrl(a, b, c, d) {
return a + '://' + b + ':' + c + d
}
// https://www.xhl.com:8080/index.html
// 书写柯理化函数
// ... 用在函数形参的位置, 表示合并
// fn 接受的是第一个实参
// 剩下的所有实参, 放在一个数组内放在 arr 里面了
function currying(fn, ...arr) {
// arr 就是我们收集的参数
// 确认我一共要多少个参数(功能函数有多少个形参, 就是需要多少个参数)
// 功能函数是 fn
// 功能函数有多少个形参: 函数名.length
const max = fn.length
function inner() {
const data = [ ...arr, ...arguments ]
console.log(data)
// 判断是否足够
if (data.length < max) {
return currying(fn, ...data)
} else {
// 利用 fn 函数来把四个参数拼装起来
// 返回的就是拼装结果
return fn(...data)
}
}
return inner
}
const f1 = currying(resolveUrl, 'http')
const f2 = f1('www.xhl.com')
const url1 = f2(8080, '/index.html')
console.log(url1)
const url2 = f2(8081, '/login.html')
console.log(url2)
const f3 = f2(8080)
const url3 = f3('/list.html')
console.log(url3)
九.复杂一些的柯理化函数 - 03
/*
复杂一些的柯理化函数
https://www.xhl.com:8080/index.html
https://www.xhl.com:8080/login.html
https://www.xhl.com:8081/index.html
https://www.gx.com:8080/index.html
http://www.xhl.com:8080/index.html
+ 以上内容包含了 四段 基本信息
=> 传输协议 http 或者 https
=> 域名 www.xhl.com 或者 www.gx.com 或者 ...
=> 端口号 8080 8081 0 ~ 65535
=> 地址 /index.html 或者 /login.html
*/
// 函数的柯理化处理
// 把一个正常逻辑的函数, 改变成 柯理化逻辑的 函数
// 正常逻辑的函数
function resolveUrl(a, b, c, d) {
return a + '://' + b + ':' + c + d
}
// 将来使用的时候
const url1 = resolveUrl('http', 'www.xhl.com', 8080, '/index.html')
const url2 = resolveUrl('http', 'www.xhl.com', 8080, '/login.html')
console.log(url1)
console.log(url2)
// 把我们的正常逻辑函数, 处理成柯理化形式
const resolveUrl = currying((a, b, c, d) => a + '://' + b + ':' + c + d)
// // 在我的开发过程中, 前三个都是固定的, 只有最后一个是变化的
const join3 = resolveUrl('http', 'www.xhl.com', 8080)
const join2 = resolveUrl('http', 'www.gx.com')
// // 开始拼接路径
const url1 = join3('/index.html')
const url2 = join3('/login.html')
console.log(url1)
console.log(url2)
const url3 = join2(8081, '/aaa.html')
const url4 = join2(8082, '/aaa.html')
console.log(url3)
console.log(url4)
// 在我的开发过程中, 前三个都是固定的, 只有最后一个是变化的
const join3 = currying((a, b, c, d) => a + '://' + b + ':' + c + d)('http', 'www.xhl.com', 8080)
const join2 = currying((a, b, c, d) => a + '://' + b + ':' + c + d)('http', 'www.gx.com')
// 开始拼接路径
const url1 = join3('/index.html')
const url2 = join3('/login.html')
console.log(url1)
console.log(url2)
const url3 = join2(8081, '/aaa.html')
const url4 = join2(8082, '/aaa.html')
console.log(url3)
console.log(url4)
2022.11.02
一.深浅拷贝
1.浅拷贝
浅拷贝
+ 指: 把 对象 或者 数组, 复制出一份一模一样的来
+ 逻辑:
=> 创建一个和原始数据结构一模一样的数据结构
=> 遍历原始数据结构, 把原始数据结构内的每个数据进行复制
+ 缺点:
=> 不能存在二维数据, 因为二维数据依旧是地址的赋值
/*
浅拷贝
+ JS 提供了一些方法
*/
// 1. 展开运算符(...)
const origin = { name: 'Jack', age: 18, gender: '男', info: { height: 180 } }
const target = { ...origin }
console.log(origin, target)
// 2. assign
const origin = { name: 'Jack', age: 18, gender: '男', info: { height: 180 } }
// 语法: Object.assign({}, origin)
// 作用: 把 origin 对象浅拷贝一份一模一样的出来
// 返回值: 浅拷贝完毕的数据结构
const target = Object.assign({}, origin)
console.log(origin, target)
// 3. freeze()
const origin = { name: 'Jack', age: 18, gender: '男', info: { height: 180 } }
// 语法: Object.freeze(origin)
// 作用: 浅拷贝并冻结数据
const target = Object.freeze(origin)
console.log(origin, target)
// 尝试修改一级数据
target.name = 'Rose'
console.log(origin, target)
// 尝试修改二级数据
target.info.height = 200
console.log(origin, target)
2.深拷贝
深拷贝
+ 不管有多少层级数据, 都能百分百复制过来
//方案一 递归思想
const origin = {
name: 'Jack',
age: 18,
gender: '男',
info: {
height: 180,
weight: 180,
desc: {
message: '你好 世界',
city: '北京'
}
},
hobby: [ '足球', '篮球', '羽毛球' ]
}
const target = {}
// 使用递归的方式来实现
// 准备一个函数
// 作用: 把 origin 的内容深拷贝一份到 target 内
function deepCopy(target, origin) {
for (let k in origin) {
// 判断
if (origin[k].constructor === Object) {
// 因为 origin[k] 是一个对象, 所以要把 target[k] 也设置为一个对象
target[k] = new Object()
// 把 origin[k] 的内容深拷贝一份到 target[k]
deepCopy(target[k], origin[k])
} else if (origin[k].constructor === Array) {
target[k] = new Array()
deepCopy(target[k], origin[k])
} else {
target[k] = origin[k]
}
}
}
// 使用 deepCopy 来完成深拷贝
// 需求: 我想把 o1 的内容深拷贝一份到 o2 内
deepCopy(o2, o1)
// 需求: 我想把 abc 的内容深拷贝一份到 def 内
deepCopy(def, abc)
// 需求: 我想把 a.b.c 的内容深拷贝一份到 d.e.f 内
deepCopy(d.e.f, a.b.c)
// 需求: 我想把 origin 的内容深拷贝一份到 target 内
deepCopy(target, origin)
console.log(origin, target)
/*
深拷贝方案2:
+ 利用 json 格式字符串
+ 语法:
=> JSON.stringify(JS的数据结构)
-> 把 JS 的数据结构转换成 json 格式字符串
=> JSON.parse(json格式字符串)
-> 把 json 格式字符串转换成 JS 的数据结构
*/
const origin = {
name: 'Jack',
age: 18,
gender: '男',
info: {
height: 180,
weight: 180,
desc: {
message: '你好 世界',
city: '北京'
}
},
hobby: [ '足球', '篮球', '羽毛球' ]
}
// console.log(origin)
// // 把 origin 转换成 json 格式字符串
// const str = JSON.stringify(origin)
// console.log(str)
// // 把 str 转换成一个 JS 的数据结构
// const target = JSON.parse(str)
// console.log(target)
const target = JSON.parse(JSON.stringify(origin))
console.log(origin, target)
// 尝试修改一级数据
target.age = 20
console.log(origin, target)
// 尝试修改二级数据
target.info.height = 200
console.log(origin, target)
// 尝试修改三级数据
target.info.desc.city = '上海'
console.log(origin, target)
二.数组扁平化
/*
数组扁平化
+ 指: 把一个多维数组转换成一维数组
+ 例子:
=> 原始数组: [ 100, 200, [ 300, 400, [ 500, 600 ] ] ]
=> 转换结果: [ 100, 200, 300, 400, 500, 600 ]
*/
const origin = [ 100, { name: 'Jack' }, [ 'hello', 400, [ true, 600, [ 700, 800 ] ] ] ]
console.log('原始数组 : ', origin)
// 1. 如果数组内的数据都是基本数据类型(对 数值 和 字符串 友好)
// 直接把数组转换成字符串即可
// 转换成字符串
const r1 = origin.toString()
console.log(r1)
// 按照 逗号(,) 切割开
const res = r1.split(',').map(item => {
if (!isNaN(item)) return item - 0
return item
})
console.log(res)
// 2. 手动实现
// 2-1. 准备一个空数组接受结果
const res = []
// 2-2. 利用递归实现
function flat(origin, res) {
origin.forEach(item => {
if (item.constructor === Array) {
flat(item, res)
} else {
res.push(item)
}
})
}
flat(origin, res)
console.log(res)
// 3. 数组常用方法 flat()
// 语法: 数组.flat(参数)
// 参数: 表示扁平多少层数据(Infinity)
// 返回值: 扁平化以后的数组
const res = origin.flat(Infinity)
console.log(res)
三.基本数据劫持
数据劫持
+ 就是另一种向对象内添加成员的方式
语法:
+ Object.defineProperty(对象, 键名, 配置项)
=> 对象: 需要向哪一个对象内设置属性
=> 键名: 你定义的属性名是什么
=> 配置项: 对当前这一个属性的配置
注意:
+ 用该访问添加的成员
+ 默认 不允许被修改
+ 默认 不能被遍历出来(不可枚举)
配置项:
+ value: 表示该属性的值
+ writable: 默认是 false, 表示不可写
+ enumerable: 默认是 false, 表示不可枚举
+ get: 书写一个函数, 表示的是 getter 获取器(注意: 不能和 value 和 writable 一起用)
=> 该函数的返回值, 就是 该键名 对应的值
+ set: 书写一个函数, 表示的是 setter 设置器
=> 当你修改该属性的值的时候, 就会触发该函数
// 需求:
// 你需要准备一个原始数据
// 利用原始数据去渲染了页面
// 当原始数据一旦发生变化的时候, 自动修改页面渲染的内容
const ele = document.querySelector('div')
const btn = document.querySelector('button')
// 准备原始数据
const data = { name: 'Jack', age: 18 }
// 利用数据劫持的语法, 把 data 劫持出一份来
// 因为 data 内的数据是非劫持数据, 做不到数据变化能触发一些效果的
const _data = {}
Object.defineProperty(_data, 'name', {
get () {
// 因为是利用 getter 获取器劫持的 data.name 数据
// 只要 data 内的 name 一旦发生变化, 这里就会从新获取一次最新的值给到 _data.name
return data.name
},
set (val) {
// console.log('你想修改 _data 内的 name 属性, 修改为 ' + val)
// 不需要直接修改, 因为当 data 内的 name 属性值一旦被修改, 马上 _data 内的 name 就会响应
data.name = val
bindHtml()
}
})
// 使用劫持后的 _data 去渲染页面
function bindHtml() {
ele.innerHTML = `你好 我是 ${ _data.name }, 我今年 ${ data.age } 岁了`
}
bindHtml()
btn.onclick = function () {
// console.log('我想修改数据')
// 因为你是使用 _data 渲染的页面
// 所以我们应该修改 _data 内的数据
_data.name = Math.random()
}
四.数据劫持升级版
/*
数据劫持
+ 原先: Object.defineProperty(对象, key, 配置项)
=> 特点: 需要出现一个 "劫匪"
=> 把原始数据劫持以后放在一个新的数据结构内
+ 现在: Object.defineProperties(对象, 配置项)
=> 特点: 自己 "劫持" 自己
=> 配置项: {
key: { 详细配置(value, writable, enumerable, get, set) }
}
=> 注意: 不要自己直接劫持自己, 需要一个中转
+ 对后期动态插入的数据不好用
*/
const ele = document.querySelector('div')
const data = { name: 'Jack', age: 18 }
console.log('原始对象 : ', data)
// 把对象内所有成员都制作一遍劫持
for (let k in data) {
// k 就是 data 内每一个键名
Object.defineProperties(data, {
// 把当前属性做一个备份
['_' + k]: {
value: data[k],
// 需要打开可以书写
writable: true
},
// 开始劫持
[k]: {
get () { return this['_' + k] },
set (val) {
// 只要把 对应的之前的备份修改掉
this['_' + k] = val
console.log('触发劫持')
rander()
}
}
})
}
rander()
function rander() {
ele.innerHTML = '我使用 data 内的数据渲染页面 : ' + data.name + ' --- ' + data.age
}
console.log(data)
五.Proxy 数据代理
Proxy 数据代理
+ 直接代理整个数据结构
+ ES6 新出的语法
语法:
const res = new Proxy({ 代理对象 }, { 配置项 })
const res = new Proxy({ name: 'Jack' }, {
get (target, property) {
// target 就是代理对象,
// property 就是每一个属性
return target[property]
},
set (target, property, value) {
// target 就是代理对象
// property 就是每一个属性名
// value 就是你要设置的值
target[property] = value
console.log('你是通过代理设置的')
// 触发渲染页面的 rander 函数
// 注意:
return true
}
})
console.log(res)
// 后期动态添加数据
res.age = 18
2022.11.03
一.异步代码
/*
异步代码
+ JS 内 WEBAPI 提供的一些代码执行机制
=> setInterval()
=> setTimeout()
=> 网络请求
=> ...
+ 异步特点:
=> 当代码执行的过程中, 如果遇到异步代码了
=> 首先把异步代码放在队列内等待
=> 继续向后执行同步代码
=> 直到所有同步代码执行完毕
=> 再来执行队列内的异步代码
对今天的代码做一个约定
+ 用 setTimout 来模拟 网络请求 状态
=> 时间定义成 2000 ~ 5000 的随机数字
=> 判定超过 3500 叫做失败
*/
// 我可能需要多个网络请求
// 把 异步代码 封装起来
// 目前: 我们能做的就是把结果打印在控制台
function getInfo(n) {
const time = Math.floor(Math.random() * 3000) + 2000
setTimeout(() => {
if (time < 3500) {
eleDiv.innerHTML = '本次成功 : ', n
} else {
console.log('本次失败 : ', n)
}
}, time)
}
getInfo(1)
getInfo(2)
getInfo(3)
二.回调函数
/*
回调函数
+ 什么情况下会用到
=> 封装 异步代码 的时候会用到
什么是回调函数
+ 把 函数A 当做参数传递到 函数B 内
+ 在 函数B 内使用 形参 来调用
+ 此时我们说 函数A 是 函数B 的回调函数(callback)
*/
function a() {
console.log('我是 a 函数')
}
function b(fn) {
console.log('我是 b 函数')
// 调用的就是全局 a 函数
fn()
}
// a 存储的是一个函数地址
// 把 a 函数的地址传递到 b 函数内, 赋值给 fn 参数了
// 从此以后, b 的参数 fn 和 全局变量 a 控制的是同一个函数存储空间
b(a)
// 封装一段异步代码
function getInfo(success, error) {
const time = Math.floor(Math.random() * 3000) + 2000
setTimeout(() => {
if (time < 3500) {
// eleDiv.innerHTML = '本次成功 : ', n
// 在成功的时候, 调用一下 success 函数
success( time )
} else {
// console.log('本次失败 : ', n)
error( time )
}
}, time)
}
// 需求:
// 在 1 号请求结束以后, 把成功的结果渲染在 div 内
// 在 2 号请求结束以后, 把成功的结果渲染在 p 内
// 在 3 号请求结束以后, 把成功的结果渲染在 span 内
getInfo(
// 1,
res => eleDiv.innerHTML = `本次成功了 耗时 : ${ res }`,
err => eleDiv.innerHTML = '当前网路环境不好, 请稍后再试'
)
getInfo(
// 2,
res => eleP.innerHTML = `本次成功了 耗时 : ${ res }`,
err => eleP.innerHTML = '当前网路环境不好, 请刷新重试 ^_^'
)
getInfo(
// 3,
res => eleSpan.innerHTML = `本次成功了 耗时 : ${ res }`,
err => eleSpan.innerHTML = '当前网路环境不好, 请刷新重试 O(∩_∩)O哈哈~'
)
三.回调地狱
/*
回调地狱
+ 当回调函数嵌套过多的时候会出现的一种代码书写结构
分析:
+ 因为你是按照 回调函数 的规则对异步代码进行的封装
+ 就要按照回调函数的方式来进行调用
+ 因为按照回调函数的方式在调用的时候, 嵌套过多
=> 导致回调地狱出现
解决:
+ 方案1: 不要嵌套(不好)
+ 方案2: 不要按照回调函数的方式进行封装
ES6 提供了一种新的东西叫做 Promise
+ 作用: 对异步代码进行封装的, 解决回调地狱问题
*/
// 封装一段异步代码
function getInfo(n, success, error) {
const time = Math.floor(Math.random() * 3000)
setTimeout(() => {
if (time < 3500) {
// eleDiv.innerHTML = '本次成功 : ', n
// 在成功的时候, 调用一下 success 函数
success( time )
} else {
// console.log('本次失败 : ', n)
error( time )
}
}, time)
}
// 需求:
// 在 1 号请求结束以后, 把成功的结果打印在控制台
// 在 1 号请求结束以后, 在进行 2 号请求, 在 2 号请求结束以后, 把成功的结果打印在控制台
// 在 2 号请求结束以后, 在进行 3 号请求, 在 3 号请求结束以后, 把成功的结果打印在控制台
getInfo(1, function (res) {
console.log(`1 成功 : 耗时 : ${ res }`)
// 这个代码执行的时候, 表示 1 号请求结束了
// 发起 2 号请求
getInfo(2, function (res) {
console.log(`2 成功 : 耗时 : ${ res }`)
// 这个代码执行的时候, 表示 2 好请求结束了
// 发起 3 号请求
getInfo(3, function (res) {
console.log(`3 成功 : 耗时 : ${ res }`)
})
})
})
四.Promise
/*
Promise
+ 是 ES6 提供的一种异步代码执行和封装方案
认识一下:
+ Promise 承诺
+ 一个 Promise 的状态
=> pending 持续
=> fulfilled 成功
=> rejected 失败
=> 状态转换只有两种
-> 持续 转换为 成功
-> 持续 转换为 失败
基础语法:
+ const p = new Promise(function () { 你要做的事情(你承诺的事情) })
+ 返回值: p 就是一个 Promise 对象(实例对象)
+ Promise 对象可以调用两个方法配置对应的函数
=> p.then(函数)
-> 配置一个会在成功后执行的函数
=> p.catch(函数)
-> 配置一个会在失败后执行的函数
*/
// 基础语法
const p = new Promise(function (resolve, reject) {
// 这里帮我们执行异步代码
// 如果成功了, 那么让当前 Promsie 的状态转换为 成功
// 如果失败了, 那么让当前 Promsie 的状态转换为 失败
// 第一个参数: 是一个函数, 当他被调用的时候, 当前 Promise 的状态会转变为 成功
// 第二个参数: 是一个函数, 当他被调用的时候, 当前 Promise 的状态会转变为 失败
const time = Math.floor(Math.random() * 3000) + 3000
setTimeout(() => {
if (time < 3500) {
resolve( time )
} else {
reject( time )
}
}, time)
})
// 配置函数
// 当 p 的状态不在是 pending 是 成功 的时候, 该函数会被自动调用
p.then(function (res) {
console.log('当前 Promise 的状态已经改变为成功了 : ', res)
})
// 当 p 的状态不在是 pending 是 失败 的时候, 该函数会被自动调用
p.catch(function (err) {
console.log('当前 Promise 的状态已经改变为失败了 : ', err)
})
五.Promise 封装代码
function getInfo() {
const p = new Promise(function ( resolve,reject) {
const time = Math.floor(Math.random() * 3000)
setTimeout(() => {
if (time < 3500) {
resolve( time )
} else {
reject( time )
}
}, time)
})
return p
}
// Promise 一种应用
// 可以在一个 then 内 return 一个新的 Promise 对象
// 可以在该 then 的后面继续第二个 then
getInfo()
.then(res => {
console.log('成功1 : ', res)
// 进行第二个 getInfo()
// 在第一个的 then 里面, 把 Promise 对象 return 出去
return getInfo()
})
.then(res => {
// 匹配的是上一个 then 里面 return 的 getInfo() 对应的 Promise 对象
console.log('成功2 : ', res)
return getInfo()
})
.then(res => {
console.log('成功3 : ', res)
})
.catch(err => {
console.log('失败 : ', err)
})
六.Promise 的其他方法
/*
Promise 的其他方法
1. Promise 对象调用的方法
1-1. then()
=> promise对象.then(函数)
=> 时机: 成功的函数
1-2. catch()
=> promise对象.catch(函数)
=> 时机: 失败的函数
1-3. finally()
=> promise对象.finally(函数)
=> 时机: 完成的函数
2. Promise 自带的方法
2-1. all()
=> Promise.all([ promise1, promise2, promise3, ... ])
=> 特点: 同时触发多个 promise
-> 只要有任何一个失败了, 得到失败的结果, 剩下的不要了
-> 只有所有的任务都成功了, 最终 all 才算成功, 得到的是所有 promise 的结果
2-2. race()
=> Promise.race([ promise1, promise2, promise3, ... ])
=> 特点: 同时触发多个 promise
-> 只要任何一个完成了, 不管成功还是失败, 其他的不要了
-> 该任务的状态就是 race 的状态
2-3. allSettled()
=> Promise.allSettled([ promise1, promise2, promise3, ... ])
=> 特点: 同时触发多个 promise
-> 等到所有任务都完成才会成功
-> 会详细记录每一个任务的状态和结果
*/
function getInfo() {
const p = new Promise(function (resolve, reject) {
const time = Math.floor(Math.random() * 3000) + 2000
setTimeout(() => {
if (time < 3500) {
resolve( time )
} else {
reject( time )
}
}, time)
})
return p
}
// promise 对象的方法
// getInfo()
// .then(res => console.log('成功'))
// .catch(err => console.log('失败'))
// .finally(() => console.log('本次 promise 完成'))
// Promise 自身的方法
// 2-1. all()
// Promise
// .all([ getInfo(), getInfo(), getInfo(), getInfo(), getInfo() ])
// .then(res => console.log('成功 : ', res))
// .catch(err => console.log('失败 : ', err))
// 2-2. race()
// Promise
// .race([ getInfo(), getInfo(), getInfo(), getInfo(), getInfo() ])
// .then(res => console.log('成功 : ', res))
// .catch(err => console.log('失败 : ', err))
// 2-3. allSettled
Promise
.allSettled([ getInfo(), getInfo(), getInfo(), getInfo(), getInfo() ])
.then(res => console.log(res))
七.async 和 await 语法
/*
async 和 await 语法
+ 目的: 专门为了配合 Promise 出现的语法
+ 是两个关键字, 目的是为了把 异步代码 写的 **看起来像** 同步代码
async 关键字:
+ 书写在函数的前面
+ 作用: 表示在该函数内可以使用 await 关键字
await 关键字:
+ 必须要书写在一个有 async 关键字的函数内
+ await 后面等待的必须是一个 Promise 对象
+ **本来应该在 then 内接受的结果, 可以直接定义变量接受**
+ 注意: await 只能等来成功的状态, 等不来失败的状态
解决失败的问题:
+ 使用 try catch 语法来实现
+ 语法:
try {
代码
} catch (err) {
代码
}
=> 如果 try 的 {} 内的代码没报错, 那么 catch 不执行
=> 如果 try 的 {} 内的代码报错了, 那么 不会报错, 而是执行 catch
*/
function getInfo() {
const p = new Promise(function (resolve, reject) {
const time = Math.floor(Math.random() * 3000) + 3500
setTimeout(() => {
if (time < 3500) {
resolve( time )
} else {
reject( '失败 : ' + time )
}
}, time)
})
return p
}
async function fn() {
let result = null
try {
// 如果这里成功, catch 不执行了
// 结果赋值给 result
result = await getInfo()
} catch (err) {
// 如果上面的代码失败了
// 会执行这里
result = err
// console.log(err);
}
console.log(result)
console.log('后续代码')
}
fn()