JS汇总

169 阅读38分钟

JS基础

数据类型-介绍

原始数据类型:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint

引用数据类型:

对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)

symbol

返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的

bigInt

参考文章

什么是BigInt?

BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

应用-大整数id

利用json-bigint处理大数字[溢出]问题

axios 为了方便我们使用数据,它会在内部使用 JSON.parse() 把后端返回的数据转为 JavaScript 对象。但是,超出安全整数范围的 id 无法精确表示。

可以通过json-bigint插件在axios中的transformResponse来自定义响应数据的格式

image.png

import axios from 'axios'import jsonBig from 'json-bigint'var json = '{ "value" : 9223372036854775807, "v2": 123 }'console.log(jsonBig.parse(json))
​
const request = axios.create({
  baseURL: 'http://ttapi.research.itcast.cn/', // 接口基础路径
​
  // transformResponse 允许自定义原始的响应数据(字符串)
  transformResponse: [function (data) {
    try {
      // 如果转换成功则返回转换的数据结果
      return jsonBig.parse(data)
    } catch (err) {
      // 如果转换失败,则包装为统一数据格式并返回
      return {
        data
      }
    }
  }]
})
​
export default request

数据类型-判断

typeof 判断原始数据类型

对于原始类型来说,除了null都可以调用typeof显示正确的类型。

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof null() // 'object'

但对于引用数据类型,除了函数之外,都会显示"object"。

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

 instanceof 判断基本数据类型

instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true

const Person = function() {} 
const p1 = new Person() 
p1 instanceof Person // true 

var str1 = 'hello world' 
str1 instanceof String // false 

var str2 = new String('hello world') 
str2 instanceof String // true

手动实现一下instanceof的功能?

核心: 原型链的向上查找。

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)

      const prototype1 = {}
      const object1 = Object.create(prototype1)
      console.log(Object.getPrototypeOf(object1) === prototype1)
      //  判断left是不是right数据类型
      function myInstanceof(left, right) {
        // 先判断是不是 简单数据 类型 和 null,直接返回false
        if (typeof left !== 'object' || left === null) return false

        // 拿到left的对象 原型
        let proto = Object.getPrototypeOf(left)

        // 往原型链上找
        while (true) {
          // a.表示一直没找到
          if (proto === null) return false
          // b.找到了,返回true
          if (proto === right.prototype) return true
          // c.如果没找到,再往上一层找
          proto = Object.getPrototypeOf(proto)
        }
      }

      const left = new Date()
      console.log(myInstanceof(left, Date)) //true

Object.is和===的区别?

Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN 。 源码如下:

