简述前端基础面试

一、vue相关

1、为什么data是一个函数?(防止数据在组件之间共享)

组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。

2、vue的生命周期一共有beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed、activated以及deactivated十个,那vue的异步请求在哪一步发起?

1、vue组件可以在created、beforeMount、mounted这三个钩子函数中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。

2、如果异步请求不需要 依赖 Dom , 推荐在 created 钩子函数中发起异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
    1> 能更快获取到服务端数据,减少页面 loading 时间;
    2> SSR(服务端渲染) 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

3、display:none、visibility:hidden 和 opacity:0 之间的区别?

    display:none会产生回流与重绘,visibility:hidden 和 opacity:0 只会产生重绘。

4、v-if 和 v-for 为什么最好不要一起使用?

v-for 和 v-if 不要在同一个标签中一起使用,因为 v-for 比 v-if 的优先级高,解析时先解析 v-for 再解析 v-if,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果遇到需要
同时使用时可以考虑写成 计算属性 的方式。

5、怎样理解vue的单向数据流?

在Vue组件中,父子组件之间的关系可以概括为props向下传递、事件向上传递;父组件可以通过props向子组件传递数据,子组件可以通过$emit来向父组件发送消息,当父组件数据变化时,
可以传递给子组件,反之不能,防止子组件无意间修改父组件的状态,导致数据流向的不清晰性,如果子组件想修改父组件的数据,可以采用一种“闭环”的思路:父组件将数据传递给子组件,
子组件可以通过事件来通知父组件修改数据,在传递给子组件。

6、computed 和 watch 以及 methods 的区别和运用的场景

  • 相同点:
    都是以函数为基础
  • 不同点:
    • 定义

      1、computed 是计算属性,属于自定义属性,依赖其他属性(依赖来自data中的值来进行计算),从而返回一个新的值;

      2、watch 是一个对象,键是需要观察的表达式,值是对应回调函数,值也可以是方法名,或者包含选项的对象;

      3、methods 是用来定义函数的;

    • 特点
      1、computed 可以通过get与set方法创建数据的双向数据绑定,并且支持缓存机制,且computed的变量不可与data中的变量冲突,可以监控到数组与对象的变化(“自动”变化);
      
      2、watch 监听的属性需要是已经存在的,存在于data中,或者是computed中;可以执行异步操作,一般使用watch做一些逻辑复杂或者是异步操作的事情,以及监听到这个数据
      变化之后去做一些别的事情(“自动”的变化);

      3、methods 每次页面进行重新渲染的时候,都会去执行一次.这样做开销比较大,如果不希望页面有缓存的情况下,可以这么做(它需要手动调用才能执行);
    • 使用场景
      1、computed 语义上是计算,适用于一个数据受多个数据影响,一般用在模板渲染中;
      
      2、watch 语义上是观察,观测某个值的变化去完成一段复杂的业务逻辑;

      3、methods  

7、vue如何检测数组和对象的变化?

  • 源码解析
    • 数据的初始化
      //创建Vue实例的时候,Vue是一个构造函数,里面的参数是一个对象 options(选项)
      new Vue({
        el: "#app",
        router,
        store,
        render: (h) => h(App),
      });
    • 源码解析:参考文章https://www.cnblogs.com/wxh0929/p/13839758.html
    • 是否能在data里面直接使用 prop 的值,为什么?
      vue实例初始化状态(initState)时候的顺序依次是:prop > methods > data > computed > watch
    • 数据的初始化:
      function initData(vm) {
        let data = vm.$options.data;
        // 实例的_data属性就是传入的data
        // vue组件data推荐使用函数 防止数据在组件之间共享
        data = vm._data = typeof data === "function" ? data.call(vm) : data || {};
      
        // 把data数据代理到vm 也就是Vue实例上面 我们可以使用this.a来访问this._data.a
        for (let key in data) {
          proxy(vm, `_data`, key);
        }
        // 对数据进行观测 --响应式数据核心
        observe(data);
      }
      // 数据代理
      function proxy(object, sourceKey, key) {
        Object.defineProperty(object, key, {
          get() {
            return object[sourceKey][key];
          },
          set(newValue) {
            object[sourceKey][key] = newValue;
          },
        });
      }
    • 思考:Object.defineProperty 有什么缺点???           

                                

    • 响应式数据的核心 ----- defineReactive 函数,主要使用 Object.defineProperty 来对数据 get 和 set 进行劫持,这里就解决了之前的问题,为啥数据变动了会自动更新视图,我们可以在 set 里面去通知视图更新(由于数组元素过多时,性能可能会受到影响,所以此方法只用来劫持对象,但是对象新增或者删除的属性无法被 set 监听到,只有对象本身存在的属性修改才会被劫持)。
      // src/obserber/index.js
      class Observer {
        // 观测值
        constructor(value) {
          this.walk(value);
        }
        walk(data) {
          // 对象上的所有属性依次进行观测
          let keys = Object.keys(data);
          for (let i = 0; i < keys.length; i++) {
            let key = keys[i];
            let value = data[key];
            defineReactive(data, key, value);
          }
        }
      }
      // Object.defineProperty数据劫持核心 兼容性在ie9以及以上
      function defineReactive(data, key, value) {
        observe(value); // 递归关键
        // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
        // 思考?如果Vue数据嵌套层级过深 >>性能会受影响
        Object.defineProperty(data, key, {
          get() {
            console.log("获取值");
            return value;
          },
          set(newValue) {
            if (newValue === value) return;
            console.log("设置值");
            value = newValue;
          },
        });
      }
      export function observe(value) {
        // 如果传过来的是对象或者数组 进行属性劫持
        if (
          Object.prototype.toString.call(value) === "[object Object]" ||
          Array.isArray(value)
        ) {
          return new Observer(value);
        }
      }
    • 数组的观测
      • 背景

由于数组考虑性能方面的原因没有用 Object.defineProperty 对数组的每一项进行拦截,而是选择对数组原型上的7种(push、pop、shift、unshift、splice、sort、reverse)方法进行重写(AOP 切片思想)

所以在 Vue 中修改数组的索引和长度是无法监控到的,需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新

8、vue的父子组件生命周期钩子函数的执行顺序?

1>加载渲染过程

父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

2>子组件更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

3>父组件更新过程

父 beforeUpdate->父 updated

4>销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

9、虚拟Dom是什么,有什么优缺点?

  • 概念:虚拟Dom 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象;
  • 为什么虚拟Dom会提高性能?
    因为虚拟Dom相当于在js和真实的Dom中间加了一个缓存,利用Dom diff算法避免了没有必要的Dom操作,从而提高性能,具体的实现步骤:
    
    1、用JavaScript对象结构表示Dom树的结构,然后用这个树构建一个真正的Dom树,插到文档中;
    2、当状态变更的时候,重新构造一颗新的对象树,然后用新的树和旧的树进行比较,记录两棵树的差异;
    3、把2所记录的差异应用到步骤1所构建的真正的Dom树上,视图就更新了;
  • 虚拟Dom 的缺点
    1、无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化;
    
    2、首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢;
  • diff算法?
    1、把树形结构按照层级分解,只比较同级元素;
    
    2、给列表结构的每个单元添加唯一的key属性,方便比较;

    3、React只会匹配相同class的component(class指的是组件的名字)

    4、合并操作,调用component的setState方法的时候,React将其标记为dirty,到每一个事件循环结束,React会检查所有标记dirty的component重新绘制;

    5、选择性子树渲染,开发人员可以重写shouldComponentUpdate提高diff算法的性能;

10、vue-router的两种模式的区别(核心:跳转URL更新视图但不重新请求/加载页面)

  • hash模式:

    在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取; 特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xiaogangzai.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

    history模式:这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面

    history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

  • 参考文章

