「1.5w字总结」Web前端开发必知必会详尽知识手册

37,667 阅读50分钟

正文中会出现的一些标记解释:

相关/扩展阅读」关联笔者之前写过的相关内容文章,同时也是避免本文篇幅过长所做的扩展。

推荐阅读」引用别的文章链接,一般是掘金高赞文章,或是官方权威文档。


写这篇文章的目的,起初只是为了记录面试中高频出现的问题,逐渐汇集了 Web 前端中重要的知识点,与个人一些理解与浅析,在这个过程中发现弥补了自己不少疏漏的知识,所以分享出来交流学习,相信即使不是为了面试准备,也是值得一看的。内容由浅入深,非常适合 1 ~ 3 年经验前端阅读学习,3 ~ 5 年内开发查漏补缺。另外个人知识水平有限,如有错误还请不吝指正。

温馨提示:即使不包含链接的文本内容也超过 1w 字,全文阅读时间在 30min 以上,如果觉得文章有用建议收藏阅读~


基础部分

CSS盒模型

标准盒模型:box-sizing: content-box

浏览器默认的标准,元素宽度即为内容宽度。

IE盒模型:box-sizing: border-box

元素宽度为内容宽度+边距+边框(content + padding + border = width)

相关阅读为什么大家偏爱怪异盒模型 border-box?

BFC 块级格式化上下文

简单列举几个常见触发条件:

  1. float 不为 none
  2. overflow 不为 visible
  3. display 为 inline-block、table-caption 或 table-cell
  4. position 不为 static 或 relative

应用:

  1. 阻止 margin 重叠
  2. 阻止元素被浮动元素覆盖(以前常用于自适应两栏布局
  3. 清除内部浮动(父级元素高度塌陷问题)

总结:

BFC 可以视为一种布局的手段,它的目的在于创建出一块独立区域,同时让其内部元素更好地在这片区域中布局。

由于现代 CSS 还在不断发展当中,触发 BFC 的条件可能多达十余种,包括 flex元素、grid元素内也会产生 BFC,又如 display: flow-root 属性值可以创建无副作用的 BFC 等。

推荐阅读MDN - Block_formatting_context

回流与重绘

记住引起元素 大小位置 改变的情况,均会触发回流(Reflow)。反之,大小位置不变的情况(如颜色样式colorbackground-coloroutline-style改变),就发生重绘 (Repaint)。

哪些情况会导致回流:

  1. 页面首次渲染
  2. 浏览器窗口变化
  3. 元素尺寸位置变化(宽高、边距、边框等)
  4. 元素内容发生变化(文字数量、图片大小、字体大小变化)
  5. 添加删除可见的 DOM 节点
  6. 激活 css 伪类(hover、active等)
  7. 查询某些属性或调用某些方法(浏览器会必须回流来保证数据的准确性)

注意:outline-widthbox-shadowborder-radius 这些属性并不会引起元素大小的改变,而是样式形状的改变,所以属于重绘

总结:

回流必将引起重绘,重绘不一定引起回流。所以回流的性能开销更大。

思考:

visibility 属性会引起回流还是重绘?opacity 呢?

查看答案 答案是 只导致重绘,因为 visibility 控制的元素 大小位置 是不变的。那么同样位置大小不改变的 opacity 呢?其实 opacity 更加特殊一点,因为它触发的是 css3 硬件加速(GPU渲染),所以它可以 既不触发回流也不触发重绘

常见的触发硬件加速属性有:transformopacityfilter等。

如何减少回流重绘(性能优化):

HTML层面

  1. 避免使用 table 布局
  2. 在 DOM 树最末端改变 class

CSS层面

  1. 尽量减少使用 CSS 表达式(如:calc
  2. 避免多层内联样式
  3. 将复杂动效应用在脱离文档流的元素上(position: absolute / fixed

JS层面

  1. 避免用 JS 操作样式(多个样式改变尽量合并为一次操作
  2. 如无法避免多次应用样式或操作 DOM,则可以先设置元素隐藏(先 display:none 再操作)
  3. 重复使用元素属性时赋值给变量(避免重复查询元素导致回流)
  4. 某些操作尽量采用防抖节流(如 resize、scroll)

DOM事件流及事件委托机制

在如图这样一段 html 结构中,我们点击 button 相当于同时点击了 div、body、以及窗口,所以需要规定事件触发的顺序

image.png

如果直观地认为点击了 button 则应该先触发 button 的事件,外层 div 和 body 于用户而言是无感知的,那么这时的事件流就描述为 冒泡,意为从里向外触发事件。

反之就叫做 捕获 事件流,即无论点击的是什么,都先从最外层触发事件

我们假设一段DOM结构如下:

<ul>
    <li> 1 </li>
    <li> 2 </li>
    .....
</ul>

如果为每个 li 都赋予点击事件,会注册多个方法,但是给 ul (父层)中赋予点击事件,利用捕获冒泡的原理,触发 ul 的点击事件时,通过 e.target 判断点击的是哪个 li,就只需要注册一次方法即可(而且动态添加子节点无需绑定新的事件),这就是JS的事件委托机制

相关阅读:《哪些浏览器事件不会冒泡?

BOM 和 DOM 的区别

BOM 全称是 Browser Object Model,DOM 全称是 Document Object Model,顾名思义一个指的是浏览器对象模型,另一个则指的是文档对象模型。后者由 w3c 制定,是所有浏览器都应该遵守的标准。而 BOM 则是由各个浏览器自己扩展的对象模型,实现标准并不相同。

BOM 可以看做指代的是浏览器的 window 对象,DOM 则指的是 window.document 对象,可以看出 DOM 的核心是 BOMwindow 对象中的子对象 document(即 BOM 包含了 DOM)。

常见的 window 对象属性:documentlocationscreenhistoryframes

什么是 Ajax

Ajax 并不指代某种编程语言或具体技术,它可以看做是一种标准或者思想,区别于传统 Web 网页应用,它最早提出使用异步 JS 技术来创建动态的网页,通过与服务器进行少量数据交换,在不重新加载整个网页的情况下来对网页部分内容进行异步动态更新,自 2005 年开始 Ajax 被大众所接受并逐渐成为主流,直至今天我们的大部分现代网页都在遵循着 Ajax 标准。

狭义的 Ajax 主要关注点在于 XMLHttpRequest 对象,它用于与服务器交互,实现这样一个 Ajax 实例基本四步走:

function myAjax() {
    var xhr = new XMLHttpRequest()
    // 1. 处理响应回调
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                ...
            }
        }
    }
    // 2. 初始化一个请求
    xhr.open('post', '/xxx', true)
    // 3. 设置请求头信息
    xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8')
    // 4. 发送请求
    var params = { ... }
    xhr.send(params)
}

除了使用 XMLHttpRequest 实现异步资源获取,Fetch 也是一个更理想、更简单、合理的替代方案。

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data))

