关于javascript:前端面试那些题

4次阅读

共计 29121 个字符,预计需要花费 73 分钟才能阅读完成。

对对象与数组的解构的了解

解构是 ES6 提供的一种新的提取数据的模式,这种模式可能从对象或数组里有针对性地拿到想要的数值。1)数组的解构 在解构数组时,以元素的地位为匹配条件来提取想要的数据的:

const [a, b, c] = [1, 2, 3]

最终,a、b、c 别离被赋予了数组第 0、1、2 个索引位的值:

数组里的 0、1、2 索引位的元素值,精准地被映射到了左侧的第 0、1、2 个变量里去,这就是数组解构的工作模式。还能够通过给左侧变量数组设置空占位的形式,实现对数组中某几个元素的精准提取:

const [a,,c] = [1,2,3]

通过把两头位留空,能够顺利地把数组第一位和最初一位的值赋给 a、c 两个变量:

2)对象的解构 对象解构比数组构造略微简单一些,也更显弱小。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。当初定义一个对象:

const stu = {
  name: 'Bob',
  age: 24
}

如果想要解构它的两个自有属性,能够这样:

const {name, age} = stu

这样就失去了 name 和 age 两个和 stu 平级的变量:

留神,对象解构严格以属性名作为定位根据,所以就算调换了 name 和 age 的地位,后果也是一样的:

const {age, name} = stu

Iterator 迭代器

Iterator(迭代器)是一种接口,也能够说是一种标准。为各种不同的数据结构提供对立的拜访机制。任何数据结构只有部署 Iterator 接口,就能够实现遍历操作(即顺次解决该数据结构的所有成员)。

Iterator 语法:

const obj = {[Symbol.iterator]:function(){}
}

[Symbol.iterator] 属性名是固定的写法,只有领有了该属性的对象,就可能用迭代器的形式进行遍历。

  • 迭代器的遍历办法是首先取得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 办法,扭转指针的指向,让其指向下一条数据
  • 每一次的 next 都会返回一个对象,该对象有两个属性

    • value 代表想要获取的数据
    • done 布尔值,false 示意以后指针指向的数据有值,true 示意遍历曾经完结

Iterator 的作用有三个:

  • 创立一个指针对象,指向以后数据结构的起始地位。也就是说,遍历器对象实质上,就是一个指针对象。
  • 第一次调用指针对象的 next 办法,能够将指针指向数据结构的第一个成员。
  • 第二次调用指针对象的 next 办法,指针就指向数据结构的第二个成员。
  • 一直调用指针对象的 next 办法,直到它指向数据结构的完结地位。

每一次调用 next 办法,都会返回数据结构的以后成员的信息。具体来说,就是返回一个蕴含 value 和 done 两个属性的对象。其中,value 属性是以后成员的值,done 属性是一个布尔值,示意遍历是否完结。

let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next())  // {value: Object { num: 1}, done: false }
console.log(it.next())  // {value: 2, done: false}
console.log(it.next())  // {value: 3, done: false}
console.log(it.next())  // {value: undefined, done: true}

对象没有布局 Iterator 接口,无奈应用for of 遍历。上面使得对象具备 Iterator 接口

  • 一个数据结构只有有 Symbol.iterator 属性,就能够认为是“可遍历的”
  • 原型部署了 Iterator 接口的数据结构有三种,具体蕴含四种,别离是数组,相似数组的对象,Set 和 Map 构造

为什么对象(Object)没有部署 Iterator 接口呢?

  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,须要开发者手动指定。然而遍历遍历器是一种线性解决,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
  • 对对象部署 Iterator 接口并不是很必要,因为 Map 补救了它的缺点,又正好有 Iteraotr 接口
let obj = {
    id: '123',
    name: '张三',
    age: 18,
    gender: '男',
    hobbie: '睡觉'
}

obj[Symbol.iterator] = function () {let keyArr = Object.keys(obj)
    let index = 0
    return {next() {
            return index < keyArr.length ? {
                value: {key: keyArr[index],
                    val: obj[keyArr[index++]]
                }
            } : {done: true}
        }
    }
}

for (let key of obj) {console.log(key)
}

介绍 plugin

插件零碎是 Webpack 胜利的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 能够监听这些事件,依据需要在相应的工夫点对打包内容进行定向的批改。

一个最简略的 plugin 是这样的:

class Plugin{
      // 注册插件时,会调用 apply 办法
      // apply 办法接管 compiler 对象
      // 通过 compiler 上提供的 Api,能够对事件进行监听,执行相应的操作
      apply(compiler){
          // compilation 是监听每次编译循环
          // 每次文件变动,都会生成新的 compilation 对象并触发该事件
        compiler.plugin('compilation',function(compilation) {})
      }
}

注册插件:

// webpack.config.js
module.export = {
    plugins:[new Plugin(options),
    ]
}

事件流机制:

Webpack 就像工厂中的一条产品流水线。原材料通过 Loader 与 Plugin 的一道道解决,最初输入后果。

  • 通过链式调用,按程序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 能够插入到整个生产过程中的每个步骤中;

Webpack 事件流编程范式的外围是根底类 Tapable,是一种 观察者模式 的实现事件的订阅与播送:

const {SyncHook} = require("tapable")

const hook = new SyncHook(['arg'])

// 订阅
hook.tap('event', (arg) => {
    // 'event-hook'
    console.log(arg)
})

// 播送
hook.call('event-hook')

Webpack 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也领有这样的事件流机制。

  • Compiler : 能够简略的了解为 Webpack 实例,它蕴含了以后 Webpack 中的所有配置信息,如 options,loaders, plugins 等信息,全局惟一,只在启动时实现初始化创立,随着生命周期逐个传递;
  • Compilation: 能够称为 编译实例。当监听到文件产生扭转时,Webpack 会创立一个新的 Compilation 对象,开始一次新的编译。它蕴含了以后的输出资源,输入资源,变动的文件等,同时通过它提供的 api,能够监听每次编译过程中触发的事件钩子;
  • 区别:

    • Compiler 全局惟一,且从启动生存到完结;
    • Compilation对应每次编译,每轮编译循环均会从新创立;
  • 罕用 Plugin:

    • UglifyJsPlugin: 压缩、混同代码;
    • CommonsChunkPlugin: 代码宰割;
    • ProvidePlugin: 主动加载模块;
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离款式,生成 css 文件;DefinePlugin: 定义全局变量;
    • optimize-css-assets-webpack-plugin: CSS 代码去重;
    • webpack-bundle-analyzer: 代码剖析;
    • compression-webpack-plugin: 应用 gzip 压缩 js 和 css;
    • happypack: 应用多过程,减速代码构建;
    • EnvironmentPlugin: 定义环境变量;
  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件

