【2023.9.18】前端高频八股文——面试看这篇就够了!

10,245 阅读42分钟

HTML

position取值

  • static 默认定位类型,这个时候top right值无效,浏览器决定位置
  • relative 搭配top right bottom left 使用,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)
  • absolute 元素会被移出文档流,并不为元素预留空间,通过指定元素相对于最近的非static定位祖先元素的偏移,来确定元素位置。
  • fixed 元素会被移出文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。fixed 属性会创建新的层叠上下文。当元素祖先的 transform、perspective、filter 或 backdrop-filter 属性非 none 时,容器由视口改为该祖先。
  • sticky 元素根据正常文档流进行定位,然后相对它的最近滚动祖先,包括 table-related 元素,基于 top、right、bottom 和 left 的值进行偏移。偏移值不会影响任何其他元素的位置。

事件模型

事件与事件流

事件流都会经历三个阶段:

  • 事件捕获阶段(capture phase)
  • 处于目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

image.png

  • 事件冒泡从下往上传播,由具体的元素逐渐向上传播到DOM中最高节点(一般是document)
  • 事件捕获与事件冒泡相反,由最高节点向下捕获至具体节点

事件模型

一般分为三种事件模型:

  • 原始事件模型(DOM0级)
  • 标准事件模型(DOM2级)
  • IE事件模型(基本不用)

原始事件模型:

原始事件模型中绑定监听函数比较简单,有两种方式:

  • HTML中直接绑定
<input type="button" onclick="fun()">
  • 通过JS代码绑定
let btn = document.getElementById('.btn')
btn.onclick = fun
特性
  • 绑定速度快

DOM0级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能页面还未完全加载出来,以至于事件可能无法正常运行

  • 只支持冒泡,不支持捕获
  • 同一个类型的事件只能绑定一次
<input type="button" id="btn" onclick="fun1()">

var btn = document.getElementById('.btn');
btn.onclick = fun2;

如上,当希望为同一个元素绑定多个同类型事件的时候(上面的这个btn元素绑定2个点击事件),是不被允许的,后绑定的事件会覆盖之前的事件

删除 DOM0 级事件处理程序只要将对应事件属性置为null即可

标准事件模型

因为原始事件模型无法重复绑定,且无法控制捕获或冒泡阶段调用,因此诞生了标准事件模型

在该事件模型中,一次事件一共有三个过程:

  • 事件捕获阶段:事件从document一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
  • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数
  • 事件冒泡阶段:事件从目标元素冒泡到document, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行

事件绑定监听函数的方式如下:

addEventListener(eventType, handler, useCapture)

事件移除监听函数的方式如下:

removeEventListener(eventType, handler, useCapture)

参数如下:

  • eventType指定事件类型(不要加on)
  • handler是事件处理函数
  • useCapture是一个boolean用于指定是否在捕获阶段进行处理,一般设置为false与IE浏览器保持一致

IE事件模型(基本不用)

IE事件模型共有两个过程:

  • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数。
  • 事件冒泡阶段:事件从目标元素冒泡到document, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行

事件绑定监听函数的方式如下:

attachEvent(eventType, handler)

事件移除监听函数的方式如下:

detachEvent(eventType, handler)

举个例子:

var btn = document.getElementById('.btn');
btn.attachEvent(‘onclick’, showMessage);
btn.detachEvent(‘onclick’, showMessage);

H5 语义化

html 语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;在没有样式 CCS 情况下也以一种文档格式显示,并且是容易阅读的。搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。

延迟加载,script 标签为什么放后面?引出 defer 和 async 区别

前端加载 html,html 解析器运行于主线程中,如果遇到<script> 标签后会阻塞,直到脚本从网络中下载并被执行,也就是说<script> 标签的脚本会阻塞浏览器的渲染。这里还涉及到页面生命周期:

  • DOMContentLoaded 页面已经完全加载了 html 并且构建了 dom 树,但样式和 img 这样的资源还没有加载完
  • load —— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
  • beforeunload/unload —— 当用户正在离开页面时。

当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束。如何解决这个问题,可以使用 script 标签的两个属性,defer 和 async。

  • 没有 defer 或 async,就会阻塞了,浏览器会立即加载执行这个script 脚本,就是卡在这个标签之后的这些文档元素前加载并执行
  • 有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行。
  • 有 defer ,同样也是并行异步的,不同的地方就是 script js 的执行会在所有元素解析完,并且在DOMContentLoaded 事件触发前完成。
  • async 执行时机:下载完后,立即执行
  • defer 下载完后,在 dom 解析完之后、触发 DOMContentLoaded 方法之前执行

image.png

BFC

BFC(Block Formatting Context),即块级格式化上下文,它是页面中的一块渲染区域,容器页面里的子元素不会影响到外部的元素,并且有一套属于自己的渲染规则:

  • BFC 是一个块级元素,块级元素会在垂直方向上一个接一个的排列。
  • BFC 就是页面中的一个隔离的独立容器,容器里的元素不会影响到容器外的元素。
  • 垂直方向的距离由margin决定,属于同一个 BFC 内的两个相邻标签的外边距会发生重叠。
  • 计算 BFC 的时候,浮动元素也参与其中。

