常用正则

. - 除换行符以外的所有字符。
^ - 字符串开头。
$ - 字符串结尾。
\d,\w,\s - 匹配数字、字符、空格。
\D,\W,\S - 匹配非数字、非字符、非空格。
[abc] - 匹配 a、b 或 c 中的一个字母。
[a-z] - 匹配 a 到 z 中的一个字母。
[^abc] - 匹配除了 a、b 或 c 中的其他字母。
aa|bb - 匹配 aa 或 bb。
? - 0 次或 1 次匹配。
* - 匹配 0 次或多次。
+ - 匹配 1 次或多次。
{n} - 匹配 n次。
{n,} - 匹配 n次以上。
{m,n} - 最少 m 次,最多 n 次匹配。
(expr) - 捕获 expr 子模式,以 \1 使用它。
(?:expr) - 忽略捕获的子模式。
(?=expr) - 正向预查模式 expr。
(?!expr) - 负向预查模式 expr。

任意正负数字 ^(\-|\+)?\d+(\.\d+)?$
邮箱 ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
最多保留2位小数的非负数字
匹配中文 ^[\u4e00-\u9fa5]*$

常用正则表达式

校验数字的表达式

  • n 位的数字:^\d{n}$
  • 至少 n 位的数字:^\d{n,}$
  • m-n 位的数字:^\d{m,n}$
  • 零和非零开头的数字:^(0|[1-9][0-9]*)$
  • 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
  • 带 1-2 位小数的正数或负数:^(\-)?\d+(\.\d{1,2})$
  • 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
  • 有两位小数的正实数:^[0-9]+(\.[0-9]{2})?$
  • 非零的正整数:^[1-9]\d*$
  • 非零的负整数:^-[1-9]\d*$
  • 非负整数:^\d+$
  • 非正整数:^-[1-9]\d*|0$

校验字符的表达式

  • 汉字:^[\u4e00-\u9fa5]{0,}$
  • 英文和数字:^[A-Za-z0-9]+$
  • 长度为 3-20 的所有字符:^\.{3,20}$
  • 由 26 个英文字母组成的字符串:^[A-Za-z]+$
  • 由 26 个大写英文字母组成的字符串:^[A-Z]+$
  • 由 26 个小写英文字母组成的字符串:^[a-z]+$
  • 由数字和 26 个英文字母组成的字符串:^[A-Za-z0-9]+$
  • 由数字、26 个英文字母或者下划线组成的字符串:^\w+$
  • 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
  • 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$

钱的输入格式:
有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]$
这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9]
)$
一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9])$
这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$
这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})
(.[0-9]{1,2})?$
1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3}))(.[0-9]{1,2})?$
备注:这就是最终结果了,别忘了”+”可以用”
“替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里

浏览器的组成

  • 用户界面- 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
  • 浏览器引擎- 用来查询及操作渲染引擎的接口
  • 渲染引擎(浏览器内核)- 用来显示请求的内容,例如,如果请求内容为 html,它负责解析 html 及 css,并将解析后的结果显示出来
  • 网络- 用来完成网络调用,例如 http 请求,它具有平台无关的接口,可以在不同平台上工作
  • UI 后端- 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
  • JS 解释器- 用来解释执行 JS 代码
  • 数据存储- 属于持久层,浏览器需要在硬盘中保存类似 cookie 的各种数据,HTML5 定义了 Storage 技术,这是一种轻量级完整的客户端存储技术

主流的渲染引擎

浏览器的渲染引擎也叫排版引擎,或者是浏览器内核

主流的 渲染引擎 有

  • Chrome 浏览器: Blink 引擎(WebKit 的一个分支)。
  • Safari 浏览器: WebKit 引擎,windows 版本 2008 年 3 月 18 日推出正式版,但苹果已于 2012 年 7 月 25 日停止开发 Windows 版的 Safari。
  • FireFox 浏览器: Gecko 引擎。
  • Opera 浏览器: Blink 引擎(早期版使用 Presto 引擎)。
  • Internet Explorer 浏览器: Trident 引擎。
  • Microsoft Edge 浏览器: EdgeHTML 引擎(Trident 的一个分支)。

渲染引擎工作原理

渲染引擎解析的基本流程:

  • 解析 HTML 构建 DOM树,同时解析所有的 css 样式,构建 css 规则
  • 根据 DOM 树和 css 规则合并构建 渲染树
    • DOM 树上的节点没有样式的,渲染树的节点有样式的
    • 渲染树上的节点都是需要渲染的,所以渲染树上没有像head标签 或 display: none这样的元素,但是它们在 DOM 树中
  • 对渲染树进行布局,定位坐标和大小、确定是否换行、确定 position、overflow、z-index 等等,这个过程叫layoutreflow
  • 绘制渲染树,调用操作系统底层 API(UI Backend)进行绘图操作