loader 和 plugin 有什么区别?

webapck 默认只能打包 JS 和 JOSN 模块,要打包其它模块,须要借助 loader,loader 就能够让模块中的内容转化成 webpack 或其它 laoder 能够辨认的内容。

  • loader就是模块转换化,或叫加载器。不同的文件,须要不同的 loader 来解决。
  • plugin是插件,能够参加到整个 webpack 打包的流程中,不同的插件,在适合的机会,能够做不同的事件。

webpack 中都有哪些插件,这些插件有什么作用?

  • html-webpack-plugin 主动创立一个 HTML 文件,并把打包好的 JS 插入到 HTML 文件中
  • clean-webpack-plugin 在每一次打包之前,删除整个输入文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离 CSS 代码,放到一个独自的文件中
  • optimize-css-assets-plugin 压缩 css

渲染机制

1. 浏览器如何渲染网页

概述:浏览器渲染一共有五步

  1. 解决 HTML 并构建 DOM 树。
  2. 解决 CSS构建 CSSOM 树。
  3. DOMCSSOM 合并成一个渲染树。
  4. 依据渲染树来布局,计算每个节点的地位。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上

第四步和第五步是最耗时的局部,这两步合起来,就是咱们通常所说的渲染

具体如下图过程如下图所示

渲染

  • 网页生成的时候,至多会渲染一次
  • 在用户拜访的过程中,还会一直从新渲染

从新渲染须要反复之前的第四步 (从新生成布局)+ 第五步(从新绘制) 或者只有第五个步(从新绘制)

  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建实现。并且构建 CSSOM 树是一个非常耗费性能的过程,所以应该尽量保障层级扁平,缩小适度层叠,越是具体的 CSS 选择器,执行速度越慢
  • HTML 解析到 script 标签时,会暂停构建 DOM,实现后才会从暂停的中央从新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也能够认为这种状况下,CSS 也会暂停构建 DOM

2. 浏览器渲染五个阶段

2.1 第一步:解析 HTML 标签,构建 DOM 树

在这个阶段,引擎开始解析 html,解析进去的后果会成为一棵domdom的目标至多有 2

  • 作为下个阶段渲染树状图的输出
  • 成为网页和脚本的交互界面。(最罕用的就是 getElementById 等等)

当解析器达到 script 标签的时候,产生上面四件事件

  1. html解析器进行解析,
  2. 如果是内部脚本,就从内部网络获取脚本代码
  3. 将控制权交给 js 引擎,执行 js 代码
  4. 复原 html 解析器的控制权

由此能够失去第一个论断 1

  • 因为 <script> 标签是阻塞解析的,将脚本放在网页尾部会减速代码渲染。
  • deferasync 属性也能有助于加载内部脚本。
  • defer使得脚本会在 dom 残缺构建之后执行;
  • async标签使得脚本只有在齐全 available 才执行,并且是以非阻塞的形式进行的

2.2 第二步:解析 CSS 标签,构建 CSSOM 树

  • 咱们曾经看到 html 解析器碰到脚本后会做的事件,接下来咱们看下 html 解析器碰到样式表会产生的状况
  • js会阻塞解析,因为它会批改文档 (document)。css 不会批改文档的构造,如果这样的话,仿佛看起来 css 款式不会阻塞浏览器 html 解析。然而事实上 css样式表是阻塞的。阻塞是指当 cssom 树建设好之后才会进行下一步的解析渲染

通过以下伎俩能够加重 cssom 带来的影响

  • script 脚本放在页面底部
  • 尽可能快的加载 css 样式表
  • 将样式表依照 media typemedia query辨别,这样有助于咱们将 css 资源标记成非阻塞渲染的资源。
  • 非阻塞的资源还是会被浏览器下载,只是优先级较低

2.3 第三步:把 DOM 和 CSSOM 组合成渲染树(render tree)

2.4 第四步:在渲染树的根底上进行布局,计算每个节点的几何构造

布局 (layout):定位坐标和大小,是否换行,各种position, overflow, z-index 属性

2.5 调用 GPU 绘制,合成图层,显示在屏幕上

将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting

3. 渲染优化相干

3.1 Load 和 DOMContentLoaded 区别

  • Load 事件触发代表页面中的 DOMCSSJS,图片曾经全副加载结束。
  • DOMContentLoaded 事件触发代表初始的 HTML 被齐全加载和解析,不须要期待 CSSJS,图片加载

3.2 图层

一般来说,能够把一般文档流看成一个图层。特定的属性能够生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁须要渲染的倡议独自生成一个新图层,进步性能。但也不能生成过多的图层,会引起副作用。

通过以下几个罕用属性能够生成新图层

  • 3D 变换:translate3dtranslateZ
  • will-change
  • videoiframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

3.3 重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一大节,然而这两个步骤对于性能影响很大

  • 重绘是当节点须要更改外观而不会影响布局的,比方扭转 color 就叫称为重绘
  • 回流是布局或者几何属性须要扭转就称为回流。

回流必定会产生重绘,重绘不肯定会引发回流。回流所需的老本比重绘高的多,扭转深层次的节点很可能导致父节点的一系列回流

以下几个动作可能会导致性能问题

  • 扭转 window 大小
  • 扭转字体
  • 增加或删除款式
  • 文字扭转
  • 定位或者浮动
  • 盒模型

很多人不晓得的是,重绘和回流其实和 Event loop 无关

  • Event loop 执行完Microtasks 后,会判断 document 是否须要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  • 而后判断是否有 resize 或者 scroll,有的话会去触发事件,所以 resizescroll 事件也是至多 16ms才会触发一次,并且自带节流性能。
  • 判断是否触发了 media query
  • 更新动画并且发送事件
  • 判断是否有全屏操作事件
  • 执行 requestAnimationFrame 回调
  • 执行 IntersectionObserver 回调,该办法用于判断元素是否可见,能够用于懒加载上,然而兼容性不好
  • 更新界面
  • 以上就是一帧中可能会做的事件。如果在一帧中有闲暇工夫,就会去执行 requestIdleCallback 回调

常见的引起重绘的属性

  • color
  • border-style
  • visibility
  • background
  • text-decoration
  • background-image
  • background-position
  • background-repeat
  • outline-color
  • outline
  • outline-style
  • border-radius
  • outline-width
  • box-shadow
  • background-size

3.4 常见引起回流属性和办法

任何会扭转元素几何信息 (元素的地位和尺寸大小) 的操作,都会触发重排,上面列一些栗子

  • 增加或者删除可见的 DOM 元素;
  • 元素尺寸扭转——边距、填充、边框、宽度和高度
  • 内容变动,比方用户在 input 框中输出文字
  • 浏览器窗口尺寸扭转——resize事件产生时
  • 计算 offsetWidthoffsetHeight 属性
  • 设置 style 属性的值