推荐阅读:《 MDN Web API 接口参考:Fetch API


浏览器相关

输入 URL 回车后经过哪些过程

  1. 解析 URL,判断是否命中缓存(DNS prefetch)
  2. 访问 DNS 服务器,将域名解析获取 IP 地址
  3. 三次握手建立 TCP 连接
  4. 发送 HTTP 请求
  5. 服务器处理请求并返回 HTTP 报文
  6. 浏览器解析渲染页面
  7. 断开连接:TCP 四次挥手

跨域问题

  1. JSONP(利用 script 标签,前端需要定义一个回调函数接收数据,兼容性好,但只能发送 get 请求
  2. CORS(与服务端配置相关)
  3. PostMessageWebSocket(HTML5 新特性)
  4. Nginx 反向代理(偏运维知识)
  5. img 标签(知识广度向,与 script 一样可以跨域)

另外本地工程化项目可以配置 Proxy 代理来跨域请求后端(Webpack、Vite)

移动端屏幕适配

  1. 利用 meta 标签,viewport 缩放(页面拉伸模糊)
  2. 响应式布局(css 媒体查询
  3. rem%vh vw 等弹性单位(可通过 postcss 插件自动转化 px 单位)
Postcss插件影响下如果一定要使用px,如何让该单位不转换?
1. 把那部分 px 写成大写 PX(但代码格式化可能会fix掉)
2. 通过 JS 写入样式(额外触发回流重绘,但是稳定)

数据存储

  • Cookie:有过期时间,长度限制4kb,且每次都会携带在请求头中,不推荐使用
  • SessionStorage:无过期时间,容量大,但窗口关闭自动删除
  • LocalStorage:无过期时间,容量大,应用场景很广
  • IndexedDB:存储更大量的结构化数据,浏览器本身不限制其容量

浏览器缓存

缓存策略都是通过设置 HTTP Header 来实现的,分为强缓存与协商缓存。

强缓存就是由浏览器决定的,若命中缓存则无论资源是否更新都不会请求接口(状态码:200)

协商缓存就是服务器收到请求后进行更新判断,如果资源没有修改就返回 Not Modified(状态码:304)。

  • 强缓存(Expires,cache-control)
  1. Expires(http 1.0 时期产物):设置的是具体的过期时间。
  2. cache-control(http 1.1 时期产物):设置的是经过多少时间(单位秒)之后过期,与 Expires 同时存在的话优先级更高。
  • 协商缓存(Last-modified ,Etag)
  1. Last-modified:顾名思义,最后一次更改时间。
  2. ETag:优先级更高,资源的唯一标识。优点是精度更高,因为 Last-modified 时间单位是秒,如果文件在 1 秒内被修改多次就很难侦测到。缺点是性能有一定消耗,因为获得资源的hash值需要额外计算。

异步加载

async 和 defer 作用?以及有什么区别?

  • 都是可以用于异步加载脚本,即 script 放在 HTML 中不会阻塞页面渲染。
  • 不同点是 defer 会在 DOMContentLoaded 事件之后才会执行,也就是会等待页面准备完毕时再执行。

前端路由

  • History 路由

主要是利用实例提供的 API 操纵浏览器会话历史记录。

  • Hash 路由

这种路由是利用了锚点(url 中的 #)功能来实现的,当 # 后面的内容发生改变时,可以通过 hashchange 事件监听到,这也是此路由方式称为hash路由的原因。

垃圾回收机制

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放

像 JavaScript 这些高级语言中,第 1 和第 3 部分并不像底层语言那样明确。JS 是在定义变量时就完成了内存分配,而“垃圾回收器”的工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。

  • 引用计数算法

这是最初级的垃圾收集算法,此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果一个对象被引用次数为 0,对象将被垃圾回收机制回收。

缺陷:该方式存在无法处理循环引用的问题。当两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,却因为它们互相都有至少一次引用,所以不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o
}
  • 标记清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……直到找到所有可以获得的对象和收集所有不能获得的对象。

从 2012 年起,所有现代浏览器都使用了「标记-清除」垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于「标记-清除」算法的改进,并没有改变算法本身和它对“对象是否不再需要”的简化定义。


网络基础

HTTP 1.0/1.1/2.0/3.0 的特性

HTTP 1.0:

  • 确定了协议是无状态的,即同一客户端每次请求都没有任何关系
  • 消息结构包含请求头和请求体

HTTP 1.1:

  • 引入了持久连接,即 TCP 连接默认不关闭,可以被多个请求复用
  • 在同一个 TCP 连接里面,客户端可以同时发送多个请求
  • 虽然允许复用 TCP 连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。
  • 新增了一些请求方法(如 PUT、DELETE 等)、新增一些请求头和响应头

HTTP 2.0:

  • 采用二进制格式而非文本格式
  • 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
  • 使用报头压缩,降低开销
  • 支持服务器推送

HTTP 3.0

  • 弃用 TCP 协议,采用一种新的更快的网络协议 QUIC(基于 UDP 协议)

HTTPS

  • 使用了 SSL/TLS 协议进行了加密处理,相对更安全
  • 默认端口 443
  • 由于需要涉及加密以及多次握手,实际性能会稍逊 HTTP

WebSocket

  • 是一种建立在单个 TCP 连接上进行全双工通信的协议。
  • 浏览器和服务器两者之间可以创建持久性的连接,并进行双向数据传输。(HTTP/2 虽然也具备服务器推送功能,但 HTTP/2 只能推送静态资源)
  • 没有同源限制,天然支持跨域
  • 可以发送文本格式和二进制格式,性能和效率也更高。

缺点:

  • 不支持 HTTP 请求头,无法做缓存等操作。
  • 由于 WebSocket 可以直接操作底层 TCP 连接,可能存在一些安全问题。
  • 长连接协议,即使在无通信状态时也会维持连接。这可能会导致过多的空闲连接,从而消耗过多的资源。
  • WebSocket 的标准化程度比较低,有些功能只是由特定的浏览器供应商支持,可能会导致一些兼容性的问题。

GET 和 POST 区别

  • Get 传输大小相对受限(不同浏览器之间不同),Post 大小不受限制
  • Get 通过 URL 编码传输数据,Post 通过 body 传输,支持多种编码格式(两者都是明文传输,都不是安全的,但 Get 参数直接暴露在 URL 上,不能用来传递敏感信息)
  • 浏览器会缓存 Get 请求,Post 则不会缓存。(在该特性下Get请求可能会出现 304 不更新,解决方法:链接加个随机参数)

网络安全

XSS(跨站脚本攻击):利用了浏览器对于从服务器所获取的内容的信任,注入恶意脚本在受害者的浏览器中得以运行,分为反射型、储存型、DOM型。开启 CSP(内容安全策略)可以减少或消除这类攻击,副作用是 evalFunction() 等方法会失效。类似的还有 CRLF 攻击,防御此类攻击的核心就是严格控制用户提交的内容,对输入进行过滤,对输出进行转义。

CSRF(跨站伪造请求):利用受害者的登录凭证(cookie)达到冒充该用户执行操作的目的,这在被攻击方很难完全防御,所以只能尽量减少 cookie 的使用,目前大部分网站也都是用 Token 来进行身份验证的,可以有效避免该类攻击。

Injection(注入攻击) 这种攻击主要是接口设计不当导致的,例如接口根据用户传递的内容拼接 SQL,那么就可以通过传递 SQL 语句来注入攻击;又例如根据传递的内容来拼接 Shell 命令,那么攻击者如果传了类似 && rm -rf xxx 这样的命令就会被执行,后果不言而喻。

DoS(服务拒绝)、DDoS(分布式服务拒绝) 通过构造大量特定请求,导致服务器资源被消耗过度,挤压正常的请求,进而产生雪崩效应。在 DDoS 攻击中更是利用了僵尸网络,使得追溯源头的可能性几乎为零,这类攻击主要目的在于消耗服务器带宽,非常难防御,常见的有 SYN 洪泛攻击(利用 TCP 三次握手),只能通过一些手段缓解,例如缩短超时,让服务器更快地释放掉长时间响应的连接,从而增加攻击者的成本。

中间人攻击 通过拦截窃取手段破坏通信,信息在用户和服务之间的传递都会暴露在攻击者的视野中。使用 HTTPS 协议传输一般可以避免,还有就是敏感信息不要使用明文传输,剩下的就只能交给用户的安全意识了,比如用户连接了不安全的公共 wifi,那么他就有可能被攻击。

TCP 三次握手四次挥手的理解

首先 TCP 协议旨在提供一种可靠的,面向链接的传输服务。它对传输效率的要求低,对传输准确性要求高

建立一个 TCP 连接需要先进行三次握手,因为双方要确认各自发送与接收功能是否都为正常,最少需要三步才能验证。

  1. 第一次握手:客户端向服务端发起请求,服务端接收了报文,此时服务端可以确定客户端发送功能正常,自己接收也正常。
  2. 第二次握手:服务端响应报文给客户端,客户端接收了报文,此时客户端确认了自己和服务端的发送、接收功能都正常,但此时服务端还不确定自己的发送是否正常(也就是服务端还不知道客户端是否能收到它的信息)。
  3. 第三次握手:客户端回应了服务端的响应报文,说明服务端也可以正常发送,至此双方都确认了各自的收发功能正常。
谈谈我对于“握手”的理解

我认为是“触达”的意思,比如在第二次握手的时候,如果服务端响应给客户端的报文没有收到回答,那么就无法确定自己的发送是否触达,此时会进行重试,如在规定次数内成功了则还是可以建立有效的链接,而实际请求次数已经不止三次了。

前两次握手是必定不携带数据的,因为只是为了确认可用性,没必要让请求变得复杂。

关闭一个 TCP 链接需要四次挥手,这是因为半关闭的特性,客户端与服务端在任一方停止发送数据时还是能够接收另一方发送的数据,也就是说两次挥手就能结束其中一端的 TCP 链接,那么两端完全释放一共就需要四次。

用通俗的例子来说,四次挥手就好比 A 和 B 结束对讲机通话的场景。

A:“我这边没有要说的了,完毕”,B:“好的收到”;B:“我这边也没有要说的了完毕”,A:“好的知道了”。对讲终止。

域名发散与域名收敛

浏览器对于 TCP 链接有并发数限制,主要是为了不让 DoS 这类攻击的成本太低。

域名发散:将静态资源放在多个子域名下,以此实现多线程下载,突破并行数,在客户端加载大量静态资源时可以更迅速。

域名收敛:相反地,将静态资源集中放在一个域名下,刻意减少 DNS 解析的开销。


CSS基础

CSS选择器优先级顺序(从高到低):

  1. !important;
  2. 行内样式
  3. ID 选择器
  4. 类选择器
  5. 标签选择器
  6. 通配符选择器
  7. 浏览器的自定义属性和继承。

CSS3 常见新特性:各种选择器(如:not())、圆角、阴影反射、文字特效、线性渐变、旋转,transition(用于过渡),animation(用于动画)

实现水平垂直居中

  1. flex 布局(常用)
.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}
  1. grid 布局(更简洁)
.parent {
    display: grid;
    place-items: center;
}

3. translate 偏移居中(绝对定位中最好用的方法,不定宽高)

.parent {
    position: relative;
}
.child {
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
}

其它的方式实战意义不大,其中 grid 是二维布局特化的属性,只做居中布局其实一维的 flex 就够用了。

图片居中

  1. 利用背景实现:
background: url(...) no-repeat center center;
background-size: contain;

2. 更优方案:

object-fit: contain;

有多个属性值可选,这里只举了 contain(等比缩放居中)的例子,常用的还有 cover(等比填充居中)

隐藏元素的方法

  1. display: none:结构消失,触发回流重绘
  2. visibility: hidden:结构保留,占据空间,触发重绘,不可选中
  3. opacity: 0:占据空间,不回流不重绘,可以被选中
  4. position: absolute/fixed; left/top:-9999px;:利用绝对定位,设置超大负边距将元素抛出视图,脱离文档流所以不占据空间
  5. transform: translate(-9999px):利用偏移抛出视图,空间会占据(注意:行内元素无效)
  6. transform: scale(0) / transform: skew(90deg):变形来达到隐藏效果(注意:行内元素无效)
  7. clip-path: circle(0px):利用裁剪创建元素的可显示区域,区域外会隐藏,占据空间,不可选中(这里我用 circle 是因为它参数最少)
  8. z-index:层叠上下文有可能盖住,也是一种隐藏方式
  9. content-visibility: hidden:只能设置内容隐藏,设置的元素本身不受影响。隐藏效果类似方法 1,隐藏原理是基于浏览器渲染的,性能上有优势
  10. height: 0:根据盒模型原理,需要额外代码处理,还需隐藏子元素

相关阅读:《 CSS 隐藏元素的 10 种实用方法

常见问题

图片不能自动撑满怎么解决

display:block; // 把 img 设置为块元素,解决

li 与 li(或行内块元素)之间看不见的空白间隔怎么产生的

设置 font-size: 0; // 是受空格影响的,display: inline-block 也会产生间隔

css 绘制三角形原理

利用边框(border)属性实现,div 宽高设为 0,然后设置不同方向的三条边颜色为透明,剩下的边就是三角形。

width: 0;
height: 0;
border: 50px solid transparent;
border-top: 50px solid blue;

通过变换 border 宽度调整大小形状,变换剩下那条边的颜色改变三角形颜色。


JS基础知识

相关阅读搞懂 this、闭包、作用域,就用代码来理解

数据类型

基本类型有7种: undefinednullstringnumberbooleanbigintsymbol

引用类型有: ObjectArray

  • 其中基本类型存放在栈内存中,大小相对是可预期的。
  • 引用类型放在堆内存中,主要储存复杂数据。
谈谈我的一些思考
  1. 引用类型可能只有一种,毕竟在 JS 中万物皆对象,严格上讲数组应该也属于一种对象。

  2. 并不是所有基本类型都会放在栈内存中,有些存的也是引用值,所以我认为常见的说法只是利用计算机领域的通用概念作出笼统的区分,目的是让初学者更快理解,并非完全准确。

其实在 JS 中我们只要区分 “值类型”“引用类型” 即可,至于一个值类型变量储存的是引用类型的数据时,怎么保证它是唯一的,这就是 v8 引擎做的事了,在高级语言中不必太过纠结。


怎样理解 堆、栈、队列 ?

】:想象一个仓库,在申请到一片空间后就可以放任何东西,但是东西放多了找起来就比较麻烦,所以需要一份“清单”,通过查找清单上的索引去找你要的那堆东西,就不用每次都进仓库乱翻。

】:想象一个箱子,先放进去的东西反而被压在了箱底,也就最慢才会被拿出来,所以说先进后出,后进先出

队列】:排队没什么好说吧,讲究一个先来后到,所以肯定先进先出

For 循环

for..of..
for..in..
Array.prototype.foreach()
Array.prototype.map()
Array.prototype.filter()
Array.prototype.every()
Array.prototype.some()
Array.prototype.reduce()

数组原型链上的方法可能经常会拿出来单独考,如果平时不常使用就很容易卡壳,可以从数组去重这个切入点来巩固 for 循环。

数组方法

数组常考,实际开发中也常用,需要尽量全面地了解。

push:末尾添加
unshift:首部添加
pop:末尾删除
shift:首部删除
concat:数组合并
join:数组元素通过连接符变成字符串
reverse:数组反转
sort:数组排序
flat:数组拍平
slice (start, end):切割,不改变原数组,返回新数组
splice (start, length, newItem):切割,改变数组,从指定位置开始删除,同时可插入新元素
indexOf:查找元素下标
map、foreach、filter ....

闭包

简单来说就是函数中嵌套函数,这个内部函数暴露给了外部调用。作用是可以访问局部变量,缺点是容易发生内存泄漏(即变量不会被自动回收)。

  1. 可以用来封装私有变量,编写 JS 库/插件可能会用到
  2. 用来实现节流这类函数
  3. 可以作为缓存数据的一种策略

相关阅读JS 闭包的应用场景

说到内存泄漏,除了闭包以外还有哪些常见的场景容易引起内存泄漏?
  1. 意外的全局变量(全局变量不会被系统自动回收,要注意)

  2. 被遗忘的定时器(垃圾回收机制不会自动回收,所以定时器有对应的销毁函数)


内存泄漏和内存溢出有什么区别?

内存泄漏就是变量没有及时回收而常驻内存,最终结果就是导致内存溢出。而内存溢出的原因可能不止有一个。

原型链

每个函数都有 prototype 属性,每个函数实例对象都有一个 __proto__ 属性,__proto__ 指向了 prototype,当访问实例对象的属性或方法,会先从自身构造函数中查找,如果找不到就通过 __proto__ 去原型中查找。

作用域链

在当前作用域下找不到某个变量时就去父级作用域查找,依次向上一级一层层查找变量的过程就叫做作用域链。

相关阅读通过代码理解原型链

call、apply、bind

共同点:都可以改变函数的作用域(改变 this 指向)

call / apply :会立即执行函数,两者基本区别在于传参不同

bind:不会立即执行

相关阅读代码模拟实现一个 call 函数

相关阅读call 和 apply 哪个性能更好?

如何理解 this

记住 this 永远指向最后调用它的那个对象

相关阅读关于 this 常见的 5 种场景

箭头函数可以改变 this 吗?

箭头函数的设计之初就是为了提供一种更为简短的函数方式,它自身并没有 this 所以不能主动改变,是根据上下文决定的。

new关键字

相关阅读代码模拟 new 一个对象发生的过程

常见问题

如何检测数据类型

  1. typeof 关键字:检测基本数据类型,检测不出 null 和 Array
  2. Object.prototype.toString.call():可检测所有类型

如何判断数组?

1. Array.isArray([]) // ES6
2. Object.prototype.toString.call([]) // 返回 "[Object Array]"

数组去重?

[...new Set(arr)] // 这种最简单,其他方法参考后面的编程题

如何理解 foreach?

相关阅读foreach 能不能跳出循环?

关于深浅拷贝

浅拷贝: 使用新变量等号赋值对象,它们引用相同的地址

深拷贝:

  1. JSON.parse(JSON.Stringify()),优点是简单,缺点是不能拷贝Functionundefined 会丢失,时间对象会变成字符串。
  2. 深度递归遍历
  3. Object.assign (一层是深拷贝,嵌套二层以上为浅拷贝)

一道值得反复品味的经典面试题:

相关阅读这道 JS 经典面试题不要背,今天帮你彻底搞懂它


JS进阶

ES6 新语法/特性

  1. 模板字符串
  2. letconst 关键字
  3. 箭头函数(没有自己的 this,不能使用 new 命令,不能调用 call
  4. class
  5. Promiseasync/await(es7之后才支持)
  6. exportimport 模块化(ES Module)
  7. 对象扩展(很常用,键值对同名简写,Object.assign 拷贝或合并对象)
  8. 展开运算符(很常用,... 用于组装数组/对象)
  9. 解构赋值(可简化提取数组/对象中的值,变量交换不用中间变量)
  10. Map(性能更好的对象)
  11. Set(元素值是唯一的,常用于数组去重)

Map和普通对象的区别:

  • 键值类型;
  • Map有记录顺序,对象是无序的;
  • Map采用红黑树来存储键值,储存大量数据时性能更优,对象需要维护键值对映射关系,会受到内存上的一些限制。

由于这些差异,通常在需要存储大量数据且需要按照特定规则查找和排序时,使用 Map 对象更为合适;而在存储少量数据,并且知道属性名的情况下,使用对象更加方便。

特殊类型 WeakMap:

  • Map 对象的键可以是任何类型,但 WeakMap 对象中的键只能是引用对象(如果对象不再被引用,则会自动GC)
  • WeakMap 对象是不可枚举的,无法获取集合的大小

WeakMap 由于无法遍历和清空,使用场景比较有限,通常可以用于避免循环引用造成内存泄漏。

继承

JS 实现继承写法如下,JAVA 直呼内行

class B extends A {
    constructor() {
        super();
    }
}

实战中技能树点满 ES6 Class 即可,但在远古时期,JS 继承还有哪些实现的方式呢?又分别有哪些缺点?继续往下看:

  1. 原型继承
  2. 构造函数继承
  3. 组合继承(call / apply)
  4. 寄生组合继承(最终优化版本,前三种都是推导)
  5. class继承(es6)

相关阅读详细解析 JS 面向对象编程、原型,以及 5 种继承方式

高阶函数

高阶函数泛指那些操作其他函数的函数。简单来说,就是一个将函数作为参数或者返回值的函数。

例如 Array.prototype.mapArray.prototype.filterArray.prototype.reduce 这些都是 JavaScript 原生的高阶函数。


JS异步编程

EventLoop

JavaScript 是单线程的,包含了同步任务与异步任务,同步任务直接在调用栈(主线程)中执行,异步任务会放入任务队列(TaskQueue)中,等待同步任务全部执行完毕再取出来,所以如果异步之中仍有异步任务,在调用栈中执行时会继续放入任务队列中,这就是 JS 的事件循环机制(EventLoop)。

宏任务与微任务

异步任务队列实际上分为两种,分别是宏任务队列与微任务队列,在当前循环中会优先执行微任务,当微任务队列被清空后才会执行宏任务队列。

宏任务和微任务的执行顺序?

当事件循环开始时,宏任务与微任务都是不执行的,此时只有同步任务与异步任务之分,等同步任务执行完之后,再先后执行微/宏任务。

而在微任务被清空时,这一轮循环就结束了,宏任务的处理,即进入了新一轮事件循环,那么此时的宏任务已经可以看做是同步任务了。

异步任务里可以看出是微任务比宏任务先执行,当然前提是还要加上一个条件:在同一轮循环当中。

常见宏任务

  1. 定时器系列:setTimeOutsetIntervalrequestAnimationFrame
  2. I/O流操作:这种比较耗性能的操作浏览器会交给单独的线程去办,得到结果后再通知回来

常见微任务

  1. Promise.then() / .catch() / .finaly()MutationObserver
  2. process.nextTick( 仅在 Node.js )

执行顺序

同步任务 > 微任务 > requestAnimationFrame > DOM渲染 > 宏任务

requestAnimationFrame

  • 仅对浏览器生效,回调属于高优先级任务
  • 会将每一帧中所有 DOM 操作集中一次渲染(所以性能较好)
  • 回流重绘的时间会跟随刷新频率动态改变,不能主动控制(由于是帧操作,必须递归调用)
  • 因为是异步任务,在调用后实际还可以取消
  • 浏览器页面不是激活状态下(或离开标签页),会自动暂停执行
  • 根据以上特性该方法常用于处理帧动画的操作,性能优于 setInterval

requestIdleCallback

  • 该回调函数是低优先级的任务,仅在浏览器空闲时期被调用(目前仍处于实验功能阶段,在微前端中常有应用)

Callback

容易造成回调地狱,不能 try...catch 捕获,不能 return

Promise

也会造成回调地狱,但优化了 callback 方式的回调地狱问题,而 asyncawait(ES7)才真正解决了异步回调的问题。

面试高频:

  • Promise.all:将一个包含 Promise 实例的数组传入,数组内所有Promise实例执行完毕时,该方法会返回结果数组

  • Promise.race:返回的是最快成功回调的一个结果。

  • 实现 Promise.all 的思路:返回一个 promise 对象,方法中设有变量 countresults,循环执行每个 promise,then 回调中 count++,当 count === promises.lengthresolve(results)

相关阅读从零开始 - 40行代码实现一个简单Promise函数

推荐阅读Async / Await 的原理及实现(作者:写代码像蔡徐抻)


流行框架

React、Vue 框架对比

框架对比我认为是开放问题,每个人都有不同见解,官网是这么描述的:

React:用于构建用户界面的 JavaScript 库(只为提供构建界面解决方案)

Vue:渐进式 JavaScript 框架(可以看出致力于构建框架生态,且易上手)

点此看看我的观点

React 和 Vue 都是非常快的,所以在速度上没有对比性,它们最大的不同在于,React 把问题都交给社区,形成了很分散的生态系统,相对地这也使得 React 生态更繁荣。而 Vue 是集大成者,比如它的很多灵感其实借鉴了 Angular(例如 ng-if 与 v-if 这类语法)、又如 React 社区在状态管理方面的创新精神启发了 Vue 官方出了个 Vuex。而有些人可能会认为 React 使用 JSX 编程是两者本质差别,这并不准确,其实 Vue 也有渲染函数,甚至也能支持 JSX 编程。如今都是组件化开发,组件则大致可以抽象地分为“视图表现类”和“逻辑类”,很明显逻辑类的组件才更适合用 JSX 开发,但事实上,视图类的组件在实际开发中的占比是要远大于逻辑类组件的,这也就是为什么 Vue 更推荐使用 template。

当然以上观点不全是我个人的看法,事实上大部分是从 Vue 官方文档提炼再加工,可以精读这篇文档,多说点东西利于面试中杀时间(笑)。而且由于观点来源官方,也不容易被质疑。

推荐阅读Vue 2.x 官网文档 - 对比其他框架

React

  • JSX 语法,Hooks
  • 单向数据流
  • Virtual DOM、Fiber
  • Redux 状态管理器

Vue 相关

组件通信

相关阅读Vue 12 种组件通信方式及理解

数据双向绑定原理

采用了 "发布-订阅" 的设计模式,通过 Object.defineProperty() 劫持各个属性的getter、setter,在数据变动时调用 Dep.notify 发布消息给订阅者 Watcher,使之更新相应的视图。

Vue2 使用 Object.defineProperty 的缺陷:

  1. 不能监听数组变化,所以 vue2 中需要对数组原型链上的方法进行一些修改来实现监听。
  2. 只能劫持对象的属性,所以需要深度遍历对象。

Vue3 中将 Object.defineProperty 替换为 Proxy 解决了对象深度监听的问题,因为 Proxy 代理了整个对象。

相关阅读:Vue3 更多差异记录

diff 算法

一般面试会直接问 diff 算法然后以此引伸出虚拟DOM。diff 的过程其实就是不断调用一个叫 patch 的函数,找到并记录需要修改的值,最后统一做视图更新。它接收新旧两个节点作为参数,如果比对节点类型发生了改变,那么会直接替换掉整个节点,否则继续比对其他属性是否有差异,找出并记录下改变的属性。如果节点下有子节点就递归继续比对,总之这个过程最终就是尝试找出最小代价更新视图的办法。

参考下 chatGPT 如何回答~

Vue中的diff算法也叫做虚拟DOM的周界算法,主要用来比较两个虚拟DOM节点的差异。当一个组件的数据发生变化时,Vue通过重新渲染一个虚拟DOM树,然后将新旧虚拟DOM树进行对比。通过比较新旧虚拟DOM树的差异,Vue就能够明确哪些地方需要被更新,哪些地方无需重新渲染。

Vue的diff算法是通过深度优先、先序遍历的方式进行的,它将两个虚拟DOM树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点。当所有的子节点都完成了比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点,可以更新它的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。

通过这种逐层对比,Vue的diff算法能够快速高效地计算出哪些界面需要更新,从而避免了不必要的渲染和重绘,提高了渲染性能和用户体验。

Virtual Dom(虚拟DOM)

将真实 DOM 转化为一个 model 对象(类似 AST 抽象语法树),通过 diff 算法对比新旧差异,以最小代价转换成 DOM 操作。

其实虚拟 DOM 并不能直接提升 DOM 操作性能,它出现的理由是 JS 执行速度远比真实 DOM 操作要快,所以在某些场景下性能更好,例如:

  1. 合并多次操作:比如某元素添加 1000 个子节点,通过虚拟 DOM 只需要把过程运算好并记录下来,然后一次性添加就行。
  2. 减少 DOM 操作:比如 90 个子节点变成了 100 个,最简单的操作就是把这 100 个新的节点全部重新渲染,而借助 diff 算法可能就会发现实际只有 10 个节点是新增的。(有点 Ajax 的味道了)
  3. 跨平台:虚拟 DOM 分离了数据与视图,带来了数据驱动的新模式,可维护性也大大加强,所以可移植性非常好。例如基于 React 的 RN(React Native),基于 Vue 的 weex 都是跨平台框架。Flutter 也受到启发引入虚拟 DOM。

实战技巧

Vue2 中视图不更新问题

原因是对象层级嵌套过深或添加了根级数据时发生。Vue3不会有这个问题(proxy解决)。

  1. 深拷贝覆写对象(稳定,但产生额外性能损耗)
  2. $forceUpdate() 强制刷新数据(不稳定,有几率失败)
  3. this.$set Vue 提供的方法,动态添加响应式数据(推荐)

v-if 和 v-for 可以共用吗?为什么不可以?

当 v-if 与 v-for 一起使用时,v-if 比 v-for 有更高的优先级,这会导致 v-for 无法正常绑定属性。( 官网文档

正确写法:
<template v-if="show">
    <i v-for="item in items" ...... />
</template>

组件中的 data 属性为什么不是对象

组件 data 必须是一个函数而不是对象,如果定义成了对象形式,vue 在创建组件的时候 data 变成了引用类型,组件之间的 data 就会共用一个内存地址,所以采用函数定义避免了实例对象之间数据的污染

.......
    data() {
        return {
             foo: "foo"
        }
    }

为什么需要设置 key 值

key 值相当于提供了一个创建元素时的唯一标识,是 diff 算法比对新旧节点的依据,如果不设置会对性能造成一定影响。

为何不推荐 index 作为 key 值

index 的值是动态的,如果列表发生删改或在中间插入数据,就会导致所有的元素的 key 值发生改变,这样在 diff 算法的比对中就会认为是新创建的元素,最终导致重复渲染浪费性能。

常见指令:

v-once // 只渲染一次视图
v-pre // 不编译视图,原样输出

常用事件修饰符:

.stop // 阻止冒泡
.prevent // 阻止默认事件
.self // 仅绑定元素自身触发
.once // 事件只会触发一次
.passive // 不能和 .prevent 一起使用

Vue 进阶

相关阅读从零开始 - 50 行代码实现一个 Vuex 状态管理器

相关阅读探索 Vue3 响应式数据原理 ( Proxy 与 Reflect )

相关阅读Vue 处理错误上报原来如此简单

相关阅读Keep-Alive 有什么问题?如何销毁?

相关阅读Vue 自定义指令让你的开发变得更简单


前端性能优化

网络请求层面上讲:

  1. 减少 HTTP 请求:构建 SSR 服务、搭建 BFF 层聚合 API
  2. 使用字体图标集,图片图标采用 css 精灵图(雪碧图)
  3. 其它:Gzip 压缩、资源 CDN 等等
  4. 图片懒加载、预加载、打包时适当压缩静态资源的质量,长列表优化(可扩展,下文补充)

从 JS / CSS 角度上讲:

  1. 尽量不使用 cookie、iframe、flash 等性能缺陷的功能
  2. 避免内存泄漏(如避免使用闭包),减少页面回流重绘(前面有详细介绍到)
  3. 减少第三方库的依赖,对大型框架库尽量采用按需加载,可以异步加载非必须的 JS 文件,项目中使用路由懒加载减少首次加载体积
  4. CSS 样式分离在单独的文件中引入(减少内联样式,可以利用浏览器缓存),避免使用 * 通配符,减少嵌套(因为 CSS 解析规则是从右往左的),避免使用 CSS 表达式(耗性能)
  5. 利用 Chrome 开发者工具内置的 Performance 来分析代码执行耗时
  6. 使用 WebWorker 多线程来优化那些高耗时、计算密集型任务
  7. Webpack 性能优化(可扩展,下文补充)

推荐阅读:《 性能优化经验分享 》(作者:字节前端)

webpack 性能优化

从体积角度:

  1. 首先通过 webpack-bundle-analyzer 插件可视化分析包内各个模块的大小和依赖关系,判断优化指标
  2. DllPlugin:把基础模块独立打包出来放到单独的动态链接库里
  3. CDN:配置 Enternals 把一部分包分离出来在外部引用而不是打包进项目,然后可以储存在 CDN
  4. uglifyJsPlugin:压缩混淆去注释插件
  5. image-webpack-loader:对图片进行转化或者压缩处理
  6. tree shaking:删除冗余代码(最早由 Rollup 提出,依赖于 ES6 模块化中的静态结构特性,也就是在编译时而非运行时就已经知道哪些模块被用到了)
  7. 异步加载模块:即路由懒加载,把 import 放到函数中,这样执行时才会引用组件

从速度角度:

  1. 首先通过 speed-measure-webpack-plugin 插件测量各个 loader 花费的时间,量化打包速度,判断优化指标
  2. noParse 忽略对部分没采用模块化的文件的递归解析处理
  3. babel-loader 开启缓存,cacheDirectory 减少重新编译的消耗
  4. 导入文件后缀要写全,使用 alias 别名设置路径而非相对路径(利用别名 Alisa 功能),避免查找尝试和递归路径的消耗。
  5. happypack 多进程打包(不维护了,可以用 thread-loader 替代)

优化滚动性能

扩展阅读:《 一行 CSS 代码即可提升网页滚动性能

优化动画性能

requestAnimationFrame(参考上面的JS异步编程 - 宏任务与微任务)

图片懒加载

1. 滚动事件

利用页面滚动事件监听,结合 scrollTop 与浏览器窗口高度判断图片是否在当前窗口中。

2. Intersection Observer

const observer = new IntersectionObserver((entries) => {
    entries.forEach((item) => {
      if (item.isIntersecting) {
        // TODO: 换上真实的图片链接
        observer.unobserve(item.target) // 停止监听该节点
      }
    })
  }) //不传options参数,默认根元素为浏览器视口
document.querySelectorAll('.img-box').forEach((div) => observer.observe(div)) // 遍历监听所有图片DOM节点

3. 原生 loading="lazy"

设置简单,不过需要浏览器本身支持。而且除了可以设置在图片 img 标签上,还能支持 iframe 标签。

长列表优化

1. 分片加载

将大的数据集切分为小的数据集然后利用事件循环不断依次渲染出来,从而优化加载速度,缺点是性能依旧会达到瓶颈。

2. 虚拟列表

控制渲染视图的数据范围,不仅优化了加载速度,理论上也可以支撑无限长的数据渲染,但如果快速滚动可能会出现白屏等问题,并且列表的每一项高度需要是固定的,如果高度不定则无法很好地工作,因为 DOM 必须渲染完成才能知道真实高度是多少。

3. content-visibility

该属性控制一个元素是否渲染其内容,它允许用户代理(浏览器)潜在地省略大量布局和渲染工作,直到需要它为止。

主要是设置其 auto 这个属性值,它表示该元素内容(也就是所有子节点)仅在浏览器可视区域内才渲染。

优势:不破坏完整的文档树,可以支持静态全文检索;浏览器原生的方法,不用考虑上下屏缓冲区。

要实现效果更好的虚拟列表还需要搭配一些属性使用:

  1. contain-intrinsic-size:可以指定的元素的自然大小,防止滚动条抖动。

  2. loading="lazy":由于文档树是完整的,未被渲染的资源实际上仍会全部发起请求,这就无法像上面实现的虚拟列表一样自带的懒加载效果。

缺点:兼容性还不是很好,对性能要求可能会比较高,所以在低端机器上难以运作。


工程化

前端工程化涉及:模块化、组件化、规范化、自动化、构建工具、提效工具

扩展阅读:《 浅谈前端工程化的发展以及相关工具介绍

模块化发展

  • 早期是利用函数自执行实现,在单独的函数作用域中执行代码(如 JQuery )
  • AMD:引入 require.js 编写模块化,引用依赖必须提前声明
  • CMD:引入 sea.js 编写模块化,特点是可以动态引入依赖
  • CommonJS:NodeJs 中的模块化,所以只在特殊环境适用,是同步加载
  • ES Modules:ES6 规范中新增的模块化,是目前的主流

webpack

推荐阅读:《 再来一打 Webpack 面试题 》(作者:童欧巴)

webpack 热更新原理(HMR):

项目运行时启动一个本地服务,与浏览器建立 websocket 链接,此后 webpack 开始监听文件变动,如有变动重新编译,将通过 websocket 推送更新消息给浏览器,浏览器发起 http 请求去服务端获取打包好的资源解析并局部刷新页面。

一些碎碎念:

Webpack 虽然是老生常谈了,但 Vite 的盛大登场也不容小视,而且其生态才刚刚起步没多久,就出现了 Turbopack 强势竞争,今年字节又发布了 Rspack,构建工具也卷起来了,此情此景老一辈的 gulp、grunt 流下了时代的眼泪😂

NodeJs

真正用 NodeJs 作为后端开发其实并不多,同样作为脚本语言对比 PHP 并不逊色,但在生态与发展基础上不占优势。就本人在实际工作中的运用,除了作为中间件开发,写写脚本做做自动化也是非常方便,可以说对前端开发而言已经是必修课。

扩展阅读:《 前端都应该了解的 NodeJs 知识及原理浅析


常见设计模式

设计模式代表了编程中某些最佳的实践,是无数优秀代码设计经验的总结。学习设计模式有利于写出更容易被他人理解的代码,并一定程度上保证其可靠性。

单例模式

一个类只有一个实例,并提供一个访问它的全局访问点。应用场景:Vuex 等状态管理器的设计。缺点是强耦合,所以需要注意使用场景。

工厂模式

定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。优点:灵活性高,扩展性好。缺点:一定程度上增加了系统的复杂度。

适配器模式

将一个类的接口转化为另外一个接口。例如同时满足安卓和苹果手机充电,通常需要两根不同的充电线,但使用转接头就能使苹果线给安卓充电,这时充电线就被复用了。应用场景:二次封装。

策略模式

定义一系列的算法,将他们独立封装,在同一方法中动态地在几种算法中选择使用,易于主体代码的理解与扩展。

观察者模式

一对多模式,多个观察者对象监听某一个对象,该对象的状态发生变化时所有的观察者都能做出响应。(反过来看,实现了对象改变时可以广播出去的效果)

订阅-发布模式和观察者模式之间有什么区别?

在观察者模式中只存在两个角色,发布者与观察者,观察者直接监听发布者的消息。而在订阅发布模式中,还存在一个消息收发器作为第三者,发布者通过将消息递交给第三者来告知订阅者,而这也是两种模式的本质区别,即订阅发布模式是一个解耦的实现。

设计模式的最基本原则

开闭原则

对扩展开放,对修改关闭。这意味着当需要添加新的功能时,不应该修改已有的代码,而是应该通过扩展已有的代码来实现新的功能。

单一职责

复杂方法拆分成各自独立方法,一个方法内只做好一件事。


编程题

数组去重

一般编程题涉及较多的都是数据的操作,下面这 5 种去重数组的方法基本覆盖了数组操作的技巧,可以当作热身。

// 题目:
const arr = [1, 1, 'true', 'true', true, true, 1, 1, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log( unique(arr) ) // 实现一个 unique 函数去重

1. splice + 两层嵌套循环

function unique(arr = []) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1)
                j--
            }
        }
    }
    return arr
}