下列方式会创建块格式化上下文:

  • 文档的根元素(<html>)。
  • 浮动元素(即 float 值不为 none 的元素)。
  • 绝对定位元素(position 值为 absolutefixed 的元素)。
  • 行内块元素(display 值为 inline-block 的元素)。
  • 表格单元格(display 值为 table-cell,HTML 表格单元格默认值)。
  • 表格标题(display 值为 table-caption,HTML 表格标题默认值)。
  • 匿名表格单元格元素(display 值为 table(HTML 表格默认值)、table-row(表格行默认值)、table-row-group(表格体默认值)、table-header-group(表格头部默认值)、table-footer-group(表格尾部默认值)或 inline-table)。
  • overflow 值不为 visibleclip 的块级元素。
  • display 值为 flow-root 的元素。
  • contain 值为 layoutcontentpaint 的元素。
  • 弹性元素(display 值为 flexinline-flex 元素的直接子元素),如果它们本身既不是弹性网格也不是表格容器。
  • 网格元素(display 值为 gridinline-grid 元素的直接子元素),如果它们本身既不是弹性网格也不是表格容器。
  • 多列容器(column-countcolumn-width (en-US) 值不为 auto,且含有 column-count: 1 的元素)。
  • column-span 值为 all 的元素始终会创建一个新的格式化上下文,即使该元素没有包裹在一个多列容器中(规范变更Chrome bug

(创建BFC的方式:根元素、浮动元素和绝对定位元素,非块级盒子的块级容器,overflow 值不为 visiable 的块级盒子)

应用场景:

  • 防止 margin 塌陷,例如两个 p 取大的值,这个时候可以在外层包一层 div
  • 清除浮动,例如在子元素浮动的情况,父元素没有被撑开,BFC 在计算高度时浮动元素也会计算的,给父元素加上 overflow hidden
  • 自适应多栏布局,例如使用 float 布局两栏布局,左侧 float left,左边依然会与包含块的左边相接触,这个时候可以给外边距加 overflow: hidden;

重排和重绘

当我们通过 url 链接访问一个页面时,在加载完 html、css 、script资源后会有一个解析布局绘制页面的过程,对应的三个操作解析、布局、绘制,网页生成的时候,至少需要排列及绘制一次,随着用户的操作后续有可能而后面会触发重排和重绘。

过程

  • html会解析成一个dom树
  • css会解析成一个cssom树
  • 结合这两个树,会生成一个渲染树Render Tree,这个过程被称为Attachment
  • 生成布局,也就是在屏幕上画出渲染树节点的位置
  • 将布局绘制在屏幕上,即显示出整个画面

重排(reflow):当 dom 的变化影响了元素的几何信息(例如它的位置,尺寸等),这个时候浏览器会重新计算它的属性值,并且把它放在正确位置上,这个叫重排(重新生成布局,重新排列)

重绘(repaints):当一个元素的外观发生了变化,但没有改变布局,浏览器会把元素外观重新绘制出来。常见的触发属性: color,border-style、background-position 等

重绘不一定会导致重排,但是重排一定会导致重绘。

减少不必要的重排

  • 减少不必要的 DOM 深度,一个级别进行更改可能会致使该树的所有级别(上至根节点,下至所修改节点的子级)都随之变化。这会导致花费更多的时间来执行重排。
  • 如果您想进行复杂的渲染更改(例如动画),请在流程外执行此操作,position-absolute 或 position-fixed 来实现此目的。
  • 避免使用不必要且复杂的 CSS 选择器(尤其是后代选择器),因为此类选择器需要耗用更多的 CPU 处理能力来执行选择器匹配。
  • 在大部分浏览器有渲染队列优化,建议通过改变class 来集中改样式
  • 缓存布局信息,集中统一更新

link 和 @import 的区别

  • 两种都是加载 css 的方式,link 是一个 html 标签,但 import 是 css 提供的
  • link 会跟页面加载时同时加载,import 会等页面加载完再加载
  • link 的权重会高于 import

HTTP

本地存储方式的区别和应用场景

cookie

http协议是无状态的协议,会话结束也就终止了联系,为了能够维持请求之间的状态,cookie诞生了

特点:

  • 本质是一段存储在本地不超过4kb的小型文本
  • 内部以键值对的方式来存储

常见字段

  • Expries 用于设置cookie过期时间
Expires=Wed, 21 Oct 2015 07:28:00 GMT
  • Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比Expires高)
Max-Age=604800
  • CookieSameSite属性
    • strict模式,完全禁止第三方请求携带,完全遵守同源策略
    • lax模式,get提交的时候可以携带
    • none模式,自动携带
  • domain属性用于限制Cookie的作用域,只有在指定域名下才能够使用该Cookie
Domain=example.com
  • path属性用于限制Cookie的生效路径,只有在特定的路径下才能够使用该Cookie
  • secure:一个布尔值,表示是否只在 HTTPS 连接时发送 Cookie。
  • http-only:一个布尔值,表示是否禁止通过 JavaScript 访问 Cookie,从而提高安全性。
  • name:Cookie 的名称,通常是一个字符串。
  • value:Cookie 的值,可以是一个字符串或其他类型的数据。

localStorage

localStorage 是一种用于在客户端(浏览器)中存储数据的 Web API,可以用于长期存储非敏感数据,例如用户的个人偏好、应用程序状态等。

特点:

  • 持久化的本地存储,除非主动删除,否则永远不会过期。
  • 在同一域名中,存储的信息是共享的。
  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。通过 window.addEventListener('storage', listener) 方法注册一个事件监听器,其中 listener 是用于处理 storage 事件的回调函数,也就是说本页改变localStorage不会触发这个这个事件,也不会执行回调函数。
  • 大小:5M(跟浏览器厂商有关系)。
  • 只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题安全问题
  • 接口封装。通过localStorage暴露在全局,并通过它的 setItemgetItem等方法进行操作,非常方便。

缺点:

  • 无法像Cookie一样设置过期时间
  • 只能存入字符串,无法直接存对象

sessionStorage

sessionStoragelocalStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。

应用场景

  • 可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
  • 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用sessionStorage就再合适不过了。

indexedDB

indexedDB是运行在浏览器中的非关系型数据库IndexDB的一些重要特性,除了拥有数据库本身的特性,比如支持事务存储二进制数据,还有这样一些特性需要格外注意:

虽然 Web Storage对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存JS的对象
  • 是个正经的数据库,意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 本身有一定门槛

关于indexedDB的使用基本使用步骤如下:

  • 打开数据库并且开始一个事务
  • 创建一个 object store
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 在操作结果上进行一些操作(可以在 request对象中找到)

HTTP发展史

HTTP/0.9——单行协议

由万维网协会和互联网工程任务组制定的初版HTTP标准,该协议诞生之初的作用只是传输超文本内容HTML,并且只支持GET请求

协议定义了客户端发起请求,服务端响应请求的通信模式。所以当时的请求报文只有一行:GET+请求的文件路径。服务端在收到请求后会返回一个以ASCII字符流编码的HTML文档。

HTTP/0.9 虽然简单,但是它充分验证了 Web 服务的可行性

  • 首先它只有一个命令GET。
  • 它没有HEADER等描述数据的信息。因为这个时候的请求非常简单,它需要达到的目的也非常简单,没有那么多数据格式。
  • 服务器发送完内容之后,就关闭TCP连接。这里需要注意一点,这里的TCP连接和http请求是不一样的。http请求和TCP连接不是一个概念。一个http请求通过TCP连接发送,而一个TCP连接里面可以发送很多个http请求(HTTP/0.9不能这么做,但是HTTP/1.1可以这么做,而且在HTTP/2这方面会更大程度地优化,来提高HTTP协议传输的效率以及服务器的性能)