回流影响的范畴

因为浏览器渲染界面是基于散失布局模型的,所以触发重排时会对四周 DOM 重新排列,影响的范畴有两种

  • 全局范畴:从根节点 html 开始对整个渲染树进行从新布局。
  • 部分范畴:对渲染树的某局部或某一个渲染对象进行从新布局

全局范畴回流

<body>
  <div class="hello">
    <h4>hello</h4>
    <p><strong>Name:</strong>BDing</p>
    <h5>male</h5>
    <ol>
      <li>coding</li>
      <li>loving</li>
    </ol>
  </div>
</body>

p 节点上产生 reflow 时,hellobody 也会从新渲染,甚至 h5ol都会收到影响

部分范畴回流

用部分布局来解释这种景象:把一个 dom 的宽高之类的几何信息定死,而后在 dom 外部触发重排,就只会从新渲染该 dom 外部的元素,而不会影响到外界

3.5 缩小重绘和回流

应用 translate 代替 top

<div class="test"></div>
<style>
    .test {
        position: absolute;
        top: 10px;
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<script>
    setTimeout(() => {
        // 引起回流
        document.querySelector('.test').style.top = '100px'
    }, 1000)
</script>
  • 应用 visibility 替换 display: none,因为前者只会引起重绘,后者会引发回流(扭转了布局)
  • DOM 离线后批改,比方:先把 DOMdisplay:none (有一次 Reflow),而后你批改 100 次,而后再把它显示进去
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为须要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要应用 table 布局,可能很小的一个小改变会造成整个 table 的从新布局
  • 动画实现的速度的抉择,动画速度越快,回流次数越多,也能够抉择应用 requestAnimationFrame
  • CSS选择符从右往左匹配查找,防止 DOM深度过深
  • 将频繁运行的动画变为图层,图层可能阻止该节点回流影响别的元素。比方对于 video标签,浏览器会主动将该节点变为图层。

viewport

<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
    // width    设置 viewport 宽度,为一个正整数,或字符串‘device-width’// device-width  设施宽度
    // height   设置 viewport 高度,个别设置了宽度,会主动解析出高度,能够不必设置
    // initial-scale    默认缩放比例(初始缩放比例),为一个数字,能够带小数
    // minimum-scale    容许用户最小缩放比例,为一个数字,能够带小数
    // maximum-scale    容许用户最大缩放比例,为一个数字,能够带小数
    // user-scalable    是否容许手动缩放
  • 延长发问

    • 怎么解决 挪动端 1px 被 渲染成 2px问题

部分解决

  • meta标签中的 viewport属性,initial-scale 设置为 1
  • rem依照设计稿规范走,外加利用transfromescale(0.5) 放大一倍即可;

全局解决

  • mate标签中的 viewport属性,initial-scale 设置为 0.5
  • rem 依照设计稿规范走即可

DNS 同时应用 TCP 和 UDP 协定?

DNS 占用 53 号端口,同时应用 TCP 和 UDP 协定。(1)在区域传输的时候应用 TCP 协定

  • 辅域名服务器会定时(个别 3 小时)向主域名服务器进行查问以便理解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送应用 TCP 而不是 UDP,因为数据同步传送的数据量比一个申请应答的数据量要多得多。
  • TCP 是一种牢靠连贯,保障了数据的准确性。

(2)在域名解析的时候应用 UDP 协定

  • 客户端向 DNS 服务器查问域名,个别返回的内容都不超过 512 字节,用 UDP 传输即可。不必通过三次握手,这样 DNS 服务器负载更低,响应更快。实践上说,客户端也能够指定向 DNS 服务器查问时用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅反对 UDP 查问包。

TCP 和 UDP 的概念及特点

TCP 和 UDP 都是传输层协定,他们都属于 TCP/IP 协定族:

(1)UDP

UDP 的全称是 用户数据报协定,在网络中它与 TCP 协定一样用于解决数据包,是一种无连贯的协定。在 OSI 模型中,在传输层,处于 IP 协定的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的毛病,也就是说,当报文发送之后,是无奈得悉其是否平安残缺达到的。

它的特点如下:

1)面向无连贯

首先 UDP 是不须要和 TCP 一样在发送数据前进行三次握手建设连贯的,想发数据就能够开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协定,UDP 只会给数据减少一个 UDP 头标识下是 UDP 协定,而后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2)有单播,多播,播送的性能

UDP 不止反对一对一的传输方式,同样反对一对多,多对多,多对一的形式,也就是说 UDP 提供了单播,多播,播送的性能。

3)面向报文

发送方的 UDP 对应用程序交下来的报文,在增加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因而,应用程序必须抉择适合大小的报文

4)不可靠性

首先不可靠性体现在无连贯上,通信都不须要建设连贯,想发就发,这样的状况必定不牢靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关怀对方是否曾经正确接管到数据了。

再者网络环境时好时坏,然而 UDP 因为没有拥塞管制,始终会以恒定的速度发送数据。即便网络条件不好,也不会对发送速率进行调整。这样实现的弊病就是在网络条件不好的状况下可能会导致丢包,然而长处也很显著,在某些实时性要求高的场景(比方电话会议)就须要应用 UDP 而不是 TCP。

5)头部开销小,传输数据报文时是很高效的。

UDP 头部蕴含了以下几个数据:

  • 两个十六位的端口号,别离为源端口(可选字段)和指标端口
  • 整个数据报文的长度
  • 整个数据报文的测验和(IPv4 可选字段),该字段用于发现头部信息和数据中的谬误

因而 UDP 的头部开销小,只有 8 字节,相比 TCP 的至多 20 字节要少得多,在传输数据报文时是很高效的。

(2)TCP TCP 的全称是传输控制协议是一种面向连贯的、牢靠的、基于字节流的传输层通信协议。TCP 是面向连贯的、牢靠的流协定(流就是指不间断的数据结构)。

它有以下几个特点:

1)面向连贯

面向连贯,是指发送数据之前必须在两端建设连贯。建设连贯的办法是“三次握手”,这样能建设牢靠的连贯。建设连贯,是为数据的牢靠传输打下了根底。

2)仅反对单播传输

每条 TCP 传输连贯只能有两个端点,只能进行点对点的数据传输,不反对多播和播送传输方式。

3)面向字节流

TCP 不像 UDP 一样那样一个个报文独立地传输,而是在不保留报文边界的状况下以字节流形式进行传输。

4)牢靠传输