11、Vue.mixin的原理

当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”

12、nextTick的原理

  • 产生原因:当用户修改数据时,dom 的更新并不会立即执行,而是采用异步队列的方式批量更新视图,Vue 将开启一个队列,并缓冲在【同一事件循环】中发生的所有数据改变。如果同一个观察者(watcher)被多次触发,只会被推入到队列中一次。这种缓冲行为可以避免不必要的计算和 DOM 操作。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际(已去重的)工作,统一进行视图更新
  • 作用:处理 vue 里面的【异步渲染】【数据 ===》视图】
  • 场景:

     1、vue生命周期的created中进行的dom操作一定要放在nextTick中,因为created()钩子函数执行dom并没有进行渲染,这时要进行dom操作无效

    2、当你修改了响应式 data 数据的值,然后需要马上获取这个 dom元素的值,是不获取更新后的值。需要使用 $nextTick 回调获得更新后的 dom

响应式数据变化后想要立即获取dom更新后的状态,比如

1、更新后列表的高度;

2、在页面上的弹框组件,在设置弹框的 visible 为 true 时,获取弹框里的某个 dom 元素拿不到,用了nexttick就可以

  • 主要思路就是采用 微任务优先的方式(
    Promise -> MutationObserver -> setImmediate -> setTimeout
      调用异步方法 去执行 nextTick 包装的方法,如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false;
  • // src/util/next-tick.js
    
    let callbacks = [];
    let pending = false;
    function flushCallbacks() {
      pending = false; //把标志还原为false
      // 依次执行回调
      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i]();
      }
    }
    let timerFunc; //定义异步方法  采用优雅降级
    if (typeof Promise !== "undefined") {
      // 如果支持promise
      const p = Promise.resolve();
      timerFunc = () => {
        p.then(flushCallbacks);
      };
    } else if (typeof MutationObserver !== "undefined") {
      // MutationObserver 主要是监听dom变化 也是一个异步方法
      let counter = 1;
      const observer = new MutationObserver(flushCallbacks);
      const textNode = document.createTextNode(String(counter));
      observer.observe(textNode, {
        characterData: true,
      });
      timerFunc = () => {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
      };
    } else if (typeof setImmediate !== "undefined") {
      // 如果前面都不支持 判断setImmediate
      timerFunc = () => {
        setImmediate(flushCallbacks);
      };
    } else {
      // 最后降级采用setTimeout
      timerFunc = () => {
        setTimeout(flushCallbacks, 0);
      };
    }

13、keep-alive

  • 概念:
    keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中,使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • 属性:
    include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用 LRU的策略 置换缓存数据。
  • LRU(最近最少使用算法----队列)
    LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据
    即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件(头部数据丢失)。
  • 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象this.cache中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置
  • 参考文章

14、首屏加载优化的方案

1 Vue-Router路由懒加载(利用Webpack的代码切割)(如果使用了一些UI库,采用按需加载)
2 使用CDN加速,将通用的库从vendor进行抽离
3 Nginx的gzip压缩、Webpack开启gzip压缩
4 Vue异步组件
5 服务端渲染SSR
6 如果首屏为登录页,可以做成多入口
7 Service Worker缓存文件处理
8 使用link标签的rel属性设置   prefetch(这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低,prefetch通常用于加速下一次导航)、
preload(preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度)

15、SSR

  • 概念
    SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端;
  • 优点
    • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题;
    • 首屏加载速度快:首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。
  • 缺点
    • 服务端压力较大:本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源;

    • 开发条件受限:在服务端渲染中,created和beforeCreate之外的生命周期钩子不可用,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境;
    • 学习成本相对较高:除了对webpack、Vue要熟悉,还需要掌握node、Express相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

16、v-model的原理

  • 示例:
    / 定义 v-model 示例组件
    Vue.component('bindData', {
      template:`
        <div>
          <p>this is bindData component!</p>
                <button @click="handleChange">change input value</button>
          <input type="text" v-model="inputValue" />
                <p>{{inputValue}}</p>
        </div>
      `,
      data() {
        return {
          inputValue: 'hello'
        }
      },
      methods:{
        // 点击按钮改变 input 的值
        handleChange() {
          this.inputValue = `I'm changed`;
        }
      }
    });
    
    const app=new Vue({
      el: '#app',
      template: `
        <div>
          <bindData />
        </div>
      `
    });
    View Code
    // 定义 v-model 示例组件改写
    Vue.component('bindData1', {
      template:`
        <div>
          <p>this is bindData1 component!</p>
                <button @click="handleChange">change input value</button>
          <input type="text" :value="inputValue" @change="handleInputChange" />
                <p>input 中的值为:{{inputValue}}</p>
        </div>
      `,
      data() {
        return {
          inputValue: 'hello'
        }
      },
      methods:{
        // 处理 input 输入 change 事件
        handleInputChange(e) {
          this.inputValue = e.target.value;
        },
        
        // 点击按钮改变 input 的值
        handleChange() {
          this.inputValue = `I'm changed`;
        }
      }
    });
    
    const app=new Vue({
      el: '#app',
      template: `
        <div>
          <bindData1 />
        </div>
      `
    });
    View Code

    第二个主要改动:

    • 1、将 input 标签上的 v-model 指令去掉了,换成了用 v-bind:value(缩写 :value) 指令来绑定 inputValue 属性,并且加上了一个 v-on:change(缩写 ) 事件;

    • 2、添加了一个 input change 处理函数,函数逻辑是将   当前 input 标签的值   赋值给 $data 的 inputValue 属性

  • 总结

       v-model 只是一种封装或者语法糖,负责监听用户的输入事件以更新数据,并对一些极端场景进行特殊处理,v-model 在不同的 HTML 标签上会使用v-bind监控不同的属性和使用v-on抛出不同的事件。

  • text 和 textarea 元素使用 value 属性和 input 事件;

  • checkbox 和 radio 使用 checked 属性和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件

17、vue.set的原理

Vue 的 $set 方法是 Vue 提供的一个全局方法,用于向响应式对象添加一个属性,并确保新属性同样是响应式的,也就是说,当这个属性变化时,视图会更新。它的实现原理基于 Vue 的响应式系统。

  • 原理
    class Observer {
      // 观测值
      constructor(value) {
        Object.defineProperty(value, "__ob__", {
          //  值指代的就是Observer的实例
          value: this,
          //  不可枚举
          enumerable: false,
          writable: true,
          configurable: true,
        });
      }
    }
    
    Vue.prototype.$set = function (target, key, value) {
      // 如果 target 是一个数组,使用 splice 方法触发数组的响应式
      if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, value);
        return value;
      }
      // 如果 target 已经有了这个属性,直接赋值
      if (key in target) {
        target[key] = value;
        return value;
      }
      // 如果 target 是一个 Vue 实例,将属性添加到实例的数据对象上
      if (target instanceof Vue) {
        target.$data[key] = value;
        return value;
      }
      // 如果 target 是一个普通的 JavaScript 对象,使用 Object.defineProperty 添加属性并设置为响应式
      Object.defineProperty(target, key, {
        value: value,
        enumerable: true,
        configurable: true
      });
      return value;
    };
       给每个响应式数据增加了一个不可枚举的__ob__属性,并且指向了 Observer 实例 ,那么我们 首先 可以根据这个属性来防止已经被响应式观察的数据反复被观测 ,其次 响应式数据可以使用__ob__来获取 Observer 实例的相关方法这对数组很关键,
当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象__ob__的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组