function is(x, y) {
   // +0与-0的比较,+0===-0结果为true,是不对的
  if (x === y) {
    //运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    //NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
    //两个都是NaN的时候返回true
    return x !== x && y !== y;
  }

数据类型-转换

JS中,类型转换只有三种:

  • 转换成数字
  • 转换成布尔值
  • 转换成字符串 image.png

隐式转换

参考文章

注意:

数组转换为Number,首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理

1.1 ToString

这里所说的ToString可不是对象的toString方法,而是指其他类型的值转换为字符串类型的操作。

这里我们讨论nullundefined布尔型数字数组普通对象转换为字符串的规则。

  • null:转为"null"
  • undefined:转为"undefined"
  • 布尔类型:truefalse分别被转为"true""false"
  • 数字类型:转为数字的字符串形式,如10转为"10"1e21转为"1e+21"
  • 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的nullundefined,会被当做空字符串处理
  • 普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"
  String(null) // 'null'
  String(undefined) // 'undefined'
  String(true) // 'true'
  String(10) // '10'
  String(1e21) // '1e+21'
  String([1,2,3]) // '1,2,3'
  String([]) // ''
  String([null]) // ''
  String([1, undefined, 3]) // '1,,3'
  String({}) // '[object Objecr]'

对象的toString方法,满足ToString操作的规则。

注意:上面所说的规则是在默认的情况下,如果修改默认的toString()方法,会导致不同的结果

1.2 ToNumber

ToNumber指其他类型转换为数字类型的操作。

  • null: 转为0
  • undefined:转为NaN
  • 字符串:如果是纯数字形式,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN
  • 布尔型:truefalse被转为10
  • 数组:数组首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理,关于ToPrimitive,会在下文中讲到
  • 对象:同数组的处理
  Number(null) // 0
  Number(undefined) // NaN
  Number('10') // 10
  Number('10a') // NaN
  Number('') // 0 
  Number(true) // 1
  Number(false) // 0
  Number([]) // 0
  Number(['1']) // 1
  Number({}) // NaN

1.3 ToBoolean

ToBoolean指其他类型转换为布尔类型的操作。

js中的假值只有falsenullundefined空字符0NaN,其它值转为布尔型都为true

  Boolean(null) // false
  Boolean(undefined) // false
  Boolean('') // flase
  Boolean(NaN) // flase
  Boolean(0) // flase
  Boolean([]) // true
  Boolean({}) // true
  Boolean(Infinity) // true

ToPrimitive

ToPrimitive指对象类型类型(如:对象、数组)转换为原始类型的操作。

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

  1. 如果Symbol.toPrimitive()方法,优先调用再返回
  2. 调用valueOf,如果转换为原始类型,则返回
  3. 调用toString,如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错

注意:Date对象会优先尝试toString()方法来实现转换 截屏2021-09-09 下午4.41.30.png

var obj = {
  value: 3,
  valueOf() {
    return 4;
  },
  toString() {
    return '5'
  },
  [Symbol.toPrimitive]() {
    return 6
  }
}
console.log(obj + 1); // 输出7
      // [].valueOf()为数组本身,[].toString()为'', Number('')为0
      Number([]) // 0

      // ['10'].valueOf()为数组本身,['10'].toString()为'10', Number('10')为10
      Number(['10']) //10

      // obj1.valueOf()为100
      const obj1 = {
        valueOf() {
          return 100
        },
        toString() {
          return 101
        }
      }
      Number(obj1) // 100

      // obj1.toString()为{},不为原始数据类型,抛出异常
      const obj3 = {
        toString() {
          return {}
        }
      }
      Number(obj3) // TypeError

== 和 ===有什么区别?

===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1的结果是false,因为一边是string,另一边是number

==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:

  • 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false

  • 判断的是否是null和undefined,是的话就返回true

  • 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较

  • 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较

  • 判断Boolean和String,都会就将其转换成Number,再进行比较

  • 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较

console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true

null、undefined和其他类型的比较

  • nullundefined宽松相等的结果为true,这一点大家都知道

其次,nullundefined都是假值,那么

  null == false // false
  undefined == false // false

为什么呢? 首先,false转为0,然后呢? 没有然后了,ECMAScript规范中规定nullundefined之间互相宽松相等(==),并且也与其自身相等,但和其他所有的值都不宽松相等(==)

如何让if(a == 1 && a == 2)条件成立?

      let a = {
        value: 1,
        valueOf: function () {
          return this.value++
        }
      }

      console.log(a == 1 && a == 2)

生成器Generator

async,await与forEach引发的血案

迭代器Iterator


JS核心

闭包

闭包是指有权访问另外一个函数作用域中的变量的函数

当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。

闭包产生的原因?

在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

闭包产生的本质就是,当前环境中存在指向父级作用域的引用

如何形成闭包

// 方式一
function f1() {
  var a = 2
  function f2() {
    console.log(a);//2
  }
  return f2;
}
var x = f1();
x();

// 方式一
// 外面的变量`f3存在着父级作用域的引用`,因此产生了闭包
var f3;
function f1() {
  var a = 2
  f3 = function() {
    console.log(a);
  }
}
f1();
f3();

闭包的实际应用

  1. 返回一个函数。刚刚已经举例。
  2. 作为函数参数传递
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();
  1. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){
  console.log('111');
},100)

// 事件监听
$('#app').click(function(){
  console.log('DOM Listener');
})
  1. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window当前函数的作用域,因此可以全局的变量。
var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

防抖节流

    <input type="text" id="ipt" />
    <div id="msg"></div>
    <div>
      <img src="" data-src="../pic/samuel-scrimshaw-361584-unsplash.jpg" alt="" />
    </div>
    ... 很多个img
    <div>
      <img src="" data-src="../pic/samuel-scrimshaw-361584-unsplash.jpg" alt="" />
    </div>

    <script>
      let ipt = document.querySelector('#ipt')
      let msg = document.querySelector('#msg')
      let images = document.querySelectorAll('img')
      // 防抖
      ipt.addEventListener('keyup', debounce(fn1, 500))
      function fn1(e) {
        msg.innerHTML = e.target.value
      }
      function debounce(callback, delay) {
        let timer = null
        return function () {
          let context = this
          if (timer) clearTimeout(timer)
          timer = setTimeout(() => {
            callback.call(context, ...arguments)
          }, delay)
        }
      }
      
      window.addEventListener('scroll', throttle(lazyLoad, 1000))
      // 首屏加载一次
      lazyLoad()
      // 懒加载
      function lazyLoad() {
        let viewHeight = document.documentElement.clientHeight //视口高度
        let scrollTop = document.documentElement.scrollTop //滚动条卷去的高度

        for (let i = 0; i < images.length; i++) {
          // 如果图片出现在视口中
          if (images[i].offsetTop < viewHeight + scrollTop) {
            images[i].src = images[i].getAttribute('data-src')
          }
        }
      }
      // 节流
      function throttle(callback, interval) {
        let flag = true
        return function () {
          let context = this
          if (!flag) return
          flag = false
          setTimeout(() => {
            callback.call(context, ...arguments)
            flag = true
          }, interval)
        }
      }

      // 防抖+节流
      function deThrottle(callback, delay) {
        let last = 0,
          timer = null
        return function () {
          let context = this
          let now = +new Date()
          if (now - last < delay) {
            clearTimeout(timer)
            timer = setTimeout(() => {
              callback.call(context, ...arguments)
              last = now
            }, delay)
          } else {
            // 这个时候表示时间到了,必须给响应
            callback.call(context, ...arguments)
            last = now
          }
        }
      }
    </script>