对于牢靠传输,判断丢包、误码靠的是 TCP 的段编号以及确认号。TCP 为了保障报文传输的牢靠,就给每个包一个序号,同时序号也保障了传送到接收端实体的包的按序接管。而后接收端实体对已胜利收到的字节发回一个相应的确认 (ACK);如果发送端实体在正当的往返时延(RTT) 内未收到确认,那么对应的数据(假如失落了)将会被重传。

5)提供拥塞管制

当网络呈现拥塞的时候,TCP 可能减小向网络注入数据的速率和数量,缓解拥塞。

6)提供全双工通信

TCP 容许通信单方的应用程序在任何时候都能发送数据,因为 TCP 连贯的两端都设有缓存,用来长期寄存双向通信的数据。当然,TCP 能够立刻发送一个数据段,也能够缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于 MSS)

参考:前端进阶面试题具体解答

模块化

js 中当初比拟成熟的有四种模块加载计划:

  • 第一种是 CommonJS 计划,它通过 require 来引入模块,通过 module.exports 定义模块的输入接口。这种模块加载计划是服务器端的解决方案,它是以同步的形式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取十分快,所以以同步的形式加载没有问题。但如果是在浏览器端,因为模块的加载是应用网络申请,因而应用异步加载的形式更加适合。
  • 第二种是 AMD 计划,这种计划采纳异步加载的形式来加载模块,模块的加载不影响前面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载实现后再执行回调函数。require.js 实现了 AMD 标准
  • 第三种是 CMD 计划,这种计划和 AMD 计划都是为了解决异步模块加载的问题,sea.js 实现了 CMD 标准。它和 require.js 的区别在于模块定义时对依赖的解决不同和对依赖模块的执行机会的解决不同。
  • 第四种计划是 ES6 提出的计划,应用 import 和 export 的模式来导入导出模块

在有 Babel 的状况下,咱们能够间接应用 ES6的模块化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJsNode 独有的标准,浏览器中应用就须要用到 Browserify解析了。