2. sort 排序后循环

function unique(arr = []) {
    arr = arr.sort()
    const result = []
    for (let i = 0; i < arr.length; i++) {
        arr[i] !== arr[i - 1] && result.push(arr[i])
    }
    return result
}

3. includes / indexOf

function unique(arr = []) {
    const result = []
    for (item of arr) {
        !result.includes(item) && result.push(item) // result.indexOf(item) === -1 && result.push(item)
    }
    return result
}

4. filter + indexOf

function unique(arr = []) {
    return arr.filter((item, index) => arr.indexOf(item) === index)
}

5. reduce + includes

function unique(arr = []) {
    return arr.reduce((prev, cur) => prev.includes(cur) ? prev : prev.concat(cur), [])
}

常规手写题

相关阅读手写函数:call、防抖、节流

手写深拷贝:

function deepClone (target, hash = new WeakMap()) { // 额外开辟一个存储空间WeakMap来存储当前对象
  if (target === null) return target // 如果是 null 就不进行拷贝操作
  if (target instanceof Date) return new Date(target) // 处理日期
  if (target instanceof RegExp) return new RegExp(target) // 处理正则
  if (target instanceof HTMLElement) return target // 处理 DOM元素

  if (typeof target !== 'object') return target // 处理原始类型和函数 不需要深拷贝,直接返回

  // 是引用类型的话就要进行深拷贝
  if (hash.get(target)) return hash.get(target) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
  const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
  hash.set(target, cloneTarget) // 如果存储空间中没有就存进 hash 里

  Reflect.ownKeys(target).forEach(key => { // 引入 Reflect.ownKeys,处理 Symbol 作为键名的情况
    cloneTarget[key] = deepClone(target[key], hash) // 递归拷贝每一层
  })
  return cloneTarget // 返回克隆的对象
}