原型链

原型对象和构造函数有何关系?

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。

当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。

能不能描述一下原型链?

每一个实例对象又有一个__proto__属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有__proto__属性,这样一层一层往上找就形成了原型链。

JavaScript对象通过__proto__ 指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

image.png

成员的查找机制

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)。
  3. 如果还没有就查找原型对象的原型(Object的原型对象)。

... 依此类推一直找到 Object 为止(null)。

// Object是内置对象,只能添加方法,是不能覆盖对象的
Object.prototype.sing1 = function() {
    console.log('我会拍戏');
}
Object.prototype.uname1 = '你好啊';

function Star(uname, age) {
    this.uname = uname;
    this.age = age;
}
Star.prototype.sing = function() {
    console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
console.log(ldh.uname1);
console.log(ldh.__proto__);
// 1. 只要是对象就有__proto__ 原型, 指向原型对象
console.log(Star.prototype);

// 2.我们Star原型对象里面的__proto__原型指向的是 Object.prototype
console.log(Object.prototype.__proto__);

// 3. 我们Object.prototype原型对象里面的__proto__原型  指向为 null
console.log(Star.prototype.__proto__);

注意:

  • 对象的 Object.prototype.hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
  • Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)

ES6 类继承

// 父类有加法方法
class Father {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    sum() {
        console.log(this.x + this.y);
    }
}
// 子类继承父类加法方法 同时 扩展减法方法
class Son extends Father {
    constructor(x, y, z) {
        // 利用super 调用父类的构造函数
        // super 必须在子类this之前调用
        super(x, y);
        this.z = z;
    }
    
    say() { 
        // console.log('我是儿子'); 
        console.log(super.say() + '的儿子'); 
        // super.say() 就是调用父类中的普通函数 say() 
    }

    subtract() {
        console.log(this.x - this.y - this.z);
    }
}
var son = new Son(5, 3, 9);
son.subtract(); // -7
son.sum(); // 8

JS ES5实现继承

寄生组合继承

ES5继承.jpg

  function Parent () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child() {
    Parent.call(this);
    this.type = 'child';
  }
  
  // 注意:子构造函数 的 原型对象上 的 方法 通过继承之后 会直接被移除掉
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;

面向对象的设计一定是好的设计吗?

不一定。从继承的角度说,这一设计是存在巨大隐患的。

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

现在可以实现车的功能,并且以此去扩展不同的车。

但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。

如果让新能源汽车的类继承Car的话,也是有问题的。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。

这时,如果再创建一个父类,把加油的方法给去掉,也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。

那如何来解决继承的诸多问题呢?

用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。

顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

function drive(){
  console.log("wuwuwu!");
}
function music(){
  console.log("lalala!")
}
function addOil(){
  console.log("哦哟!")
}

let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);

代码干净,复用性也很好。这就是面向组合的设计方式。

面向过程与面向对象对比

面向过程面向对象
优点性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点不易维护、不易复用、不易扩展性能比面向过程低
  • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;

  • 面向对象是把构成问题事务分解成各个对象,然后由对象之间分工与合作。建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

V8引擎的内存机制

基本数据类型用存储,引用数据类型用存储。

注意:闭包变量是存在内存中的。

特点总结

  • 栈:

    • 存储基础数据类型
    • 按值访问
    • 存储的值大小固定
    • 由系统自动分配内存空间
    • 空间小,运行效率高
    • 先进后出,后进先出
    • 栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行。
    • 微任务和宏任务
  • 堆:

    • 存储引用数据类型
    • 按引用访问
    • 存储的值大小不定,可动态调整
    • 主要用来存放对象
    • 空间大,但是运行效率相对较低
    • 无序存储,可根据引用直接获取

为什么不全部用栈来保存呢?

  • 首先,对于系统栈来说,它的功能除了保存变量之外,还有创建并切换函数执行上下文的功能

  • 如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得巨大!

不过堆内存虽然空间大,能存放大量的数据,但与此同时垃圾内存的回收会带来更大的开销

V8引擎如何进行垃圾内存的回收

V8垃圾回收low.jpg

强引用和弱引用

弱引用和强引用图解

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

弱引用: 在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的: image.png

const myMap = new Map()
let my = {
    name: "jean",
    sex: "女"
}
myMap.set(my, 'info');
my=null

当执行my = null时会解除my对原数据的引用,如果是强引用关系则引用计数为 1 ,不会被垃圾回收机制清除。

再来看WeakMapimage.png

const myMap = new WeakMap()
let my = {
    name: "jean",
    sex: "女"
}
myMap.set(my, 'info');
my=null

myMap实例对象对my所引用对象是弱引用关系,该数据的引用计数为 0 ,程序垃圾回收机制在执行时会将引用对象回收。

V8 中执行一段JS代码的整个过程

  1. 首先通过词法分析和语法分析生成 AST
  2. 将 AST 转换为字节码
  3. 由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码, 以优化执行效率

事件循环机制

浏览器的事件循环