// a.js
module.exports = {a: 1}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代码中,module.exportsexports 很容易混同,让咱们来看看大抵外部实现

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立刻执行函数,这样就不会净化全局变量了,// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {a: 1}
// 根本实现
var module = {exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法类似的起因
var exports = module.exports
var load = function (module) {
    // 导出的货色
    var a = 1
    module.exports = a
    return module.exports
};

再来说说 module.exportsexports,用法其实是类似的,然而不能对 exports 间接赋值,不会有任何成果。

对于 CommonJSES6 中的模块化的两者区别是:

  • 前者反对动静导入,也就是 require(${path}/xx.js),后者目前不反对,然而已有提案, 前者是同步导入,因为用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。
  • 而后者是异步导入,因为用于浏览器,须要下载文件,如果也采纳同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会扭转,所以如果想更新值,必须从新导入一次。
  • 然而后者采纳实时绑定的形式,导入导出的值都指向同一个内存地址,所以导入值会追随导出值变动
  • 后者会编译成 require/exports 来执行的

AMD

AMD 是由 RequireJS 提出的

AMD 和 CMD 标准的区别?

  • 第一个方面是在模块定义时对依赖的解决不同。AMD 推崇依赖前置,在定义模块的时候就要申明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
  • 第二个方面是对依赖模块的执行机会解决不同。首先 AMD 和 CMD 对于模块的加载形式都是异步加载,不过它们的区别在于模块的执行机会,AMD 在依赖模块加载实现后就间接执行依赖模块,依赖模块的执行程序和咱们书写的程序不肯定统一。而 CMD 在依赖模块加载实现后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行程序就和咱们书写的程序保持一致了。
// CMD
define(function(require, exports, module) {var a = require("./a");
  a.doSomething();
  // 此处略去 100 行
  var b = require("./b"); // 依赖能够就近书写
  b.doSomething();
  // ...
});

// AMD 默认举荐
define(["./a", "./b"], function(a, b) {
  // 依赖必须一开始就写好
  a.doSomething();
  // 此处略去 100 行
  b.doSomething();
  // ...
})
  • AMDrequirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置
  • CMDseajs 在推广过程中对模块定义的规范化产出,提早执行,推崇依赖就近
  • CommonJs:模块输入的是一个值的 copy,运行时加载,加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才会生成
  • ES6 Module:模块输入的是一个值的援用,编译时输入接口,ES6模块不是对象,它对外接口只是一种动态定义,在代码动态解析阶段就会生成。

谈谈对模块化开发的了解

  • 我对模块的了解是,一个模块是实现一个特定性能的一组办法。在最开始的时候,js 只实现一些简略的性能,所以并没有模块的概念,但随着程序越来越简单,代码的模块化开发变得越来越重要。
  • 因为函数具备独立作用域的特点,最原始的写法是应用函数来作为模块,几个函数作为一个模块,然而这种形式容易造成全局变量的净化,并且模块间没有分割。
  • 前面提出了对象写法,通过将函数作为一个对象的办法来实现,这样解决了间接应用函数作为模块的一些毛病,然而这种方法会裸露所有的所有的模块成员,内部代码能够批改外部属性的值。
  • 当初最罕用的是立刻执行函数的写法,通过利用闭包来实现模块公有作用域的建设,同时不会对全局作用域造成净化。

垃圾回收

  • 对于在 JavaScript 中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当 JavaScript 的解释器耗费完零碎中所有可用的内存时,就会造成零碎解体。
  • 内存透露,在某些状况下,不再应用到的变量所占用内存没有及时开释,导致程序运行中,内存越占越大,极其状况下能够导致系统解体,服务器宕机。
  • JavaScript 有本人的一套垃圾回收机制,JavaScript 的解释器能够检测到什么时候程序不再应用这个对象了(数据),就会把它所占用的内存开释掉。
  • 针对 JavaScript 的来及回收机制有以下两种办法(罕用):标记革除,援用计数
  • 标记革除

v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。

  • 新创建的对象或者只经验过一次的垃圾回收的对象被称为新生代。经验过屡次垃圾回收的对象被称为老生代。
  • 新生代被分为 From 和 To 两个空间,To 个别是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当咱们执行垃圾回收算法的时候应用逻辑将会进行,等垃圾回收完结后再继续执行。

这个算法分为三步:

  • 首先查看 From 空间的存活对象,如果对象存活则判断对象是否满足降职到老生代的条件,如果满足条件则降职到老生代。如果不满足条件则挪动 To 空间。
  • 如果对象不存活,则开释对象的空间。
  • 最初将 From 空间和 To 空间角色进行替换。

新生代对象降职到老生代有两个条件:

  • 第一个是判断是对象否曾经通过一次 Scavenge 回收。若经验过,则将对象从 From 空间复制到老生代中;若没有经验,则复制到 To 空间。
  • 第二个是 To 空间的内存应用占比是否超过限度。当对象从 From 空间复制到 To 空间时,若 To 空间应用超过 25%,则对象间接降职到老生代中。设置 25% 的起因次要是因为算法完结后,两个空间完结后会替换地位,如果 To 空间的内存太小,会影响后续的内存调配。

老生代采纳了标记革除法和标记压缩法。标记革除法首先会对内存中存活的对象进行标记,标记完结后革除掉那些没有标记的对象。因为标记革除后会造成很多的内存碎片,不便于前面的内存调配。所以了解决内存碎片的问题引入了标记压缩法。

因为在进行垃圾回收的时候会暂停利用的逻辑,对于新生代办法因为内存小,每次进展的工夫不会太长,但对于老生代来说每次垃圾回收的工夫长,进展会造成很大的影响。为了解决这个问题 V8 引入了增量标记的办法,将一次进展进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行

HTTP3

Google 在推 SPDY 的时候就曾经意识到了这些问题,于是就重整旗鼓搞了一个基于 UDP 协定的“QUIC”协定,让 HTTP 跑在 QUIC 上而不是 TCP 上。次要个性如下:

  • 实现了相似 TCP 的流量管制、传输可靠性的性能。尽管 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的根底之上减少了一层来保证数据可靠性传输。它提供了数据包重传、拥塞管制以及其余一些 TCP 中存在的个性
  • 实现了疾速握手性能。因为 QUIC 是基于 UDP 的,所以 QUIC 能够实现应用 0 -RTT 或者 1 -RTT 来建设连贯,这意味着 QUIC 能够用最快的速度来发送和接收数据。
  • 集成了 TLS 加密性能。目前 QUIC 应用的是 TLS1.3,相较于晚期版本 TLS1.3 有更多的长处,其中最重要的一点是缩小了握手所破费的 RTT 个数。
  • 多路复用,彻底解决 TCP 中队头阻塞的问题。

intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否呈现在对象的原型链中的任何地位。

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就持续从其原型上找,Object.getPrototypeOf 办法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

escape、encodeURI、encodeURIComponent 的区别

  • encodeURI 是对整个 URI 进行本义,将 URI 中的非法字符转换为非法字符,所以对于一些在 URI 中有非凡意义的字符不会进行本义。
  • encodeURIComponent 是对 URI 的组成部分进行本义,所以一些特殊字符也会失去本义。
  • escape 和 encodeURI 的作用雷同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是间接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格局,再在每个字节前加上 %。

高低垂直居中计划

  • 定高:marginposition + margin(负值)
  • 不定高:position + transformflexIFC + vertical-align:middle
/* 定高计划 1 */
.center {
  height: 100px;
  margin: 50px 0;   
}
/* 定高计划 2 */
.center {
  height: 100px;
  position: absolute;
  top: 50%;
  margin-top: -25px;
}
/* 不定高计划 1 */
.center {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
/* 不定高计划 2 */
.wrap {
  display: flex;
  align-items: center;
}
.center {width: 100%;}
/* 不定高计划 3 */
/* 设置 inline-block 则会在外层产生 IFC,高度设为 100% 撑开 wrap 的高度 */
.wrap::before {
  content: '';
  height: 100%;
  display: inline-block;
  vertical-align: middle;
}
.wrap {text-align: center;}
.center {
  display: inline-block;  
  vertical-align: middle;
}

内存泄露

  • 意外的全局变量: 无奈被回收
  • 定时器: 未被正确敞开,导致所援用的内部变量无奈被开释
  • 事件监听: 没有正确销毁 (低版本浏览器可能呈现)
  • 闭包

    • 第一种状况是咱们因为应用未声明的变量,而意外的创立了一个全局变量,而使这个变量始终留在内存中无奈被回收。
    • 第二种状况是咱们设置了 setInterval 定时器,而遗记勾销它,如果循环函数有对外部变量的援用的话,那么这个变量会被始终留在内存中,而无奈被回收。
    • 第三种状况是咱们获取一个 DOM 元素的援用,而前面这个元素被删除,因为咱们始终保留了对这个元素的援用,所以它也无奈被回收。
    • 第四种状况是不合理的应用闭包,从而导致某些变量始终被留在内存当中。
  • dom 援用: dom 元素被删除时,内存中的援用未被正确清空
  • 控制台 console.log 打印的货色

可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变动状况,找出异样点。

内存泄露排查办法(opens new window)

9 种前端常见的设计模式

1. 外观模式

外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个对立的高层接口,使子系统更容易应用。简而言之外观设计模式就是把多个子系统中简单逻辑进行形象,从而提供一个更对立、更简洁、更易用的 API。很多咱们罕用的框架和库根本都遵循了外观设计模式,比方 JQuery 就把简单的原生 DOM 操作进行了形象和封装,并打消了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中咱们也会常常用到外观模式进行开发,只是咱们不自知而已

兼容浏览器事件绑定

let addMyEvent = function (el, ev, fn) {if (el.addEventListener) {el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {el.attachEvent('on' + ev, fn)
    } else {el['on' + ev] = fn
    }
};

封装接口

let myEvent = {
    // ...
    stop: e => {e.stopPropagation();
        e.preventDefault();}
};

场景

  • 设计初期,应该要无意识地将不同的两个层拆散,比方经典的三层构造,在数据拜访层和业务逻辑层、业务逻辑层和表示层之间建设外观 Facade
  • 在开发阶段,子系统往往因为一直的重构演变而变得越来越简单,减少外观 Facade 能够提供一个简略的接口,缩小他们之间的依赖。
  • 在保护一个遗留的大型零碎时,可能这个零碎曾经很难保护了,这时候应用外观 Facade 也是十分适合的,为系零碎开发一个外观 Facade 类,为设计毛糙和高度简单的遗留代码提供比拟清晰的接口,让新零碎和 Facade 对象交互,Facade 与遗留代码交互所有的简单工作。

长处

  • 缩小零碎相互依赖。
  • 进步灵活性。
  • 进步了安全性

毛病

不合乎开闭准则,如果要改货色很麻烦,继承重写都不适合。

2. 代理模式

是为一个对象提供一个代用品或占位符,以便管制对它的拜访

假如当 A 在情绪好的时候收到花,小明表白胜利的几率有 60%,而当 A 在情绪差的时候收到花,小明表白的成功率有限趋近于 0。小明跟 A 刚刚意识两天,还无奈分别 A 什么时候情绪好。如果不合时宜地把花送给 A,花被间接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。然而 A 的敌人 B 却很理解 A,所以小明只管把花交给 B,B 会监听 A 的情绪变动,而后抉择 A 情绪好的时候把花转交给 A,代码如下:

let Flower = function() {}
let xiaoming = {sendFlower: function(target) {let flower = new Flower()
    target.receiveFlower(flower)
  }
}
let B = {receiveFlower: function(flower) {A.listenGoodMood(function() {A.receiveFlower(flower)
    })
  }
}
let A = {receiveFlower: function(flower) {console.log('收到花'+ flower)
  },
  listenGoodMood: function(fn) {setTimeout(function() {fn()
    }, 1000)
  }
}
xiaoming.sendFlower(B)

场景

HTML 元 素事件代理

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {console.log(event.target);
  });
</script>

长处

  • 代理模式能将代理对象与被调用对象拆散,升高了零碎的耦合度。代理模式在客户端和指标对象之间起到一个中介作用,这样能够起到爱护指标对象的作用
  • 代理对象能够扩大指标对象的性能;通过批改代理对象就能够了,合乎开闭准则;

毛病

解决申请速度可能有差异,非间接拜访存在开销

3. 工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化提早到了子类。而子类能够重写接口办法以便创立的时候指定本人的对象类型。

class Product {constructor(name) {this.name = name}
    init() {console.log('init')
    }
    fun() {console.log('fun')
    }
}

class Factory {create(name) {return new Product(name)
    }
}

// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()

场景

  • 如果你不想让某个子系统与较大的那个对象之间造成强耦合,而是想运行时从许多子系统中进行筛选的话,那么工厂模式是一个现实的抉择
  • 将 new 操作简略封装,遇到 new 的时候就应该思考是否用工厂模式;
  • 须要依赖具体环境创立不同实例,这些实例都有雷同的行为, 这时候咱们能够应用工厂模式,简化实现的过程,同时也能够缩小每种对象所需的代码量,有利于打消对象间的耦合,提供更大的灵活性

长处

  • 创建对象的过程可能很简单,但咱们只须要关怀创立后果。
  • 构造函数和创建者拆散, 合乎“开闭准则”
  • 一个调用者想创立一个对象,只有晓得其名称就能够了。
  • 扩展性高,如果想减少一个产品,只有扩大一个工厂类就能够。

毛病

  • 增加新产品时,须要编写新的具体产品类, 肯定水平上减少了零碎的复杂度
  • 思考到零碎的可扩展性,须要引入形象层,在客户端代码中均应用形象层进行定义,减少了零碎的抽象性和了解难度

什么时候不必

当被利用到谬误的问题类型上时, 这一模式会给应用程序引入大量不必要的复杂性. 除非为创建对象提供一个接口是咱们编写的库或者框架的一个设计上指标, 否则我会倡议应用明确的结构器, 以防止不必要的开销。

因为对象的创立过程被高效的形象在一个接口前面的事实, 这也会给依赖于这个过程可能会有多简单的单元测试带来问题。

4. 单例模式

顾名思义,单例模式中 Class 的实例个数最多为 1。当须要一个对象去贯通整个零碎执行某些工作时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的应用,因为单例模式会引入全局状态,而一个衰弱的零碎应该防止引入过多的全局状态。

实现单例模式须要解决以下几个问题:

  • 如何确定 Class 只有一个实例?
  • 如何简便的拜访 Class 的惟一实例?
  • Class 如何管制实例化的过程?
  • 如何将 Class 的实例个数限度为 1?

咱们个别通过实现以下两点来解决上述问题:

  • 暗藏 Class 的构造函数,防止屡次实例化
  • 通过裸露一个 getInstance() 办法来创立 / 获取惟一实例

Javascript 中单例模式能够通过以下形式实现:

// 单例结构器
const FooServiceSingleton = (function () {
  // 暗藏的 Class 的构造函数
  function FooService() {}

  // 未初始化的单例对象
  let fooService;

  return {
    // 创立 / 获取单例对象的函数
    getInstance: function () {if (!fooService) {fooService = new FooService();
      }
      return fooService;
    }
  }
})();

实现的关键点有:

  • 应用 IIFE 创立部分作用域并即时执行;
  • getInstance() 为一个 闭包,应用闭包保留部分作用域中的单例对象并返回。

咱们能够验证下单例对象是否创立胜利:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();

console.log(fooService1 === fooService2); // true

场景例子

  • 定义命名空间和实现分支型办法
  • 登录框
  • vuex 和 redux 中的 store

长处

  • 划分命名空间,缩小全局变量
  • 加强模块性,把本人的代码组织在一个全局变量名下,放在繁多地位,便于保护
  • 且只会实例化一次。简化了代码的调试和保护

毛病

  • 因为单例模式提供的是一种单点拜访,所以它有可能导致模块间的强耦合
  • 从而不利于单元测试。无奈独自测试一个调用了来自单例的办法的类,而只能把它与那个单例作为一 个单元一起测试。

5. 策略模式

策略模式简略形容就是:对象有某个行为,然而在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们能够相互替换

<html>
<head>
    <title> 策略模式 - 校验表单 </title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        明码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit"> 提交 </button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {isNoEmpty: function (value, errorMsg) {if (value === '') {return errorMsg;}
          },
          isNoSpace: function (value, errorMsg) {if (value.trim() === '') {return errorMsg;}
          },
          minLength: function (value, length, errorMsg) {if (value.trim().length < length) {return errorMsg;}
          },
          maxLength: function (value, length, errorMsg) {if (value.length > length) {return errorMsg;}
          },
          isMobile: function (value, errorMsg) {if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {return errorMsg;}                
          }
        }

        // 验证类
        class Validator {constructor() {this.cache = []
          }
          add(dom, rules) {for(let i = 0, rule; rule = rules[i++];) {let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {let errorMsg = validatorFunc()
              if (errorMsg) {return errorMsg}
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不容许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于 2 位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '明码长度不能小于 6 位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输出正确的手机号码格局'
          }])
          return validator.start()}

        registerForm.onsubmit = function() {let errorMsg = validataFunc()
          if (errorMsg) {alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>

场景例子

  • 如果在一个零碎外面有许多类,它们之间的区别仅在于它们的 ’ 行为 ’,那么应用策略模式能够动静地让一个对象在许多行为中抉择一种行为。
  • 一个零碎须要动静地在几种算法中抉择一种。
  • 表单验证

长处

  • 利用组合、委托、多态等技术和思维,能够无效的防止多重条件抉择语句
  • 提供了对凋谢 - 关闭准则的完满反对,将算法封装在独立的 strategy 中,使得它们易于切换,了解,易于扩大
  • 利用组合和委托来让 Context 领有执行算法的能力,这也是继承的一种更轻便的代替计划

毛病

  • 会在程序中减少许多策略类或者策略对象
  • 要应用策略模式,必须理解所有的 strategy,必须理解各个 strategy 之间的不同点,这样能力抉择一个适合的 strategy

6. 迭代器模式

如果你看到这,ES6 中的迭代器 Iterator 置信你还是有点印象的,下面第 60 条曾经做过简略的介绍。迭代器模式简略的说就是提供一种办法程序一个聚合对象中各个元素,而又不裸露该对象的外部示意。

迭代器模式解决了以下问题:

  • 提供统一的遍历各种数据结构的形式,而不必理解数据的内部结构
  • 提供遍历容器(汇合)的能力而无需扭转容器的接口

一个迭代器通常须要实现以下接口:

  • hasNext():判断迭代是否完结,返回 Boolean
  • next():查找并返回下一个元素

为 Javascript 的数组实现一个迭代器能够这么写:

const item = [1, 'red', false, 3.14];

function Iterator(items) {
  this.items = items;
  this.index = 0;
}

Iterator.prototype = {hasNext: function () {return this.index < this.items.length;},
  next: function () {return this.items[this.index++];
  }
}

验证一下迭代器是否工作:

const iterator = new Iterator(item);

while(iterator.hasNext()){console.log(iterator.next());
}
// 输入:1, red, false, 3.14

ES6 提供了更简略的迭代循环语法 for…of,应用该语法的前提是操作对象须要实现 可迭代协定(The iterable protocol),简略说就是该对象有个 Key 为 Symbol.iterator 的办法,该办法返回一个 iterator 对象。

比方咱们实现一个 Range 类用于在某个数字区间进行迭代:

function Range(start, end) {
  return {[Symbol.iterator]: function () {
      return {next() {if (start < end) {return { value: start++, done: false};
          }
          return {done: true, value: end};
        }
      }
    }
  }
}

验证一下:

for (num of Range(1, 5)) {console.log(num);
}
// 输入:1, 2, 3, 4

7. 观察者模式

观察者模式又称公布 - 订阅模式(Publish/Subscribe Pattern),是咱们常常接触到的设计模式,日常生活中的利用也亘古未有,比方你订阅了某个博主的频道,当有内容更新时会收到推送;又比方 JavaScript 中的事件订阅响应机制。观察者模式的思维用一句话形容就是:被察看对象(subject)保护一组观察者(observer),当被察看对象状态扭转时,通过调用观察者的某个办法将这些变动告诉到观察者。

观察者模式中 Subject 对象个别须要实现以下 API:

  • subscribe(): 接管一个观察者 observer 对象,使其订阅本人
  • unsubscribe(): 接管一个观察者 observer 对象,使其勾销订阅本人
  • fire(): 触发事件,告诉到所有观察者

用 JavaScript 手动实现观察者模式:

// 被观察者
function Subject() {this.observers = [];
}

Subject.prototype = {
  // 订阅
  subscribe: function (observer) {this.observers.push(observer);
  },
  // 勾销订阅
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {return observer !== observerToRemove;})
  },
  // 事件触发
  fire: function () {
    this.observers.forEach(observer => {observer.call();
    });
  }
}