webkit 内核工作流程

gecko 内核工作流程

重绘与回流

重绘与回流

回流(reflow): 又叫重排,当 render tree 中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。

重绘(repaint):当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。

  1. 每个页面至少需要一次回流+重绘
  2. 回流必将引起重绘

回流什么时候发生?

  • 添加或者删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变
  • 页面渲染初始化
  • 浏览器窗口尺寸改变(resize事件)发生时
var s = document.body.style
s.padding = '2px' // 回流+重绘
s.border = '1px solid red' // 再一次 回流+重绘
s.color = 'blue' // 再一次重绘
s.backgroundColor = '#ccc' // 再一次 重绘
s.fontSize = '14px' // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'))

聪明的浏览器

从上个实例代码中可以看到几行简单的 JS 代码就引起了 6 次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句 JS 操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护 1 个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前 flush 队列,这样浏览器的优化可能就起不到作用了。当你向浏览器请求一些 style 信息的时候,就会让浏览器 flush 队列,比如:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • width,height
  • 请求了 getComputedStyle(), 或者 IE 的 currentStyle

如何性能优化

减少回流与重绘的次数,就需要简单对渲染树的操作

  • 直接使用 className 修改样式,少用 style 设置样式
  • 让要操作的元素进行”离线处理”,处理完后一起更新
    • 使用 DocumentFragment 进行缓存操作,引发一次回流和重绘
    • 使用 display:none 技术,只引发两次回流和重绘
  • 将需要多次重排的元素,position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素为动画的 HTML 元素,例如动画,那么修改他们的 CSS 是会大大减小 reflow

生命周期介绍

vue 生命周期钩子函数

beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用;此时组件的选项还未挂载,因此无法访问 methods,data,computed 上的方法或数据;使用场景 : 几乎不用

created:在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见;可以调用 methods 中的方法、改变 data 中的数据、获取 computed 中的计算属性等;使用场景:发送 ajax、本地存储获取数据

beforeMounted():在挂载开始之前被调用(挂载:DOM 渲染)

mounted():这个周期可以获取 DOM;指令的生效在 mounted 周期之前;在这个周期内,对 data 的改变可以生效。但是要进下一轮的 DOM 更新;使用场景:发送 ajax、操作 DOM

beforeUpdate():数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程;此处获取的数据是更新后的数据,但是获取页面中的 DOM 元素是更新之前的

updated():由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子;组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作

beforeDestroy():实例销毁之前调用。在这一步,实例仍然完全可用;使用场景:实例销毁之前,执行清理任务,比如清除定时器等

destroyed():Vue 实例销毁后调用。调用后,Vue 实例的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁

组件生命周期调用顺序

组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程
父 beforeUpdate -> 父 updated

销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

组件通讯

父组件到子组件:通过绑定属性传递给子组件

子组件到父组件:自定义事件,父组件给子组件传递一个函数,由子组件调用($emit())这个函数

非父子组件通讯

  • 是通过 事件总线 (event bus 公交车) 机制 来实现的

  • 事件总线:实际上就是一个 空 Vue 实例

  • 可以实现任意两个组件之间的通讯而不管两个组件到底有什么样的层级关系

  • $emit():发送数据

  • $on():接收数据

// 实例化事件总线 bus
const bus = new Vue()

// 触发组件 A 中的事件
bus.$emit('id', 1)

// 在组件 B 创建的钩子中监听事件
bus.$on('id', id => {
// ...
})

双向数据绑定

https://cn.vuejs.org/v2/guide/forms.html
https://cn.vuejs.org/v2/guide/components-custom-events.html#自定义组件的-v-model

vue 数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。其实主要是用了 Es5 中的 Object.defineProperty 来劫持每个属性的 getter 和 setter。这也正是 Vue 不兼容 IE8 以下的原因。

v-model 原理(语法糖):v-model是 :value=”msg” @input=”msg=$event.target.value” 的语法糖

Object.defineProperty 不可以用来监听数组

vue 是通过重写数组的方法(变异方法)实现对数组的监听

key属性作用

官方文档中说:

当 Vue.js 用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

没有key属性,Vue 无法跟踪每个节点

文档类型<!DOCTYPE>

<!DOCTYPE html> 声明告诉浏览器按照HTML5规范解析页面

字符集

<meta charset="UTF-8" />

告诉浏览器当前 html 文档使用 UTF-8 进行的字符编码

link 标签 ref 属性值 prefetch preload 用于预加载