进阶手写题

函数柯里化 currying

优点

  1. 参数复用,简化代码。

  2. 延迟执行,使得我们可以组合现有的函数来创建新的函数。

缺点

  1. 可能会带来性能问题,由于每次返回一个新函数,它需要在内存中分配新的空间。

  2. 可能会导致一些问题难以定位,特别是在柯里化嵌套的情况下,因为产生的新函数可能会难以理解。

实现原理

闭包,接收并缓存参数,接收到参数时挂起,无参数时立即执行。

function currying(fn) {
  let allArgs = [] // 闭包

  return function next() {
    const args = [].slice.call(arguments)

    if (args.length > 0) { // 有传入参数时挂起
      allArgs = allArgs.concat(args)
      return next
    } else {
      return fn.apply(null, allArgs) // 立即执行
    }
  }
}

如下定义一个 add 方法,实现 add(1)(2, 4)(3)() = 10

const add = currying(function () {
  let sum = 0
  for (let i = 0; i < arguments.length; i++) {
    sum += arguments[i]
  }
  return sum
})

console.log( add(1)(2, 4)(3)() ) // 10

compose 函数

假设现有三个函数分别为 a() b() c(),请用一个函数实现 c(b(a())) 这种调用效果:

  1. 数组出栈循环执行
function compose(...funcs) {
  return function(result) {
    while(funcs.length > 0) {
      result = funcs.pop()( result )
    }
    return result
  }
}