参考文章

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。 说说事件循环机制 image.png 栈:先进后出-----------------队列:先进先出

  1. 一开始整段脚本作为第一个宏任务执行

  2. 执行过程中同步代码放入执行栈中直接执行,宏任务进入宏任务队列,微任务进入微任务队列

  3. 当前宏任务执行完出栈,检查微任务队列,如果有则依次执行,直到微任务队列为空

  4. 执行浏览器 UI 线程的渲染工作

  5. 检查是否有Web worker任务,有则执行

  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

微任务macro-task和宏任务micro-task

微任务和宏任务都是属于队列,而不是放在栈中

在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务。

为什么要有微任务?

按照官方的设想,任务之间是不平等的,有些任务对用户体验影响大,就应该优先执行,而有些任务属于背景任务(比如定时器),晚点执行没有什么问题,所以设计了这种优先级队列的方式

利用微任务解决了两大痛点:

    1. 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
    1. 放到当前宏任务最后执行,解决了回调执行的实时性问题。

执行机制:

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

macro-task大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:

  • process.nextTick
  • Promise
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

async/await

参考文章

async就是promise的一种语法包装(所谓语法糖)

await其实是异步的,跟then差不多(从语法上来说,await其实就是promise的then

  1. 如果await 后面直接跟的为一个变量或者是同步任务

    • 如await 1或await console.log(222)

    • 这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)

(async ()=>{
  console.log(111);
  
  await console.log(222);
  console.log(333);
})()

// 相当于
(async ()=>{
  console.log(111);
  
  new Promise(resolve => { 
      console.log(222) 
      resolve() 
  }).then(()=>{
      console.log(333)
  })
})()

结果:111-222-333

再举个完整🌰

console.log('aaa');

setTimeout(()=>console.log('t1'), 0);
(async ()=>{
  console.log(111);
  await console.log(222);
  console.log(333);
  setTimeout(()=>console.log('t2'), 0);
})().then(()=>{
  console.log(444);
});

console.log('bbb');

aaa
111
222
bbb
333
444
t1
t2
  • 第1步、毫无悬念aaa,过

  • 第2步、t1会放入任务队列等待

  • 第3步、虽然async是异步操作,但async函数本身(也就是111所在的()=>{}),其实依然是同步执行的,除非有await出现,这个下面会说,所以,这里111直接同步执行,而不是放到队列里等待

  • 第4步、222这里很重要了,首先,console.log自己是同步的,所以立即就会执行,我们能直接看到222,但是await本身就是then,所以接下来的console.log(333);setTimeout(()=>console.log('t2'), 0);无法直接执行,就塞到微任务队列里等待了

  • 第5步、bbb毫无疑问,而且当前任务完成,优先执行微任务队列,也就是console.log(333)开始的那里

  • 第6步、执行333,然后定时器t2会加入任务队列等待(此时的任务队列里有t1和t2两个了),并且async完成,所以console.log(444)进入微任务队列等待

  • 第7步、优先执行微任务,也就是444,此时所有微任务都完成了

  • 第8步、执行剩下的普通任务队列,这时t1t2才会出来

  1. 如果await后面跟的是一个异步函数的调用,如下

    • 此时执行完awit并不先把await后面的代码注册到微任务队列中去

    • 而是执行完await之后,直接跳出async1函数,执行其他代码。

    • 然后遇到promise的时候,把promise.then注册为微任务。

    • 其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。

    • 所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end).

    • 可以理解为,这种情况下,await 后面的代码会在本轮循环的最后被执行.

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')
}, 0)

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})

console.log('script end')

输出结果:
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout

谈谈你对JS中this的理解

隐式绑定

  1. 全局上下文
  2. 直接调用函数
  3. 对象.方法的形式调用
  4. DOM事件绑定(特殊)
  5. new构造函数绑定
  6. 箭头函数

显示绑定

call/apply/bind

箭头函数和普通函数的区别

箭头函数的this指向规则:

  1. 箭头函数没有prototype(原型),所以箭头函数本身没有this

  2. 箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系。

  3. 不能直接修改箭头函数的this指向

    • 但是被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变
  4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)

箭头函数的arguments

  1. 如果箭头函数的this指向window(全局对象)使用arguments会报错,未声明arguments

    PS:如果你声明了一个全局变量为arguments,那就不会报错了,但是你为什么要这么做呢?