GET /mypage.html

HTTP/1.0——构建可扩展性

随着时代发展,简单的HTML传输已经无法满足用户需求了,浏览器希望可以通过HTTP传输脚本,样式,图片,音视频等不同类型的文件,所以在1996年时进行了一次版本更新:

  • 协议版本信息现在会随着每个请求发送(HTTP/1.0 被追加到了 GET 行)。
  • 状态码会在响应开始时发送,使浏览器能了解请求执行成功或失败,并相应调整行为(如更新或使用本地缓存)。
  • 引入了 HTTP 标头的概念,无论是对于请求还是响应,允许传输元数据,使协议变得非常灵活,更具扩展性。
  • 在新 HTTP 标头的帮助下,具备了传输除纯文本 HTML 文件以外其他类型文档的能力(凭借 Content-Type 标头)。
GET /mypage.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html
<HTML>
一个包含图片的页面
  <IMG SRC="/myimage.gif">
</HTML>

HTTP/1.0最主要的缺点还是跟HTTP/0.9一样,每一个TCP连接只能发送一个HTTP请求,服务器发送完响应,就关闭连接。如果后面需要请求新的数据,则需要再次建立TCP连接,但是TCP建立连接的三次握手成本比较高,并且TCP连接初始的时候发送数据的速度相对较慢,有一个慢启动和拥塞避免的阶段。极端情况,如果每次请求的数据很少,但是请求很频繁,这样每次请求很少的数据都需要建立连接然后断开。

为了解决这个问题,在1.0版本使用了一个非标准的Connection头部字段。当客户端再请求头部信息里面带上Connection:keep-alive的时候,服务器在发送完响应数据之后,就不会断开TCP连接了,从而达到复用同一个TCP连接的目的。但是由于不是标准字段,不同的实现可能导致表现得不一致,因此不能从根本上解决这个问题。

HTTP/1.0最核心的改变是增加了头部设定,头部内容以键值对的形式设置。请求头部通过 Accept 字段来告诉服务端可以接收的文件类型,响应头部再通过 Content-Type 字段来告诉浏览器返回文件的类型。头部字段不仅用于解决不同类型文件传输的问题,也可以实现其他很多功能如缓存、认证信息等。

HTTP/1.1——标准化协议

因为HTTP/1.0多为浏览器和服务器自行扩展的,因此出现了很多版本,使得在HTTP使用过程中非常混乱,因此1997年HTTP/1.1标准发布

HTTP/1.1 消除了大量歧义内容并引入了多项改进:

  • 连接可以复用,节省了多次打开 TCP 连接加载网页文档资源的时间。
  • 增加管线化技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟。
  • 支持响应分块。
  • 增加PUT,DELETE,OPTIONS,PATCH等新方法
  • 引入额外的缓存控制机制。
  • 引入内容协商机制,包括语言、编码、类型等。并允许客户端和服务器之间约定以最合适的内容进行交换。
  • 凭借 Host 标头,能够使不同域名配置在同一个 IP 地址的服务器上。
GET /zh-CN/docs/Glossary/Simple_header HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/zh-CN/docs/Glossary/Simple_header

200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 20 Jul 2016 10:55:30 GMT
Etag: "547fa7e369ef56031dd3bff2ace9fc0832eb251a"
Keep-Alive: timeout=5, max=1000
Last-Modified: Tue, 19 Jul 2016 00:59:33 GMT
Server: Apache
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding

(content)


GET /static/img/header-background.png HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/zh-CN/docs/Glossary/Simple_header

200 OK
Age: 9578461
Cache-Control: public, max-age=315360000
Connection: keep-alive
Content-Length: 3077
Content-Type: image/png
Date: Thu, 31 Mar 2016 13:34:46 GMT
Last-Modified: Wed, 21 Oct 2015 18:27:50 GMT
Server: Apache

(image content of 3077 bytes)

HTTP管道机制(pipeling)

它指的是在一个TCP连接内,多个HTTP请求可以并行,客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能 够区分出每次请求的响应内容。

域名分片

域名分片其实就是资源分域,将资源放在不同域名下,就可以针对不同的域名创建链接并请求,从而突破限制。但是此技术的滥用也造成了额外的CPU和内存占用,对于服务器来说过多的连接也会造成网络拥堵

HTPP/2

客户端在发送请求时会将每个请求的内容封装成不同的带有编号的二进制帧(Frame),然后将这些帧同时发送给服务端。服务端接收到数据之后,会将相同编号的帧合并为完整的请求信息。同样,服务端返回结果、客户端接收结果也遵循这个帧的拆分与组合的过程。

有了二进制分帧后,对于同一个域,客户端只需要与服务端建立一个连接即可完成通信需求,这种利用一个连接来发送多个请求的方式称为多路复用。每一条路都被称为一个 stream(流)

HTTP/2 在 HTTP/1.1 有几处基本的不同:

  • 二进制协议: HTTP/1.1版本的头部信息是文本,数据部分可以是文本也可以是二进制。HTTP/2版本的头部和数据部分都是二进制,且统称为‘帧’

  • 多路复用: 废弃了 HTTP/1.1 中的管道,同一个TCP连接里面,客户端和服务器可以同时发送多个请求和多个响应,并且不用按照顺序来。由于服务器不用按顺序来处理响应,所以避免了“对头堵塞”的问题。

  • 头部信息压缩: 使用专用算法压缩头部,减少数据传输量,主要是通过服务端和客户端同时维护一张头部信息表,所有的头部信息在表里面都会有对应的记录,并且会有一个索引号,这样后面只需要发送索引号即可

  • 服务端主动推送: 允许服务器主动向客户推送数据

  • 数据流: 由于HTTP/2版本的数据包不是按照顺序发送的,同一个TCP连接里面相连的两个数据包可能是属于不同的响应,因此,必须要有一种方法来区分每一个数据包属于哪个响应。HTTP/2版本中,每个请求或者响应的所有数据包,称为一个数据流(stream),并且每一个数据流都有一个唯一的编号ID,请求数据流的编号ID为奇数,响应数据流的编号ID为偶数。每个数据包在发送的时候带上对应数据流的编号ID,这样服务器和客户端就能分区是属于哪一个数据流。最后,客户端还能指定数据流的优先级,优先级越高,服务器会越快做出响应。