<link ref="preload" href="*.js" as="script" />
<!-- preload需要写上正确的as属性,如果不写或错误,等同于XHR请求,优先级非常低 -->
<link rel="preload" href="font.woff" as="font" crossorigin />
<!-- 预加载字体你还必须设置crossorigin 属性 -->
  • preload 是用于预加载当前页的资源,浏览器会优先加载它们(加载后并不执行,在需要执行的时候再执行)

将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件
提前加载指定资源,不再出现依赖的 font 字体隔了一段时间才刷出

  • prefetch 是用于预加载后续页面使用的资源,浏览器也会加载它们,但优先级不高

  • 避免混用 preload 和 prefetch,混用的话,并不会复用资源,而是会重复加载

<!-- 浏览器会加载两次改字体 -->
<link rel="preload" href="https://at.alicdn.com/t/font.woff" as="font">
<link rel="prefetch" href="https://at.alicdn.com/t/font.woff" as="font">

插入的脚本(无论在什么位置)在网络优先级中是很低级

script 标签的 defer 和 async 异步加载

这两个属性都告诉浏览器,它可以 “在后台” 加载脚本的同时继续解析 HTML,并在脚本加载完之后再执行。这样,脚本下载就不会阻塞 DOM 构建和页面渲染了

defer 和 async 之间的不同是他们开始执行脚本的时机的不同

async(H5) 一旦脚本可用,则会异步执行(仅适用于外部脚本) 脚本在它们完成下载完成后的第一时间执行,如果一个指定 async 的脚本很快就完成了下载,那么它的执行会阻塞 DOM 构建以及所有在之后才完成下载的同步脚本。

defer 规定当页面已完成解析后,执行脚本(仅适用于外部脚本) 脚本会按照它在 HTML 中出现的顺序执行,并且不会阻塞解析。

表格标签 table

table 属性 重点记住 cellspacing 、 cellpadding

  • cellspacing 设置单元格与单元格边框之间的空白距离
  • cellpadding 设置单元格内容与单元格边框之间的空白距离

table 合并单元格

  • 跨行合并(向下合并):rowspan=”合并单元格的个数”
  • 跨列合并(向右合并):colspan=”合并单元格的个数”

input file 控件

使用 input file 进行文件上传时,重复选择相同文件时,change 事件不再触发
解决方式:手动将 file 的 value 值设置为空

<input type="file" id="file" accept="image/gif,image/jpeg" @change="uploadFile($event)" />
$event.target.value = ''
// 或
var file = document.getElementById('file')
file.value = ''

HTML5 语义化标签

HTML5 新的语义化标签

header 头部、nav 导航、footer 底部、aside 侧边栏、article 文章、section 区块、main 主体区域

类名操作

js 在 H5 中给所有的 DOM 对象新增了一个属性 classList
classList 是一个集合,会存储某个元素上所有的类名,使用 classList 来替代 className 操作 class 类

// 添加类
div.classList.add('classname')
// 移除类
div.classList.remove('classname')
// 切换类
div.classList.toggle('classname')
// 判断类
div.classList.contains('classname')

自定义属性操作

H5 规定,以后但凡给标签增加自定义属性,都应该用 data- 开头
H5 给所有的 DOM 对象增加了一个 dataset 的属性,这个属性中会包含所有 data- 开头的属性

<div id="box" data-name="zs" data-age="10" data-user-name="ls"></div>
<script>
var box = document.querySelector('#box')
console.log(box.dataset) // DOMStringMap {name: 'zs', age: '10', userName: 'ls'}
box.dataset.aaBb = 'cc' // 在html结构中或添加 data-aa-bb="cc" 的自定义属性
</script>

注意:html 中属性是忽略大小写的,如果需要,应使用中划线 - 进行分隔,在 js 中会转换成驼峰的形式,如data-user-name ==> userName

网络状态

navigator.onLine 属性,是一个布尔值:脱机状态返回 false,在线状态返回 true

注意:返回 true 不代表不一定能访问互联网,因为有可能连接的是局域网。但是返回 false 则表示一定连不上网。

监听网络变化

// 网络连接时会被调用
window.addEventListener('online', function() {
alert('online')
})

// 当网络断开时会被调用
window.addEventListener('offline', function() {
alert('offline')
})

web 存储

特点:

  • 大小 4k
  • 生命周期:默认会话级别,但是可以设置过期时间
  • 数据可以在同一个网站的页面共享
  • 在请求时会自动携带
  • 以字符串形式存储,这个字符串有固定的格式:key=value;key1=value1;
  • 一般用于存储 sessionId,可以实现登录状态保持 (会话保持)
document.cookie = 'name=zhangsan'
document.cookie = 'age=18'
document.cookie = 'sex=23'

// 设置过期时间
document.cookie = 'sex=12;max-age=3600'