结果:

console.log( c(b(a(1))) === compose(c, b, a)(1) ) // true
  1. 利用 reduce 实现
function compose(...funcs) {
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

面试真题:前提条件同上,要求实现 compose(fnArray, init) 这样的函数,fnArray为abc函数数组,init为初始调用值。我们上面封装的 compose 思路不变,在此基础上封装一个变种函数即可实现:

function modifyCompose(arr, init) {
    return compose(...arr)(init)
}

结果:

console.log(modifyCompose([c, b, a], 1) === c(b(a(1)))) // true

pipe 函数

其实和上面的 compose 是一样的,都是函数平铺组合的思想,只不过 compose从右向左执行函数的,如果串联的函数有顺序需求就不是很符合直观,所以 pipe 就是反过来从左到右执行而已。

function pipe(...funcs) {
    return funcs.reduce((a, b) => (...args) => b(a(...args)))
}

结果:

console.log(pipe(a, b, c)(1) === c(b(a(1)))) // true

JS 求交并差集

在工作中常用到,所以记录一下,面试应该不一定会考察。

a = [1,2,3,4,5] b = [2]

ES7:

// 并集
let union = a.concat(b.filter(v => !a.includes(v))) // [1,2,3,4,5]
// 交集
let intersection = a.filter(v => b.includes(v)) // [2]
// 差集
let difference = a.concat(b).filter(v => !a.includes(v) || !b.includes(v)) // [1,3,4,5]

ES6:

let aSet = new Set(a)
let bSet = new Set(b)
// 并集
let union = Array.from(new Set(a.concat(b))) // [1,2,3,4,5]
// 交集
let intersection = Array.from(new Set(a.filter(v => bSet.has(v)))) // [2]
// 差集
let difference = Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v)))) // [1,3,4,5]