HTTP/3

HTTP/2 由于采用二进制分帧进行多路复用,通常只使用一个 TCP 连接进行传输,在丢包或网络中断的情况下后面的所有数据都被阻塞。

HTTP/2 的问题不能仅靠应用程序层来解决,因此协议的新迭代必须更新传输层。但是,创建新的传输层协议并非易事。传输协议需要硬件供应商的支持,并且需要大多数网络运营商的部署才能普及。

幸运的是还有另一种选择。UDP 协议与 TCP 一样得到广泛支持,但前者足够简单,可以作为在其之上运行的自定义协议的基础。**UDP 数据包是一劳永逸的:没有握手、持久连接或错误校正。**HTTP3 背后的主要思想是放弃 TCP,转而使用基于 UDP 的 QUIC (快速UDP互联网连接)协议。

与 HTTP2 在技术上允许未加密的通信不同,QUIC 严格要求加密后才能建立连接。此外,加密不仅适用于 HTTP 负载,还适用于流经连接的所有数据,从而避免了一大堆安全问题。建立持久连接、协商加密协议,甚至发送第一批数据都被合并到 QUIC 中的单个请求/响应周期中,从而大大减少了连接等待时间。如果客户端具有本地缓存的密码参数,则可以通过简化的握手重新建立与已知主机的连接。

为了解决传输级别的线头阻塞问题,通过 QUIC 连接传输的数据被分为一些流。流是持久性 QUIC 连接中短暂、独立的“子连接”。每个流都处理自己的错误纠正和传递保证,但使用连接全局压缩和加密属性。每个客户端发起的 HTTP 请求都在单独的流上运行,因此丢失数据包不会影响其他流/请求的数据传输。

JavaScript相关

原型与原型链

每个js复杂数据类型(Object Function Array)等都会自带一个 prototype 对象,这个对象就是我们说的原型。

访问器属性__proto__,它指向原型对象,所以不管你是 Function 还是 Object 都会有__proto__属性,这些最终都指向了Object.protoype原型对象,它也是对象,它也有 proto ,它的原型对象指向了 null。

在 JavaScript 中原型是一个 prototype 对象,用于表示类型之间的关系。

prototype只是一个普通的对象。

原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。

JavaScript 万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。

function Fn() {} //构造函数
let f1 = new Fn() //f1是Fn构造函数创建出来的对象
Fn.prototype //是Fn的原型
f1.__proto__ === Fn.prototype //__proto__指向原型对象
Fn.__proto__ === Function.prototype //Fn的__proto__指向它的原型对象
Fn.prototype === Object.prototype //原型对象只是简单的对象,因此它的原型对象是Object

image.png

深入理解js中的this值

this在JS中是一个关键字,不是变量也不是属性名,JS中不允许给this赋值,且其是动态作用域,所取值取决于被谁调用

this指向的是函数运行时所在的环境,也就是说函数在哪个环境中运行,this的值就指向哪个环境,函数的this在调用时绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。为了搞清楚this的指向是什么,必须知道相关函数是如何调用的。

全局上下文