let b = () => {
  console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined
  1. 箭头函数的this如果指向普通函数,它的argumens继承于该普通函数。

rest参数(...扩展运算符)获取函数的多余参数

这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments

let a = (first, ...abc) => {
  console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);

rest参数的用法相对于arguments的优点:

  1. 箭头函数和普通函数都可以使用。

  2. 更加灵活,接收参数的数量完全自定义。

  3. 可读性更好

    参数都是在函数括号中定义的,不会突然出现一个arguments,以前刚见到的时候,真的好奇怪了!

  4. rest是一个真正的数组,可以使用数组的API。 注意:

  • rest必须是函数的最后一位参数:
  • 函数的length属性,不包括 rest 参数
let a = (first, ...rest, three) => {
  console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);

(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

使用new调用箭头函数会报错

无论箭头函数的thsi指向哪里,使用new调用箭头函数都会报错,因为箭头函数没有constructor

let a = () => {};
let b = new  a(); // a is not a constructor

箭头函数不支持new.target

new.target是ES6新引入的属性,普通函数如果通过new调用,new.target会返回该函数的引用。

此属性主要:用于确定构造函数是否为new调用的

更多解释

  1. 箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错

    let a = () => {
      console.log(new.target); // 报错:new.target 不允许在这里使用
    };
    a();
    
  2. 箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。

    new bb();
    function bb() {
      let a = () => {
        console.log(new.target); // 指向函数bb:function bb(){...}
      };
      a();
    }
    

JS深入

函数的arguments为什么不是数组?如何转化成数组?

因为arguments本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从0开始排,依次为0,1,2...最后还有callee和length属性。我们也把这样的对象称为类数组。

常见的类数组还有:

    1. 用getElementsByTagName/ClassName()获得的HTMLCollection
    1. 用querySelector获得的nodeList

1. Array.prototype.slice.call()

function sum(a, b) {
  let args = Array.prototype.slice.call(arguments);
}
sum(1, 2);//3
复制代码

2. Array.from()

从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

function sum(a, b) {
  let args = Array.from(arguments);
}
sum(1, 2);//3

这种方法也可以用来转换Set和Map哦!

扩展:数组去重合并

function combine(){
    let arr = [].concat.apply([], arguments);  //没有去重复的新数组
    return Array.from(new Set(arr));
}

var m = [1, 2, 2], n = [2,3,3];
console.log(combine(m,n));       

3. ES6展开运算符

function sum(a, b) {
  let args = [...arguments];
}
sum(1, 2);//3

4. 利用concat+apply

function sum(a, b) {
  let args = Array.prototype.concat.apply([], arguments);//apply方法会把第二个参数展开
  console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3

forEach

forEach实现原理

      //  forEach的实现原理
      Array.prototype.baseForEach = function (callback) {
        for (let i = 0; i < this.length; i++) {
          callback(this[i], i, this)
        }
      }

如何中断forEach循环?

官方推荐方法(替换方法):用every和some替代forEach函数。every在碰到return false的时候,中止循环。some在碰到return true的时候,中止循环

  1. 使用try监视代码块,在需要中断的地方抛出异常。
      let arr = [0, 1, 'stop', 3, 4]
      try {
        arr.forEach((item, index) => {
          if (item === 'stop') {
            throw new Error('中断')
          }
          console.log(index) // 输出 0 1 后面不输出
        })
      } catch (err) {
        console.log(err.message) // 中断
      }
  1. 手写forEach中断循环
      //  手写实现退出循环
      Array.prototype.myForEach = function (fn) {
        for (let i = 0; i < this.length; i++) {
          let ret = fn(this[i], i, this)

          // 如果没有return,或return后面没有值,结果都是undefined,不会中断
          // 让return false,或return null,就会中断循环
          if (ret !== undefined && (ret === null || ret === false)) break
        }
      }

当 forEach 遇上了 async 与 await

现象: keys 的每一项都是age。理由只有一个:forEach 循环没有等待 await 的执行。

原因: 从上面的baseForEach实现原理,可以看到 myForEach 的参数 callback 是一个异步函数,但是在内部进行调用的时候并没有使用 await 关键字来等待异步执行的结果,而是直接进行循坏,所以当然不会拿到结果。

      let arr = ['jean', '17']
      let keys = ['name', 'age']
      let i = 0
      let obj = {}
      arr.forEach(async (item, index) => {
        i++
        const value = await Promise.resolve(item)
        obj[keys[i]] = value
        console.log(obj)
      })
      // {age: "jean"}
      // {age: "17"}

解决方案

  1. 改造我们的 myForEach 函数
  • 让 callback 函数不要立即执行就好,而是等待异步任务的状态改变后执行
Array.prototype.myForEach = async function(callback, thisArg) {
  for (let i = 0; i < this.length; i ++) {
    await callback(this[i], i, this)
  }
}

// 用myForEach来循环
arr.myForEach(async (item, index) => {
  i++
  const value = await Promise.resolve(item)
  obj[keys[i]] = value
  console.log(obj)
})
// {name: "jean"}
// {name: "jean", age: "17"}
  1. 普通的 for 循环、for ... in ... for ... of ... ,这些循环结构都会在本身得异步函数作用域中执行,可以在其内部直接添加 await 关键字获取到值。

JS判断数组中是否包含某个值

1.Array.prototype.indexOf

此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。

注意:只能判断普通数据类型!!因为普通数据类型,匹配值;复杂数据类型,匹配地址

    # 基本的数组
    let a = [1,2,3,4,5]
    //判断a当中有没有2
    //方式1.通过indexOf
    let i = a.indexOf(2)
    console.log(i!==-1)//索引
    
    # 复杂数组(对象数组)
    let a = [{ name: 1 }, { name: 2 }, { name: 3 }]
    //判断a当中有没有name为2
    //方式1.通过indexOf,不可以用
    let i = a.indexOf({name: 2})//普通数据类型,匹配值;复杂数据类型,匹配地址
    console.log(i)//-1,

2.Array.prototype.includes(searcElement[,fromIndex])

此方法判断数组中是否存在某个值,如果存在返回true,否则返回false

注意:只能判断普通数据类型!!因为普通数据类型,匹配值;复杂数据类型,匹配地址

    # 基本的数组
    //方式2-通过inclueds.
    let i = a.includes(2)
    console.log(i)// true
    
    # 复杂数组(对象数组)
    //方式2-通过inclueds,,不可以用
    let i = a.includes({name: 2})//普通数据类型,匹配值;复杂数据类型,匹配地址
    console.log(i)// false

3.Array.prototype.find(callback[,thisArg])

返回数组中满足条件的第一个元素的值,如果没有,返回undefined

    # 基本的数组
    //方式3-find
    let i = a.find(t=>t===2)
    console.log(i) //2
    
    # 复杂数组(对象数组)
    //方式3-find-可以
    let i = a.find(t=>t.name===2)
    console.log(i) //{name: 2}

4.Array.prototype.findeIndex(callback[,thisArg])

返回数组中满足条件的第一个元素的下标,如果没有找到,返回-1]

    # 基本的数组
    //方式4-findIndex
    let i = a.findIndex(t=>t === 2)
    console.log(i)//索引
    
    # 复杂数组(对象数组)
    //方式4-findIndex-可以
    let i = a.findIndex(t=>t.name === 2)
    console.log(i)//索引

5.Array.prototype.some(callback[,thisArg])

此方法判断数组中是否存在某个值,如果存在返回true,否则返回false

    # 基本的数组
    //方式5-some:是否有一个满足...条件
    let i = a.some(t=>t === 2)
    console.log(i)//true
    
    # 复杂数组(对象数组)
    //方式5-some:是否有一个满足...条件,true
    let i = a.some(t => t.name === 2)
    console.log(i) //true

JS中flat---数组扁平化

JS数组的高阶函数

Set 集合

数组去重

  • 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组
  • ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值
  • Set 本身是一个构造函数,用来生成 Set 数据结构
  • 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法
const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);   // 2 3 5 4
}