18、vue自定义指令的原理

  • 理解
    指令本质上是装饰器,是vue对HTML元素的扩展,给HTML元素增加自定义功能,语义化HTML标签。vue编译DOM时,会执行与指令关联的JS代码,即找到指令对象,执行指令对象的相关方法
  • 1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

    2.通过 genDirectives 生成指令代码

    3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

    4.当执行指令对应钩子函数时,调用对应指令定义的方法

 19、

 

 

20、

 

 

 

 

 

 

 

 

 

 

二、js相关

1、什么是闭包?

  • 作用域:作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)的规则 ----- 严格来讲是变量可以被访问到的空间
  • 作用域的分类:ES5包括(函数作用域以及全局作用域),ES6新增了块级作用域;
  • 作用域链:每个函数都有一个作用域,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称 作用域链;
  • 参考文章:
    1. https://juejin.cn/post/6844903606311714824
    2. https://juejin.cn/post/6844903612879994887
  • 闭包的基本原理
    在正常情况下,外界是无法访问函数内部的变量的,因为在函数执行完毕之后,上下文即将被销毁。但是如果我们在函数中,返回了另一个函数,且这个返回的函数使用了函数内的变量,
    那么外界便可以通过这个返回的函数获取原函数内部的变量值,这就是闭包的基本原理 ;
  • 闭包的概念
    有权访问另一个函数作用域中变量的函数,闭包的存在形式在于在一个函数中返回了另一个函数,并且通过另一个函数访问函数内部的变量;
  • 闭包的特点
    1 函数内嵌套函数;
    2 内部的函数可以访问外部的参数和变量;
    3 外部的参数和变量不会被垃圾回收机制回收;
  • 闭包的优点
    1 突破了作用域链,将函数内部的参数和变量传递到外部;
  • 闭包的缺点
  • 什么是内存泄漏
内存空间明明已经不在被使用,但由于某种原因并没有被释放的现象,内存泄漏的危害非常直观,它会直接导致程序运行缓慢,甚至崩溃。
  • JS哪些操作会引起内存泄漏???
    • 意外的全局变量引起的内存泄漏
    • 闭包引起的内存泄漏,参考文章:https://blog.csdn.net/Leo____Wang/article/details/80115050
      var a = [];
      
      for(var i = 0;i < 10;i++){
           a[i] = function(){
             console.log(i)
           }
      }
      
      a[6]()// 10
      
      //解析:var声明的变量在全局范围内有效,所以全局只有一个变量i
      
      
      解决方法
      1、加一层闭包,i 以函数参数形式传递给内层函数,自执行函数,第一个i是函数的形参,是私有变量,与外面的i没有关系,被私有作用域保护起来了,
      第二个i才是函数中外面的i(也就是说第一个i只是一个迷惑人的量,你改成k也是一样的结果,只不过是把i赋给k而已)
      var a = []; for(var i = 0;i < 10;i++){ (function(i){ a[i] = function(){ console.log(i) } })(i) } 2、加一层闭包,返回一个函数作为响应事件 var a = []; for(var i = 0;i < 10;i++){ a[i] = function(){ return function(){ console.log(i) } }(i) } 3、找个属性将i值保存起来 4、let
    • 没有清理的Dom元素引用
      var element = document.getElementById("element");
      element.marked = "marked";
      
      
      //移除节点
      function remove(){
        element.parentNode.removeChild(element);
      }
      
      //解析:只是移除了dom节点,但是element变量依然存在,需要添加element  = null;
    • 被遗忘的定时器或者回调
      function foo(){
          let name = 'lucas';
          window.setInterval(()=>{
            console.log(name);
          },1000)
      }
      
      foo();
      
      //解析:一般函数执行完毕之后,上下文即将被销毁,但是由于存在window.setInterval,所以name的内存空间始终无法释放,所以我们要在合适的时机使用
      clearInterval对其进行清理
    • 子元素存在引起的内存泄漏
      var element = document.getElementById("element");
      element.innerHTML = '<button id = 'button'>点击</button>'
      
      var button = document.getElementById("button");
      button.addEventListener('click',function(){
      
      })
      
      element.innerHTML = ''
      
      
      //解析:element.innerHTML = ''表示button已经从dom中移除,但是事件处理程序还在,所以变量依旧没有办法回收,所以我们需要添加removeEventListener移除事件,
      防止内存泄漏
  • 闭包总结
    所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包;
  • 闭包的注意事项
    1、因为闭包可以使函数中的参数和变量都保存在内存中,造成很大的内存消耗,所以如果不是某些特定的任务需要使用闭包,我们不要滥用它。
    
    2、使用不当的闭包会在  IE(IE9)之前  造成内存泄漏问题。因为它的JavaScript引擎  使用的垃圾回收算法是引用计数法,对于循环引用将会导致GC无法回收“应该被回收”的内存。
    造成了无意义的内存占用,也就是内存泄漏。

2、垃圾回收机制GC(Garbage Collection)

  • 内存管理:是指对内存生命周期的管理,而内存的生命周期无外乎分配内存、读写内存、释放内存,对于分配内存和读写内存的行为,所有语言都较为一致,但是释放内存的行为在不同语言之间有差异;
  • 内存空间的类型
    1 栈空间(操作系统自动分配释放,内存大小固定)
    2 堆空间(开发者分配释放,内存大小不一定,也不一定会自动释放)
  • 垃圾回收的概念
    GC把程序不用的内存空间视为垃圾,找到它们并且将它们回收,让程序员可以再次利用这部分空间,当然,不是所有的语言都有GC,一般存在于高级语言中,如Java、JavaScript、Python
    。那么在没有GC的世界里,程序员就比较辛苦,只能手动去管理内存,比如在C语言中我们可以通过malloc/free,在C++中的new/delete来进行管理
  • 垃圾回收算法
    • 引用计数法

                             

      • 概念
        引入了计数器的概念,通过计数器来表示对象的“人气指数”,也就是有多少个程序引用了这个对象。当计数器(引用数)为0时,垃圾立刻被回收
      • 优点
        1、可以立即回收垃圾,当被引用数值为0时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
        2、因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
        3、不用去遍历堆里面的所有活动对象和非活动对象
      • 缺点
        1、计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计数器就需要32位。
        2、最大的劣势是无法解决循环引用无法回收的问题 这就是前文中IE9之前出现的问题
      • 举例 ----- 循环引用
        function f(){
          var o = {};
          var o2 = {};
          o.a = o2; // o 引用 o2,o2的引用次数是1
          o2.a = o; // o2 引用 o,o的引用此时是1
        
          return "azerty";
        }
        
        f();

        解析:

        fn在执行完成之后理应回收fn作用域里面的内存空间,但是因为o里面有一个属性引用o2,导致o2的引用次数始终为1,o2也是如此,而又非专门当做闭包来使用,所以这里就应该使oo2被销毁。因为算法是将引用次数为0的对象销毁,此处都不为0,导致GC不会回收他们,那么这就是内存泄漏问题。

        该算法已经逐渐被 ‘标记-清除’ 算法替代,在V8引擎里面,使用最多的就是 标记-清除算法

    • GC标记-清除算法:是一个定时运行的任务(会按照固定的时间间隔周期性的进行),也就是说当程序运行一段时间后,统一GC

               