// 读取cookie
var result = document.cookie
console.log(result)

WebStorage

  • sessionStorage 和 localStorage 特点

    • 都保存在客户端
    • 大小为 5M 左右
    • 使用方法相同
    • 以键值对的方式,存储字符串格式的数据
  • sessionStorage 和 localStorage 区别

    • sessionStorage 生命周期默认为一个会话周期,且不能设置周期,一旦关闭浏览器,就销毁了,不能在不同页面共享数据
    • localStorage 永久生效,除非手动删除,可以多个窗口共享
  • 使用方法

setItem(key, value) // 设置存储内容
getItem(key) // 读取存储内容
removeItem(key) // 删除键值为key的存储内容
clear() // 清空所有存储内容

cookie

  • 大小受限
  • 用户可以操作(禁用)cookie,使功能受限
  • 安全性较低
  • 有些状态不可能保存在客户端
  • 每次访问都要传送 cookie 给服务器,浪费带宽

WebStorage

  • 存储空间更大:cookie 为 4KB,而 WebStorage 是 5MB
  • 对于那种只需要在用户浏览一组页面期间保存而关闭浏览器后就可以丢弃的数据,sessionStorage 会非常方便
  • WebStorage 不会随着 HTTP header 发送到服务器端,所以安全性相对于 cookie 来说比较高一些,不会担心截获,但是仍然存在伪造问题
  • WebStorage 数据操作比 cookie 方便

sessionStorage 多标签页共享

window.addEventListener('storage', function (event) {
if (event.key === 'token') {
this.sessionStorage.setItem('token', event.newValue)
}
})

文件读取

通过 FileReader 对象我们可以读取本地存储的文件(用户通过 input:file 上传的文件),可以使用 File 对象来指定所要读取的文件或数据。其中 File 对象可以是来自用户在一个<input>元素上选择文件后返回的 FileList 对象,也可以来自由拖放操作生成的 DataTransfer

files

对于 file 类型的 input 框,DOM 对象中存在一个 files 属性,这个属性是 FileList 对象,是一个伪数组,里面存储着上传的所有文件,当 input 框指定了 multiple 属性之后,就可以上传多个文件了。

file 对象

File 对象中包含了文件的最后修改时间、文件名、文件类型等信息。

FileReader 对象

FileReader 是一个 HTML5 新增的对象,用于读取文件(必须通过 input:file 上传)

var file = input.files[0]
// 创建一个fileReader对象
var fr = new FileReader
// 读取文件的两个方法
fr.readAsText(file) // 以文本的方式读取文件 ,文本文件
fr.readAsDataURL(file) // 以DataURL形式读取文件,图片,视频
// 文件读取完成事件:
fr.onload = function(){
// 当文件读取完成,可以通过result属性获取结果
console.log(fr.result)
}

图片预览

// 1. FileReader 是异步的
var file = document.getElementById('file')
var box = document.getElementById('box')
file.addEventListener('change', function() {
console.dir(this) // file 中files 属性里面存储了所有上传的文件
// 这个data就是我们上传的那个文件
var data = file.files[0]
// 1. 创建一个文件读取器
var fr = new FileReader()
// 2. 让文件读取器读取整个文件
fr.readAsDataURL(data)
// 3. 等待文件读取完
// onload:文件读取完成后,就会触发
fr.onload = function() {
// 通过 fr.result 就可以获取到最终的结果
var img = document.createElement('img')
img.src = fr.result
box.innerHTML = ''
box.appendChild(img)
}
})
// 2. URL.createObjectURL(file)  缺点:同步(阻塞),占用内存
var file = document.getElementById('file')
file.addEventListener('change', function() {
var data = this.files[0]
var result = URL.createObjectURL(data)
img.src = result
})

  • 新建一个数组,遍历去要重的数组,当值不在新数组的时候(indexOf 为-1 或 includes 为false)就加入该新数组中
function unique(arr) {
var newArr = []
for (var i = 0; i < arr.length; i++) {
if (newArr.indexOf(arr[i]) === -1) {
newArr.push(arr[i])
}
}
return newArr
}
  • 数组下标判断:如果当前数组的第 i 项在当前数组中第一次出现的位置不是 i,那么表示第 i 项是重复的,忽略掉。否则存入结果数组
function unique(arr) {
var newArr = []
for (var i = 0; i < arr.length; i++) {
if (arr.indexOf(arr[i]) === i) {
newArr.push(arr[i])
}
}
return newArr
}
function unique(arr) {
return arr.filter((v, i, array) => array.indexOf(v) === i)
}
  • hash去重

虽然对象属性同样可以用来做数组去重,但是会将 number,NaN,undefined,null,变为字符串形式,因为对象的属性名就是一个字符串