非严格模式和严格模式中的this都是指向顶层对象(浏览器中是window

this === window // true
'use strict'
this === window // true

函数上下文

普通函数调用模式

非严格模式:
var name = 'window'
var doSth = function() {
	console.log(this.name)
}
doSth() // 'window'

需要注意的是,这里输出的是window

  • 一是因为doSth()的调用者是window对象,此时的this会指向window对象。
  • 二是因为使用了ES5操作符varvar会将声明的变量挂载到全局变量中。

让我们换成let看一看会输出什么。

let name = 'window'
let doSth = funtion(){
	console.log(this == window)
	console.log(this.name)
}
doSth() // true, undefined

在这个例子中,let没有给顶层对象window添加属性,因此window.name和window.doSth都是undefined

严格模式

严格模式中的普通函数this则表现不同

// 严格模式
'use strict'
var name = 'window';
var doSth = function(){
    console.log(typeof this === 'undefined');
    console.log(this.name);
}
doSth(); // true,// 报错,因为this是undefined

看过的《你不知道的JavaScript》上卷的读者,应该知道书上将这种叫做默认绑定。 对callapply熟悉的读者会类比为:

doSth.call(undefined);
doSth.apply(undefined);

对象中的函数(方法)调用模式

var name = 'window'
var doSth = function(){
	console.log(this.name)
}
var student = {
	name: 'myName',
	doSth: doSth,
	other: {
		name: 'other',
		doSth: doSth,
		}
}
doSth() // window
student.doSth() // myName
student.other.doSth() // other

var studentDoSth = student.doSth //注意,此时并未被调用
studentDoSth() // 此时被调用,调用者是顶层对象window,this == window

构造函数调用模式

function Student(name) {
	this.name = name
	console.log(this) // {name: 'myName'}
}
var result = new Student('myName')

使用new操作符调用函数,会自动执行一下步骤

  • 创建了一个全新的对象。
    • let obj = {}
  • 这个对象会被执行[[Prototype]](也就是__proto__)链接。
    • obj.__proto__ = Func.prototype
  • 生成的新对象会绑定到函数调用的this
  • 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
    • let res = Func.apply(obj, arguments)
  • 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
    • return typeof res === 'object' ? res || obj : obj

箭头函数调用模式

箭头函数与普通函数的重要区别:

  • 没有自己的thissuperargumentsnew.target绑定。
  • 不能使用new来调用。
  • 没有原型对象。
  • 不可以改变this的绑定。
  • 形参名称不能重复。

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。

var name = 'window'
var student = {
	name: 'myName',
	doSth: function() {
		var self = this
		var arrowDoSth = () => {
			console.log(this.name) // 被绑定,this === self === student
		}
		arrowDoSth()
	},
	arrowDoSth2: () => {
		console.log(this.name) // this === window
	}
}
student.doSth() // myName
student.arrowDoSth2() //window

箭头函数的this是缓存的该箭头函数上层的普通函数的this。如果没有普通函数,则是全局对象(浏览器中则是window)。 也就是说无法通过callapplybind绑定箭头函数的this(它自身没有this)。而callapplybind可以绑定缓存箭头函数上层的普通函数的this。从而间接改变箭头函数的指向

var student = {
    name: 'myName',
    doSth: function(){
        console.log(this.name);
        return () => {
            console.log('arrowFn:', this.name);
        }
    }
}
var person = {
    name: 'person',
}
student.doSth().call(person); // 无法绑定箭头函数的this 'myName'  'arrowFn: myName' 'myName'
student.doSth.call(person)(); // 绑定箭头函数上层this,因此箭头函数的this发生改变 'person' 'arrowFn:' 'person'

重点:

  • 箭头函数永远缓存上层函数的this,注意是上层函数,并非上层对象,当没有上层函数时,会绑定全局对象
  • 无法直接改变箭头函数的this指向

总结

如果要判断一个函数的this绑定,一定要看它的运行时,也就是找到这个函数的调用位置。找到后就可以利用以下四点规则来判断this的绑定对象

  • new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。

  • call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,nullundefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。

  • 对象上的函数调用:绑定到那个对象。

  • 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。

垃圾回收

分代收集

垃圾回收将堆结构分成了新生代和旧生代

新生代

新生代主要用于存放存活时间较短的对象。新生代内存是由两个空间组成,在新生代的垃圾回收过程中主要采用了Scavenge 算法。这是一种典型的牺牲空间换时间的算法。

Scavenge算法的具体实现中,主要采用了Cheney 算法,它将新生代内存一分为二,每一个部分的空间称为semispace

,也就是我们在上图中看见的new_space中划分的两个区域,其中处于激活状态的区域我们称为From

空间,未激活(inactive new space)的区域我们称为To

空间。这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到From

空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To 空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To 空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To 空间。

image.png

当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代代过程为 晋升

  • 对象是否经历过一次Scavenge算法
  • To空间的内存占比是否已经超过25%

image.png

旧生代

可达性

JavaScript中主要的内存管理概念是可达性,简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 固有可达值的基本集合
  • 当前执行的函数,他的局部变量和参数
  • 当前嵌套调用链上的其他函数,他们的局部变量和参数(例如闭包)
  • 全局变量

这些值被称为

  1. 如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的
  • 全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则该对象被认为是可达的,而且他引用的内容也是可达的
// user具有对这个对象的引用
let user = {
  name: "John",
};

image.png

如果user的值被重写了,那么这个引用就没了

image.png

现在John变成不可达的了,因为没有引用,也无法访问到他,因此垃圾回收器会将其标记为垃圾数据并进行回收,然后释放内存

循环引用

function marry(man, woman) {
  woman.husban = man;
  man.wife = woman;
  return {
    father: man,
    mother: woman,
  };
}

let family = marry(
    {
      name: "John",
    },
    {
      name: "Ann",
    },
);

image.png

倘若我们现在移除外部引用

family = null;

则内存状态会变为

image.png

因为可达性的存在,虽然John和Ann依然在互相引用,但是没有任何外部对其的引用,因此这些循环引用也可以被垃圾回收程序成功标记清除

内部算法

这套算法被称为mark-and-swap,其包含以下步骤

  • 垃圾收集器找到所有的根,并标记他们
  • 然后垃圾收集器遍历并标记来自根的所有引用
  • 如此循环便利,直到所有从根部可达的引用都被访问到
  • 所有没有被标记的对象都会被删除

image.png

WeakMap和WeakSet(弱映射和弱集合)

通常,当对象,数组之类的数据结构在内存中时,**他的子元素,如对象的属性,数组的元素都被认为是可达的。 **

let john = {name: "John"};

let array = [john];

john = null;
// 即使用null覆盖了john 的引用,因为array的存在,john也不会被垃圾回收程序回收

WeakMapWeakSet在这方面有根本的不同,它不会阻止垃圾回收机制对作为键的对象的回收

let john = {
  name: "john",
};
let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null;
// john被从内存中删除了

因为WeakMap对键是弱引用,因此当键原有的引用不存在时,将会被垃圾回收工具回收。在上述代码中,删除了john的引用后,该对象只有weakMap 对其的弱引用,垃圾回收工具会忽略这个弱引用,当检查无其他引用时,会将其删除

可以将若引用看作,weakMap本身只是保留对对象的索引,但在垃圾回收工具中并不会当作对该对象的 引用

需要注意的是:WeakMap不支持迭代以及key(),values(),entries() 方法,因此没有办法获取WeakMap的所有键或值

JavaScript数据类型

JavaScript数据类型分为基本数据类型和引用数据类型,其中基本数据类型保存在栈中,引用数据类型保存在堆中

  • 基本数据类型
    • undefiend
    • null
    • number
    • string
    • boolean
    • symbol
    • bigint
  • 引用数据类型
    • object

为什么不都放到栈空间呢

注意! JavaScript引擎需要使用栈来维护程序执行上下文状态,如果所有数据都放在栈空间中,就会影响到上下文切换效率。而且栈空间通常不会设置的很大,基本数据类型通常占用空间较小,但是引用数据类型object占用空间很大,因此引用数据类型被放置到空间更大的堆中

Symbol

Symbol 是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储:

var a = Symbol('1')
var b = Symbol(1)
a.description === b.description // true
var c = Symbol({id: 1})
c.description // [object Object]
var d = Symbol('1')
d == a // false

symbol.for()

Symbol.for(key) 方法会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。

Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol

Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol

var sym = Symbol.for("mario");
sym.toString();
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串

symbol.keyFor()

Symbol.keyFor(sym) 方法用来获取全局 symbol 注册表中与某个 symbol 关联的键。

// 创建一个全局 Symbol
var globalSym = Symbol.for("foo");
Symbol.keyFor(globalSym); // "foo"

var localSym = Symbol();
Symbol.keyFor(localSym); // undefined,

// 以下 Symbol 不是保存在全局 Symbol 注册表中
Symbol.keyFor(Symbol.iterator); // undefined

BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数 BigInt()(但不包含 new 运算符)并传递一个整数值或字符串值。

它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

JavaScript手写题

部分收录自死磕 36 个 JS 手写题

EventEmitter

class EventEmitter {
  constructor() {
    this.events = {}; // 用于存储事件及其对应的回调函数列表
  }

  // 订阅事件
  on(eventName, callback) {
    this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
    this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
  }

  // 发布事件
  emit(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => {
        callback(data); // 执行回调函数,并传递数据作为参数
      });
    }
  }

  // 取消订阅事件
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
    }
  }
  
  // 添加一次性的事件监听器 
  once(eventName, callback) { 
      const onceCallback = data => { 
          callback(data); // 执行回调函数 
          this.off(eventName, onceCallback); // 在执行后取消订阅该事件 
      }; 
      this.on(eventName, onceCallback); 
  }
}