由标记阶段和清除阶段构成,标记阶段将所有的活动对象做上相应的标记,清除阶段把那些没有标记的对象,也就是非活动对象进行回收。在搜索对象并进行标记的时候使用了深度优先搜索,尽可能的从深度上搜索树形结构

      • 活动对象与非活动对象
        var a = {name: 'bar'} // '这个对象'被a引用,是活动对象。
        
        
        a=null; // ‘这个对象’没有被a引用了,这个对象是非活动对象。
      • 工作流程

                                 标记阶段:

                            

                                  解析:

        根可以理解成我们的全局作用域,GC从全局作用域的变量,沿作用域   逐层往里遍历(深度遍历或者深度优先搜索),当遍历到堆中对象时,说明该对象被引用着,则打上一个标记,继续递归遍历(因为肯定存在堆中对象引用另一个堆中对象),直到遍历到最后一个(最深的一层作用域)节点

                

                                  清除阶段:遍历整个堆,回收没有打上标记的对象

      • 优点
        1实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
        2、解决了循环引用的问题,因为两个对象从全局对象出发无法获取。因此,他们无法被标记,他们将会被垃圾回收器回收。              

                       

      • 缺点
        1造成碎片化(有点类似磁盘的碎片化),导致无数的小分块散布在堆的各个地方;

          2、分配速度,由于分块的不连续性,算法每次分配的时候都需要遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)为了找到足够大的分块,这样最糟糕的情况就是遍历到最后才找到合适的分 块,影响了分配速度;

      • 举例

3、箭头函数与普通函数的区别?

  • 箭头函数的特点
     1 语法更简洁
     2 没有this
     3 捕获其所在上下文的 this 值,作为自己的 this 4 箭头函数作为匿名函数,是不能作为构造函数的,不能使用 new 
     5 箭头函数没有原型属性,prototype是undefined
     6 不能简单返回对象,如果要返回对象时需要用小括号包起来,因为大括号被占用解释为代码块了
     7 箭头函数不能换行
     8 箭头函数不能当做Generator函数,不能使用yield关键字
     9 不绑定arguments,使用时报错arguments is not undefined,所以用 rest参数...解决
    10     /*箭头函数使用reset参数...解决*/
    11     let test3=(...a)=>{console.log(a[1])} //22
    12     test3(33,22,44);
    13 由于 this 已经在词法层面完成了绑定,通过 call() 或 apply() 方法调用一个函数时,只是传入了参数而已,对 this 并没有什么影响
  • var a = ()=>{
      return 1
    }
    
    function b(){
      return 2
    }
    
    console.log(a.prototype)
    console.log(b.prototype)

4、判断参数是否是数组的方式

1 Array.isArray(arr)
2 Object.prototype.toString.call(arr) === '[Object Array]'
3 arr instanceof Array
4 array.constructor === Array

5、判断类型的方式

 1 typeof 
 2 //解析:只能区分基本类型,对于null、array、function、object来说,使用typeof都会统一返回object字符串
 3 
 4 instanceof:用来判断对象是不是某个构造函数的实例。会沿着原型链找的
 5 
 6 
 7 Object.prototype.toString.call()
 8 //解析:判断某个对象属于哪种内置类型
 9 var toString = Object.prototype.toString;
10 
11 toString.call(new Date); // [object Date]
12 toString.call(new String); // [object String]
13 toString.call(Math); // [object Math]
14 toString.call([]); // [Object Array]
15 toString.call(new Number) // [object Number]
16 toString.call(true) // [object Boolean]
17 toString.call(function(){}) // [object Function]
18 toString.call({}) // [object Object]
19 toString.call(new Promise(() => {})) // [object Promise]
20 
21 toString.call(new Map) // [object Map]
22 toString.call(new RegExp) // [object RegExp]
23 toString.call(Symbol()) // [object Symbol]
24 toString.call(function *a(){}) // [object GeneratorFunction]
25 toString.call(new DOMException()) // [object DOMException]
26 toString.call(new Error) // [object Error]
27 
28 toString.call(undefined); // [object Undefined]
29 toString.call(null); // [object Null]
30 
31 // 还有 WeakMap、 WeakSet、Proxy 等

6、在浏览器输入URL点击回车之后发生了什么?

7、new操作符具体干了什么?


function Fn(){
}
let fn= new Fn();
new共经历了四个过程:
1、创建了一个空对象
     let obj = new Object();

2、设置原型链,此时便建立了 obj 对象的原型链:obj->Fn.prototype->Object.prototype->null
     obj.__proto__ = Fn.prototype;

3、让Fn的this指向obj,并执行Fn的函数体
     let result  = Fn.call(obj);

4、判断Fn的返回值类型,如果是值类型,返回obj,如果是引用类型,就返回这个引用类型的对象
     if(typeof(result)  === 'object){
        fn = result
     }else{
        fn = obj
     }

8、TCP的三次握手与四次挥手?(发生在传输层)来源:@bear鲍的熊

  • TCP协议是面向连接的运输层协议在数据传输前必须建立连接,数据传输之后释放连接;
  • TCP提供全双工通信所谓全双工是指一端既可以是客户端,也可以是服务器;
  • TCP的三次握手:

       

1、第一次握手:起初两端都处于CLOSED关闭状态,Client将标志位SYN置为1,随机产生一个值seq=x,并将该数据包发送给Server,Client进入SYN-SENT状态,等待Server确认;

2、第二次握手:Server收到数据包后由标志位SYN=1得知Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给Client以
确认连接请求,Server进入SYN-RCVD状态,此时操作系统为该TCP连接分配TCP缓存和变量; 3、第三次握手:Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并且此时操作系统为该TCP连接分配TCP缓存和变量,并将该数据包发
送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client和Server就可以开始传输数据。
  • TCP的四次挥手:
        假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,
则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

         

1、A的应用进程先向其TCP发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
2、B收到连接释放报文段后即发出确认报文段,(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
3、A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
4、B没有要向A发出的数据,B发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
5、A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,
A才进入CLOSED状态。
  • **主要为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误**如A发出连接请求,但因连接请求报文丢失而未收到确认,于是A再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A发出了两个连接请求报文段,其中第一个丢失,第二个到达了B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接,不采用三次握手,只要B发出确认,就建立新的连接了,此时A不理睬B的确认且不发送数据,则B一致等待A发送数据,浪费资源
  • Server端易受到SYN泛洪攻击?
  • 服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN泛洪攻击,SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。

  • 防范SYN攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某IP的重复SYN则丢弃后续请求。

  • 为什么 A 在TIME-WAIT状态必须等待2MSL的时间?(MSL最长报文段寿命Maximum Segment Lifetime,MSL=2)

原因:1)**保证A发送的最后一个ACK报文段能够到达B。**这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,B超时重传FIN+ACK报文段,而A能在2MSL时间内收到这个重传的FIN+ACK报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则B无法正常进入到CLOSED状态。


原因:2)防止“已失效的连接请求报文段”出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

  • 为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手
  • 优化,我们可以通过修改系统参数来优化服务器
    tcp_tw_reuse: 是否重用处于TIME_WAIT状态的TCP链接  (设为true)
    tcp_max_tw_buckets: 处于TIME_WAIT状态的SOCKET最大数目 (调大,这个参数千万不要调小了)
    tcp_fin_timeout: 处于FIN_WAIT_2的时间  (调小)