// 去除数组的重复成员
let array = [1,2,1,4,5,3];
[...new Set(array)]     // [1, 2, 4, 5, 3]

Set实例的属性和方法

  • Set的属性:

    • size:返回集合所包含元素的数量
  • Set的方法:

    • 操作方法

      • add(value):向集合添加一个新的项
      • delete(value):从集合中移除一个值
      • has(value):如果值在集合中存在,返回true,否则false
      • clear(): 移除集合里所有的项
    • 遍历方法

      • keys():返回一个包含集合中所有键的数组
      • values():返回一个包含集合中所有值的数组
      • entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了)
      • forEach():用于对集合成员执行某种操作,没有返回值

Map 字典

数据存储 集合又和字典有什么区别呢:

  • 共同点:集合、字典可以存储不重复的值
  • 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储

所以这一下让我们明白了,Map其实的主要用途也是用于存储数据的,相比于Object只提供 '字符串—值的对应,Map提供了“值—值”的对应。也就是说如果你需要“键值对”的数据结构,Map比Object更合适

const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

Map的属性和方法

属性:

  • size:返回字典所包含的元素个数

操作方法:

  • set(key, val): 向字典中添加新元素
  • get(key):通过键值查找特定的数值并返回
  • has(key):如果键存在字典中返回true,否则false
  • delete(key): 通过键值从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法:

  • keys():将字典中包含的所有键名以数组形式返回
  • values():将字典中包含的所有数值以数组形式返回
  • forEach():遍历字典的所有成员

JS手动实现

模拟实现JS的new操作符

new 做了什么?

(1)创建一个新的对象

(2)将构造函数的作用域赋给新的对象(因此this就指向了这个新对象)

(3)执行构造函数中的代码(为这个新对象添加属性)

(4)绑定原型

(5)返回新对象

如何实现new

  1. 创建了一个全新的对象。

  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。

  3. 生成的新对象会绑定到函数调用的this

  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。

  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

      /**
       * 模拟实现 new 操作符
       * @param  {Function} ctor [构造函数]
       * @return {Object|Function|Regex|Date|Error}      [返回结果]
       * **/
      function newOperator(ctor) {
        //  0.判断ctor是否是个函数
        if (typeof ctor !== 'function') {
          throw new TypeError('newOperator function the first param must be a function')
        }
        // 1.创建了一个全新的对象。2. 4.被[[Prototype]]链接到这个函数的prototype对象上。
        const newObj = Object.create(ctor.prototype)
        // 2.拿到传入的参数 args
        const [first, ...args] = arguments
        // 3.生成的新对象会绑定到函数调用的`this`。
        const ctorReturnResult = ctor.apply(newObj, args)
        // 5.判断 是否返回对象类型Object|Function,

        //  先判断return结果是不是 Object|Function
        const isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null
        const isFunction = typeof ctorReturnResult === 'function'
        if (isObject || isFunction) {
          // 5.1如果是会返回 这个return的结果
          return ctorReturnResult
        }
        // 5.2如果不是,就直接返回newObj这个对象
        return newObj
      }
      function Student(name) {
        this.name = name
        // return {}
      }
      var student = newOperator(Student, 'jean')
      console.log(student)