function Deduplication(arr) {
var result = []
var hashMap = {}
for (var i = 0; i < arr.length; i++) {
var temp = arr[i]
if (!hashMap[temp]) {
hashMap[temp] = true
result.push(temp)
}
}
return result
}
Deduplication([[undefined, 'undefined']]) // [undefined]
  • ES6 Set
function unique(arr) {
var x = new Set(arr)
return [...x]
}
  • 对象数组去重
// arr: 目标数组
// id: 唯一属性
function distinct(arr, id) {
let map = new Map()
return arr.filter((item, index) => {
if (map.get(item[id])) return false
map.set(item[id], true)
return true
})
}

数组去重

冒泡排序

详情
// 将数组中的数从小到大排列
var arr = [1, 4, 6, 7, 9, 3, 5, 8]
// var numi = 0
// var numj = 0
for (var j = 0; j < arr.length - 1; j++) {
// numj += 1
var flag = true
for (var i = 0; i < arr.length - 1 - j; i++) {
// document.write('(' + arr[i] + ',' + arr[i + 1] + ')')
// numi += 1
// 两两比较,如果前面的大于后面的,交换位置
if (arr[i] > arr[i + 1]) {
flag = false
var temp
temp = arr[i]
arr[i] = arr[i + 1]
arr[i + 1] = temp
// document.write('交换了')
}
}
// document.write(',arr=(' + arr + ')')
// document.write('<br>')
// 如果一趟下来,一次交换都没有做,说明就已经排好序,就不需要继续比
if (flag) {
break
}
}

实现 call apply bind 方法

详情

call 和 apply 区别

call:

Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
var args = Array.prototype.slice.apply(arguments, [1]) // 获取额外参数
// var args = [...arguments].slice(1)
context.fn = this
var res = context.fn(...args)
delete context.fn
return res
}

context 为要绑定的 this,不传默认为 window
给 context 创建一个 fn 属性,并将值设置为需要调用的函数
调用 context.fn,并将额外参数 args 传递进去
删除 context 上的 fn 函数

apply:

Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
let res
if (!arguments[1]) {
res = context.fn()
} else if (arguments[1].constructor.name === 'Array') {
res = context.fn(...arguments[1])
} else {
return console.error('Uncaught TypeError: CreateListFromArrayLike called on non-object')
// throw 'Uncaught TypeError: CreateListFromArrayLike called on non-object'
}
delete context.fn
return res
}

bind:

Function.prototype.myBind = function() {
var self = this // 保存原函数
var args = Array.prototype.slice.call(arguments) // 参数转为数组
// var args = [...arguments].slice(1) // 参数转为数组
var context = args.shift() // 保存需要绑定的this上下文
return function() {
// 返回一个新函数
self.apply(context, args.concat([].slice.call(arguments)))
}
}
function aaa(val, val1) {
console.log(val)
console.log(val1)
console.log(this.name)
}
aaa()
aaa.myCall({ name: '123' }, '121', 122)
aaa.myApply({ name: '123' }, ['121', 122])
aaa.myBind({ name: '123' })('111', '222')

防抖和节流

深浅拷贝

手写 Promise

手写 Promise.all

手写 Promise.race

异步调度

实现继承的方式

参考:https://www.cnblogs.com/humin/p/4556820.html

原型链继承,将父类的实例作为子类的原型,无法实现多继承

function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

构造继承,使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}

组合继承,通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

调用了两次父类构造函数,生成了两份实例

寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

es6 中的继承

class Super {}
class Sub extends Super {}

const sub = new Sub()

Sub.__proto__ === Super

子类可以直接通过 __proto__ 寻址到父类

function Super() {}
function Sub() {}

Sub.prototype = new Super()
Sub.prototype.constructor = Sub

var sub = new Sub()

Sub.__proto__ === Function.prototype

而通过 ES5 的方式,Sub.__proto__ === Function.prototype

es5继承 与 es6 继承的区别参考 https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/20

发布订阅

扁平数据转tree

实现 instanceof

// a instanceof b  b.prototype 是否在 a 的原型链中
function myInstanceof(a, b) {
if (typeof a === 'object' || a === null) return false

// getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(a)
while (true) {
// 查找到尽头,还没找到
if (proto == null) return false
// 找到相同的原型对象
if (proto == b.prototype) return true
proto = Object.getPrototypeof(proto)
}
}

实现一个 new 操作符

实现 JSON.stringify

手机号 3-4-4 分割
千分位格式化数字

function Student(name) {
this.name = name
}

let stu = new Student('zs') // Student {name: 'zs'}

new 做了哪些事

  1. 创建一个新的空对象,类型是 Student
  2. 将 this 指向这个新的对象
  3. 执行构造函数 目的:给这个新对象加属性和方法
  4. 返回这个新对象