验证一下订阅是否胜利:

const subject = new Subject();

function observer1() {console.log('Observer 1 Firing!');
}


function observer2() {console.log('Observer 2 Firing!');
}

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();

// 输入:Observer 1 Firing! 
Observer 2 Firing!

验证一下勾销订阅是否胜利:

subject.unsubscribe(observer2);
subject.fire();

// 输入:Observer 1 Firing!

场景

  • DOM 事件
document.body.addEventListener('click', function() {console.log('hello world!');
});
document.body.click()
  • vue 响应式

长处

  • 反对简略的播送通信,主动告诉所有曾经订阅过的对象
  • 指标对象与观察者之间的形象耦合关系能独自扩大以及重用
  • 减少了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的单方都依赖于形象,而不是依赖于具体。从而使得各自的变动都不会影响到另一边的变动。

毛病

适度应用会导致对象与对象之间的分割弱化,会导致程序难以跟踪保护和了解

8. 中介者模式

  • 在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的形式,使得这些对象不用间接相互作用,而是由中介者协调它们之间的交互,从而使它们能够涣散偶合。当某些对象之间的作用产生扭转时,不会立刻影响其余的一些对象之间的作用,保障这些作用能够彼此独立的变动。
  • 中介者模式和观察者模式有肯定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是解决同级对象之间的交互,而观察者模式是解决 Observer 和 Subject 之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能间接交换,而是要通过中介去筛选匹配再决定谁和谁见面。

