es6 笔记

159 阅读53分钟

Es5 的变量声明问题

  • 允许重复的变量声明: 导致数据被覆盖

  • 变量提升: 怪异的数据访问,闭包问题

    • for (var i = 0;i < 10; i++ ) {
          setTimeout(() => {
              console.log(i)  // 输出 10
          },1000)
      }
      
    • 由于var会进行变量提升,导致i放入了全局环境,由于settimeout是异步执行,在触发执行的时候,i 都是使用的全局环境的这个i,此时i 已经变为10了
  • 全局变量挂载到全局对象造成全局对象成员污染

    • var 创建的变量或者函数 会将其放入全局作用域,
    • var 只有 函数作用域 和 全局作用域

es6的解决方案

  • let 声明的变量不会挂载到全局对象

  • let 声明的变量不允许在当前作用域重复声明

    • Let 在声明的时候会检查当前作用域下该变量名是否已经存在
  • 块级作用域概念

    • 块级作用域只有let 和 const有
    • 代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域
    • 在块级作用域内用let 定义的变量,在作用域外不能访问
    • let a = 123;
      {
        let a = 456
        console.log(a) // 456
      }
      console.log(a) // 123
      
  • 使用let 不会有变量提升,因此不能再定义let 之前使用

    • 在底层实现上,let也会有变量提升,但是在提升之后,会将其放入一块特殊的内存空间(暂时性死区),如果读取在该内存空间的变量,就会提示 Identifier 'xx' has already been declared
    • 当代码运行到该变量的声明语句时,会将其从暂时性死区移除
  • 在循环中声明的let会特殊处理

    • 在循环中,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域
    • 相当于每次循环,使用的都是一个全新的变量
    • 相当于每一次都是用的形参, 在块级作用域运行结束后销毁形参

const 和 let

  • const 和 let 完全相同,区别仅在于用const声明的变量必须在声明时赋值,并且后续不能重新赋值

  • 常量不可变

    • 是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变
    • const a = {
        name :"111"
      }
      a.name =  '222'  
      //const只能保证a 这个变量指向的地址不能被改变,并不能保证a指向的地址内容被改变
      
  • 变量的命名

    • 特殊的常量

      • 该常量从字面意义上,一定是不可变的,比如圆周率,月底距离或其他一些绝对不可能变化的配置
      • 该常量的名称全部使用大写,多个单词直接使用下划线拼接
      • const PI = 3.141
        const MOON_EARTH = 384403.9
        
    • 不能把握的常量就使用普通的命名就行

  • 在for 循环中,循环变量不可以使用常量

    • for in 里面是因为每次都是全新的常量

unicode支持

  • 在js中 获取字符串长度目前还是一个码元的长度,如果是出现出汉字(一些古汉字由两个码元组成)比较特殊,就会获取到的length就是2

    • 一个码元代表 16位2进制
  • 新增了对 码点 查询的api(一个码点等于两个码元)

    • codePointAt
  • 同时 es6 为正则新增了一个flag : u,如果添加了该配置则使用码点匹配

    • /^*$/u
      

字符串新增的api

在字符串原型属性上新增的api

  • includes

    • 判断字符串中是否包含指定的子字符串
    • '我是谁'.includes('我',1)
      // 第二个参数指定开始查找的位置
      
  • startswith

    • 是否以指定字符串开始
    • 也可以指定从哪里开始查找
  • endswith

    • 是否以指定字符串结尾
    • 也可以指定从哪里开始查找
  • repeat

    • 将字符串重复指定的次数后返回操作后的字符串

模版字符串

es6之前处理字符串繁琐的处理方式

  • 多行字符串

    • const str = '我\n是\n谁'
      
  • 字符串拼接

    • const str = '我' + '1' + '2'
      

es6模版字符串使用 ` 替代之前的引号,会保留字符串直接的空格,tab,换行

const str = `sss
    ccc
`

如果需要再字符串中拼接js 表达, 只需要在模版字符串中使用 ${}

  • 他会把表达式的结果直接放在模版字符串中

  • 表达式可以是任意有意义的数据

    • 可以是计算结果
    • 表达式是可以嵌套的
    • 也是可以 \n 这种转义符
    • 可以使用 \ $ 转义,使表达式成为字符串,不再能够进行
const test = 'cs'
// 换行处理
const str = `sss
    ccc${test}