手写call

      // !!注意:不能使用let,因为let声明的变量不会挂载到window上
      var name = '这是window下的name'
      Function.prototype.myCall = function (context) {
        // this指的就是getName这个函数 ---- ƒ () {return this.name}
        // 1. 先判断 调用的函数 是不是 个function
        if (typeof this !== 'function') {
          throw new TypeError('Error')
        }
        // 2. 再判断有没有传入 context
        context = context || window
        // const result = this() 此时调用这个函数 的是window

        // 3. 假设 context的fn属性 指向了 该函数
        context.fn = this

        // 4. 实现传参  拿掉取出 第一个参数【this的指向--context】 的后面传的参数
        const args = [...arguments].splice(1)

        // arguments --- [0: {name: "jean", fn: ƒ}, 1: 1, 2: 2, 3: 3]
        // args --- [1,2,3]

        // 5. 此时调用这个函数的就是 context了
        const result = context.fn(...args)

        // 6. 记得 执行函数完后 ,要把先前添加的fn属性delete掉
        delete context.fn
        return result
      }

      let obj = {
        getName: function () {
          console.log([...arguments]) // [1, 2, 3]
          return this.name + [...arguments] // jean1,2,3
        }
      }

      let obj2 = {
        name: 'jean'
      }
      console.log(obj.getName.myCall(obj2, 1, 2, 3))

手写apply

    <script>
      // !!注意:不能使用let,因为let声明的变量不会挂载到window上
      var name = '这是window下的name'

      Function.prototype.myApply = function (context) {
        // 先判断是不是个函数
        if (typeof this !== 'function') {
          throw new TypeError('Error')
        }
        // 再判断有没有 传入this需要指向的对象
        context = context || window

        // 给context临时添加一个新的fn属性,指向this
        context.fn = this

        // 处理传入的参数, 拿到传入的第二个参数 即为数组
        const args = arguments[1]
        // args--- [1, 2, 3]

        let result
        // 判断有没有传入数组 形参
        if (args) {
          result = context.fn(...args)
        } else {
          result = context.fn()
        }

        delete context.fn
        return result
      }

      let obj = {
        getName: function () {
          console.log(this.name + [...arguments])
        }
      }

      let obj2 = {
        name: 'jean'
      }

      obj.getName.myApply(obj2, [1, 2, 3])
    </script>

手写bind

  1. 对于普通函数,绑定this指向【如下述代码】
  2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失 参考链接
    <script>
      var name = 'window下的name'
      Function.prototype.myBind = function (context) {
        // 1. 先判断this是不是个函数
        if (typeof this !== 'function') {
          throw new TypeError('Error')
        }
        // 2. 判断有没有传入 this的指向
        context = context || window

        // 保存this的值,它代表调用 bind 的函数
        const _this = this
        // 3. 拿到参数 删除第一个
        const args = [...arguments].slice(1)

        // 返回的是一个函数
        return function () {
          // 再调用apply
          return _this.apply(context, args.concat(...arguments))
        }
      }
      let obj = {
        getName: function () {
          console.log(this.name + [...arguments])
        }
      }

      let obj2 = {
        name: 'jean'
      }

      const fn = obj.getName.myBind(obj2)
      fn()
    </script>

赋值|浅拷贝|深拷贝的区别

针对简单数据类型

拷贝的是值,是深拷贝

针对引用数据类型

参考文章

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

  • 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响

拷贝.jpg

实现浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

1.手动实现

function shallowClone(target) {
  //  1.1先判断是否为引用类型数据
  if (typeof target === 'object' || target != null) {
    // 2.1判断是数组 还是 对象, 初始化数据
    const cloneTarget = Array.isArray(target) ? [] : {}
    for (let key in target) {
      //   2.2判断 key 是否是 target 的自身属性,而不是object的继承属性
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = target[key]
      }
    }
    return cloneTarget
  } else {
    //   1.2 如果是简单数据类型或为null,直接赋值即可
    return target
  }
}

2.Array.prototype.slice()

let arr = [1, 3, {
    username: ' jean'
    }];
let arr3 = arr.slice();
arr3[2].username = 'ximi'
console.log(arr); // [ 1, 3, { username: 'ximi' } ]

3.Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    

4.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "jean", age: 41},sports:'basketball' };

let obj2 = Object.assign({}, obj1);
obj2.person.name = "ximi";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'ximi', age: 41 }, sports: 'basketball' }
```-开运算符...
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

```js
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}

6.函数库lodash的_.clone方法

该函数库也有提供_.clone用来做 Shallow Copy,利用这个库也实现深拷贝。

终端下载lodash

npm i lodash

在node.js 或 vue-cli环境下,引入插件并使用

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true