简单实现

function NEW(fun) {
// 判断是否是函数
if (typeof fun !== 'function') {
throw new Error('第一个参数应该是函数')
}

// 创建一个空对象,并将原型指向 fun.prototype
// const newObj = {}
// newObj.__proto__ = fun.prototype
const newObj = Object.create(fun.prototype)

const argsArr = [].slice.call(arguments, 1)

// 将构造函数 this 指向 newObj,并执行构造函数
const result = fun.apply(newObj, argsArr)

// 如果构造函数本身有返回值,且返回值为对象时,会将本身返回值返回,如果返回值为简单类型,会忽略
const isObject = typeof result === 'object' && result !== null
const isFunction = typeof result === 'function'
if (isObject || isFunction) {
return result
}
// 返回新对象
return newObj
}
const stu = NEW(Student, 'ls')
console.log(stu) // Student {name: 'ls'}

lodash 插件 cloneDeep

浅拷贝

数组:slice()/concat()/Array.from()/扩展运算符
对象:Object.assign()/扩展运算符

深拷贝

  • 通过 JSON.parse(JSON.stringify(obj))

这种方法只能复制 JSON 格式支持的属性名和值,不支持的属性名和值会直接忽略:会忽略 undefined、symbol,不能序列化函数,不能解决循环引用的对象 参考MDN

JSON.parse(JSON.stringify({
[Symbol('a')]: 'abc',
b: function() {},
c: undefined,
d: Infinity,
e: NaN,
}))
// 返回 {d: null, e: null}
  • 实现简单深拷贝
function deepCopy(source) {
if (Array.isArray(source)) {
const target = []
for (const [index, value] of source.entries()) {
target[index] = deepCopy(value)
}
return target

// 简化 => return source.map(elem => deepCopy(elem))
} else if (typeof source === 'object' && source !== null) {
const target = {}
for (const [key, value] of Object.entries(source)) {
target[key] = deepCopy(value)
}
return target

// 简化 => return Object.fromEntries(Object.entries(source).map(([key, val]) => [key, deepCopy(val)]))
} else {
// 基础类型无需拷贝
return source
}
}

Object.fromEntries() 方法把键值对列表转换为一个对象,是 Object.entries 的反转

循环引用(环)

解决思路: 通过一个WeakMap来存储拷贝过的对象

let hash = new WeakMap()
if (hash.has(source)) {
return hash.get(source)
}

hash.set(source, target)

特殊对象的拷贝

// 拷贝 Function
target = new Function(`return ${source.toString()}`)()
// 拷贝 Date
target = new Date(source.getTime())

// 拷贝 RegExp
target = new RegExp(source)

防抖和节流都是为了解决短时间内大量触发某函数而导致的性能问题,比如触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象

防抖(debounce)

在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时(短时间内连续触发的事件 只有效执行一次)

应用场景

  • 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询请求,这样可以有效减少请求次数,节约请求资源

  • window 的 resize、scroll 事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次

节流(throttle)

规定一个单位时间 n,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内事件被触发多次,只有一次能生效(每 n 秒触发一次)

应用场景

  • 鼠标连续不断地触发某事件(如点击),n 秒内只触发一次

  • 监听滚动事件,比如是否滑到底部自动加载更多

区别

防抖的作用是将多个连续的debounced调用合并为一次callback调用。防抖是基于最近次 debounced 调用来重置 waitTime,如果debounced事件触发间隔小于 waitTimecallback就不会执行;

节流的作用是限制callback调用的频率(每waitTime调用一次)。是基于上次 callback 调用来计算 waitTime 的,不管callback 事件触发有多频繁,只要距离上次 callback 调用超过了 waitTime,就一定会进行下次 callback 调用。

– 原理:

防抖是 debounced 维护了一个计时器,规定在 waitTime 时间后触发 callback,但是在 waitTime 时间内再次触发 debounced 的话,会清除当前的 timer 然后重新计时,这样一来,只有最后一次debounced 操作才能触发 callback

节流是通过判断是否到达一定时间 (waitTime) 来再次触发 callbackfuncwaitTime 时间内不能被再次触发。

实现

throttle-debounce 插件

简单实现

// 节流
function throttle(delay, func) {
let flag = true
return function() {
const context = this
const arg = arguments
if (flag) {
flag = false
setTimeout(() => {
func.apply(context, arg)
}, delay)
}
}
}

// 防抖
function debounce(delay, func) {
let timer = null
return function() {
const context = this
const args = arguments

clearTimeout(timer)
timer = setTimeout(function() {
func.apply(context, args)
}, delay)
}
}

let 与 const

[知乎]我用了两个月的时间才理解 let