`
const str1 = `  computed${1 + 2 * 3}` //使用表达式
const str2 = `moubam${`${1 + 2}`}` // 嵌套
const str2 = `${wenzi}`  //使用转义

模版字符串拓展用法

控制模版字符串结果,使用标记

  • 标记是一个函数

    • 参数1: 被插值分割的字符串数组,末尾有一个空字符串
    • 后续参数(数组: 所有的插值
// 这个时候相当于使用一个方法解析模版字符串
const text = myTest`ceshi${1},还不错${2}`
// 会被转换成类似以下代码
// text = myTest(['cheshi',',还不错'],1,2)
//定义解析字符串的方法
function myTest(parts) {
  let str = ''
  // 获取模版字符串中表达式结果
  const values = Array.prototype.slice.apply(arguments).slice(1)
  for(let i = 0;i < values.length;i++) {
    // 可以在这里修改拼接结果
    str += parts[i] + values[i]
    if(i === values.length - 1) {
      str += parts[i + 1]
    }
  }
  return str
}
//如果方法没有写返回值 得到就是undefind
console.log(text)  //undefind

es6提供了一些内置的标记

let text = String.raw`abc\t\nbdc`
// 这个标记会让字符串模版不再有转义的能力

通过自己实现一个标记方法,删除用户输入的 html标记(删除< > )

const input = safe`<h1>input</h1>`;
​
function safe(parts) {
  let str = "";
  const values = Array.prototype.slice.apply(arguments).slice(1);
  for (let i = 0; i < parts.length; i++) {
    // 输入信息转换 表达式和输入内容都转换
    const _v = parts[i].replace(/(</)|(<)/g, "&lt;").replace(/>/g, "&rg;");
    const _v1 = values[i]?.replace(/(</)|(<)/g, "&lt;")?.replace(/>/g, "&rg;");
    str += (_v || "") + (_v1 || "");
    if (i === values.length - 1) {
      str += parts[i + 1] || "";
    }
  }
  return str;
}
​
console.log(input);

es6函数参数默认值

  • 书写形参时,直接给形参赋值,赋的值即为默认值

    • function getContainer (a = 1) {}
      
  • 在调用函数时,如果没有传入时是undefined时 就会使用默认值

    • function getContainer (a = 1) {
        console.log(a) // 1
      }
      getContainer()
      
  • 如果是null的不会使用默认值

    • function getContainer (a = 1) {
        console.log(a) // null
      }
      getContainer(null)
      
  • 可以使用表达式函数形参默认值

    • function getContainer () {
        return document.getElementById('container')
      }
      ​
      //可以使用函数 
      //可以直接写 document.getElementById('container')
      function createElement(name = 'div',container = getContainer(),content = '') {
        const ele = document.createElement(name) 
        if(content) {
          ele.innerHTML = content
        }
        container.appendChild(ele)
      }
      ​
      creatElement(undefind,undefind,'测试')
      

对 arguments 的影响

  • 在默认情况下,arguments 和形参 是指向的同一片地址(浅拷贝)
  • 在严格模式下,arguments 和形参 是两块不同的地址( 深拷贝)
  • 如果函数使用了参数默认值,该函数会自动变成严格模式下的模式,形成深拷贝

暂时性死区

  • 形参和 let 或者const 声明一样,具有作用域并且根据参数的声明顺序,存在暂时性死区

    • // b 使用a 做为默认值,因为运行到函数时a 
      // 是先创建所有有了具体的值,b 就可以直接使用a作为默认值
      function test(a, b = a) {}
      //报错 因为b还没有创建 存在暂时性死区里面不能使用
      function test1(a = b,a) {} 
      //报错 自定义的参数不能和变量重名
      function test2(a) {
        let a = 1;
      } 
      test(1)
      

函数剩余参数

使用arguments的缺陷

  • 如果和形参配合使用,容易导致混乱
  • 从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图
  • 在上面写了默认参数和arguments的影响关系(浅拷贝和深拷贝)

剩余参数的功能

  • 专门用于收集末尾的所有参数,将其放置到一个形参数组中

  • function test(...args) {}
    // args 收集了所有参数,形成了一个数组
    
  • 细节部分

    • 一个函数,仅能出现一个剩余参数
    • 一个函数如果有剩余参数必须是最后一个参数

展开运算符

  • 函数传参展开, 可以把数组拆分成单个的参数做为实参传递进函数

    • const arr = [1,2,3,4]
      function test(...args) {}
      test(...arr) 
      
  • 数组克隆,存在对象时也是浅拷贝

    • const arr1 = [2,3,4,5]
      const arr2 = [...arr1,'2','3'] 
      
    • 可以在克隆的基础上,新增属性
  • 对象展开,对象克隆(浅拷贝)

    • const obj1 = {  name :"222",age:11 }
      const obj2 = { ...obj1,name:"333" } // 克隆并覆盖相同属性
      // 相当于是将旧的对象的指向赋值给了新的对象
      
    • const obj1 = {  name :"222",age:11,address: { test:1} }
      // 通过新建属性 然后在新的属性里面使用展开对象 实现深拷贝
      const obj2 = {
        ...obj1,
        address:{
          ...obj1.adress
        }
      }
      

展开运算符练习

// 实现接收到指定参数长度后才执行的函数
function cal(a,b,c,d) {
  return a + b * c - d
}
function curry(func,...args) {
  return function (...subArgs) {
    // 合并开始传入的参数,和后面调用传入的参数
    const allArgus = [...args,...subArgs]
    // 判断参数是否达到函数形参要求
    if(allArgus.length >= func.length) {
      return func(...allArgus)
    }else {
      // 没有的话 继续接收参数
      return curry(func,...allArgus)
    }
  }
}
const newCal = curry(call,1,2)
newCal(3,5)
newCal(7,8)
const newCal2 = newCal(9)
newCal2(8)

明确函数的双重用途

  • 在es6之前,使用普通函数会存在二义性,可以是构造函数也可以是普通函数

    • function Person() {
        if(!(this instanceof Proson)) {
          new thorr('错误使用')
        }
      }
      
    • 通过判断this的原型是否是当前构造函数可以判断当前执行的操作,但是可以通过call等方式绕过
    • const user = Person.call(new Person)
      
  • 在es6提供了api 检测调用方式

    • function Person() {
        // 在没有使用new的情况下 都是返回的undefined
         if(!new.target) {
            new thorr('错误使用')
         }
      }
      

箭头函数

this指向(普通函数)

  • 通过对象调用函数,this指向对象
  • 直接调用函数,this指向window对象
  • 如果通过new调用函数,this指向新创建的对象
  • 如果通过apply等调用,this指向指定的数据
  • 如果是dom事件函数,this指向事件源
const obj = {
  count:0,
  start:function () {
    setInterVal(function() {
      this.count++;
      console.log(this.count) // NaN
    },1000)
  },
  regEvent:function () {
    window.onClick = function () {
       console.log(this.count); // undefined
    }
  }
}
obj.start();
// 老的解决方法 使用闭包的形式,存储执行环境的this
const _this = this
console.log(_this)

箭头函数是什么

  • 箭头函数是一个表达式,理论上,任何使用函数表达式的场景都可以使用箭头函数
  • (参数) => {  
      //函数体
    }
    

注意细节

  • 箭头函数的this指向,取决于箭头函数定义的位置的this指向,而与如何调用无关

    • const obj = {
        count:0,
        start:function () {
          // 改为箭头函数调用 此时创建在对象内部 this指向obj
          setInterVal(() =>  {
            this.count++;
            console.log(this.count) // 0 1 2
          },1000)
        },
      }
      obj.start();
      
  • 使用语法

    • // 如果参数只有一个,可以省略小括号
      const todo = data => {}
      // 没有参数的情况下,不能省略小括号
      const todo = () => {}
      // 如果箭头函数只有一条返回语句,可以省略大括号和return关键字
      const todo = a => a > 2
      // 箭头函数返回表达式,简化代码
      const sun = (a,b) => ({ a,b,sum:a + b})  
      // 如果写{} 是不能被识别的,会被当做函数执行体
      // 使用()包裹就把{}包装成一个表达式,箭头函数会把表达式的结果返回,此时就是一个对象
      
  • 箭头函数中,没有自己的 this arguments new.target

    • 如果使用了,则使用的是函数外层对应的 this arguments new.target
    • 多层箭头函数嵌套,最里面的函数使用了this,依然是最外层箭头函数之外的this
    • const obj = {
        say:function () {
           const todo =  () => {
              const test = () => {
                console.log(this) // obj 
              }
              test()
           }
           todo()
        }
      }
      obj.say()
      
  • 箭头函数没有原型

  • 箭头函数不能作为构造函数使用

新增对象字面量语法

  • 如果对象字面量初始化时.成员的名称来自于一个变量并且和变量的名称相同,则可以简写

    • const createObj = (name,user) => {
        const say = () => 'xxx'
        return {
          name,
          user,
          say
        }
      }
      
    • 实际上是语法糖,将对象的变量名作为key,变量值作为value
  • 字面量对象初始化时,对象的方法可以省略冒号和function关键字

    • const obj  = {
        name:"222",
      	say() {
        		console.log(this.name) //222
        }
      }
      obj.say()
      
  • 计算属性名

    • 有的时候初始化对象时,某些属性名,可能来自于某个表达式的值,
    • 在es6中可以使用 [] 来表示该属性是通过计算得到的
    • const a = 'name'
      const obj = {
      	[a] :'test'  
      }
      

es6中Object新增的api

  • Object.is

    • 用于判断两个数据是否相等,基本上跟严格相等是一致的,除了一下两点

      • NaN 和 NaN 相等 ( === 的结果是 false)
      • +0 和 -0 不相等 ( === 结果是 true )
  • Object.assign

    • 将对象的属性进行合并,同属性名会被覆盖
    • const obj1 = {name  :"111"}
      const obj2 = {age :22}
      const obj3 = Object.assign(obj1,obj2) 
      // 上面操作会将 obj1 和 obj2 的属性合并之后返回成新的对象
      // 但是此次操作的副作用 会影响obj1(也被修改),就会出现 obj1 == obj3
      // 第二个参数不会被修改,只有第一个参数会被修改
      //解决方法
      const obj4 = Object.assign({},obj1,obj2) 
      
  • Object.getOwnPropertyNames 的枚举顺序(之前就存在,顺序没有规定)

    • 获取对象的所有属性,不会返回原型链上的属性

    • 获取属性的顺序完全由浏览器厂商决定

    • 在es6统一了排序顺序如下

      • 先排数字,再排其他
      • 数字会进行升序排序,字母按书写顺序排
  • Object.setPrototypeOf

    • 该函数用于设置某个对象的隐式原型
    • Object.setPrototypeOf(obj1,obj2)
      // 相当于
      obj1.__proto__ = obj2
      

面向对象简介

面向对象是一种编程思想,跟具体的语言无关

对比面向过程:

  • 面向过程: 思考的切入点是功能的步骤

    • const collectMonkey = () => {}
      collectMonkey()
      const feedForMonkey = () => {}
      feedForMonkey()
      
    • 将每一件事情当成一个独立的过程,一件件的处理
  • 面向对象: 思考的切入点是对象的划分

    • const monkey  = function (){}
      const zookeeper = function (){}
      zookeeper.prototype.collectMonkey = () => {}
      zookeeper.prototype.feedForMonkey = () => {}
      
    • 将猴子和管理员分为两个个体,由个体单独触发

构造函数的语法糖

es5的构造函数

  • 会出现构造函数和原型方法之间存在别的代码,维护困难

    • function Animals(name,sex) {
        this.name = name
        this.sex = sex
      }
      console.log('aaa')
      //通过原型的形式新增方法
      Animals.prototype.say = function () {
        console.log(this.name)
      }
      
  • 原型成员可以被枚举

    • const a = new Animals('cscs',111)
      for(const prop in a) {
        console.log(prop)   //会出现say方法
      }
      
  • 默认情况下,构造函数仍然可以被当做普通函数使用

    • Animals()
      // 直接调用构造函数
      

在面向对象编程思想中,将 一个对象的所有的成员的定义,统称为类

es6 构造函数

class Animal {
  constructor(name,age) {
		this.name = name
    this.age = age
  }
  say() {
    console.log(this.name)
  }
}
const a = new Animals('cscs',111)

类的特点

  • 类声明不会被提升,和 const 和 let 一样,存在暂时性死区

  • 类中的所有代码均在严格模式下执行

  • 类的所有方法都是不可枚举的

    • 老版本需要通过 Object.defineProperty 控制 wireable
  • 类的所有方法都无法被当做构造函数使用

    • class Person() {
        test () {} 
      }
      const user = new Person()
      new user.test // 报错
      
  • 类的构造器必须使用 new 来调用

  • 写在 类里面的方法会被自动放到原型上

    • 普通函数,箭头函数会放在实例上(下面有代码解释)
  • 不是this绑定的属性 是不会被枚举出来的( 方法 )

类的其他语法

  • 类型的成员名可以通过计算获取

    • const name = 'name'
      class Person{
        [name] () {}
      }
      const user = new Person()
      user[name]()
      
  • getter 和 setter

    • es5的处理方法

      • Object.defineProperty(this,'age',{
          set() {},
          get() {}
        })
        
    • 新的处理方式

      • class Person {
          // 创建一个age属性,并且给它加上getter,读取该属性时,会运行该函数
          get age() {
            return this._age + '岁'
          }
           // 创建一个age属性,并且给它加上setter,给该属性复制时,会运行该函数
          set age(age) {
            if(typeof age !== 'number') {
              throw new TypeError('类型错误')
            }
            if(age < 0) {
              age = 0
            }else if (age > 1000) {
              age = 1000
            }
            this._age = age
          }
        }
        
      • 使用 get 和 set 属性控制的方式,不是在原型上,是在实例上
  • 静态成员

    • 构造函数本身的成员
    • class Person {}
      Person.test = '222'// 等于下面写法
      class Person {
          static test = '222'
      }
      
  • 字段初始化语法(es7)

    • class Person {
        constructor() {
          // 需要手动往实例加属性
          	this.a = 1
        }
      }
      // 可以使用下面写法简写,
      // 实际上最终都会在创建时转换成上面代码
      class Person {
        // 直接创建字面量属性给实例
        a = 1
      }
      
    • 使用 static 的字段初始化器,添加的是静态成员,会往构造函数本身上新增属性

      • class Person() {
          static test = '22'
        }
        Person.test // '222'
        
    • 没有使用 static 的字段初始化器,会往实例 上加属性

      • class Person() {
          test = '22' 
          //等于以下写法
        }
        Person.test // undefined
        const user = new Person()
        user.test  // '22'
        
    • 箭头函数使用字段初始化器时,会往实例上加属性,this 指向实例

      • class Person() {
           say = () => {}
        }
        // 以上代码相当于简写会被转换为
        class Person {
          constructor() {
            	this.say = () => {}
          }
        }
        const user = new Person();
        const user1 = new Person();
        console.log(user.say === user1.say);  // false
        
    • 如果使用普通函数创建,因为是放在原型上,所有实例的该方法都是相等的,和箭头函数有区别

      • class Person {
          say() {
            console.log(1);
          }
        }
        
        const user = new Person();
        const user1 = new Person();
        
        console.log(user.say === user1.say);  // true
        
  • 装饰器

    • 通过增加标注的形式,修改绑定的方法或者属性或者class
    • class Test {
        @Obsolete
        print() {
          console.log("print方法");
        }
      }
      ​
      function Obsolete(target, methodName, descriptor) {
        // function Test
        // print
        // { value: function print() {}, ...}  // 跟Object.defineProperty 的第三个参数一样
        const oldFunc = descriptor.value; // 保存旧的方法
        //产生一个新方法去调用原来的方法,然后给一个提示
        descriptor.value = function (...args) {
          console.warn(`${methodName} 方法已经过时`);
          oldFunc.apply(this, args);
        };
      }
      
    • // 最新ts实现装饰器
      class Test {
        @Obsolete
        print() {
          console.log("print方法");
        }
      }
      ​
      function Obsolete(originalMethod: any, context: any) {
        console.log("🚀 ~ Obsolete ~ originalMethod:", originalMethod);
        console.log("🚀 ~ Obsolete ~ context:", context);
        const methodName = String(context.name);
        function test(...args: any[]) {
          console.warn(`${methodName} 方法已经过时`);
          originalMethod.apply(this, args);
        }
      ​
        return test;
      }
      ​
      const user = new Test();
      user.print();
      ​
      

类的继承

如果两个类 A 和 B,如果可以描述为B 是A(B 是 A 的扩展集,B 拥有A 里面的所有信息), 则A和B形成继承关系,比如说 猫(B) => 动物 (A)

继承关系专业术语(面向对象) (B 是 A的扩展集,是子类的情况下)

  • B 继承自 A
  • A 派生 B
  • B 是 A 的子类
  • A 是 B 的父类

如果 A 是 B 的父类,B 会自动拥有 A 的所有实例成员

es5实现继承

function Animal(type,name,sex) {
  this.type = type
  this.name = name
  this.sex = sex
}
Animal.prototype.say = function () {  console.log(this.name) }
// 修改 Dog的隐式原型指向 不然拿不到Animal的原型属性
Object.setPrototypeOf(Dog.prototype,Animal.prototype)
function Dog(name,sex) {
  Animal.call(this,'狗',name,sex)
}

es6 实现继承

使用新的关键字:

  • extends

    • 继承,用于类的定义
  • super

    • 直接当做函数调用,表示父类构造函数,在表现上相当于使用call 调用了父类的构造函数
    • 如果当做对象使用,则表示父类的原型

特殊注意

  • es6要求,如果定义了constructor,并且该类是子类(使用了extends),则必须在constructor的第一行手动调用父类的构造函数(调用super)
  • 如果不写constructor的话,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用
class Animal {
	constructor(type,name,age,sex) {
    this.type = type
    this.name = name
    this.age = age
    this.sex = sex
  }
  print() {
     console.log(this.age,this.type,this.sex,this.name)
  }
}
class Dog extends Animal {
  constructor(name,age,sex) {
    super('犬',name,age,sex)  // 有点像使用call调用父类一样
    this.abc = 123  // 子类特有的属性
  }
  print() {
    	super.print() // 调用父类原型的方法
    	console.log(this.abc)  // 打印自己新增的属性
  }
  // 子类原型属性
  jiao () {
    console.log('aaa')
  }
}

类的冷知识

js制作抽象类

  • 抽象类,一般是父类,不能通过该类创建对象
  • 抽象类对一个事/物的高度抽象,例如动物,人类,他不能具体到某一个实际上的东西
  • 所以在概念上是不能直接进行实例化的,需要进行继承指明所对应的分类或者目标
  • class Animal {
      constructor(type) {
        // 通过判断构造器对象来确定来源,非自己才可以调用
        if(new.target == 'Animal') {
          thorw new TypeError('你不能直接创建Animal对象')
          return
        }
        this.type = type
      }
    }
    // 不能这样直接写
    const animal = new Animal()
    

this 的指向

  • 正常情况下,this的指向,始终指向实例
  • 使用super 时this也是执行当前实例

解构

对象解构

  • 使用es6的一种语法规则,将对象或者数组的某个属性提取到某个变量中
  • 解构不会对原始影响对象,只是读取对象的属性值
  • 不能从null 或者 undefined 解构,会报错

es5处理对象属性

const obj = {
  name:"222",
  age:11
}
const name = obj.name
const age = obj.age

解构处理

let name,age;
// 对象解构 会根据左边的变量名去对象里面查找,然后进行赋值操作
// 需要使用小括号包裹,不然会被当做 { name,age } 和  = obj  
// 右边的赋值操作就会报错,会把左边当做对象创建
({name,age} = obj) 
//--------------
// 简化声明和取值 实际上跟上面执行一致
// 先定义4个变量,然后从对象中读取同名属性,放到变量中
// 使用const 的话 定义的变量就是不可变的
const {age,name} = obj
  • 在解构中使用默认值

    • 在解构没有找到值的时候,会是undefined(因为是先定义然后查找,定义之后默认就是undefined)
    • const {title} = obj
      console.log(title) // undefined
      // 设置默认值,拿不到同名属性的时候就使用默认值
      const {type = 1} = obj
      console.log(type) // 1
      
  • 非同名属性解构

    • 定义的变量和获取的对象属性名不一致
    • const user = {
        sex:"男",
        name:"测试"
      }
      const {name,sex:gender} = user
      // 先定义两个变量, name  gender
      // 再从对象user里面读取同名属性赋值,(其中gender读取的sex属性)
      
  • 使用默认值加非同名属性(null 的情况不会取默认值)

    • const {name,sex:gender = '男'} = user
      
  • 通过解构读取多层数据

    • const user = {
        name:"测试",
        address:{
          province:"四川"
        }
      }
      // 解构出user 中的name,province
      // 定义 name 和 province
      // 此时address 是不能用的,这里结构拿的是 user.address.province
      const {name,address:{ province }} = user
      //如果需要address
      const { name,address, address: { province }} = user;
      

数组解构

在js中数组本质上也是对象,支持key是连续数字的对象而已

// 通过下标修改名称解构数组
const arr = [1,2,3,4]
const {0:n1,1:n2} = arr
console.log(n1,n2) // 1  2

es6 对数组解构提供了单独的语法糖(按顺序取值,与变量名无关)

const arr = [1,2,3,4]
let name,age;
// 按数组顺序取值,此时跟变量名无关了
([name,age] = arr) 
//简化写法
let [name,age] = arr
  • 跳过下标取值

    • // 需要跳过的部分使用不传入变量即可
      // 取不到的话就是undefined
      // 可以使用默认值
      let [n1,,n2,n3 = 1] = arr
      
  • 数组嵌套取值

    • const numbers = ["a", "b", "c", "d", [1, 2, 3, 4]];
      // 得到numbers下标为4的数组中的下标为2的数组,放到变量n中
      const [, , , , [, , n]] = numbers;
      console.log(n); //3
      
  • 数组嵌套对象取值

    • const numbers = ["a", "b", "c", "d", { a: 1, b: 2 }];
      const [, , , , { a }] = numbers;
      console.log(a); //1
      

解构剩余参数

  • 剩余项的解构放在最后面,
// 提取name属性 ,将user里面剩余参数全部放入 obj 里面
const {name ,...obj} = user
// 将数组前两项放入n1 n2,剩余数据放入 arr2 里面
const [n1,n2,...arr2] = arr

通过解构交换变量

const user = {x: 1,y: 22,};
const { x: y, y: x } = user;
console.log(x, y);  // 22 1
//-------
let a = 1,b =2 
[b,a] = [a,b]
console.log(a,b) // 2 1

函数参数解构

// 由于从undefined 或者 null 解构会报错
// 这个时候需要设置参数默认值,让解构发生在安全的情况下
function todo({name = '111',age} = {}) {
	console.log(name,age)
}
todo({name:"11",age:222})

普通符号(symbol)

  • 符号是es6新增的数据类型,它通过使用函数 Symbol 来创建

  • 符号的设计初衷是为了对象设置私有属性

    • 私有属性的含义是只能在对象内部使用,外面无法使用
// 函数的参数是对这个符号的描述信息
const syb1 = Symbol()
const syb2 = Symbol('aa')
// 描述信息可以写中文
const syb3 = Symbol('这是描述')
// syb1  =>  Symbol()
// syb2  =>  Symbol(aa)

符号的特点

  • 符号没有字面量

  • 使用typeof 得到的结果 就是 "symbol"

  • 每一次调用Symbol 函数得到的结果永远是不相等的(哪怕描述信息一样),(符号是永远唯一)

    • 类似于重新开辟内存地址)
  • 符号可以作为对象的属性名存在,这种属性称为符号属性(以往对象的属性名一定是字符串,数组也不例外)

    • const syb1 = Symbol('属性名')
      const obj = {
        a:1,
        [syb1]:3
      }
      
    • 可以通过符号属性实现私有属性不能被外界访问(常规方式)

      • const hero = (function () {
          // 由于是局部变量的形式 且是一个symbol 变量
          // 内部可以通过变量访问,外部拿不到这个唯一值,无法访问
          const getRandom = Symbol();
          return {
            name: "222",
            [getRandom](min, max) {
              return Math.random() * (max - min) + min;
            },
          };
        })();
        
    • 符号属性不能被枚举,(for in 拿不到) (Object.keys 也拿不到)

      • const syb = Symbol()
        const obj = {
          a:1,
          [syb]:2
        }
        for(const prop in obj) {
         	console.log(prop) // 1 
        }
        
    • Object.getOwnPropertyNames() 可以枚举一些无法被枚举的特殊值,但是也不能枚举符号属性

    • es6 新增的了Object.getOwnPropertySymbols() 可以读取到符号属性

      • 返回了 符号属性的数组集合
  • 符号无法被隐式转换,不能进行数学运算

    • const syb = Symbol()
      syb + 1 // 报错
      
  • 符号可以进行显示的转换成字符串的操作

    • 使用console之所以能输出,就是系统做了显示转换
    • const syb = Symbol()
      console.log(String(syb))
      

共享符号

根据符号名称(符号描述)能够得到同一个符号

// 是 Symbol.for 创建的符号和Symbol不一样,符号名一致的时候,创建的一致
const syb = Symbol.for('符号描述/符号名')
//
const syb1 = Symbol.for('测试')
const syb2 = Symbol.for('测试')
syb1 === syb2  //true

通过共享符号暴漏属性给外部

const obj = {
	a:1,
  [Symbol.for('c')]:3
}
console.log(obj[Symbol.for('c')])

模拟Symbol.for 实现

const SymbolFor = (() => {
  const global = {}
  return function (name) {
    	if(!global[name]) {
					global[name] = Symbol(name)
      }
      return global[name]
  }
})()

知名(公共,具名)符号

  • 知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到

  • Es6 使用知名符号 暴露了某些场景的内部实现

    • 可以通过修改这些具名符号的实现,影响js一些api的实现
  • js对外暴露的一些Symbol符号

Symbol.hasInstance

  • 该符号用于定义构造函数的静态成员,它将影响 instanceof 的判断 ( 默认判断原型 )
  • // instanceof 的调用,现在可以通过修改具名符号修改计算结果
    // instanceof 调用就是触发调用方的api
    function A() {}
    const obj = new A();
    console.log(obj instanceof A) // true
    // 本质上触发的是 A[Symbol.hasInstance] 方法
    // 修改hasInstance 的返回结果,只能通过Object.defineProperty
    Object.defineProperty(A, Symbol.hasInstance, {
      value: function () {
        return false;
      },
    });
    console.log(obj instanceof A); //false
    console.log(A[Symbol.hasInstance](obj)); // false
    

Symbol.isConcatSpreadable

  • 会影响数组的concat方法
  • const arr = [12,3,45]
    const arr1 = arr.concat(0,[1,2,3,4])
    // arr1 => [12, 3, 45, 0, 1, 2, 3, 4]
    arr[Symbol.isConcatSpreadable] = false
    const arr3 = arr.concat(0,[1,2,3,4])
    //arr2 => [[12,3,45], 0, 1, 2, 3, 4]
    
  • // 对 对象的处理
    const arr = [12]
    const obj = {
      0:3,
      1:4,
      length:2,
       [Symbol.isConcatSpreadable]: true, //设置后分割 默认会把整个对象丢进去
    }
    const arr3 = arr.concat(2,obj)
    

Symbol.toPrimitive

  • 影响类型转换的结果
  • const obj = { a : 1,b : 2}
    // 会先调对象的 valueOf 然后调用 toString() 最后在运算
    console.log(obj + 123)
    
  • // 接收 默认转换行为  默认是default 后续接收的是 目标类型 
    obj[Symbol.toPrimitive] = function(type) {
      return 2
    }
    // 函数进行转换时会调用上面方法
    console.log(obj * 2) // 4
    
  • class Temperature {
      constructor(degree) {
        this.degree = degree;
      }
      [Symbol.toPrimitive](type) {
        if (type == "default") {
          return this.degree + "摄氏度";
        } else if (type === "number") {
          return this.degree;
        } else if (type == "string") {
          return this.degree + "摄氏度";
        }
      }
    }
    const temp = new Temperature(30);
    console.log(temp + "!");
    console.log(temp + 30);
    console.log(String(temp));
    

Symbol.toStringTag

  • 会影响 Object.prototype.toString 的返回值
  • const obj = {
      [Symbol.toStringTag]: "测试",
    };
    console.log(Object.prototype.toString.apply(obj)); //[object 测试]
    

Promise

  • 目的是解决es5的回调地狱问题(使用链式编程解决)
  • 规范是在Api出来之前就产生了,后续在es6实现了Promise api,完成了Promise A+在js的实践
// 接收回调函数执行
const todo = (onSuccess, onFail) => {
  if (Math.random() > 0.3) {
    onSuccess();
  } else {
    onFail();
  }
};
// 在执行失败后继续执行 直到全部执行失败或者执行成功
todo(
  () => {
    // 成功操作
    console.log("成功");
  },
  () => {
    // 失败后继续执行
    todo(
      () => {
        console.log("成功2");
      },
      () => {
        // 失败后继续执行
        console.log("失败2");
      }
    );
  }
);

Promise规范

  • Promise 是一套专门处理异步场景的规范,可以有效的避免回调地狱的产生,使异步代码更加清晰简洁统一
  • 这套规范的名称为 Promise A+

Promise A+ 规定

  • 所有的异步场景,都可以看做是一个异步任务,每个异步任务在 js 中应该表现为一个对象,该对象称之为Promise对象,也叫做任务对象

    • 当前拿不到结果的任务,例如 定时器,网络请求, 需要等到结果的任务
  • 每个任务对象,都应该有两个阶段,三个状态

    • 未决阶段 (unsettled), 还没有确切的结果正在等待

      • pending (挂起)
    • 已决阶段 ( settled ), 产生了结果,可以进行下一步处理

      • fulfilled (完成)
      • rejected (失败)
    • 它们之间存在的逻辑

      • 一个任务总是从 未决 阶段变成 已决 阶段, 无法逆行
      • 一个任务总是 从 挂起 状态变成 完成或者 失败 状态,无法逆行
      • 任务一旦完成或者失败,状态就固定了,无法再次改变
  • 挂起到完成我们称之为 resolve

    • 挂载到失败我们称之为 reject
    • 任务完成时可能有一个相关数据
    • 任务失败时可能有一个失败原因
  • 可以针对任务进行后续处理,针对完成状态的后续处理称之为 onFulfilled ,针对失败的后续处理称之为 onRejected

  • promise对象必须要有一个then方法

总结:

  • Promise A+ 认为当前直接拿不到结果的操作都可以是作为异步任务,Promise可以作为在异步任务js中具体表现
  • Promise 第一阶段是状态扭转,从未决阶段到已决阶段,同时状态从pending 到 fulfilled / rejected
  • Promise 第二阶段 是对状态扭转后的后续处理, 触发 onFulfilled / onRejected

Promise API

Promise 构造函数如果在状态变更之前出现内部错误, 这个时候状态会自动变更为 失败

// Promise 构造函数 接收一个函数作为参数(必填)不然会报错
// Promise 构造函数的结果是一个 promise 对象
// 执行该构造函数时 状态默认会为pending
const pro = new Promise(() => {})
​
// 函数用来描述任务执行过程 在构造函数执行时会立即执行
// 函数会接收两个参数,可以扭转promise的状态
// resovle 把状态 从 挂起变为 完成
// reject  把状态 从 挂起变为 失败
// Promise 构造函数如果在状态变更之前出现内部错误, 这个时候状态会自动变更为 失败
const pro1 = new Promise((resolve, reject) => {
  console.log("开始跑马拉松");
  const duration = Math.floor(Math.random() * 3000);
  setTimeout(() => {
    if (Math.random() < 0.5) {
      resolve("我跑完了");
    } else {
      reject("脚伤了");
    }
  }, duration);
});
console.log(pro) // Promise {<pending>}// 拿到promise返回值,调用 then 方法执行下一步( onFulfilled,onRejected )
// then 接收两个函数作为参数
// 参数一函数的参数 接收成功状态的数据 (resolve 里面的内容,如果没有就是空)
// 参数二函数的参数 接收失败状态的数据 (reject 里面的内容,如果没有就是空)
// 成功/失败 会执行对应的函数,另一个不会执行
pro1.then(res =>  { console.log(res) },reson => { console.log(reson) })
// 只处理失败 成功函数传null
pro1.then(null,reson => { console.log(reson) })

Promise 的构造函数

  • 当触发状态变更操作时不会终止构造函数执行(也就是说仅是执行了Promise的状态变更)

  • 而是要等所有的代码执行完毕后(状态变更不会终止)才会执行对应的then函数,注册微任务等待执行

  • 也就是说先执行完函数体代码后,里面执行执行then函数,如果有处理函数就放入到微任务等待执行

    • new Promise((res) => {
        console.log(1);
        res(2);
        console.log(3);
      }).then(res => {
        console.log(res)
      })
      // 1 3 2
      
  • 当所有的代码执行完毕之后才会有返回值

    • let a ;
      a = new Promise((res) => {
        console.log(a) // undefined
      })
      console.log(a) // Promise { <pending> }
      

Promise 的 catch 方法

pro1.then(null,reson => { console.log(reson) })
// 等同于
pro1.catch(reson => { console.log(reson) })

Promise 链式调用

  • then 方法必定返回一个新的Promise(用于处理 pormise 后续操作)

  • catch 方法必定返回一个新的 Promise(用于处理失败的promise)

    • 也就是说 后续处理(then的返回值)也是一个 promise 任务
    • const pr1 = new Promise(res =>{
        res()
      })
      // 得到then的结果
      const pr2 = pr1.then(res =>{ console.log('任务2') }).catch(err => console.log(err))
      console.log(pr2) // Promise{<pending>} 此时是同步任务 拿不到变更后的状态
      setTimeout(()=>{
        // 延迟获取
        console.log(pr2) // Promise{<fulfilled>}
      })
      
  • 新任务的状态取决于后续处理

    • 若没有相关的后续处理,新任务的状态和前任务一致, 数据为前任务的数据

      • 也就是说,then 没有写对应的resolve 或者 reject 的处理函数,状态就是调用then的promise的状态和响应值

        • // 当 promise 变为 成功 状态之后,没有做 then 的成功操作
          // 那么 then 的返回值(promise状态)就是上一个的状态 
          // 数据也是上一个完成传递的数据
          const pr1 = new Promise(res =>{
            res(1)
          })
          const pr2 = pr1.catch(err => {})
          setTimeout(() => { console.log(pr2) },1000) // Promise {<fulfilled>: 1}
          
        • // 反过来也是同样的道理 ,promise 变为 失败 的状态之后,没有做失败的处理
          const pr1 = new Promise((res,rej) =>{
            rej(1)
          })
          const pr2 = pr1.then(() => { })
          setTimeout(() => { console.log(pr2) },1000) // Promise {<rejected>: 1}
          
      • 如果传的不是函数会直接跳过 进入下一个then,相当于没写处理函数保留上一步的promise状态和数据

        • // 如果传的不是函数 相当于就是处理函数传了一个null ,就直接使用上一步的Promise状态
          Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
          // 1
          
    • 若有后续处理但还未执行,新任务挂起

      • 也就是说执行调用then的时候,then的调用方的promise任务还没有结果(没有发生状态变更),这个时候then返回的promise也会跟着一起等结果(状态变更)
      • // promise 在 2000 后才有结果,then这个时候做了后续处理
        // 但是接收的promise 状态还没有变更,也就只能跟着一起等
        const pr1 = new Promise((res) =>{
           setTimeout(() => {
              res(1)
           },2000)
        })
        const pr2 = pr1.then(() => { })
        setTimeout(() => { console.log(pr2) },1000) // Promise {<pending>}
        // 这个时候能拿到 状态已经变更
        setTimeout(() => { console.log(pr2) },4000) // Promise {<fulfilled>: undefined}
        
    • 若后续处理执行了,则根据后续处理的情况确定新任务的状态(看then 的执行情况,报错或者不报错)

      • 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值

        • 假如then里面处理resolve的函数执行成功,那么then返回promise的状态就是成功.
        • 而这个promise的返回值就是resolve执行函数的return数据
        • const pr1 = new Promise((res) =>{
            res()
          })
          // 执行没有报错 状态就是 pr2 promise 状态就是成功
          // pr2 的promise 的 响应值 就是 then的成功函数的 return 结果
          const pr2 = pr1.then(() => { })
          const pr3 = pr1.then(() => { return 100 })
          // pr2 Promise{<fulfilled> : undefind}
          // pr2 Promise{<fulfilled> : 100}
          //------------
          // 起始状态是 失败
          // then 处理失败了之后  没有发生错误 返回的就是成功
          const pr1 = new Promise((res,rej) =>{
            rej()
          })
          const pr2 = pr1.catch(() => { 
            return 123 
          })
          //pr2 Promise {<fulfilled>: 123}
          
      • 后续处理执行有错,新任务的状态为失败,数据为异常对象

        • 和上面处理成功的情况类似,如果处理成功的函数执行失败,那么then返回promise 状态就是 失败
        • 而返回的promise的响应值就是 异常对象
        • const pr1 = new Promise((res) =>{
            res()
          })
          // 在后续执行出现错误
          const pr2 = pr1.then(() => { 
            // 模拟出现错误
            throw 123 
          })
          //pr2 Promise {<rejected>: 123}
          
      • 后续执行后返回的是一个新的任务对象,新任务的状态和数据与该任务对象一致

        • then里面如果返回了新的promise对象,then的返回值取决于新的promise 的返回值(状态和数据)
        • const pr1 = new Promise((res) =>{
            res()
          })
          // 此时 跟新的 promise 的状态绑定
          const pr2 = pr1.then(() => { 
            // 此时没有变更状态 那么就是pending
            // 如果变更了状态 那么pr2状态就是变更后的状态
            return new Promise((res) =>{})
          })
          // pr2  Promise {<pending>}
          

总结

  • 调用then返回就是一个promise,但是返回的promise状态和数据有几种情况

    • 如果调用then的时候 ,then的调用方此时还是 pending 状态,那么then会先返回 一个promise,状态为pending,

      • 当then的调用方状态发生变更后才会执行then里面的处理函数,此时then的返回值就要看以下处理情况
    • 没有写处理对应状态的函数,那么使用then的调用放的状态和数据

    • 写了处理函数

      • 处理函数内部执行错误,那么返回的就是 失败 状态,数据就是 异常对象

        • promise的静态方法接收的promise数组中,如此存在方法执行报错,也会立即终止执行,返回失败的promise状态,响应值是错误对象
      • 处理函数执行成功

        • 如果 return 一个新的promise,那么then返回的状态就是这个promise 的状态和返回值
        • 如果return 字面量或者不写, 那么 状态就是完成 ,返回值就是return的字面量,没写就是undefined
// 测试1
const pr1 = new Promise((res) => {
  setTimeout(() => {
    res(1);
  }, 1000);
});
​
const pr2 = pr1.then((data) => {
  console.log(data);
  return data + 1;
});
​
const pr3 = pr2.then((res) => {
  console.log(res);
});
​
console.log(pr1, pr2, pr3);
setTimeout(() => {
  console.log(pr1, pr2, pr3);
}, 2002);
//Promise { <pending> } Promise { <pending> } Promise { <pending> }
//1
//2
//Promise { 1 } Promise { 2 } Promise { undefined }
// 测试2
// po的结果就是最后一个then的返回值
const po = new Promise((res) => {
  res(1);
})
  .then((res) => {
    console.log(res);
    return 2;
  })
  .catch((err) => {
    throw 3;
  })
  .then((res) => {
    console.log(res);
  });
​
setTimeout(() => {
  console.log(po) // Promise{<fulfilled>:undefined}
},1000)
// 1  2  
// 测试3
const pro1 = () => {
  return new Promise((resolve, reject) => {
    console.log("开始跑马拉松");
    const duration = Math.floor(Math.random() * 3000);
    setTimeout(() => {
      if (Math.random() < 0.1) {
        resolve("我跑完了");
      } else {
        reject("脚伤了");
      }
    }, duration);
  });
};
​
// catch 链式处理
pro1()
  .catch((err) => {
    console.log("失败了 继续调用");
    return pro1();
  })
  .catch((err) => {
    console.log("失败了 继续调用1");
    return pro1();
  })
  .catch((err) => {
    console.log("失败了 继续调用2");
    return pro1();
  })
  .then(
    (res) => {
      console.log(res);
    },
    (err) => {
      console.log(err);
    }
  );

使用catch 捕获前面任意一步的错误

  • 因为then只能处理完成状态,根据没有处理函数状态会使用上一步Promise的返回值和状态的定论,状态会一直保留,直到碰到异常处理函数
new Promise((res, rej) => {
  res(1);
})
  .then((res) => {
    throw 3;
  })
  .then((res) => {
    return 1;
  })
  .catch((err) => {
    console.log(err);
  });

Promise的静态方法

Promise.resolve 和 Promise.reject

  • 直接返回 完成/失败 状态 的promise对象,结果就是调用值
const pr = Promise.resolve(1)
const pr1 = Promise.reject(1)
// Promise { <fulfilled>1 }
// Promise { <rejected>1 }
// --- 
// 实现原理
Promise.resolve = (value) => {
  return new Promise(res=> {
    res(value)
  })
}

Promise.all

  • 接收一个由promise任务组成的数组,按顺序执行这些promise,等待所有promise执行完成,然后统一执行then或者进入catch

  • 如果数组里面存在传入的不是promise对象,会通过Promise.resolve包装成promise

  • 会返回一个新的Promise,状态由接收的promise数组执行结果决定

    • 只要其中一个promise状态处于pending 那么返回的promise 状态就是pending

    • 当所有的 promise 状态变为 完成 返回的状态就是完成

      • 当所有的变成完成后 得到的结果 是所有promise 数组中所有promise执行完成的结果形成的数据数组
    • 当其中有一个失败,那么返回的状态就是 失败

      • 此时返回的数据不是数组,只能拿到失败的哪一个数据
const pr = Promise.all([Promise.resolve(1), Promise.resolve(2)]);
// pr Promise{<fulfilled> [1,2]}const pr1 = Promise.all([Promise.resolve(1), Promise.reject(2)]);
// pr Promise{<rejected> 2}

Promise.any

  • 接收一个由promise任务组成的数组,按顺序执行这些promise,等待所有promise执行完成,

  • 和 Promise.all 相反

    • 只要有一个成功(完成状态),Promise状态就变为 完成,数据就是这个率先完成的任务的数据
    • 全部任务失败 那么就失败了,返回的就是所有失败的数据
    • 只要其中一个promise状态处于pending 那么返回的promise 状态就是pending
const pr = Promise.any([Promise.reject(1), Promise.reject(2)]);
// pr Promise{<rejected> [1,2]}const pr1 = Promise.any([Promise.resolve(1), Promise.reject(2)]);
// pr Promise{<fulfilled> 1}

Promise.allSettled

  • 跟上边类似, 接收一个Promise组成的数组
  • 等所有promise状态变更才会返回
  • 返回的数据是所有promise的状态和数据的对象数组
const pr1 = Promise.allSettled([Promise.resolve(1), Promise.reject(2)]);
pr1.then((res) => console.log(res));
// 得到结果
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 }
]

Promise.race

  • 跟上边类似, 接收一个Promise组成的数组
  • 得到Promise中最先完成的结果,状态就是这个promise的状态和结果
const pr1 = Promise.race([
  new Promise((res) => {
    setTimeout(() => {
      res();
    }, 100);
  }),
  Promise.reject(2),
]);
​
pr1.then((res) => console.log(res)).catch((err) => console.log(err));
//2 

总结

  • 需要拿到所有结果,但是其中有一个请求失败了就放弃所有数据,这种情况推荐使用 Promise.all
  • 需要拿到所有结果,失败的也要拿到,最终我们自己处理成功的数据,这种情况推荐使用 Promise.allSettled
  • 需要拿到所有数据中第一个成功的数据, 这种情况下推荐使用 Promise.any
  • 需要拿到第一个完成的数据,不区分成功或者失败, 这种情况下推荐使用 Promise.race

Promise.finally

  • 无论是什么结果,都会执行,且没有任何参数
  • 也会返回一个新的promise,但是状态和数据是跟调用方一样,跟函数返回值无关
  • 但是如果finally执行出现错误 这个时候状态就会变为 失败 ,响应值就是 错误对象
const pr1 = new Promise((res) => {
  res(1)
})
const pr2 = pr1.finally(() => {
  console.log('finally')
})
// pr2 Promise{<fulfilled> 1}

async和await

消除回调

  • 有了Promise后,异步任务就有了一种统一的处理方式
  • es7 推出了两个关键字 async 和 await 用于消除异步场景回调写法,让代码进入同步写法

async

  • async 关键字用于修饰函数, 被它修饰的函数,一定返回Promise

  • 标记的函数返回值会被当做 promise的数据,如果没有返回就是undefined

    • 如果函数内部执行不报错那么返回的 promise状态就是 成功,
    • 执行出现报错就是 失败
    • 但是如果出现函数返回的promise,那么函数返回值就是 return 出去的 promise的结果和状态
async function m () {}  // Promise {<fulfilled>: undefined}
async function m1 () { a.to() } // Promise {<rejected>: ReferenceError: a is not defined } 
m()
m1()
// 使用async 标记箭头函数
const todo = async () => {}

await

  • 必须写的 async 标记的函数体内部
  • 消除promise的then的链式调用,直接拿到promise的执行结果
  • 作用是等待所标记的 promise执行完成,然后返回 promise 的执行结果(数据)
async function test() {
  return 123
}
async function m () {
  const res = await Promise.resolve(1)
  const res1 = await test() // 执行函数得到函数结果 Promise{<fulfilled>:1}
  console.log(res) //1
  console.log(res1) //123
} 
m()
  • await 标记的表达式如果不是promise,在执行过程中会把等待的非Promise的内容使用Promise.resolve进行转换

    • (async () => {
        const data = await 1 //会把这个1 变成 Promise.resolve(1)
        console.log(data)  //1
      })()
      
    • 标记的表达式如果是函数

      • 需要执行这个函数,得到函数返回的结果

        • async function async1(params) {
            console.log("async1 start");
            // 会直接执行await 绑定的函数
            await async2();
            console.log("async1 end");
          }
          ​
          async function async2(params) {
            console.log("async2");
          }
          async1();
          // async1 start
          // async2
          // async1 end
          
      • 执行失败会将promise的状态变为失败,同时整个代码会报错,终止代码执行

        • function todo() {
            a.todo();
          }
          (async () => {
            const data = await todo();
            console.log(data);
            console.log(4);
          })();
          
  • 使用try catch 处理await的异常

    • (async () => {
        try {
          const data = await (() => Promise.reject(2))();
          console.log(data);
        } catch (err) {
          // 接收失败的原因
          console.log(err); // 2
        }
      })();
      
  • await可以理解为使用 then 帮我们处理了await 后面的代码

    • (async () => {
        Promise.resolve().then((res) => console.log(1));
        Promise.resolve().then((res) => console.log(2));
        const data = await 3
        console.log(data)
        console.log(4)
      })();
      // 代码分析 把await的后续代码放入一个then里面执行,
      (async () => {
        Promise.resolve().then((res) => console.log(1));
        Promise.resolve().then((res) => console.log(2));
        Promise.resolve(3).then(data=> {
          console.log(data)
          console.log(4)
        })
      })();
      //最后输出1234
      
    • 如果说我们在 await 后面使用的表达 一直没有返回结果,这个时候代码的执行就会被阻塞

      • function todo() {
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve(222);
            }, 10000);
          });
        }
        (async () => {
          const data = await todo();
          console.log(data);
          console.log(4);
        })();
        
    • async 函数体内部的阻塞不会影响到外部,如果内部阻塞(就是遇到await)就会放入微队列,此时整个函数相当于执行完毕回到全局任务

      • const todo = async () => {
          // await直接放入微任务 函数体代码执行完毕
          const res = await (() => {
            return new Promise((res) => {
              setTimeout(() => {
                res(222);
              }, 10000);
            });
          })();
          console.log(res);
        };
        todo();
        console.log(3);
        // 3   222
        
//代码测试1
async function m1() {
  return 1;
}
async function m2() {
  const n = await m1(); // 相当于 Promise.resolve(1)
  console.log(n);
  return 2;
}
async function m3() {
  // 没有await 拿到就是m2的返回值 
  // 但是由于m2 async函数里面await 那么函数返回值就是undefined
  const n = m2();
  console.log(n);  // Promise { <pending> }
  return 3;
}
m3().then((m) => console.log(m));
m3();
console.log(4);
// Promise { <pending> }  
                          // m3 因为没有await 直接拿到函数结果 输出 n
                          // 进入m2函数,插入一条微任务
                          // 此时m3函数结果已经变为fulfilled <3>    
                          // 第一次m3执行完毕 插入一条微任务( console.log(n); ) 
// Promise { <pending> }  
                          // 第二次m3执行  因为没有await 直接拿到函数结果 输出 n 
                          // 进入m2函数,插入一条微任务
                          // 执行完毕
// 4                      执行全局任务 console.log(4) ,后续开始执行 微任务
// 1                      m2函数输出 1  (之前插入的微任务)
// 3                      然后执行 (m) => console.log(m) 3 
// 1                      m2函数输出 1   (之前插入的微任务)

手写Promise

目的就是完成Promise A+ 规范(实现大概)

实现构造器

class MyPromise {
  /**
   * @param { Function } executor - 传入的执行函数
   */
  constructor(executor) {
    this._state = PENDING;
    this._value = undefined;
    this._handles = []; //处理函数形成的数组
    executor(this._resolve.bind(this), this._rejected.bind(this));
  }
  /**
   * 向处理队列里面添加一个函数
   * @param {*} executor
   * @param {*} state
   * @param {Function} resolve 修改then函数返回的状态
   * @param {Function} reject 修改then函数返回的状态
   */
  _pushHandler(executor, state, resolve, reject) {
    this._handles.push({
      executor,
      state,
      resolve,
      reject,
    });
  }
  /**
   * 根据实际情况执行队列
   */
  _runHandlers() {
    // 任务处于挂起
    if (this._state == PENDING) {
      return;
    }
    while (this._handles[0]) {
      const handler = this._handles[0];
      this._runOneHandler(handler);
      this._handles.shift();
    }
  }
  /**
   *
   * @param {object} handler
   */
  _runOneHandler({ executor, state, resolve, reject }) {
    runMicroTask(() => {
      if (this._state !== state) {
        // 状态不一致 不处理
        return;
      }
      // 后续处理不是函数
      if (typeof executor !== "function") {
        this._state == FULFILLED ? resolve(this._value) : reject(this._value);
        return;
      }
      try {
        const result = executor(this._value);
        if (isPromise(result)) {
          result.then(resolve, reject);
        } else {
          resolve(result);
        }
      } catch (error) {
        console.log("🚀 ~ MyPromise ~ runMicroTask ~ error:", error);
        reject(error);
      }
    });
  }
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this._pushHandler(onFulfilled, FULFILLED, resolve, reject);
      this._pushHandler(onRejected, REJECTED, resolve, reject);
      this._runHandlers();
    });
  }
​
  _changeState(newState, value) {
    if (this._state !== PENDING) {
      return;
    }
    this._value = value;
    this._state = newState;
    this._runHandlers(); // 状态变化执行队列
  }
  _rejected(reason) {
    this._changeState(REJECTED, reason);
  }
  _resolve(data) {
    this._changeState(FULFILLED, data);
  }
}

实现catch 方法

/**
 * 仅处理失败的场景
 */
MyPromise.prototype.catch = function (callback) {
  return this.then(null, callback);
};

实现finally方法

/**
 * 无论成功还是失败都执行
 * @param {*} onSettled
 */
MyPromise.prototype.finally = function (onSettled) {
  return this.then(
    (data) => {
      onSettled();
      return data;
    },
    (reason) => {
      onSettled();
      throw reason;
    }
  );
};

实现Promise.resolve

/**
 * 得到一个被拒绝的promise
 * @param {*} reason
 */
MyPromise.reject = function (reason) {
  return new MyPromise((resolve, reject) => {
    reject(reason);
  });
};
​
/**
 * 返回一个已完成的promise
 * 1. 传递的data本身就是es6的promise对象
 * 2. 不是es6的promise,但是是PromiseLike(实现了Promise A+)
 * @param {*} data
 */
MyPromise.resolve = function (data) {
  // data instanceof Promise
  if (data instanceof MyPromise) {
    return data;
  }
​
  return new MyPromise((resolve) => {
    if (isPromise(data)) {
      data.then(resolve, this.reject);
    } else {
      resolve(data);
    }
  });
};

实现 Promise.all

/**
 * 得到一个新的Promise
 * 该Promise的状态取决于 proms 执行
 * proms 是一个迭代器,包含多个Promise
 * 全部Promise成功,则返回的Promise成功,数据为所有Promise成功的数据,并且顺序按照传入数组的顺序排列
 * 只要有一个失败,则返回的promise失败,原因是失败的原因
 * @param {Iterator} proms
 */
MyPromise.all = function (proms) {
  return new MyPromise((resolve, reject) => {
    try {
      const result = [];
      let count = 0; // promise总数
      let fulfilledCount = 0; // promise完成的数量
      for (const p of proms) {
        let i = count;
        count++;
        MyPromise.resolve(p).then((data) => {
          fulfilledCount++;
          result[i] = data;
          if (fulfilledCount === count) {
            resolve(result);
          }
        }, reject);
      }
      if (count == 0) {
        resolve(result);
      }
    } catch (error) {
      reject(error);
    }
  });
};

实现Promise.allsettled

/**
 * 等待所有promise有结果之后
 * 该方法返回的promise完成
 * 并且安装顺序将所有结果汇总
 * @param {Iterator} proms
 */
MyPromise.allSettled = function (proms) {
  const ps = [];
  for (let p of proms) {
    ps.push(
      MyPromise.resolve(p).then(
        (data) => ({
          status: FULFILLED,
          value: data,
        }),
        (reason) => ({
          status: REJECTED,
          value: reason,
        })
      )
    );
  }
  return MyPromise.all(ps);
};
​
MyPromise.allSettled([
  (() => {
    return new MyPromise((res) => {
      setTimeout(() => {
        res(1);
      }, 100);
    });
  })(),
  MyPromise.resolve(2),
  MyPromise.reject(3),
  4,
]).then((res) => {
  console.log(res);
});

实现Promise.race方法

MyPromise.race = function (proms) {
  return new MyPromise((resolve, reject) => {
    for (let p of proms) {
      MyPromise.resolve(p).then(resolve, reject);
    }
  });
};

Fetch Api

XMLHttpRequest 的问题

  • 所有的功能全部集中在同一个对象上,容易书写出混乱不易维护的代码
  • 采用传统的事件驱动模式(回调函数),无法适配新的Promise Api

Fetch Api 的特点

  • 并非取代AJAX, 而是对 AJAX 传统 Api 的改进

    • AJAX 是一种标准,异步请求刷新
  • 精细的功能分割,头部信息,请求信息,相应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景

  • 使用 Promise Api 更利于异步代码的书写

  • Fetch Api 并非ES6的内容,术语HTML5 新增的 Web Api

    • Es6 是语言标准,浏览器厂商根据这个标准去实现这些api
    • webApi 是浏览器提供的api,包括dom bom 以及es 语法
    • 只有浏览器可以使用(因为是web提供)

fetch Api 使用

使用 fetch 函数 即可立即向服务器发送网络请求

参数

  • 必填,字符串,请求地址
  • 选填,对象,请求配置
fetch('https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal')  // 发出请求

请求配置

  • method

    • 请求方式,默认是GET
  • headers

    • 请求头配置,配置为对象
  • body

    • 请求体配置
    • 请求体必须匹配请求头中的 Content-Type
  • mode

    • 请求模式,跨域和不跨域,默认是cors
  • credentials

    • 如何携带凭据(cookie)
    • omit 默认值,不懈怠cookie
    • same-origin ,请求同源地址携带cookie
    • include,请求任何地址都携带cookie
  • cache

    • 缓存模式
    • default ,请求之前将检查一下http的缓存

返回值

  • 返回的就是一个promis 对象

  • 当收到服务器的返回结果后,Promise进入resolved状态,状态数据为Response对象

    • 网络请求发起成功并且已经得到响应
  • 当网络发生错误(或者其他导致无法完成交互的错误) 时,Promise 进入 rejected 状态,状态数据为错误信息

    • 网络请求无法到达,可能是请求错误可能是配置错误
fetch(url, config).then(res=> {
         console.log(res) //Response对象
}).catch(err => console.log(err))

如何使用 Response 对象

  • ok 属性

    • 当响应码在200 - 299 之间返回的都是true,其他为false
    • fetch api 能拿到 ok 属性说明请求一定发送成功
  • status 属性, 响应的状态码

  • text()

    • 用于处理文本格式的 AJAX 响应,他从响应中获取文本流,将其读完,然后返回一个被解决为 string 对象的Promise
  • blob()

    • 用于处理二进制文件格式(图文表格之类)的Ajax 响应,它读取文件的原始数据,一旦读取完整个文件,就返回一个被解决为 blob 对象的 Promise
  • json()

    • 用于处理 JSON 格式的Ajax 响应,它将json 数据流转换成一个被解决的 js 对象的promise
  • redirect()

    • 用于重定向到另一个URL,他会创建一个新的 Promise.以解决来自重定向的 URL 响应
    • 服务器返回302并且附带另一个接口的地址,然后我们可以直接调用这个api 自动请求
fetch(url, config)
          .then((res) => res.json())   // 调用api 返回一个新的promise
          .then((res) => console.log(res))  // 调用then 拿到返回数据
          .catch((err) => console.log(err));

Request 对象

  • 在调用fetch 的时候,在内部执行会将url和config合并创建一个Request对象
  • 在调用 fetch 的时候,也可以直接传递 Request 对象
new Request(url,config)
// 封装请求配置
function getRequestInfo() {
  const url =
    "https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal";

  const config = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      a: 1,
    },
    body: JSON.stringify({
      a: 1,
    }),
  };
  const req = new Request(url, config);
  return req.clone();
}

注意点:

  • 尽量保证每次请求都是一个新的request对象
  • 调用 req.clone(); 克隆一个新的对象

Response 对象

  • 一般情况下 调用fetch 会自动创建Response对象
  • 可以手动构建 Response对象 ,实现mock
function getData1() {
  // 参数1  数据
  // 参数2  跟fetch api 的返回的属性一样
  const rps = new Response(`[{"id":1}]`, {
    ok: true,
    status: 200,
  });

  rps.json().then((res) => console.log(res));
}

Headers 对象

  • 在Request 和 Response 对象内部,会将传递的请求头对象,转换为 Headers 对象
  • 所以我们在调用 fetch 的 headers 就可以直接传入 Headers 对象
  headers: {
    "Content-Type": "application/json",
    a: 1,
  },
  // 改为
  headers:new Headers({
    "Content-Type": "application/json",
    a: 1,
  }) 

 const req = new Request(url, {
   headers
 });
console.log(req.headers)

Headers 对象中的方法

  • has( key )

    • 检查请求头中是否存在指定的 key 值
  • get( key )

    • 得到请求头中指定的key 的值
  • set( key,value )

    • 修改对应的键值对,不存在的时候会新增
  • append ( key,value )

    • 新增键值对,如果已经存在会追加 .value变成多个
  • keys()

    • 得到所有的key
  • values()

    • 得到所有的value
  • entries

    • 得到所有的键值对

文件上传

流程

  • 客户端将文件数据发送给服务器
  • 服务端保存上传的文件数据到服务器端
  • 服务器响应客户端一个文件访问地址
// 测试地址
http://101.132.72.36:5100/api/upload
// 键的名称(表单域名称) 
imagefile
// 使用 Html5提供的 FormData 这个构造函数构建表单
  • 请求方式 : POST

  • 请求格式为表单: multipart/form-data

    • 如果使用fetch,当body指定的数据格式为formData的时候,会自动设置请求头
  • 请求体中必须包含一个键值对,键的名称是服务器要求的名称,值就是文件数据

    • html5中,js 无法随意的获取文件,但是可以获取input组件中用户选择的文件
async function upload() {
  const inp = document.getElementById("id");
  if (!inp?.files?.length) {
    alert("请选择文件");
    return;
  }
  const formData = new FormData();
  formData.append("imagefile", inp.files[0]);

  const url = "http://101.132.72.36:5100/api/upload";
  const resp = await fetch(url, {
    method: "POST",
    body: formData,
  });
  const res = await resp.json();
  return res.path;
}
document.querySelector("button").onclick = async function () {
  const res = await upload();
  const img = document.getElementById("avater");
  img.src = res;
};

迭代器

  1. 什么是迭代

    1. 从一个数据集合汇总按照一定的顺序,不断取出数据的过程
  2. 迭代和遍历有什么区别

    • 迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完

      • 不知道数据源的具体情况,不需要管 目标数据, 只需要从迭代器里面一直拿
    • 遍历强调的是要把整个数据依次全部取出

      • 清楚数据源的情况,需要一直操作 需要遍历的数据
  3. 迭代器

    • 对迭代过程的封装,在不同的语言中又不同的表现形式,通常为对象
    • 也就是说把 拿数据的过程 进行封装,我们操作这个封装后的对象就行了
  4. 迭代模式

    • 是一种设计模式,用于统一迭代过程,并规范了迭代器规格

      • 迭代器应该具有得到下一个数据的能力
      • 迭代器应该具有判断是否还有后续数据的能力

js 中的迭代器

js规定,如果一个对象具有 next 方法,并且该方法返回一个对象,该对象的格式如下,则认为该对象是一个迭代器

const obj = {
  next() {
    return {
      value:"",
      done:false
    }
  }
}
  • 迭代器对象

    • next方法 : 用于得到下一个数据

    • 返回的对象

      • value : 下一个数据的值
      • done : boolean 标记是否迭代完成(取不到数据就是true)
    • const arr = [1, 2, 3, 4, 5];
      // 用于迭代数组arr
      // 封装取数据的过程 形成迭代对象
      const iterator = {
        i: -1, // 当前数组下标
        next() {
          this.i++;
          return {
            value: arr[this.i],
            done: this.i >= arr.length,
          };
        },
      };
      ​
      ​
      // 让迭代器不断取出下一个数据,直到没有数据为止
      let data = iterator.next();
      ​
      // 没有迭代完成 再次取数据
      while (!data.done) {
        console.log(data.value);
        data = iterator.next();
      }
      
  • 迭代器函数

    • 一个返回迭起器函数的函数
    • /**
       * 迭代器创建函数 iterator creator
       * @param {*} arr
       */
      function createIterator(arr) {
        let i = 0; // 当前数组下标
        return {
          next() {
            const result = {
              value: arr[i],
              done: i >= arr.length,
            };
            i++;
            return result;
          },
        };
      }
      

结合链表的数据结构就很好理解,迭代就是从开始一直取,因为我们不知道链表的尾部在哪里,所有通过迭代器一直从链表里面拿数据,直到 done 标记为true,表示已经到了链表尾部,迭代结束

// 迭代器实现斐波拉契数列
const createIterator = () => {
  let prv1 = 1,
    prv2 = 1,
    n = 1;
  return {
    next() {
      let value;
      if (n <= 2) {
        value = 1;
      } else {
        value = prv1 + prv2;
        prv1 = prv2;
        prv2 = value;
      }
      const result = {
        value,
        done: false,
      };
​
      n++;
      return result;
    },
  };
};

可迭代协议 (可迭代对象)

  • es6规定,如果一个对象具有知名符号属性 Symbol.iterato

  • 并且该属性值是一个迭代器创建函数,则该对象是可迭代的 ( iterable )

    • 在es6中数组就是可迭代对象, 数组的隐式原型上 有 Symbol.iterator

      • const arr = [1, 2, 3, 4];
        const iterator = arr[Symbol.iterator]();
        console.log(iterator.next());
        
    • document.querySelectorAll 也可以使用迭代器

// 可迭代对象
const obj = {
  [Symbol.iterator]() {
    return {
      next() {
        return {
          value: "111",
          done: false,
        };
      },
    };
  },
};

for-of 循环

  • for-of( 语法糖) 循环专门用于遍历可迭代对象
  • 在实现上就是调用迭代对象的 Symbol.iterator 拿到迭代器函数,创建迭代器,执行迭代器直到done 为true
const arr = [1, 2, 3, 4];
const iterator = arr[Symbol.iterator]();
let data = iterator.next();
​
while (!data.done) {
  console.log(data.value);
  data = iterator.next();
}
// 使用for-of处理 效果跟上边写法一样
// 要求目标对象 一定要是可迭代对象
for (const data of arr) {
  console.log(data);
}

使用for-of 迭代普通对象

const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator]() {
    const keys = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        const result = {
          value: {
            propName: keys[i],
            value: this[keys[i]],
          },
          done: i >= keys.length,
        };
        i++;
        return result;
      },
    };
  },
};
​
for (const data of obj) {
  console.log(data);
}

展开运算符与可迭代对象

展开运算符可以作用于可迭代对象,实现方式就是使用迭代器一个个取值然后放入数组

  • 将可迭代对象转换为数组

    • // 这个obj 是上面我们自己写的可迭代对象
      // 如果不是迭代对象,是不能在数组里面展开,会直接报错
      const arr = [...obj]
      // arr => [{propName:'a',value:1},{propName:'b',value:2}]
      
  • 可以在函数传值使用展开运算符操作迭代器

    • function test(a,b) {
        console.log(a,b)
        // a =>  {propName:'a',value:1}
        // b =>  {propName:'b',value:2}
      }
      // 如果不是迭代器就会报错
      // 数组之所以可以这么使用 是因为数组就是迭代器对象
      test(...obj) 
      

生成器 ( Generator )

目的: 简化创建迭代器和迭代器函数的创建流程

  • 生成器是一个通过构造函数 Generator 创建的对象(没有对外暴露)

  • 生成器既是一个迭代器,同时又是一个可迭代对象

  • 生成器的创建必须使用生成器函数 ( Generator Function )

    • 在函数名前面使用 * 标识该函数为生成器函数
    • // 这是一个生成器函数,该函数一定返回一个生成器
      // * 不能和async 关键字一起使用
      function *method () {}
      const iterator = method();
      console.log(iterator.next());
      ​
      //标记对象的方法为生成器函数
      {
        * todo(){
        }
      }
      

生成器函数

  • 调用生成器函数,在函数体内部的代码是不会运行,调用函数只是返回一个生成器

  • 函数体内部是用于给生成器的每次迭代提供数据(没有调用next是不会运行)

  • 每次调用生成器的next方法,将导致生成器函数运行到下一个 yield 关键字位置

    • 调用一次next 会运行到下一个yield 然后暂停(有中间代码也会执行),等待下一次next执行
    • yield 关键字只能在生成器函数内部使用,表达 ' 产生 ' 一个迭代数据
    • yield 后面的的数据 就是返回给next 的数据
  • 迭代器函数体执行完毕 此时 迭代器属性 done 为true

function* test() {
  console.log("第1次执行");
  yield 1;
  console.log("第2次执行");
  yield 2;
  console.log("第三次执行");
}
​
const iterator = test();
console.log(iterator.next());  // 执行开始到yield 1之间的代码,然后返回1,等待
console.log(iterator.next()); // 执行yield 1到yield2直接的代码,然后返回2,等待
console.log(iterator.next());// 由于没有yield了 执行yield 2之后的代码,然后返回undefined,结束
// 使用生成器加for-of 构建数组的迭代器函数
const arr = [1, 2, 3, 4];
function* iterator(arr) {
  for (const data of arr) {
    yield data;
  }
}
const iterator2 = iterator(arr);
// 使用生成器器改造斐波拉契数列迭代器函数
function* iterator() {
  let prv1 = 1,
    prv2 = 1,
    n = 1;
  while (true) {
    if (n <= 2) {
      yield 1;
    } else {
      let newValue = prv1 + prv2;
      yield newValue;
      // 这里会在下一次yield 之前调用
      prv1 = prv2;
      prv2 = newValue;
    }
    n++;
  }
}

生成器需要注意的细节

  • 生成器函数可以有返回值,返回值出现在第一次 done 为true时的value属性中

    • 只有第一次时候会使用这个值,后续还是undefined
    • function *todo() {
        yield 1
        yield 2 
        return 10
      }
      const handle = todo()
      handle.next() //  1
      handle.next() //  2
      handle.next() //  10
      handle.next() // undefined
      
  • 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值

    • 第一次调用next方法时,传参没有意义
    • 如果调用next 没有传参,赋值运算结果就是undefined
    • function *todo() {
        // 第一次调用传参触发只是触发yield 1
        // 第二次调用才会触发 info = 的赋值操作 由于传入了参数 这个info 接受的next的入参
        // info = 2  这里如果没有传参拿到就是 undefined
        // 然后触发next 返回值就是 2 + 5
        let info = yield 1  
        yield  2 + info
      }
      ​
      const handler = todo()
      handler.next() // 1
      handler.next(5) // 7  
      

生成器api和属性

  • return 方法,调用该方法可以提前结束生成器函数,从而提前让让整个迭代过程结束

    • 可以传入参数,参数就是第一次done的value值
    • 调用之后 后续触发的next 都是完成状态,value 为undefined
    • const handler = todo()
      handler.next() 
      handler.return()
      
  • throw 方法,该方法可以在生成器中产生一个错误

    • 调用该方法后,后续的next不会继续执行
    • function* todo() {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
      }
      ​
      const handler = todo();
      ​
      handler.next();
      handler.throw(new Error("错误代码"));
      handler.next();
      
  • 在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

    • function* helper() {
        yield 'a'
        yield 'b'
      }
      function* todo() {
        // 调用其它生成器函数 本质上可以看做吧对应函数内部代码复制过来了
        yield* helper();
        yield 2;
      }
      

生成器控制异步调用

function* task() {
  const d = yield 1;
  console.log(d);
  const resp = yield fetch(
    "https://66d7d20037b1cadd80525d16.mockapi.io/api/local/getApiLocal"
  );
  const result = yield resp.json();
  console.log(result);
}
run(task);
​
function run(generatorFunc) {
  const generator = generatorFunc();
​
  let result = generator.next();
​
  handleResult();
  function handleResult() {
    if (result.done) {
      return;
    }
    /**
     迭代没有完成的情况
     1.迭代的数据是一个Promise
     2.迭代的数据是其他数据
     */
    //判断是promise
    if (typeof result.value.then === "function") {
      // 需要等待promise 完成后进行下一次迭代
      result.value.then(
        (res) => {
          result = generator.next(res);
          handleResult();
        },
        (err) => {
          result = generator.throw(err);
          handleResult();
        }
      );
    } else {
      result = generator.next(result.value);
      handleResult();
    }
  }
}

Set 集合

  • 用于存放不重复的数据
const sets = new Set() // 创建空的set 集合
// 使用可迭代对象创建 set 集合
// 使用迭代器挨个取数据放入集合
const set2 = new Set(iterable)
// 如果出现重复的数据会自动去除
const set3 = new Set([1,2,3,4,5,1,2])
// set3 => 1,2,3,4,5
// 字符串会被转为字符串对象,字符串对象是可迭代对象
// 可以实现字符串去重
const set4 = new Set('cscscsscscs')
// set4 => cs
  • 对set进行后续操作(实例方法)

    • add(数据), 添加数据到set集合末尾,如果数据已经存在则不进行操作

      • set 使用Object.is 判断两个数据是否相同,但是针对 +0 和 -0 认为是相等的
      • sets.add(1)
        // 这一句无效
        sets.add(1)
        sets.add(+0)
        // 认为相同
        sets.add(-0)
        
    • has(数据), 判断set中是否存在对应的数据

      • sets.has(-0) //true
        
    • delete(数据),删除指定数据,返回是否删除成功

      • sets.delete(-0) //true
        
    • clear(), 清空整个set集合

    • size属性, 获取 set 集合的元素数量

  • 和数组进行相互转换

    • set 本身也是可迭代对象,每次迭代的结果就是每一项的值

      • const s1 = new Set([1,2,3,4])
        const arr = [...s1]
        
    • 实现数组去重

      • const arr = [...new Set([1,2,3,4,1,2,3])]
        
    • 字符串去重

      • const arr = [...new Set('dsadasdadada')].join('')
        
  • 如何遍历

    • 使用 for-of 循环

      • const s1 = new Set([1,2,3,4])
        for(const props of s1){}
        
    • 使用实例方法 forEach

      • set 集合中不存在下标,index 和 item 一致,均表示set的每一项
      • const s1 = new Set([1,2,3,4])
        s1.forEach((item,index,s) => {})
        
const arr1 = [2, 52, 23, 55, 77, 55, 44];
const arr2 = [33, 53, 98, 55, 77, 22, 44];
​
// 并集
console.log([...new Set([...arr1, ...arr2])]);
​
// 交集
const s1 = new Set(arr1);
const j = arr2.filter((el) => s1.has(el));
console.log(j);
​
// 差集
console.log(
  [...new Set([...arr1, ...arr2])].filter(
    (el) =>
      (arr1.indexOf(el) >= 0 && arr2.indexOf(el) < 0) ||
      (arr1.indexOf(el) < 0 && arr2.indexOf(el) >= 0)
  )
);

手写Set

class MySet {
  constructor(iterator = []) {
    //验证是否是可迭代对象
    if (typeof iterator[Symbol.iterator] !== "function") {
      throw new TypeError(`提供的${iterator}不是一个可迭代对象`);
    }
    this._datas = [];
    for (const item of iterator) {
      this.add(item);
    }
  }
  add(data) {
    if (!this.has(data)) {
      this._datas.push(data);
    }
  }
  has(data) {
    for (const item of this._datas) {
      if (this.isEqual(data, item)) {
        return true;
      }
    }
    return false;
  }
​
  delete(data) {
    for (let i = 0; i < this._datas.length; i++) {
      const element = this._datas[i];
      if (this.isEqual(data, element)) {
        this._datas.splice(i, 1);
        return true;
      }
    }
    return false;
  }
  size() {
    return this._datas.length;
  }
  clear() {
    this._datas = [];
  }
  //判断数据是否相等
  isEqual(data1, data2) {
    if (data1 == 0 && data2 == 0) {
      return true;
    }
    return Object.is(data1, data2);
  }
​
  *[Symbol.iterator]() {
    for (const element of this._datas) {
      yield element;
    }
  }
  forEach(callback) {
    for (const el of this._datas) {
      callback(el, el, this._datas);
    }
  }
}
​
const s1 = new MySet([]);
​
s1.add(33);
for (const element of s1) {
  console.log(element);
}
​
s1.forEach((el) => {
  console.log(el);
});

Map 集合

键值对( key value )数据集合的特点 -- 键不可重复

  • map集合专门用于存储多个键值对数据

    • 在map出现之前我们使用对象来存储键值对,键是属性名,值是属性值

使用对象存储有以下问题

  1. 键名只能是字符串
  2. 获取数据的数量不方便
  3. 属性名容易跟原型上的名称冲突

创建map

  • 使用可迭代对象创建map时,需要保证每一项还是可迭代对象,并且长度为2

    • [['a',{a:1}],['c',1]]
new Map() // 创建空的map
// 创建一个具有初始内容的map,初始内容来自于可迭代对象每一次迭代的结果,
// 但是它要求每一次迭代的结果必须是一个长度为2的数组,数组第一项表示键,数组第二项表示值
new Map(iterable) 
// 取第一项然后迭代这个内容,拿到a 做为键,拿到{a:1}作为值,形成映射
// a => {a:1} c => 1
new Map([['a',{a:1}],['c',1]])

进行后续操作

  • size,只读属性,获取当前map的键的数量

  • set(键,值),设置一个键值对,键和值可以是任何属性

    • 如果键不存在,则添加一项
    • 如果键已存在,则修改对应的值
    • 比较键的方式和set相同,都是使用Object.is
    • const map = new Map()
      map.set(3,4)
      map.set({a:1},'123')
      
  • get(键),根据一个键得到对应的值

  • `has(键),判断某个键是否存在

  • `delete(键),删除指定的键

  • clear,清空map

map转数组(和set一样)

const map = new Map([['a',1],['b',2]]);
const arr = [...map] // 可迭代对象直接转数组
// arr => [ [ 'a', 1 ], [ 'b', 2 ] ]

遍历

  • for-of 循环,每次得到都是长度为2 的数组

    • for (const [key, value] of map) {
        console.log(key, value);
      }
      
  • forEach ,通过回调遍历

    • 参数1 每一项的值
    • 参数2 每一项的键
    • 参数3 map本身####

手写map

class MyMap {
  constructor(iterable = []) {
    //验证是否是可迭代对象
    if (typeof iterable[Symbol.iterator] !== "function") {
      throw new TypeError(`你提供的${iterable}不是一个可迭代对象`);
    }
    this._datas = [];
    for (const item of iterable) {
      // item也是需要时可迭代对象
      if (typeof item[Symbol.iterator] !== "function") {
        throw new TypeError(`你提供的item-${item}不是一个可迭代对象`);
      }
      const iterator = item[Symbol.iterator]();
      const key = iterator.next().value;
      const value = iterator.next().value;
      this.set(key, value);
    }
  }
  set(key, value) {
    const obj = this._getObj(key);
    if (obj) {
      // 修改
      obj.value = value;
    } else {
      this._datas.push({
        key,
        value,
      });
    }
  }
​
  /**
   * 根据key值从内部数组找到对应的数组项
   * @param {*} key
   */
  _getObj(key) {
    for (const item of this._datas) {
      if (this.isEqual(item?.key, key)) {
        return item;
      }
    }
  }
​
  has(key) {
    return !!this._getObj(key);
  }
  get(key) {
    const item = this._getObj(key);
​
    if (item) {
      return item.value;
    }
    return undefined;
  }
  get size() {
    return this._datas.length;
  }
  clear() {
    this._datas.length = 0;
  }
  delete(key) {
    for (let i = 0; i < this._datas.length; i++) {
      const element = this._datas[i];
      if (this.isEqual(element.key, key)) {
        this._datas.splice(i, 1);
        return true;
      }
    }
    return false;
  }
  *[Symbol.iterator]() {
    for (const item of this._datas) {
      yield [item.key, item.value];
    }
  }
  forEach(callback) {
    for (const element of this._datas) {
      callback(element.value, element.key, this);
    }
  }
  //判断数据是否相等
  isEqual(data1, data2) {
    if (data1 == 0 && data2 == 0) {
      return true;
    }
    return Object.is(data1, data2);
  }
}
​
const mp = new MyMap([
  ["a", 1],
  ["b", 2],
]);
mp.set("name", "cccc");
console.log(mp);
mp.delete("name");
console.log(mp.has("name"));
console.log(mp.get("name"));
console.log([...mp]);
mp.forEach((value, key) => {
  console.log(value, key);
});

WeakSet 和 WeakMap

weakSet

  • 目的是用于观察对象垃圾回收
  1. 内部存储的地址不会影响垃圾回收

    • set会影响垃圾回收

      • let obj = { name: 1 };
        const set = new Set();
        set.add(obj);
        obj = null; //释放
        console.log(set); // { { name: 1 } }  释放后没有被回收 因为还在set里面能找到
        
    • 使用weekSet之后,就算内部存在引用也会回收

      1. let obj = { name: 1 };
        const set = new WeakSet();
        set.add(obj);
        obj = null; //释放
        console.log(set); // WeakSet { No properties }  释放后被回收 
        
    • 使用weekSet调试一些该被回收但没有被回收的场景

      1. let obj = { name: 1 };
        let obj2 = obj
        const set = new WeakSet();
        set.add(obj);
        obj = null
        console.log(set);  //WeakSet [[Entries]]
        
  2. 只能添加对象

  3. 不能遍历,不是可迭代对象,没有size属性,没有forEach方法

WeakMap

  1. 类似于map集合,不同的是它的键存储地址不会影响垃圾回收
  2. 它的键只能是对象
  3. 不能遍历,不是可迭代对象,没有size属性,没有forEach方法
let obj = { name: 1 };
const map = new WeakMap();
map.set(obj, "222");
obj = null;
console.log(map);

属性描述符(旧知识)

Property Descriptor(属性描述符) 是一个普通对象,用于描述一个属性的相关信息

通过Object.getOwnPropertyDescriptor(对象,属性名) 可以得到一个对象的某个属性的属性描述符

Object.getOwnPropertyDescriptors(对象) 得到对象的所有属性描述符

  • value: 属性值
  • configurable: 是否允许修改该属性的属性描述符
  • enumerable: 该属性是否可以被枚举,(for-in ,Object.values等循环可以得到该属性)
  • writable: 该属性是否可以重新赋值
let obj = { name: 1 };
const config = Object.getOwnPropertyDescriptor(obj, "name");
// { value: 1, writable: true, enumerable: true, configurable: true }

如果需要为某个对象添加属性时 或 修改属性时,可以使用 Object.defineProperty

Object.defineProperty(obj,name,{
   value:3,
   configurable:false,  // 不能再次使用  Object.defineProperty
   enumerable:false // for-in 循环会拿不到该属性
   writable:false // value 不能被重新修改
})

使用 Object.defineProperties 批量操作属性操作符

​
Object.defineProperties(obj,{
    a:{
        value: 3,
        configurable: false, // 不能再次使用  Object.defineProperty
        enumerable: false, // for-in 循环会拿不到该属性
    }
})

存取器属性

  • 属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性不再是一个普通属性,而变成了存取器属性

  • get 和 set 配置均为函数

    • 如果一个属性是存取器属性,则读取该属性时,会允许get方法,将get方法得到的返回值做为属性值
    • 如果给该属性赋值,则会运行set方法
    • 没有指定的内存空间,现在相当于是两个方法,每次调用属性实际上就是执行对应的方法
  • 存取器属性最大的意义就是可以控制属性的读取和赋值

  • 不能和value 和 writable 共存(配置value 会开辟内存空间),一起配置会报错

Object.defineProperty(obj, "name", {
  get() {
    console.log("执行了a的get函数");
    // 不能是直接返回  obj.name  会出现自己调自己
    return obj._name
  },
  set(val) {
    console.log("执行了a的set函数", val);
    obj._name = val
  },
});
console.log(obj.name);  // undefined 因为get函数没有返回

Reflect

  1. Reflect 是一个内置的js对象,他提供了一系列的方法,可以让开发者通过调用这些方法访问一些js底层功能

    1. 由于它类似于其他语言的 反射,因此取名叫 Reflect
  2. 他可以做什么?

    1. 属性的赋值和取值
    2. 调用普通函数
    3. 调用构造函数
    4. 判断属性是否存在对象中
    5. 等等
  3. 已经存在的功能为什么还要使用Reflect实现一次?

    1. 减少魔法,让代码更加纯粹

    2. 将一些基于底层的实现的能力提取成一个api,高度聚合到一个对象里面

      1. 基于函数式编程思想,所有的能力都是用函数实现
  4. 提供的api( 举例一部分 )

    • Reflect.set( target,propertyKey ,value)

      • 设置对象的 propertyKey 为 value,等同于给对象赋值
    • Reflect.get( target,propertyKey )

      • 获取对象的属性值
    • Reflect.apply ( target,thisArgument,argumentList )

      • 调用目标函数,返回目标函数的结果
    • Reflect.deleteProperty (target,propertyKey )

      • 认为 delete 关键字是魔法,使用api删除目标属性
    • Reflect.defineProperty( target,propertyKey.attributes )

      • 可以理解为底层就是调用 Object.defineProperty , 但是做了优化配置错误不会报错,而是返回false
    • Reflect.construct( target,argumentsList )

      • 认为 new 关键字是魔法,通过api进行实例化,返回实例对象
      • const obj = Reflect.construct(target, argumentsList);
        // 返回实例对象
        
    • Reflect.has( target,propertyKey)

      • 认为 in 关键字是是魔法,通过api判断属性是否在对象内,结果是boolean 值
    • Reflect.ownKeys ( target )

      • Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
      • 一般来说 symbol 是不能被枚举的,这个api调用会返回自定义的 symbol 属性

Proxy

代理: 提供了修改底层实现的方式(赋值,读取属性,构造实现)

// target 目标对象
// handler 目标对象,其中可以重写底层实现(Reflect实现的api都可以重写)
// 返回代理对象
new Proxy(target,handler)
  • 使用代理隔离原始对象,不再操作原始对象,后续操作都是操作代理对象
  • 重写代理对象的方法之后,一旦触发代理的事件我们可以控制它做一些事情
  • 不占用内存空间
const user = {
  name: "22",
};
const proxy = new Proxy(user, {
  set(target, propertyKey, value) {
    Reflect.set(target, propertyKey, value);
  },
  // 重写has 不会影响Reflect.has
   has(target, propertyKey) {
    return false;
  },
});
proxy.name = "2";
​
console.log(proxy.name);  // 2
console.log(user.name);    // 2
console.log("name" in proxy); // false

Proxy应用-观察者模式

有一个对象,是观察者,它用于观察另一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事

// 旧版本实现,实现了两个对象,额外占用内存空间
// 无法对新增属性进行观察
function observer(target) {
  const div = document.getElementById("box");
  const ob = {};
​
  for (const element of Object.keys(target)) {
    Object.defineProperty(ob, element, {
      enumerable: true,
      get() {
        return target[element];
      },
      set(val) {
        target[element] = val;
        render();
      },
    });
  }
  render();
  function render() {
    let html = "";
    for (const element of Object.keys(target)) {
      html += `<p><span>${element}</span><span>${target[element]}</span></p>`;
    }
    div.innerHTML = html;
  }
  return ob;
}
​
const ob = observer({
  a: 1,
  b: 2,
});
// 新版实现 能够捕获到新增属性,因为代理的是整个目标而且能够修改底层
function observer(target) {
  const div = document.getElementById("box");
  const ob = new Proxy(target, {
    set(target, prop, value) {
      Reflect.set(target, prop, value);
      render();
    },
    get(target, prop) {
      return Reflect.get(target, prop);
    },
  });

  for (const element of Object.keys(target)) {
  }
  render();
  function render() {
    let html = "";
    for (const element of Object.keys(target)) {
      html += `<p><span>${element}</span><span>${target[element]}</span></p>`;
    }
    div.innerHTML = html;
  }
  return ob;
}

const ob = observer({
  a: 1,
  b: 2,
});

Proxy应用-偷懒的构造函数

class User {}

function ConstructorProxy(Class, ...propNames) {
  return new Proxy(Class, {
    construct(target, argumentsList) {
      // 通过原来的对象创建一个新的实例
      const obj = Reflect.construct(target, argumentsList);
      // 往实例加属性
      propNames.forEach((name, index) => {
        Reflect.set(obj, name, argumentsList[index]);
      });
      return obj;
    },
  });
}

const UserProxy = ConstructorProxy(User, "name", "age");
const obj = new UserProxy("test", 18);
console.log(obj);

Proxy应用-可以验证的函数参数

function sum(a, b) {
  return a + b;
}
​
function validatorFunction(func, ...types) {
  return new Proxy(func, {
    apply(target, thisArgument, argumentsList) {
      types.forEach((t, i) => {
        const arg = argumentsList[i];
        if (typeof arg != t) {
          throw new Error(`第${i + 1}个参数${argumentsList[i]}不满足类型${t}`);
        }
      });
     return Reflect.apply(target, thisArgument, argumentsList);
    },
  });
}
​
const sumProxy = validatorFunction(sum, "number", "number");
​
console.log(sumProxy("1", "2"));

新增的数组Api

静态方法

  • Array.of( ...args ) 使用指定的数组项创建一个新数组

    • 消除 new Array 带来的歧义,创建指定长度的数组还是要 new Array
    • Array.of(1, 2, 3, 4);
      // [1,2,3,4]
      
  • Array.from( args ) 通过给定的类数组 或 可迭代对象 创建一个新的数组

    • const divs = document.querySelectorAll("div");
      Array.from(divs);
      

实例方法

  • find( callback ) 用于查找满足条件的第一个元素

    • callback 返回true 表示满足要求,终止循环
    • false就会继续找,直到结束
    • const arr = [{ name: 1 }, { name: 2 }, { name: 3 }];
      console.log(arr.find((el) => el.name == 2));
      
  • findIndex(callback) 用于查找满足条件的第一个元素的下标

    • 跟 find 类似,只不过返回的是下标
    • console.log(arr.findIndex((el) => el.name == 2));
      
  • fill ( data ) 用指定的数组填充满数组所有的内容

    • console.log(new Array(100).fill("abc"));
      
  • copyWithin ( target ,start ?, end ?) 在数组内部完成赋值

    • target 表示从下标位置开始改变
    • start 表示从下标位置开始复制( 数据来源 )
    • end 表示数据来源结束位置
  • includes( data ) 判断数组中是否包含某个值,使用 Object.is 匹配

    • const arr = [1, 2, 3, 4, 5, 6, 7, 8];
      console.log(arr.includes(5));
      

类型化数组

数字存储的前置知识

  1. 计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的

  2. n 位的无符号整数能表示的数字是 2 ^ n 个,取值范围是 0 ~ 2^n - 1

  3. n 位的有符号整数能表示的数字是 2 ^ n 个,取值范围是 -2 ^( n - 1 ) ~ 2 ^ (n - 1) - 1

  4. 浮点型表示法可以用于表示整数和小数,目前分为两种标准

    1. 32位浮点数, 又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
    2. 64位浮点数,又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
  5. js中所有数字,均使用双精度浮点数保存