场景

例如购物车需要,存在商品抉择表单、色彩抉择表单、购买数量表单等等,都会触发 change 事件,那么能够通过中介者来转发解决这些事件,实现各个事件间的解耦,仅仅保护中介者对象即可。

var goods = {   // 手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
// 中介者
var mediator = (function() {var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {changed: function(obj) {switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {mediator.changed(this);
};
memorySelect.onchange = function() {mediator.changed(this);
};
numSelect.onchange = function() {mediator.changed(this);
};
  • 聊天室里

聊天室成员类:

function Member(name) {
  this.name = name;
  this.chatroom = null;
}

Member.prototype = {
  // 发送音讯
  send: function (message, toMember) {this.chatroom.send(message, this, toMember);
  },
  // 接管音讯
  receive: function (message, fromMember) {console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }
}

聊天室类:

function Chatroom() {this.members = {};
}

Chatroom.prototype = {
  // 减少成员
  addMember: function (member) {this.members[member.name] = member;
    member.chatroom = this;
  },
  // 发送音讯
  send: function (message, fromMember, toMember) {toMember.receive(message, fromMember);
  }
}

测试一下:

const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');

chatroom.addMember(bruce);
chatroom.addMember(frank);

bruce.send('Hey frank', frank);

// 输入:bruce to frank: hello frank

长处

  • 使各对象之间耦合涣散,而且能够独立地扭转它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的简单耦合度导致保护很艰难,而且耦合度随我的项目变动增速很快,就须要中介者重构代码

毛病

零碎中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象常常是微小的。中介 者对象本身往往就是一个难以保护的对象。

9. 访问者模式

访问者模式 是一种将算法与对象构造拆散的设计模式,艰深点讲就是:访问者模式让咱们可能在不扭转一个对象构造的前提下可能给该对象减少新的逻辑,新增的逻辑保留在一个独立的访问者对象中。访问者模式罕用于拓展一些第三方的库和工具。

// 访问者  
class Visitor {constructor() {}
    visitConcreteElement(ConcreteElement) {ConcreteElement.operation()
    }
}
// 元素类  
class ConcreteElement{constructor() { }
    operation() {console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {visitor.visitConcreteElement(this)
    }
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)

访问者模式的实现有以下几个因素:

  • Visitor Object:访问者对象,领有一个 visit()办法
  • Receiving Object:接管对象,领有一个 accept() 办法
  • visit(receivingObj):用于 Visitor 接管一个 Receiving Object
  • accept(visitor):用于 Receving Object 接管一个 Visitor,并通过调用 Visitor 的 visit() 为其提供获取 Receiving Object 数据的能力

简略的代码实现如下:

Receiving Object:function Employee(name, salary) {
  this.name = name;
  this.salary = salary;
}

Employee.prototype = {getSalary: function () {return this.salary;},
  setSalary: function (salary) {this.salary = salary;},
  accept: function (visitor) {visitor.visit(this);
  }
}
Visitor Object:function Visitor() {}

Visitor.prototype = {visit: function (employee) {employee.setSalary(employee.getSalary() * 2);
  }
}

验证一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);

console.log(employee.getSalary());// 输入:2000

场景

对象构造中对象对应的类很少扭转,但常常须要在此对象构造上定义新的操作

须要对一个对象构造中的对象进行很多不同的并且不相干的操作,而须要防止让这些操作 ” 净化 ” 这些对象的类,也不心愿在减少新操作时批改这些类。

长处

  • 合乎繁多职责准则
  • 优良的扩展性
  • 灵活性

毛病

  • 具体元素对访问者颁布细节,违反了迪米特准则
  • 违反了依赖倒置准则,依赖了具体类,没有依赖形象。
  • 具体元素变更比拟艰难

async/await 如何捕捉异样

async function fn(){
    try{let a = await Promise.reject('error')
    }catch(error){console.log(error)
    }
}

性能优化

性能优化是前端开发中避不开的问题,性能问题无外乎两方面起因:渲染速度慢、申请工夫长。性能优化尽管波及很多简单的起因和解决方案,但其实只有通过正当地应用标签,就能够在肯定水平上晋升渲染速度以及缩小申请工夫

1. script 标签:调整加载程序晋升渲染速度

  • 因为浏览器的底层运行机制,渲染引擎在解析 HTML 时,若遇到 script 标签援用文件,则会暂停解析过程 ,同时 告诉网络线程加载文件,文件加载后会切换至 JavaScript 引擎来执行对应代码 代码执行实现之后切换至渲染引擎持续渲染页面
  • 在这一过程中能够看到,页面渲染过程中蕴含了申请文件以及执行文件的工夫,但页面的首次渲染可能并不依赖这些文件,这些申请和执行文件的动作反而缩短了用户看到页面的工夫,从而升高了用户体验。

为了缩小这些工夫损耗,能够借助 script 标签的 3 个属性来实现。

  • async 属性 。立刻申请文件,但不阻塞渲染引擎,而是 文件加载结束后阻塞渲染引擎并立刻执行文件内容
  • defer 属性 。立刻申请文件,但不阻塞渲染引擎, 等到解析完 HTML 之后再执行 文件内容
  • HTML5 规范 type 属性,对应值为“module”。让浏览器依照 ECMA Script 6 规范将文件当作模块进行解析,默认阻塞成果同 defer,也能够配合 async 在申请实现后立刻执行。

绿色的线示意执行解析 HTML,蓝色的线示意申请文件,红色的线示意执行文件

当渲染引擎解析 HTML 遇到 script 标签引入文件时,会立刻进行一次渲染。所以这也就是为什么构建工具会把编译好的援用 JavaScript 代码的 script 标签放入到 body 标签底部,因为当渲染引擎执行到 body 底部时会先将已解析的内容渲染进去,而后再去申请相应的 JavaScript 文件

2. link 标签:通过预处理晋升渲染速度

在咱们对大型单页利用进行性能优化时,兴许会用到按需懒加载的形式,来加载对应的模块,但如果能正当利用 link 标签的 rel 属性值来进行预加载,就能进一步晋升渲染速度。

  • dns-prefetch。当 link 标签的 rel 属性值为“dns-prefetch”时,浏览器会对某个域名事后进行 DNS 解析并缓存 。这样,当浏览器在申请同域名资源的时候,能省去从域名查问 IP 的过程,从而 缩小工夫损耗。下图是淘宝网设置的 DNS 预解析
  • preconnect。让浏览器在一个 HTTP 申请正式发给服务器前事后执行一些操作,这包含DNS 解析、TLS 协商、TCP 握手,通过打消往返提早来为用户节省时间
  • prefetch/preload。两个值都是 让浏览器事后下载并缓存某个资源,但不同的是,prefetch 可能会在浏览器忙时被疏忽,而 preload 则是肯定会被事后下载
  • prerender。浏览器不仅会加载资源,还会解析执行页面,进行预渲染

这几个属性值恰好反映了浏览器获取资源文件的过程,在这里我绘制了一个流程简图,不便你记忆。

3. 搜寻优化

  • meta 标签:提取要害信息

    • 通过 meta 标签能够设置页面的形容信息,从而让搜索引擎更好地展现搜寻后果。
    • 示例 <meta name="description" content="寰球最大的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,能够霎时找到相干的搜寻后果。">

forEach 和 map 办法有什么区别

这办法都是用来遍历数组的,两者区别如下:

  • forEach()办法会针对每一个元素执行提供的函数,对数据的操作会扭转原数组,该办法没有返回值;
  • map()办法不会扭转原数组的值,返回一个新数组,新数组中的值为原数组调用函数解决之后的值;

Sass、Less 是什么?为什么要应用他们?

他们都是 CSS 预处理器,是 CSS 上的一种形象层。他们是一种非凡的语法 / 语言编译成 CSS。例如 Less 是一种动静款式语言,将 CSS 赋予了动静语言的个性,如变量,继承,运算,函数,LESS 既能够在客户端上运行 (反对 IE 6+, Webkit, Firefox),也能够在服务端运行 (借助 Node.js)。

为什么要应用它们?

  • 构造清晰,便于扩大。能够不便地屏蔽浏览器公有语法差别。封装对浏览器语法差别的反复解决,缩小无意义的机械劳动。
  • 能够轻松实现多重继承。齐全兼容 CSS 代码,能够不便地利用到老我的项目中。LESS 只是在 CSS 语法上做了扩大,所以老的 CSS 代码也能够与 LESS 代码一起编译。

节流与防抖

  • 函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。
  • 函数节流 是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。
// 函数防抖的实现
function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = arguments;

    // 如果此时存在定时器的话,则勾销之前的定时器从新记时
    if (timer) {clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {fn.apply(context, args);
    }, wait);
  };
}

// 函数节流的实现;
function throttle(fn, delay) {var preTime = Date.now();

  return function() {
    var context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - preTime >= delay) {preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}
正文完
 0