[MDN]变量提升

let 的使用

  • let 声明的变量只有在当前作用域(块作用域)有效
{
var a = 1
let b = 2
}

console.log(a) // 1
console.log(b) // ReferenceError: b is not defined
  • 不允许重复声明
var a = 1
let a = 2 // SyntaxError: Identifier 'a' has already been declared
let b = 3
const b = 4 // SyntaxError: Identifier 'b' has already been declared
  • 使用 let 声明的全局变量,不会成为 window 的属性
let c = 1
console.log(window.c) // undefined
console.log(c) // 1
  • 存在变量提升
let a = 1
{
a = 2
let a
}
// 如果 let 不会提升,那么 a = 2 就会将外面的 a 由 1 变成 2
// 但运行发现 a = 2 报错:Uncaught ReferenceError: Cannot access 'a' before initialization
a = 1; let a  // Uncaught ReferenceError: Cannot access 'a' before initialization

总结:

  • let/const 声明的「创建」过程被提升了,但是「初始化」没有提升,var 声明的「创建」和「初始化」都被提升了,但「赋值」没被提升,function 声明的「创建」、「初始化」和「赋值」都被提升了
  • let 声明会提升到块顶部,从块顶部到该变量的初始化语句,这块区域叫做 TDZ(暂时死区),所谓暂时死区,就是不能在初始化之前,使用变量
  • 如果你在 TDZ 内使用该变量,JS 就会报错

如果 let x 的初始化过程失败了,那么

  • x 变量就将永远处于 created 状态
  • 你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)
  • 由于 x 无法被初始化,所以 x 永远处在暂时死区

const 的使用

const 声明一个常量。常量:代码执行的过程中,不可以修改常量里面的值

  • const 声明的量不可以改变
const PI = 3.1415
PI = 3 // TypeError: Assignment to constant variable
  • const 声明的变量必须赋值
const num
// SyntaxError: Missing initializer in const declaration
  • 如果 const 声明了一个对象,仅仅保证地址不变,可以修改对象的属性
const obj = { name: 'zs' }
obj.age = 18 // 正确
obj = {} // TypeError: Assignment to constant variable
  • 其他用法和 let 一样

模板字符串

// 定义一个字符串
let str = `hello world`

// 内部允许换行
let str = `
hello
world
`

// 内部可以使用表达式
let str = `你好,我是${name}`

箭头函数

特点

  • 不存在 prototype 这个属性
let a = () => {}
console.log(a.prototype) // undefined
  • 没有自己的 this,arguments

箭头函数的 this、arguments 都是在定义函数时绑定外层的 this 和 arguments,而不是在执行过程中绑定的,所以不会因为调用者不同而发生变化。
可以使用剩余参数(Rest 参数)表示法获得的自身入参列表

因为箭头函数没有 this,因此箭头函数不能作为构造函数

不能用 call()、apply()、bind() 这些方法改变 this 的指向

fn = function(){
let arrow = (...args) => {
console.log(arguments) // 外层的入参列表 -> Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(args) // 使用剩余参数表示法获得的自身入参列表 -> (3) [4, 5, 6]
}
arrow(4, 5, 6)
console.log(arrow.length) // 0
}
fn(1, 2, 3)
  • 如果函数体只有一行语句,并且需要返回这个值,那么可以省略 {} 和 return
let fn = (n1, n2) => n1 + n2
  • Rest 参数和 arguments 对象的区别:

rest 参数只包括那些没有给出名称的参数,arguments 包含所有参数

rest 参数之后不能再有其他参数,否则会报错

函数的 length 属性,不包括 rest 参数

arguments 对象不是真正的数组,而 rest 参数是数组实例,可以直接使用数组的方法

对象简化语法

// 当属性的 key 和变量的名相同时可以简写
let person = { name: name } ==> let person = { name }

// 声明函数
let cal = {
add: function () {
return 1
},
// 可以省略 `:function`
add(){
return 1
}
}

属性名表达式

  • ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo'
let methodKey = 'bar'

let obj = {
[propKey]: true, // foo: true
['a' + 'bc']: 123, // abc: 123
[methodKey]() {
return 'hi'
}
}

class 关键字

ES5 中通过 构造函数 + 原型 的方式来实现面向对象

// 构造函数
function Person() {
this.name = 'jack'
this.age = 18
}

// 在原型中添加实例方法
Person.prototype.say = function() {
console.log(this.name, this.age)
}

// 创建实例
const p = new Person()
p.say()

ES6 中出现了 class 关键字,用来实现面向对象

class 声明不允许再次声明已经存在的类,否则将会抛出一个类型错误
class 声明不可以提升
class 仅仅是一个语法结构(语法糖),本质还是函数,实现继承本质上还是通过构造函数 + 原型的方式