Promise常见API

  • Promise.all() 如果给定的 iterable 中的任意 promise 被拒绝。拒绝原因是第一个拒绝的 promise 的拒绝原因。常用情景是期望n个请求全部成功,且在成功之后进行后续操作,全部成功->执行后续操作
function myPromiseAll(array) {
  return new Promise((resolve, reject) => {
    if (array && typeof array[Symbol.iterator] === "function") {
      let arrayLength = array.length;
      let resultArray = [];
      array.forEach((value, index) => {
        Promise.resolve(value).then(
            (res) => {
              resultArray[index] = res;
              if (resultArray.length === arrayLength) {
                resolve(resultArray);
              }
            },
            (err) => {
              reject(err);
            },
        );
      });
    }
  });
}
  • Promise.allSettled()等待所有的Promise完成,不管是否拒绝
function myPromiseAllSettled(array) {
  return new Promise((resolve, reject) => {
    if (array && typeof array[Symbol.iterator] === "function") {
      let arrayLength = array.length;
      let resultArray = [];
      array.forEach((value, index) => {
        Promise.resolve(value).then(
            (res) => {
              resultArray[index] = res;
              if (resultArray.length === arrayLength) {
                resolve(resultArray);
              }
            },
            (err) => {
              resultArray[index] = err;
              if (resultArray.length === arrayLength) {
                resolve(resultArray);
              }
            },
        );
      });
    }
  });
}
  • Promise.any() 返回第一个兑现的值,当所有都被拒绝时,会以一个包含拒绝原因数组的 AggregateError 拒绝
function myPromiseAny(array) {
  return new Promise((resolve, reject) => {
    let arrayLength = array.length;
    let errList = [];
    if (array && typeof array[Symbol.iterator] === "function") {
      array.forEach((value, index) => {
        Promise.resolve(value).then(
            (res) => {
              resolve(res);
            },
            (err) => {
              errList[index] = new Error(err);
              if (errList.length === arrayLength) {
                reject(new AggregateError(errList));
              }
            },
        );
      });
    }
  });
}
  • Promise.race() 接受一个iterable ,返回一个随着第一个promise敲定的promise,当传入的iterable 为空时,返回的promise会一直保持在待定状态
  • Promise.resolve() 将给定的值转换为一个Promise。如果值本身就是一个Promise,那么该Promise将被返回;如果该值是一个thenable 对象,Promise.resolve()将调用其then()方法及其两个回调函数;否则,返回的 Promise 将会以该值兑现。

寄生组合式继承

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Dog, Animal)

数组去重

ES5:

function unique(arr) {
    var res = arr.filter(function(item, index, array){
        return array.indexOf(item) === index
    })
    return res
}

ES6:

let unique = arr => [...new Set(arr)]

函数curry化

函数有一个length参数,表示形参数量,利用length判断当前记录参数数量是否到达或超过形参数量,超过则执行,否则继续返回curry化函数

function curry(fn, ...a) {
//判断当前保存参数是否超过形参数量
    return a.length > fn.length ?
        //超过则执行
        fn(...a) :
        //否则继续curry
        (...b) => curry(fn, ...a, ...b)
}

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]

递归实现:

function flatten(arr) {
    var result = []
    for (var i = 0, len = arr.length; i < len; i++{
        if (Array.isArray(arr[i]) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result
}

ES6实现:

function flatten(arr) {
    while (arr.some(item => isArray(item))) {
        arr = [].concat(...arr)
    }
    return arr
}

防抖与节流

定义

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)  和 节流(throttle)  的方式来减少调用频率

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

image.png

防抖

function debounce(fn, delay) {
	let timer = null
	return function() {
		if (timer) {
			clearTimeout(timer)
		}
		timer = setTimeout(() => {
			timer = null
			fn.apply(this, arguments)
		}, delay)
	}
}

节流

function throttle(fn, delay) {
	let start = Date.now()
	let timer = null
	return function() {
		const now = Date.now()
		const remaining = delay - (now - start)
		clearTimeout(timer)
		if (remaining <= 0) {
			fn.apply(this, arguments)
			start = Date.now()
		} else {
			timer = setTimeout(() => {
				fn.apply(this, arguments)
			}, remaining)
		}
	}
}

驼峰转换

重点利用了replace函数

pattern

可以是字符串或者一个带有 Symbol.replace 方法的对象,典型的例子就是正则表达式。任何没有 Symbol.replace 方法的值都会被强制转换为字符串。

replacement

可以是字符串或函数。

  • 如果是字符串,它将替换由 pattern 匹配的子字符串。支持一些特殊的替换模式,请参阅下面的指定字符串作为替换项部分。
  • 如果是函数,将为每个匹配调用该函数,并将其返回值用作替换文本。下面的指定函数作为替换项部分描述了提供给此函数的参数。

指定字符串作为替换项

只有当 pattern 参数是一个 RegExp 对象时,$n$<Name> 才可用。如果 pattern 是字符串,或者相应的捕获组在正则表达式中不存在,则该模式将被替换为一个字面量。如果该组存在但未匹配(因为它是一个分支的一部分),则将用空字符串替换它。

"foo".replace(/(f)/, "$2");
// "$2oo";正则表达式没有第二个组

"foo".replace("f", "$1");
// "$1oo";pattern 是一个字符串,所以它没有任何组

"foo".replace(/(f)|(g)/, "$2");
// "oo";第二个组存在但未匹配

指定函数作为替换项

你可以将第二个参数指定为函数。在这种情况下,匹配完成后将调用该函数。函数的结果(返回值)将用作替换字符串。

function replacer(match, p1, p2, /* …, */ pN, offset, string, groups) {
  return replacement;
}

match

匹配的子字符串。(对应于上面的 $&。)

p1, p2, …, pN

如果 replace() 的第一个参数是 RegExp 对象,则为捕获组(包括命名捕获组)找到的第 n 个字符串。(对应于上面的 $1$2 等。)例如,如果 pattern/(\d+)(\w+)/,则 p1\a+ 的匹配项,p2\b+ 的匹配项。如果该组是分支的一部分(例如 "abc".replace(/(a)|(b)/, Replacer)),则不匹配的替代项将为 undefined

offset

原始字符串中匹配子字符串的偏移量。例如,如果整个字符串是 'abcd',而匹配的子字符串是 'bc',那么这个参数将是 1

string

正在检查的原始字符串。

groups

一个捕获组命名组成的对象,值是匹配的部分(如果没有匹配,则为 undefined)。仅在 pattern 包含至少一个命名捕获组时才存在。

参数的确切数量取决于第一个参数是否为RegExp对象,以及它有多少个捕获组。

通常来讲,一对小括号就是一个捕获组

实现驼峰转换

function formatToCamel(str) {
	return str.replace(/\_(\w)/g, (_, group1) => group1.toUpperCase())
}
function formatToLine(str) {
	return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}

new操作符具体干了什么

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function Person(name, age){
    this.name = name;
    this.age = age;
}
const person1 = new Person('Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

image.png

手写new操作符

function myNew(Func, ...args){
    // 操作1: 创建一个新对象obj
    const obj = {}
    // 操作2: 将新创建的对象原型与Func的原型链链接
    obj.__proto__ = Func.prototype
    // 操作3: 奖构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 操作4: 判断返回值类型,如果是对象则直接返回对象,否则返回obj
    return result instanceof Object ? result : obj
}

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

function myInstanceof(left, right) {
    // 这里先用typeof来判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) return false;
    // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {                  
        if(proto === null) return false;
        if(proto === right.prototype) return true;//找到相同原型对象,返回true
        proto = Object.getPrototypeof(proto);
    }
}

TypeScript相关

never,unknown,any,void区别

any:JavaScript转TypeScript的银弹

当我们将某个变量定义为any后,TypeScript将会跳过对这个变量的类型检查

let something: any = 'Hello World!'
something.notExistMethod() // ok!
something.notExistProperty.name() // ok!
something = false //ok

使用场景:

  • 代码迁移:在JS向TS迁移的过程中,可以采用any来快速的推进重构,但这只是一种**临时方案 **,千万不能写成AnyScript
  • 类型确缺失或者补全困难,一般发生在使用了第三方JS编写库的时候,因为没有很好的TS适配,导致我们无法准确定义某个类型,这是可以使用any去暂时规避这类问题

unknown:any的安全替代品

any会跳过所有的TS类型检查,这会为后续代码的维护埋下巨大的安全隐患,为了解决any的问题,TS在3.0版本引入了unknown类型,可以简单理解为类型安全的any

和any一样,任何类型都可以赋值给unknown类型,但不同的是,unknown类型不可以直接赋值给其他非unknown或any类型的对象,并且不可以访问上面的任何属性

let vAny: any = "Hello World!";
let vUnknown: unknown = "Hello World!";

let vNumberForAny: number = vAny; // ok! any可以直接赋值给其它任意类型
let vNumberForUnknown: number = vUnknown; // error! unknown不可以直接赋值给其它非any和unknown类型的对象

vAny.toLocaleLowerCase(); // ok! any可以访问所有的属性
vUnknown.toLocaleLowerCase(); // error! unknown对象不可以直接访问上面的属性

如果想要使用unknown,那就必须先推导出unknown的类型,比如typeof

let vUnknown: unknown = "abc";

// 使用typeof推断出vUnknown的类型是string
if (typeof vUnknown === "string") {
    vUnknown.toLocaleUpperCase(); // ok! 因为能进入这个if条件体就证明了vUnknown是字符串类型!
}

let vNumberForUnknown: number = vUnknown as number; // unknown类型一定要使用as关键字转换为number才可以赋值给number类型

unknown基本可以替代any,所以在使用any的场景,都应该优先使用unknown。使用了unknown后,我们既允许某个对象储存任意类型的变量,同时也要求别人在使用这个对象的时候一定要先进行类型推断。

never

never是TypeScript的底部类型,是一个不存在,不可能发生的类型

never类型只接受never类型,any都不可以赋值给never

let vAny: any = 1;
let vNever: never = vAny; // error! never除了自己谁都不接受!
function test(a: number | string) {
    if (typeof a === "number") {
        console.log(a);
    } else if (typeof a === "string") {
        console.log(a);
    } else {
        let check: never = a; //永远无法到达,因此a的类型为never
    }
}

never类型可以很好的帮助我们在未来添加某一个类型时能够检查到代码的逻辑漏洞

function test(a: number | string | boolean) {
    if (typeof a === "number") {
        console.log(a);
    } else if (typeof a === "string") {
        console.log(a);
    } else {
        let check: never = a; // error! boolean无法赋值给never
    }
}

void

void可以理解为null和undefined的联合类型,它表示空值,一般不用于声明值的类型,更常见的场景是表示某个函数没有返回值

注意与never的区别

function noReturn(): void {
    console.log("hello"); //函数可以正常结束,无返回值
}

function Never(): never {
    while (true) {
    } //函数永远无法结束
}

function error(): never {
    throw new Error("this function will crash");
}

React

说一说React diff的原理

与vue相同,React引入了Virtual DOM的概念,极大的避免了无效的DOM操作,拔高了前端开发的下限,使得我们页面的构建效率得到了提升。而diff算法就是更高效的通过 对比新旧Virtual DOM来找出真正的DOM变化之处

在React中,diff算法主要分为三个层级

  1. web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 一层级的一组子节点,它们可以通过唯一 id 进行区分。

1. tree层级

DOM节点跨层级操作不做优化,只会对相同层级的节点进行比较,**只有删除,创建,没有移动操作 **

image.png

2. component层级

首先会判断组件的类型是否相同

  • 对同类型的组件,按照层级比较虚拟DOM树即可
  • 对不同类型组件来说(如下图)会直接删除该组件并重新构建

image.png

3. element层级

对于同一层级的子节点,通过唯一的key进行对比

当所有节点处于同一层时,React会进行三种操作插入,移动,删除

image.png

  • 插入:新的component类型不在老集合里,是全新的节点,需要对新节点执行插入操作
  • 移动:在老集合里有component类型,且通过对比key值相同,则无需新建或删除节点,只需将旧集合中节点的位置进行移动,更新为新集合中节点的位置即可
  • 删除:老component类型,在新集合里有,但是对应的element不同则不能直接复用,需要执行删除操作,或者老component不在新集合里

image.png

  • index: 新集合的遍历下标。
  • oldIndex:当前节点在老集合中的下标
  • maxIndex:在新集合访问过的节点中,其在老集合的最大下标

操作中只比较oldIndex和maxIndex,规则如下

  • 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
  • 当oldIndex=maxIndex时,不操作
  • 当oldIndex<maxIndex时,将当前节点移动到index的位置

diff过程如下:

  • 节点B:此时 maxIndex=0,oldIndex=1;满足 maxIndex< oldIndex,因此B节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是1
  • 节点A:此时maxIndex=1,oldIndex=0;不满足maxIndex< oldIndex,因此A节点进行移动操作,此时maxIndex= Math.max(oldIndex, maxIndex),还是1
  • 节点D:此时maxIndex=1, oldIndex=3;满足maxIndex< oldIndex,因此D节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是3
  • 节点C:此时maxIndex=3,oldIndex=2;不满足maxIndex< oldIndex,因此C节点进行移动操作,当前已经比较完了

注意:

对于简单列表渲染,不使用key比使用key的性能要高,因为操作DOM的开销是昂贵的

1.加key
<div key="1">1</div>
<div key="1">1</div>
<div key="2">2</div>
<div key="3">3</div>
<div key="3">3</div>
========>
<div key="2">2</div>
<div key="4">4</div>
<div key="5">5</div>
<div key="5">5</div>
<div key="6">6</div>
操作:节点2移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4。 2.不加key
<div>1</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>3</div>
========>
<div>2</div>
<div>4</div>
<div>5</div>
<div>5</div>
<div>6</div>
操作:修改第1个到第5个节点的innerText

Vue相关

Vue样式隔离scope

Vue组件之间没有做到样式隔离,Vue中的样式隔离,是通过scoped 属性来实现的。当在<style>标签上使用scoped属性时.基本原理概括为以下几个步骤:

为当前组件模板的所有DOM节点添加相同的attribute,添加的属性与其他的scope不重复,data属性( 形如:data-v-123)来表示他的唯一性。

  1. 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如.ipt input[data-v-123])来私有化样式
  2. 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性

也是因为这个原因,所以在scoped中尝试选择子组件样式,会因为额外的属性选择器导致无法正确选择

:deep通过改变hash属性选择器的位置,来让vue的样式可以正确的选择到子组件,也就完成的样式穿透

Webpack相关

Webpack的几个关键概念

引用:www.nowcoder.com/discuss/521…

  • Entry(入口):Webpack将从指定的入口文件开始分析和构建依赖关系树
module.exports = {
  entry: './path/to/my/entry/file.js',
};
  • Output(输出):指定Webpack打包后的文件输出路径和文件名
module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};
  • Loader(加载器):Webpack本身只能处理JavaSript,但通过Loader可以加载各种各样的文件处理模块 以下是一些常用的Webpack loader及其作用:
  1. babel-loader:将ES6+代码转换为ES5语法,以便在旧版本的浏览器中运行。
  2. style-loadercss-loader :用于处理CSS文件。css-loader主要负责处理样式文件中的importurl 语句,而style-loader将转换后的CSS模块直接注入到HTML页面中。
  3. file-loaderurl-loader :用于处理图片和其他资源文件。file-loader会为每一个文件生成一个对应的文件,而url-loader将小于设定大小的文件转换为base64编码的URL,减少HTTP请求。
  4. sass-loaderless-loader:用于处理Sass和Less预处理器。它们将Sass和Less代码转换为普通的CSS代码。
  5. postcss-loader:用于为CSS代码添加浏览器兼容性前缀,以确保在不同浏览器上的一致性。
  6. html-loader:用于处理HTML文件,将其中的图片等资源转换为Webpack可以识别的模块。
