高级JS

118 阅读26分钟

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 叫做 顶级原型
*/

001.png

三.检测数据类型

 检测数据类型

  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)


二.不会销毁的空间

001.png

002.png

 /*
  不会销毁的空间
    + 当函数内返回了一个复杂数据类型
    + 并且外部有变量接受该数据的时候
    + 该空间不会被销毁

  再次想让他销毁
    + 只要让外部变量不在存储该地址即可
*/

function fn() {
  const obj = {
    name: 'Jack'
  }
  return obj
}

let res = fn()

console.log(res)

三.初识闭包

003.png 初识闭包
+ 概念: 函数里的函数
+ 私人:
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)

四.间接返回一个函数( 沙箱模式 )

004.png

/*
  间接返回一个函数( 沙箱模式 )
*/

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

六.函数 柯理化

函数柯理化:一种对封装函数的处理方式

005.png

 // 封装方案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

一.异步代码

/*
  异步代码
    + JSWEBAPI 提供的一些代码执行机制
      => 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()