class Person {}
Person instanceof Function // true

类声明

// 创建 Person 类
class Person {
// 类的构造函数 constructor 固定名称
constructor(name, age) {
this.name = name
this.age = age
}

// 添加实例方法
say() {
console.log(this.name, this.age)
}
}

// 创建实例
const p = new Person('tom', 18)
console.log(p) // Person {name: 'tom', age: 18}
p.say() // tom 18

类表达式

赋予一个命名类表达式的名称是类的主体的本地名称

// 匿名类
let Person = class {}
new Person() // Person {}

// 命名类
let Person = class A {}
new Person() // A {}
new A() // Uncaught ReferenceError: A is not defined
console.log(Person) // class A {}
console.log(A) // Uncaught ReferenceError: A is not defined

类表达式也不存在提升

static 关键字用来定义一个类的静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法

class Point {
constructor(x, y) {
this.x = x
this.y = y
}

static distance(a, b) {
const dx = a.x - b.x
const dy = a.y - b.y

return Math.hypot(dx, dy)
}
}

const p1 = new Point(5, 5)
const p2 = new Point(10, 10)

console.log(Point.distance(p1, p2))

继承:要实现至少需要两个 class(子类 和 父类),子类继承自父类,继承后,子类就可以使用父类中的属性或方法

// 继承

// 父类
class Person {
constructor(name, age) {
this.name = name
}

say() {
console.log('父类中的 say 方法', this.name)
}
}

// 子类
class Chinese extends Person {
constructor(name, age) {
// 子类中使用 constructor 必须手动调用 super
// super 表示父类的构造函数
// 先调用 super() 在使用 this
super()
this.name = name
this.age = age
}
}

// 创建实例
const c = new Chinese('zs', 18)
console.log(c)
c.say() // 父类中的方法

解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

// 1. 对象解构
var { a, b } = { a: 10, b: 20 }

// 同
;({ a, b } = { a: 10, b: 20 }) // 使用没有声明的赋值,数组解构类似
console.log(a, b) // 10 20

// 提取变量并赋值
var { a: p, b: q } = { a: 10, b: 20 }
console.log(p, q) // 10 20

// 将剩余属性赋值给一个变量
var { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 }
console.log(a, b, rest) // 10 20 {c: 30, d: 40}

// 提供默认值
var { a = 1, b = 1 } = { a: 10 }
console.log(a, b) // 10 1

// 赋值并提供默认值
var { a: aa = 10, b: bb = 1 } = { a: 10 }
console.log(aa, bb) // 10 1

// 2. 数组解构
var [a, b] = [1, 2]
console.log(a, b) // 1 2

// 将剩余数组赋值给一个变量
var [a, b, ...rest] = [1, 2, 3, 4]
console.log(a, b, rest) // 1 2 [3, 4]
// ==> var a = arr[0]; var b = arr[1]

// 提供默认值
var [c = 2, d = 2] = [10]
console.log(c, d) // 10 2

// 忽略某些值
var [a = 2, , b = 2] = [10, 20, 30]
console.log(a, b) // 10 30

// 3. 函数参数的解构赋值
function foo({ x }) {
console.log(x) // 1
}
foo({ x: 1, y: 2 })

// 函数参数默认值
function foo({ x = 10 }) {
console.log(x) // 10
}
foo()

// 4. 解构的特殊应用
// 交换变量
var a = 1
var b = 3
;[a, b] = [b, a]
console.log(a) // 3
console.log(b) // 1

// 字符串解构
var str = 'love'
var [a, b, c, d] = str
console.log(a, b, c, d) // l o v e

扩展运算符

扩展运算符(spread)是三个点(…)。作用:将一个数组转为用逗号分隔的参数序列

var arr = ['a', 'b', 'c']
console.log(...arr) // a b c

应用

// 数组深拷贝
var arr = [1, 2, 3]
var arr1 = [...arr]
console.log(arr === arr1) // false, 说明arr1和arr指向不同数组

// 把一个数组插入另一个数组字面量
var arr2 = [...arr, 4, 5, 6]
console.log(arr2) // [1, 2, 3, 4, 5, 6]

// 字符串转数组
var str = 'love'
var arr3 = [...str]
console.log(arr3) // [ 'l', 'o', 'v', 'e' ]

对象展开

let defaults = { name: 'zs', age: 18 }
let search = { ...defaults, age: 12 } // { name: 'zs', age: 12 } 后面的属性会覆盖前面的属性

对象展开仅包含对象自身的可枚举属性

class C {
p = 12
m() {}
}
let c = new C()
let clone = { ...c }
clone.p // ok
clone.m() // error!

ES6 模块化