ES5:

// 并集
var union = a.concat(b.filter(function(v) {
return a.indexOf(v) === -1})) // [1,2,3,4,5]
// 交集
var intersection = a.filter(function(v){ return b.indexOf(v) > -1 }) // [2]
// 差集
var difference = a.filter(function(v){ return b.indexOf(v) === -1 }).concat(b.filter(function(v){ return a.indexOf(v) === -1 })) // [1,3,4,5]

独立作用域

for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i); // 输出了 10 次 10
    }, 0);
}

for (let i = 0; i < 10; i++) {
    console.log(i); // 输出了 0 1 2 3 .... 9
}

改造上面的函数实现类似 let 这样的独立作用域:

for (var i = 0; i < 10; i++) {
    (function (i) { // 独立的作用域块了
        setTimeout(function () {
            console.log(i); // 0 1 2 3 .... 9
        }, 0)
    })(i);
}

算法题

此时应该怒刷999道力扣算法题,等我先去注册个账号,算法这块我必拿下

我回来了,去提莫的怒刷算法,两道简单就把我给干懵了。。

这里是 LeetCode 热门题目 100 道,有时间挑一些简单到中等的题目尽量搞懂就行。算法能力需要长期积累才有用,一定不要觉得到了面试时才去刷算法,如果没有强大的数据结构基础是比较难的。