9、使用JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑

  • 当对象里面有时间对象,深拷贝后时间将变成字符串的格式
    /**
     * 当对象里面有时间对象
     */
    let obj = {
      name: "wxh",
      data: [new Date(), new Date()],
    };
    
    let objClone = JSON.parse(JSON.stringify(obj));
    obj.name = "wxh1234";
    console.log(obj);
    console.log(typeof obj.data[0]);
    console.log("---------");
    console.log(objClone);
    console.log(typeof objClone.data[0]);
    View Code

  • 当对象里面有RegExp、Error对象,深拷贝后将变成空对象
    let obj = {
      name: "wxh",
      data: [new RegExp(/\w+/), new Error("1234")],
    };
    
    let objClone = JSON.parse(JSON.stringify(obj));
    obj.name = "wxh1234";
    console.log(obj);
    console.log("---------");
    console.log(objClone);
    View Code

     

  • 当对象里面有Function和undefined时,深拷贝将会丢失,也就是如果元素在数组中,这一项变成null
    let obj = {
      name: "wxh",
      data: function () {
        console.log("function");
      },
      data2: undefined,
    };
    
    let objClone = JSON.parse(JSON.stringify(obj));
    obj.name = "wxh1234";
    console.log(obj);
    console.log("---------");
    console.log(objClone);
    View Code

     

  • 当对象里面有NAN时,深拷贝将会变成null
    let obj = {
      name: "wxh",
      data: NaN,
    };
    
    let objClone = JSON.parse(JSON.stringify(obj));
    obj.name = "wxh1234";
    console.log(obj);
    console.log("---------");
    console.log(objClone);
    View Code

  • 当对象里面的一个参数是由构造函数生成的,则会丢失constructor
    function Person() {
      this.name = "wxh";
    }
    let person = new Person();
    let obj = {
      name: "wxh",
      data: person,
    };
    
    let objClone = JSON.parse(JSON.stringify(obj));
    obj.name = "wxh1234";
    console.log(obj);
    console.log(obj.__proto__.constructor);
    console.log("---------");
    console.log(objClone);
    console.log(objClone.__proto__.constructor);
    View Code

     

10、Object.create()、new Object()和 {} 的区别

字面量和new关键字创建的对象是Object的实例,原型指向Object.prototype,继承内置对象Object
  • 字面量的方式:字面量是创建对象最简单的一种形式,目的在于简化创建包含大量属性的对象的过程,由若干属性名与属性值成对组成的映射表,映射表用 {} 包起来;
    var objA = {};
    objA.name = 'a';
    objA.sayName = function() {
        console.log(`My name is ${this.name} !`);
    }
    // var objA = {
    //     name: 'a',
    //     sayName: function() {
    //         console.log(`My name is ${this.name} !`);
    //     }
    // }
    objA.sayName();
    console.log(objA.__proto__ === Object.prototype); // true
    console.log(objA instanceof Object); // true
  • 构造函数的方式:
    var objA = new Object();
    objA.name = "a";
    objA.sayName = function () {
      console.log(`My name is ${this.name} !`);
    };
    objA.sayName();
    console.log(objA.__proto__ === Object.prototype); // true
    console.log(objA instanceof Object); // true
  • Object.create(proto,[propertiesObject])方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__(原型对象);
    const person = {
      name: "wxh",
      show: function () {
        console.log(`My name is ${this.name}.My age is ${this.age}.`);
      },
    };
    person.show(); //My name is wxh.My age is undefined.
    const me = Object.create(person); // me.__proto__ === person
    me.name = "kevin"; // 继承的属性可以被重写
    me.age = 22; //
    me.show(); // My name is kevin.My age is 22.
    console.log(me.__proto__ === Object.prototype); // false
    console.log(me.__proto__ === person); // true
    console.log(me instanceof Object); // true

    当第一个参数为null时,生成的对象为空对象,没有原型,不继承任何内置对象

    const me = Object.create(null);
    console.log(me.__proto__); //undefined
    console.log(me.__proto__ === Object.prototype); // false
    console.log(me instanceof Object); // false

11、Event Loop 

  • 1、第一步确认宏任务,微任务
    
    宏任务:script,setTimeout,setImmediate,promise中的executor(是同步函数,立即执行的一个函数)
    微任务:promise.then,process.nextTick
    
    2、第二步解析“拦路虎”,出现async/await不要慌,他们只在标记的函数中能够作威作福,出了这个函数还是跟着大部队的潮流
    
    3、第三步,根据Promise中then使用方式的不同做出不同的判断,是链式还是分别调用
    4、最后一步记住一些特别事件:比如,process.nextTick优先级高于Promise.then
  • 参考文章:

12、HTTP与HTTPS的区别

  • HTTP的URL由http://起始且默认使用端口80,而HTTPS的URL由https://起始且默认使用端口443;
  • HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的 SSL 加密传输协议;
  • HTTP的连接很简单,是无状态的,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全;

13、

 

 

 

 

 

 

 

 