module.exports = {
  ...
  module: {
    rules: [{ test: /.txt$/, use: 'raw-loader' }],
  },
};
  • Plugin(插件):用于扩展Webpack的功能,可以在打包的不同阶段执行特定的内容。通常我们在Webpack中引入并实例化,然后加入到plugins数组
module.exports = {
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
  • Mode(模式):通过选择development,productionnone之中的一个,来设置mode 参数,可以启用 webpack 内置在相应环境下的优化。其默认值为production
选项描述
development会将DefinePluginprocess.env.NODE_ENV的值设置为development. 为模块和 chunk 启用有效的名。
production会将DefinePluginprocess.env.NODE_ENV的值设置为production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
none不使用任何默认优化选项

Webpack中,什么是代码分离(code splitting)和懒加载(lazy loading)

代码分离是将打包生成的代码文件拆分成多个较小的文件,而不是将所有代码打包到一个文件中

代码分离可以提高初始加载速度,并减少每个页面加载所需的数据量 。通过代码分离,可以按需加载所需要的模块。

懒加载是指在需要时才加载某个模块,而不是在初始加载时就将所有代码一次性加载完毕

懒加载可以根据需要动态的加载模块,只加载当前可见的模块,随着用户与页面交互,再按需加载其他模块

区别

  • 代码分离是将代码文件拆分成较小的文件,其中每个文件可能包含多个模块 。这样做可以在初始加载时减少数据量,但仍然需要一次性加载所需的文件。

  • 懒加载是将页面分成多个模块,在需要时才去加载相应的模块 。这样做可以进一步减小初始加载时间,只加载当前可见的模块,随着用户与页面交互,再按需加载其他模块。

Webpack中的热重载是什么?

热重载(Hot Module Replacement,HMR)是Webpack提供的一项功能,它允许在开发过程中,无需刷新整个页面,即可实时更新修改的模块。

1. 在Webpack配置文件中启用热模块替换。可通过配置devServer.hot选项为true 来启用HMR:

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    ...
    devServer: {
      hot: true,
    },
  };

2. 在入口文件中添加对HMR的支持。在入口文件中,需要添加HMR的逻辑以监听模块的变化,并告诉Webpack如何处理更新。

// index.js

if (module.hot) {
  module.hot.accept();
}

3.配置Webpack插件。HMR需要搭配相应的插件使用,常用的是webpack.HotModuleReplacementPlugin

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  // ...
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // ...其他插件
  ],
};