经典排序题

  • 冒泡排序

img

嵌套循环,减减加加,两两交换,代码如下:

for(let a = arr.length; a > 0; a--) {
    for(let b = 0; b < a-1; b++) {
        arr[b] > arr[b+1] && ([arr[b], arr[b+1]] = [arr[b+1], arr[b]])
    }
}
  • 插入排序

通过构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应的位置插入。

for(let a = 1; a < arr.length; a++) {
    let key = arr[a]
    let b = a - 1
    while(b >= 0 && arr[b] > key) {
        arr[b + 1] = arr[b]
        b--
    }
    arr[b + 1] = key
}

冒泡是相对比较简单的一种排序方法,插入和冒泡稳定性都比较高,两者时间复杂度都是O(n²)。


新潮技术

微前端

面试官:微前端了解过吗?你有没有考虑过用它来优化大型系统?

相关阅读微前端很好,为什么我却不使用?以及微前端原理剖析

Web Worker

扩展阅读Partytown 如何消除第三方脚本所带来的网站膨胀

WebGL

可以利用设备硬件图形加速在浏览器中渲染高性能交互式 3D 和 2D 图形,目前最流行的库是 three.js,它之于 WebGL 就如同 JQuery 之于 JavaScript 一样。

WebAssembly (abbreviated Wasm)

简单来说,该技术可以在 JS 中运行一些其他语言,帮助浏览器扩展其边界能力。

CSS 容器查询

一种比媒体查询更强大的 CSS 查询,未来势必会增强响应式布局。

相关阅读CSS 容器查询来了,10 个精彩案例分享


以上就是文章的全部内容,感谢看到这里!本人知识水平有限,如有错误望不吝指正,如果觉得写得不错,对你有所帮助或启发,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味de一天,希望与你共同成长~

本文正在参加「金石计划」