三、算法相关

  1. Promise.all
    /**
     * IsInterator 是否含有Interator接口
     * @param {*} iterators 
     */
    function IsInterator(target){
      return typeof target[Symbol.iterator] === 'function'
    }
    
    /**
     * Promise.all方法的特点
     * 1、参数必须是一个部署了iterator接口的对象
     * 2、参数中的每个实例都必须是promise的实例,如果不是,如果不是,使用Promise.resolve方法将其转换
     * 3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
     * 4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调
     * 5、返回值是一个Promise对象 
     */
    var promiseAll = function (iterators) {
      if (!IsInterator(iterators)) throw new Error('arguments is not contain interators structure');
      const resolveAry = [];
      let rejectAry = [];
      const len = iterators.length;
      let counter = 0;
      return new Promise((resolve, reject) => {
        // 保证循环的顺序
        for (let data of iterators) {
          if (!(data instanceof Promise)) {
            data = Promise.resolve(data);
          }
          data.then((res) => {
            counter++;
            resolveAry.push(res);
            if (counter === len) {
              return resolve(resolveAry);
            }
          }).catch((err) => {
            rejectAry.push(err)
            reject(rejectAry)
          });
        }
      });
    };
    
    //测试
    let res1 = promiseAll(1);
    //结果:Error: arguments is not contain interators structure
    
    let res2 = promiseAll([1,2,3,new Promise((resolve,reject)=>{
      reject("no");
    })]);   
    //结果:
    fail
    [ 'no' ]
       
    
    let res3 = promiseAll([new Promise((resolve,reject)=>{
      resolve("success1");
    }),new Promise((resolve,reject)=>{
      resolve("success2");
    })])
    //结果:
    success
    [ 'success1', 'success2' ]
    
    
    res3.then((data)=>{
      console.log('success')
      console.log(data)
    }).catch((err)=>{
      console.log('fail')
      console.log(err)
    })
    View Code

    Promise.race

    var promiseRace = function (iterators) {
      if (!IsInterator(iterators))
        throw new Error("arguments is not contain interators structure");
      return new Promise((resolve, reject) => {
        for (let data of iterators) {
          if (!(data instanceof Promise)) {
            data = Promise.resolve(data);
          }
          data
            .then((res) => {
              resolve(res);
            })
            .catch((e) => {
              reject(e);
            });
        }
      });
    };
    
    let res4 = Promise.race([
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("quickly");
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("man man ");
        }, 500);
      }),
    ])
      .then((data) => {
        console.log("race success----");
        console.log(data);
      })
      .catch((e) => {
        console.log("race fail----");
        console.log(e);
      });
    View Code

  2.  实现千分位分隔符
    /**
     * formatMoney 金额的格式化函数
     * @param {Number/String} num 要格式化的金额数值
     * @param {Number} n 要保留的小数位数,默认值为2
     * @param {String} symbol 千分位分隔符
     */
    var formatMoney = function (num, n = 2, symbol = ",") {
      if (n < 0 || n > 20) throw new Error("argument error");
      /**
       * 输入多个 0 的情况
       */
      if (Number(num) === 0) return 0;
      num = Number(num);
      /**
       * true表示是小数
       */
      let hasDot = parseInt(num) !== num;
      /**
       *
       */
      let m = n !== undefined && n !== null ? n : 1;
      /**
       * 1、我们的思路是:通过 . 为参考方向来匹配多个连接的三位数字来加千分位,因此当n === 0时,确保有一个 .
       * 2、判断是否是小数,如果是小数,且指定小数位数n,则四舍五入,若是小数没有指定小数位数,则返回原值
       * 3、若传入的值是整数,n === 0 的情况在第一种,n 不传值  以及 n > 0
       */
      num =
        m === 0
          ? num.toFixed(m) + "."
          : hasDot
          ? n
            ? num.toFixed(n)
            : num
          : num.toFixed(m);
      console.log(num);
      /**
       * 默认的千分位分隔符
       */
      // symbol = symbol || ",";
      /**
       * 用正则来匹配千分位分隔符:语法 x(?=y):  正向肯定查找,匹配后面带有y的x项目
       * 这个正则用来查找一个和 . 之间带有一个或者多个 连续三位数字的数字x
       */
      let regExp = /(\d)(?=(\d{3})+\.)/g;
      num = num.toString().replace(regExp, function (match, p1, p2) {
        /**
         * match与p1是匹配到的 x,p2是 y
         */
        console.log(match);
        console.log(p1);
        console.log(p2);
        return p1 + symbol;
      });
      /**
       * 如果n为0,或者传入的整数没有指定整数的保留位数,则去掉前面操作的整数位
       */
      if (n === 0 || (!hasDot && !n)) {
        num = num.substring(0, num.indexOf("."));
      }
      return num;
    };
    console.log(formatMoney(7654321, 0));
    View Code
  3. 防抖
    /**
     * debounce 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
     * 适用场景:
     * 1、按钮提交场景:防止多次提交按钮,只执行最后提交的一次
     * 2、服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
     */
    var debounce = function (fn, delay) {
      let timer = null;
      return function () {
        // 中间态进行清除
        if (timer) {
          clearTimeout(timer);
        }
        timer = setTimeout(() => {
          console.log("防抖");
          console.log(this);
          console.log(arguments);
          fn.apply(this, arguments);
        }, delay);
      };
    };
    console.log(1);
    let fn = debounce(() => {
      console.log("submit按钮多次提交或者搜索");
    });
    fn();
    console.log(2);
    View Code

    节流:

    /**
     * 节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
     * 适用场景:
     * 1、拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
     * 2、缩放场景:监控浏览器resize
     * 3、动画场景:避免短时间内多次触发动画引起性能问题
     */
    
    /**
     * 定时器版本:优缺点,首次不会执行,会有delay的延迟
     */
    var throttle = function (fn, delay = 500) {
      let flag = false;
      return () => {
        if (flag) return; //正在执行,不允许发送其他操作
        flag = true; //发送前,设置关卡,其他时间就不能执行了
        setTimeout(() => {
          console.log("节流");
          fn.apply(this, arguments);
          flag = false;
        }, delay);
      };
    };
    
    /**
     * 时间戳来处理首次和间隔执行问题,定时器来确保最后一次状态改变执行
     * @param {Function} fn 函数
     * @param {Number} delay  时间间隔
     * @returns
     */
    var throttling = function (fn, delay = 500) {
      let lastTime = 0;
      let timer = null;
      return function () {
        let nowTime = new Date().getTime();
        let timeDiff = nowTime - lastTime; //间隔时间
        if (lastTime && timeDiff < delay) {
          // 为了响应用户的最后一次操作:
          timer && clearTimeout(timer);
          timer = setTimeout(() => {
            lastTime = new Date().getTime();
            fn.apply(this, arguments);
          }, delay - timeDiff);
          return;
        }
        // 首次或到时间了
        lastTime = now;
        fn.apply(this, arguments);
        timer && clearTimeout(timer);
      };
    };
    View Code

    一般使用第三方库lodash来进行防抖与节流

  4. 实现instanceof
    /**
     * 模拟instanceOf方法
     * @param {*} L  实例
     * @param {*} R  构造函数
     * @returns
     */
    var instanceOf = function (L, R) {
      let __proto__ = L.__proto__;
      let prototype = R.prototype;
      while (true) {
        if (__proto__ === null) return false;
        if (__proto__ === prototype) {
          return true;
        }
        __proto__ = __proto__.__proto__;
      }
    };
    let obj = { name: "wxh" };
    console.log(instanceOf(obj, Array)); //false
    console.log(instanceOf(obj, Object)); //true
    View Code
  5. 模拟new
    /**
     * 1、创建一个空对象
     * 2、设置原型链
     * 3、改变this指向
     * 4、返回值
     */
    var newMethod = function () {
      let obj = new Object();
      console.log(arguments); //{ '0': [Function: Person], '1': 'wxh' }
      // 获取构造函数,以便实现作用域的绑定,并且会改变数组本身
      let Constructor = Array.prototype.shift.call(arguments); //Person
      console.log(Constructor);
      obj.__proto__ = Constructor.prototype;
      console.log(arguments); //{ '0': 'wxh' }
      let result = Constructor.apply(obj, arguments);
      return typeof result === "object" ? result : obj;
    };
    function Person(name) {
      this.name = name;
    }
    Person.prototype.say = function () {
      console.log(`my name is ${this.name}`);
    };
    var person = newMethod(Person, "wxh");
    person.say();
    View Code
  6. 两个数组的并集与差集
    var unionMetod = function (nums1, nums2) {
      // return [...nums1, ...nums2];
      // return nums1.concat(nums2);
      // for (let data of nums2) {
      //   nums1.push(data);
      // }
      // return nums1;
      nums1.push.apply(nums1, nums2);
      return nums1;
    
    };//let nums1 = [1, 2, 2, 1, 3]; let nums2 = [2, 2];  结果[1, 2, 2, 1, 3,2,2]
    
    var complementaryMethod = function (nums1, nums2) {
      let arr1 = new Set(nums1);
      let arr2 = new Set(nums2);
      return [...arr1].filter((item) => !arr2.has(item));//let nums1 = [1, 2, 2, 1, 3]; let nums2 = [2, 2];  结果[1,3]
    };
  7. 两个数组的交集(
    要求:输出结果中每个元素的出现次数,应与元素在两个数组中出现次数的最小值一致;并且不考虑元素的输出顺序
    // let nums1 = [1, 2, 2, 1, 3];
    // let nums2 = [2, 2];
    //结果[2,2]
    let nums1 = [4, 9, 5];
    let nums2 = [9, 4, 9, 8, 4];
    //结果[9,4]
    /**
     * 哈希表
     * intersectionMethods 求两个数组的交集,输出结果中每个元素的出现次数,应与元素在两个数组中出现次数的最小值一致
     * @param {*} nums1
     * @param {*} nums2
     */
    var intersectionHash = function (nums1, nums2) {
      let map = new Map();
      let finalAry = [];
      /**
       * 将数组中出现的元素映射成哈希表
       */
      for (let data of nums1) {
        if (map.has(data)) {
          let index = map.get(data);
          map.set(data, ++index);
        } else {
          map.set(data, 1);
        }
      }
      for (let item of nums2) {
        if (map.has(item) && map.get(item) > 0) {
          finalAry.push(item);
          let index2 = map.get(item);
          map.set(item, --index2);
        }
      }
      return finalAry;
    };
    
    /**
     * 排序 + 双指针
     * @param {*} nums1
     * @param {*} nums2
     */
    var intersectionPointer = function (nums1, nums2) {
      nums1.sort((a, b) => {
        return a - b;
      });
      nums2.sort((a, b) => {
        return a - b;
      });
      let p1 = 0;
      let p2 = 0;
      let finalAry = [];
      while (p1 < nums1.length && p2 < nums2.length) {
        if (nums1[p1] < nums2[p2]) {
          p1++;
        } else if (nums1[p1] > nums2[p2]) {
          p2++;
        } else {
          finalAry.push(nums1[p1]);
          p1++;
          p2++;
        }
      }
      return finalAry;
    };
    View Code

    二、求两个数组的交集并去重:输出的每个元素是唯一的,不考虑输入结果的顺序

    let nums1 = [1, 2, 2, 1, 3];
    let nums2 = [2, 2];
    //结果[2]
    // let nums1 = [4, 9, 5];
    // let nums2 = [9, 4, 9, 8, 4];
    //结果[9,4]
    /**
     * 根据Set结构中元素的唯一性 求交集并去重
     * @param {*} nums1
     * @param {*} nums2
     */
    var intersectionSet = function (nums1, nums2) {
      let arr1 = new Set(nums1);
      let arr2 = new Set(nums2);
      return [...arr1].filter((item) => arr2.has(item));
    };
    
    var intersectionIncludes = function (nums1, nums2) {
      return [...new Set(nums1.filter((item) => nums2.includes(item)))];
    };
  8. 动态规划算法
    1. 特点:动态规划三要素【最优子结构】【边界】和【状态转移公式】;
    2. 参考文献
  9. call的模拟实现
    /**
     * 1、将函数设为对象的属性
     * 2、执行 && 删除这个函数
     * 3、指定this到函数 并 传入给定参数 执行函数
     * 4、如果不传入参数(null),默认指向为 window
     */
    Function.prototype.callHandler = function (ctx) {
      // this 参数可以传 null,当为 null 的时候,视为指向 window
      var ctx = ctx || window;
      // 首先要获取调用call的函数,用this可以获取
      ctx.fn = this;
      let args = [];
      // 从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里
      for (let i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i + "]");
      }
      let result = eval("ctx.fn(" + args + ")"); //支持返回值, args 会自动调用 Array.toString() 这个方法
      delete ctx.fn;
      return result;
    };
    let value = "2";
    let obj = {
      value: 1,
    };
    function test(name, age) {
      console.log(name);
      console.log(age);
      console.log(this.value);
    }
    test.callHandler(obj, "wxh1", "22-1");
    console.log("--------");
    test.call(obj, "wxh2", "22-2");
    View Code

  10. apply的模拟实现
    /**
     *
     */
    Function.prototype.applyHandler = function (ctx, args) {
      var ctx = Object(ctx) || window;
      ctx.fn = this;
      let argument = [];
      let result;
      if (!args) {
        result = ctx.fn();
      } else {
        for (let i = 0; i < args.length; i++) {
          argument.push("args[" + i + "]");
        }
        result = eval("ctx.fn(" + argument + ")");
      }
      delete ctx.fn;
      return result;
    };
    
    let obj = {
      flag: true,
    };
    function test(name, age) {
      console.log(name);
      console.log(age);
      console.log(this.flag);
    }
    test.applyHandler(obj, ["wxh", "22"]);
    View Code

  11. bind的模拟实现
    /**
     * 1、会创建一个新函数
     * 2、第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数
     * 3、当返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
     */
    Function.prototype.bindHandler = function (ctx) {
      // 首先要获取调用 bind 的函数,用this可以获取
      let self = this;
      if (typeof this !== "function") {
        throw new Error(
          "Function.prototype.bind - what is trying to be bound is not callable"
        );
      }
      // 获取bindHandler函数从第二个参数到最后一个参数
      let bindArgs = Array.prototype.slice.call(arguments, 1); //[ 'wxh' ]
      let tempFn = function () {};
      let returnFn = function () {
        //回调函数的话 arguments 为 bindHandler的参数
        // 获取bindHandler 返回的函数
        let finalArys = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(
          this instanceof tempFn ? this : ctx,
          bindArgs.concat(finalArys)
        );
      };
      // 直接修改 returnFn.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转
      tempFn.prototype = this.prototype;
      // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
      returnFn.prototype = new tempFn();
      return returnFn;
    };
    let obj = {
      value: 1,
    };
    function test(name, age) {
      console.log(arguments);
      console.log(name);
      console.log(age);
      console.log(this.value);
    }
    let fun = test.bindHandler(obj, "wxh");
    fun("22");
    View Code
  12. 模拟Object.create
    Object.prototype.createHandler = function (__proto__, properties) {
      function F() {}
      F.prototype = __proto__;
      if (properties) {
        Object.defineProperties(F, properties);
      }
      return new F();
    };
    var hh = Object.createHandler(
      { a: 11 },
      {
        b: {
          value: 3,
          writable: true,
          configurable: true,
        },
      }
    );
    console.dir(hh.__proto__);
    View Code
  13. 模拟JSON的方法
    var json = '{"name":"cxk", "age":25}';
    console.log(json);
    console.log(JSON.parse(json));
    /**
     * eval函数的作用:把字符串转换为合适的表达式,并输出表达式的值
     * 但是容易造成 XSS(跨站脚本攻击):想办法“教唆”用户的浏览器去执行一些这个网页中原本不存在的前端代码。
     * 1、窃取网页浏览中的cookie值 2、劫持流量实现恶意跳转
     * 为什么要加括号呢? 因为js中{}通常是表示一个语句块,eval只会计算语句块内的值进行返回。加上括号就变成一个整体的表达式
     */
    console.log(eval("(" + json + ")"));
    /**
     * 对参数 json 做校验,只有真正符合 JSON 格式,才能调用 eval,
     */
    var rx_one = /^[\],:{}\s]*$/;
    var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
    var rx_three =
      /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
    var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
    
    if (
      rx_one.test(
        json.replace(rx_two, "@").replace(rx_three, "]").replace(rx_four, "")
      )
    ) {
      var obj = eval("(" + json + ")");
      console.log(obj);
    }
    
    /**
     * 使用递归手动扫描每个字符
     */
    var jsonStr = '{"name":"cxk", "age":22}',
      json = new Function("return " + jsonStr)();
    console.log(json);
    View Code

    JSON.stringify()

    /**
     * 把json对象转换为json字符串
     */
    let obj1 = {
      flag: true,
      Number: 1,
      String: "111",
      data: {
        name: "wxh",
      },
    };
    console.log(obj1);
    console.log(JSON.stringify(obj1));
    /**
     * 1、当对象为数字,null,boolean的时候,直接转换为相应的字符串就可以了
     * 2、当对象为string,function,undefined,object,array等,需要特殊处理
     * @param {*} obj
     * @returns
     */
    function jsonStringify(obj) {
      let type = typeof obj;
      if (type !== "object") {
        if (/string|function/.test(type)) {
          return '"' + String(obj).replace('"', '\\"') + '"';
        } else if (type === undefined) {
          return "undefined";
        } else {
          return String(obj);
        }
      } else {
        let json = [];
        let isArray = Array.isArray(obj);
        for (let k in obj) {
          let v = obj[k];
          let type = typeof v;
          if (/string|function/.test(type)) {
            v = '"' + String(v).replace('"', '\\"') + '"';
          } else if (type === undefined) {
            v = "undefined";
          } else if (type === "object") {
            v = jsonStringify(v);
          }
          json.push((isArray ? "" : '"' + k + '":') + String(v));
        }
        return (isArray ? "[" : "{") + String(json) + (isArray ? "]" : "}");
      }
    }
    console.log(jsonStringify(obj1));
    View Code

  14. 正则相关
    1. 去除字符串前后的空格
      let str1 = "    111   222  333    ";
      console.log(str1.length);
      function deleteSpace(str) {
        let reg = /^\s+|\s+$/g;
        return str.replace(reg, "");
      }
      console.log(deleteSpace(str1));
      console.log(deleteSpace(str1).length);

    2. 让首字母大写
      let str2 = "hi man good  luck";
      function capitalize(str) {
        return str.replace(/\w+/g, function (match) {
          return match.substr(0, 1).toUpperCase() + match.substr(1);
        });
      }
      console.log(capitalize(str2));

    3. 相邻字符去重
      function deleteDuplicate(str) {
        return str.replace(/([A-Za-z]{1})(\1)+/g, "$1");
      }
      console.log(deleteDuplicate("aaabbbcdfgghhjjkkk"));

    4. 模拟模板字符串
      /**
       * render JS实现模板字符串
       * @param {String} templateString 字符串的表达式
       * @param {*} data
       * @returns
       */
      function render(templateString, data) {
        let reg = /\{\{(\w+)\}\}/g;
        let matchArgs = reg.exec(templateString);
        if (matchArgs !== null && matchArgs[0]) {
          const dataReg = new RegExp(matchArgs[0], "g"); // {{name}}
          let propertiesReg = new RegExp(/\w+/, "g");
          let matchPropertiers = propertiesReg.exec(matchArgs[0])[0]; //name
          templateString = templateString.replace(dataReg, data[matchPropertiers]);
          return render(templateString, data); // 递归的渲染并返回渲染后的结构
        }
        return templateString;
      }
      let templateString = "我是{{name}},年龄{{age}},性别{{sex}}";
      let data = {
        name: "wxh",
        age: 18,
      };
      console.log(render(templateString, data)); // 我是wxh,年龄18,性别undefined
      View Code
    5.  是否是邮箱
      function isEmail(email) {
          var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
          return regx.test(email);
      }
    6. 是否是身份证

      function isCardNo(number) {
          var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
          return regx.test(number);
      }
    7. 文件命名

      let str4 = "get-element-by-id";
      function f(str) {
        return str.replace(/-\w/g, function (match, p1, p2) {
          return match.substr(1).toUpperCase();
        });
      }
      console.log(f(str4)); //getElementById
      function s(str) {
        return str.replace(/[A-Z]/g, function (match) {
          return "-" + match.toLowerCase();
        });
      }
      console.log(s("getElementById")); //get-element-by-id
      View Code               
  15. 111

 四、设计模式