实现深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

大佬的深拷贝

1.JSON.parse(JSON.stringify(obj))

注意:

  • 会忽略undefined Symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能正确处理 new Date()
  • 不能处理正则

2.jQuery.extend()

3.lodash.cloneDeep()

4.如何写出一个惊艳面试官的深拷贝?

      function deepClone(target, map = new WeakMap()) {
        if (typeof target === 'object' && target !== null) {
          let cloneTarget = Array.isArray(target) ? [] : {}
          // 判断是否有循环引用
          if (map.get(target)) {
            return map.get(target)
          }
          map.set(target, cloneTarget)
          for (let key in target) {
            if (target.hasOwnProperty(key)) {
              cloneTarget[key] = deepClone(target[key], map)
            }
          }
          return cloneTarget
        } else {
          return target
        }
      }

强引用与弱引用

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放

再来看WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;

如果是WeakMap的话,targetobj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

手写promise

// MyPromise.js

// 先定义三个常量表示状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 新建 MyPromise 类
class MyPromise {
  constructor(executor) {
    // executor 是一个执行器,进入会立即执行
    // 并传入resolve和reject方法
    try {
      executor(this.resolve, this.reject)
    } catch (error) {
      this.reject(error)
    }
  }

  // 储存状态的变量,初始值是 pending
  status = PENDING
  // 成功之后的值
  value = null
  // 失败之后的原因
  reason = null

  // 存储成功回调函数
  onFulfilledCallbacks = []
  // 存储失败回调函数
  onRejectedCallbacks = []

  // 更改成功后的状态
  resolve = value => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态修改为成功
      this.status = FULFILLED
      // 保存成功之后的值
      this.value = value
      // resolve里面将所有成功的回调拿出来执行
      while (this.onFulfilledCallbacks.length) {
        // Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空
        this.onFulfilledCallbacks.shift()(value)
      }
    }
  }

  // 更改失败后的状态
  reject = reason => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态成功为失败
      this.status = REJECTED
      // 保存失败后的原因
      this.reason = reason
      // resolve里面将所有失败的回调拿出来执行
      while (this.onRejectedCallbacks.length) {
        this.onRejectedCallbacks.shift()(reason)
      }
    }
  }

  then(onFulfilled, onRejected) {
    // 默认值处理
    const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    const realOnRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason
          }

    // 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去
    const promise2 = new MyPromise((resolve, reject) => {
      const callbackMicrotask = callback => {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            const value = callback === realOnFulfilled ? this.value : this.reason
            // 获取成功回调函数的执行结果
            const x = callback(value)
            //  集中处理
            // 如果相等了,说明return的是自己,抛出类型错误并返回
            if (promise2 === x) {
              return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
            }
            // 判断x是不是 MyPromise 实例对象
            if (x instanceof MyPromise) {
              // 执行 x,调用 then 方法,目的是将其状态变为 fulfilled 或者 rejected
              // x.then(value => resolve(value), reason => reject(reason))
              // 简化之后
              x.then(resolve, reject)
            } else {
              // 普通值
              resolve(x)
            }
          } catch (error) {
            reject(error)
          }
        })
      }
      // 判断状态
      if (this.status === FULFILLED) {
        callbackMicrotask(realOnFulfilled)
      } else if (this.status === REJECTED) {
        callbackMicrotask(realOnRejected)
      } else if (this.status === PENDING) {
        // 等待
        // 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来
        // 等到执行成功失败函数的时候再传递
        this.onFulfilledCallbacks.push(function onFulfilledCallback() {
          callbackMicrotask(realOnFulfilled)
        })
        this.onRejectedCallbacks.push(function onRejectedCallback() {
          callbackMicrotask(realOnRejected)
        })
      }
    })

    return promise2
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
  // resolve 静态方法 Promise.resolve()
  static resolve(parameter) {
    // 如果传入 MyPromise 就直接返回
    if (parameter instanceof MyPromise) {
      return parameter
    }

    // 转成常规方式
    return new MyPromise(resolve => {
      resolve(parameter)
    })
  }

  // reject 静态方法
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }

  // Promise函数对象的 all 方法,接受一个promise类型的数组
  // 返回一个新的Promise对象
  static all(promises) {
    // 保证返回的值得结果的顺序和传进来的时候一致
    // 只有全部都成功长才返回成功
    const values = new Array(promises.length) //保存所有的value
    let successCount = 0
    return new MyPromise((resolve, reject) => {
      promises.forEach((p, index) => {
        // 由于p有可能不是一个Promise
        MyPromise.resolve(p).then(
          value => {
            successCount++
            values[index] = value
            // 拿到了全部的成功结果
            if (successCount === promises.length) {
              resolve(values)
            }
          },
          // 如果失败,状态就会发生变化,就不会再往后执行了
          reason => {
            reject(reason)
          }
        )
      })
    })
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(p => {
        // 由于p有可能不是一个Promise
        MyPromise.resolve(p).then(
          value => {
            resolve(value)
          },
          reason => {
            reject(reason)
          }
        )
      })
    })
  }
}