1、发布-订阅模式

  • 概念

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

  • 优缺点
    1. 优点
        对象之间解耦
        异步编程中,可以更松耦合的代码编写
    
    2. 缺点
       创建订阅者本身要消耗一定的时间和内存
       虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
  • 实现一个EventListener
    /**
     * 1、创建一个对象
     * 2、在该对象上创建一个缓存列表(调度中心)
     * 3、on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
     * 4、emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数
     * 发布者发布事件到调度中心,调度中心处理代码)
     * 5、off 方法可以根据 event 值取消订阅(取消订阅)
     * 6、once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
     */
    class EventEmitter {
      constructor() {
        // 储存事件/回调键值对  相当于调度中心,是一个列表
        this._events = this._events || new Map();
        this._maxListeners = this._maxListeners || 10; // 设立监听上限
      }
    }
    
    /**
     * 在 EventEmitter 中监听的每一类事件都有最大监听个数,超过了这个数值,事件虽然可以正常执行
     * 但是会发出警告信息,其目的是为了防止内存泄露。
     */
    EventEmitter.defaultMaxListeners = 10;
    
    /**
     * 设置同类型事件监听最大个数
     * @param {*} count
     */
    EventEmitter.prototype.setMaxListeners = function (count) {
      this._count = count;
    };
    
    // 获取同类型事件监听最大个数
    EventEmitter.prototype.getMaxListeners = function () {
      return this._count || EventEmitter.defaultMaxListeners;
    };
    
    /**
     * emit 发布 ----- 触发名为 event 的事件
     * @param {*} event
     * @param  {...any} args
     */
    EventEmitter.prototype.emit = function (event, ...args) {
      // 从储存事件键值对的this._events中  获取对应事件  回调函数
      let handler = this._events.get(event);
      if (handler) {
        if (Array.isArray(handler)) {
          // 是数组,说明有多个监听者,需要进行依此触发
          for (let listener of handler) {
            listener.call(this, ...args);
          }
        } else {
          handler.call(this, ...args);
        }
      }
    };
    
    /**
     * addListener/on 监听/订阅 名为 event 的事件
     * @param {*} event
     * @param {*} fn
     */
    EventEmitter.prototype.addListener = function (event, fn) {
      // 兼容继承不存在 _events 的情况
      if (!this._events) this._events = new Map();
      let handler = this._events.get(event); //如果不存在event,则返回undefined
      if (!handler) {
        this._events.set(event, fn);
      } else if (handler && typeof handler === "function") {
        // 如果handler是函数说明只有一个监听者
        this._events.set(event, [handler, fn]); // 多个监听者我们需要用数组储存
      } else {
        handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可
        // 获取事件最大监听个数
        let maxListeners = this.getMaxListeners();
        // 判断 type 类型的事件是否超出最大监听个数,超出打印警告信息
        if (handler.length - 1 === maxListeners) {
          console.error(
            `MaxListenersExceededWarning: ${
              maxListeners + 1
            } ${type} listeners added`
          );
        }
      }
    };
    EventEmitter.prototype.on = EventEmitter.prototype.addListener;
    
    /**
     * once 只监听一次,调用完毕后删除缓存函数(订阅一次)
     * @param {*} event
     * @param {*} fn
     */
    EventEmitter.prototype.once = function (event, fn) {
      // 先绑定,调用后删除
      function on(...args) {
        fn(...args);
        this.removeListener(event, on);
      }
      // 自定义属性,存储 fn,确保单独使用 removeListener 删除传入的 fn 时可以被删除掉
      on.listen = fn;
      this.addListener(event, on);
    };
    
    /**
     * removeListener/off  根据 event 值取消订阅(取消订阅)
     * @param {*} event
     * @param {*} fn
     * @returns
     */
    EventEmitter.prototype.removeListener = function (event, fn) {
      let handler = this._events.get(event);
      if (handler) {
        if (fn) {
          if (typeof handler === "function") {
            if (handler === fn) {
              this._events.delete(event);
            } else {
              return false;
            }
          } else {
            handler = handler.filter((listener) => {
              return listener !== fn;
            });
            // 如果清除后只有一个函数,那么取消数组,以函数形式保存
            if (handler.length === 1) {
              this._events.set(event, handler[0]);
            } else {
              this._events.set(event, handler);
            }
          }
        } else {
          // 当不传fn时,所有的回调函数都会取消订阅
          this._events.delete(event);
        }
      } else {
        return false;
      }
    };
    EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
    
    const myEmitter = new EventEmitter();
    // 订阅
    let user1 = function (data) {
      console.log("用户1 订阅了");
      console.log(data);
    };
    let user2 = function (data) {
      console.log("用户2 订阅了");
      console.log(data);
    };
    let user3 = function (data) {
      console.log("用户3 订阅了");
      console.log(data);
    };
    let user4 = function (data) {
      console.log("用户4 订阅了");
      console.log(data);
    };
    myEmitter.on("南派三叔", user1);
    myEmitter.addListener("南派三叔", user2);
    myEmitter.off("南派三叔", user1);
    
    myEmitter.once("南派三叔", user3);
    myEmitter.addListener("南派三叔", user4);
    // 发布
    myEmitter.emit("南派三叔", {
      title: "阴山古楼",
      section:
        "话音未落,闷油瓶直接就窜了出去,我根本没反应过来,他瞬间就到了包的面前一下抓住包的背带。他猛地一拽背包....",
    });
    myEmitter.emit("南派三叔", {
      title: "万山极夜",
      section:
        "话音未落,闷油瓶直接就窜了出去,我根本没反应过来,他瞬间就到了包的面前一下抓住包的背带。他猛地一拽背包....",
    });
    View Code

  • 参考文章

2、观察者模式

  • 概念

观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件;

  • 区别
    1、在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
    
    
    2、在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
    
    
    3、观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
    
    
    4、观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

     

 

 

  •     

 

 

 

 

 

3、

 

 

 

 

 

 

 

 

 

 

 

 

 

               

 

Webpack开启gzip压缩
posted @ 2021-05-27 16:06  北栀女孩儿  阅读(378)  评论(0编辑  收藏  举报