前端语言
前端语言的基本能力
CSS
1.选择器以及选择器优先级
CSS 样式的优先级是用来确定具有相同样式属性的选择器谁更优先适用的。比如某个元素身上有多个选择器,这些选择器都定义了相同的样式属性,如何决定最终应用哪一个样式呢?这就用到优先级的计算。
CSS 选择器的优先级由四个等级组成,一般写成 a,b,c,d,其中 a,b,c,d 分别代表以下各级优先级的值:
- 内联样式(inline style)的优先级为 1000(注意是 1000 而不是 1)。内联样式是指直接在元素的 style 属性中定义的样式。
- ID 选择器的优先级为 100。
- 类选择器、伪类选择器、属性选择器的优先级为 10。如果具有相同优先级的声明,则后声明的规则将覆盖先声明的规则。
- HTML 标签选择器和伪元素选择器的优先级为 1。
当有多个选择器规则应用到同一个元素上时,每个规则会根据上述等级获取对应的优先级权重,然后进行比较。如果权重相等,则后声明的规则将覆盖先声明的规则。
需要注意的是,选择器本身的复杂度并不影响优先级的计算。只计算选择器中涉及到的选择器的类型、数量或者直接写在 style 属性中的样式。
下面是一个优先级计算例子:
<style>
/* 权重为 1 */
p {
color: red;
}
/* 权重为 10 */
.red {
color: blue;
}
/* 权重为 100 */
#content {
color: green;
}
/* 权重为 1000 */
p[style] {
color: purple;
}
</style>
<div id="content">
<p class="red" style="color: orange">这是一段文本</p>
</div>
在这个例子中,<p> 标签选择器和类选择器 .red 的权重是 10,ID 选择器 #content 的权重是 100,内联样式属性的权重是 1000。而整个例子中使用了以下这些样式属性:
color: red;color: blue;color: green;color: purple;color: orange;
由于其中样式属性的优先级相同,所以样式的实际效果会受 CSS 选择器的优先级影响。具体来说,本例中 p[style] 的优先级大于其它选择器,#content 选择器的优先级大于 p.red 的选择器,最终生效的样式是下面这个:
#content p.red[style] {
color: purple;
}
总结:
- 内联样式权重为 1000
- ID 选择器权重为 100
- 类选择器、伪类选择器和属性选择器权重为 10
- 标签选择器、伪元素选择器权重为 1
-
- 通用选择器的权重为 0
计算优先级时,只要将所有选择器的权重相加即可,然后比较各个规则的优先级大小。
2.更改优先级的方法
使用**!important**属性
3. 盒子模型 两种样式:IE盒模型、标准盒模型
4. 实现盒模型水平居中、垂直居中、水平垂直居中的方法
实现水平居中:
有多种方式可以使 `div` 元素水平垂直居中:
- 使用
flex布局
使用 flex 布局可以轻松实现水平垂直居中,只需要设置父元素为 display: flex 和 align-items: center; justify-content: center 即可。
<div class="container">
<div class="box"></div>
</div>
.container {
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 200px;
height: 200px;
background-color: red;
}
- 使用绝对定位
使用绝对定位可以使子元素相对于父元素居中对齐,可以将子元素设置为 position: absolute,并设置上下左右的位置为 0,以及设置 margin 为 auto。
<div class="container">
<div class="box"></div>
</div>
.container {
position: relative;
width: 100%;
height: 100%;
}
.box {
width: 200px;
height: 200px;
background-color: red;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
- 使用表格布局
将子元素设置为表格单元格,将其父元素设置为表格,可以轻松实现垂直水平居中效果。
<div class="container">
<div class="box"></div>
</div>
.container {
display: table;
width: 100%;
height: 100%;
}
.box {
display: table-cell;
vertical-align: middle;
text-align: center;
background-color: red;
}
4.使用position定位 + transform垂直水平居中
.father {
background-color: lightblue;
width: 300px;
height: 300px;
position:relative;
}
.son {
background-color: blueviolet;
height: 100px;
width: 100px;
position: absolute;
top:50%;
left: 50%;
transform:translate(-50%, -50%);
}
以上四种方法都可以实现 div 元素的居中,具体使用哪一种方法可以根据具体场景选择适合自己的方法。
5.闭包的理解
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。
**作用:**闭包让你可以在一个内层函数中访问到其外层函数的作用域
- 创建私有变量
- 延长变量的生命周期
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
6. new是什么
在JavaScript中,new操作符用于创建一个给定构造函数的实例对象
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'
从上面可以看到:
new通过构造函数Person创建出来的实例可以访问到构造函数中的属性new通过构造函数Person创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
返回对象有用,返回原始值没用
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'
手动实现new操作符
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
7.判断数据类型的方法
在JavaScript中有多种方法可以判断数据类型,以下是常用的方法:
- type of:可以判断出大多数的数据类型,但对于数组和null的判断并不准确。
typeof 123 // 'number'
typeof '123' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
2.instance of:主要用于判断某个对象是否为某个构造函数的实例,不适用于基本数据类型的判断。
const num = new Number(123)
num instanceof Number // true
'123' instanceof String // false,字符串是基本类型而不是对象
3.Object.prototype.toString.call():可以实现精准的判断。
Object.prototype.toString.call(123) // '[object Number]' Object.prototype.toString.call('123') // '[object String]' Object.prototype.toString.call(true) // '[object Boolean]' Object.prototype.toString.call(undefined) // '[object Undefined]' Object.prototype.toString.call(null) // '[object Null]' Object.prototype.toString.call([]) // '[object Array]' Object.prototype.toString.call({}) // '[object Object]'
8.vue组件通讯的方法
在 Vue 中,组件间通信的方式有如下几种:
-
父组件向子组件通信:通过props传递数据,子组件可通过props获取传递的值。
-
子组件向父组件通信:子组件通过$emit触发事件,父组件通过v-on监听事件并获取传递的数据。
-
兄弟组件通信:可通过一个全局事件总线(例如Vue Bus)来实现兄弟组件之间的通信。
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能 this.$bus.$emit('foo') this.$bus.$on('foo', this.handle) -
跨多层级组件通信:使用provide / inject,即父组件通过provide提供数据,子组件通过inject注入数据。
-
使用vuex来实现组件间通信:vuex是一个专门为Vue.js设计的集中式状态管理解决方案,可以将多个组件的数据存储在一个全局对象中,实现组件间的通信。
需要注意的是,不同的情况下应该选用不同的方法实现组件间通信,应根据实际场景来选择最合适的方式,避免数据混乱或者出现不必要的问题。
9.CSS模块化的理解
10.变量提升
变量提升(Hoisting)是 JavaScript 的一种默认行为,即在代码执行之前,变量和函数声明会被提升到当前作用域的顶部,便于程序的执行。变量提升只是JavaScript的一种默认行为,不是JavaScript的语法规则,为了保证代码可读性,能写在前面,就应该写在前面。
console.log(foo); // 输出undefined
var foo = "Hello World";
在上述代码中,console.log(foo)的输出结果为undefined,这是因为变量声明的过程已经被提升到了代码的最上面,但变量赋值的过程并没有被提升,因此foo的值为undefined。
另外,函数也可以提升,例如:
foo(); // 输出Hello World
function foo(){
console.log("Hello World");
}
在上述代码中,函数foo的声明被提升到了代码的最上面,因此在执行foo()之前就已经可以访问到它了,所以输出结果为Hello World。
11.作用域链的理解
作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性
function myFunction() {
let inVariable = "函数内部变量";
}
myFunction();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
上述例子中,函数myFunction内部创建一个inVariable变量,当我们在全局访问这个变量的时候,系统会报错
这就说明我们在全局是无法获取到(闭包除外)函数内部的变量
我们一般将作用域分成:
- 全局作用域
- 函数作用域
- 块级作用域
12.浏览器从输入网址到页面展现的整个过程
浏览器从输入网址到页面展现的整个过程如下:
-
DNS解析:浏览器根据URL获取到服务器的IP地址,进行DNS解析。
-
建立TCP连接:浏览器向服务器发起一个TCP连接请求。
-
发送HTTP请求:浏览器发送一个HTTP请求到服务器,请求数据。
-
服务器响应:服务器响应浏览器的请求,返回需要的数据。
-
解析响应数据:浏览器根据响应类型(HTML,CSS,JavaScript等)对响应数据进行解析。
-
构建DOM树:浏览器根据HTML代码构建DOM树,解析与构建过程中遇到外链CSS和JavaScript标签时,需进行进一步的处理。
-
构建Render树:解析并执行HTML,CSS和JavaScript代码,并基于这些代码构建Render树。(Render树是一种表示显示在浏览器中的文档的树结构,包含了HTML元素以及它们的样式和层级关系)
-
布局render树:根据Render树的结构计算每一个节点的大小、位置等,形成最终的布局。
-
绘制render树:通过计算好的渲染树进行绘制操作
-
浏览器页面渲染完成:整个过程结束,页面呈现在浏览器窗口中。
需要注意的是,虽然以上过程是一个简单的流程,但在实际操作中往往更为复杂,包含了缓存机制、请求优化、并发处理和安全性的考虑等。
13.URL由哪些部分组成
URL(Uniform Resource Locator,统一资源定位符)由以下几个部分组成:
-
协议部分:指定了客户端与服务器之间进行通信时所采用的协议类型,例如HTTP、HTTPS、FTP等。
-
域名部分:指定了服务器的域名或IP地址,用于唯一标识服务器。例如www.example.com 或192.168.1.1。
-
端口号部分:表示客户端需要连接的服务器端口号。如果未指定,默认为80端口(HTTP协议使用的常用端口)。
-
路径部分:指定了服务器上资源文件的路径。例如/index.html或/images/logo.jpg等。
-
查询参数部分:表示客户端向服务器请求的数据,可以包含多个参数,格式是?key1=value1&key2=value2,其中?key1=value1表示一个查询参数。
-
片段定位部分:用于标识在资源中某个位置的一个片段,例如网页中的锚点链接。
这些部分组成的URL形式如下所示:
协议://主机名(IP地址):端口号/路径?查询参数#片段定位
例如:www.example.com:8080/index.html?…
其中,协议部分是必需的,其他部分可以根据需要省略。
14.DNS域名解析的流程
DNS(Domain Name System,域名系统)是用于将域名转换为IP地址的分布式数据库系统。当用户在浏览器地址栏中输入一个网址时,需要进行DNS域名解析,将域名转换为对应的IP地址。DNS域名解析的过程如下:
-
浏览器向本地DNS服务器发送查询请求,询问所需域名的IP地址。
-
如果本地DNS服务器缓存中存在相应记录,则直接返回对应的IP地址;否则,本地DNS服务器向根DNS服务器进行查询。
-
根DNS服务器返回所查询域名所属的顶级域的DNS服务器IP地址(例如.com、.cn等)。
-
本地DNS服务器向所查询域名所属顶级域的DNS服务器进行查询。
-
顶级域DNS服务器返回所查询域名所属的二级域DNS服务器IP地址(例如example.com)。
-
本地DNS服务器向所查询域名所属的二级域DNS服务器进行查询。
-
二级域DNS服务器返回所查询域名的IP地址。
-
本地DNS服务器将查询结果保存到缓存中,以便下次查询时使用,并向浏览器返回查询结果。
-
浏览器在获取到对应IP地址后,向该IP地址发起请求,获取访问所需的网页等资源。
需要注意的是,以上步骤在实际应用中可能存在多级缓存优化,部分DNS记录可能存在TTL,过期需要重新查询等情况。
15.浏览器访问缓存的过程
当浏览器发起请求时,它会先检查本地缓存是否已经存在所请求的资源,如果存在且处于有效期内,则浏览器会直接从缓存中获取该资源,而不会向服务器发送请求。
如果本地缓存不存在请求的资源,或者存在但已经过期,则浏览器会向服务器发送请求,服务器根据请求头信息来判断返回的响应是否可以被缓存。
如果服务器返回的响应可缓存,则浏览器会将返回的响应缓存到本地,并根据响应头信息设置该资源的缓存时间和缓存策略等参数。
当浏览器再次请求该资源时,它会先检查本地缓存中是否有该资源,如果有则会根据缓存策略决定是否使用本地缓存,如使用强制缓存或协商缓存等。
如果浏览器决定使用本地缓存,则它会从本地缓存中获取该资源,如果该资源依然处于有效期内,则浏览器会直接使用缓存,否则会重新向服务器发送请求,获取最新的资源。
当资源过期或被修改时,服务器会返回新的响应,并将新的响应缓存到本地。如果缓存空间不足,则浏览器会根据一定的策略(如LRU)来清除一些缓存资源,以腾出空间来缓存新的资源。
16.浏览器缓存机制是什么
浏览器的缓存机制是指在页面请求和响应的过程中,浏览器会将一些静态资源如图片、JavaScript、样式表等本地保存,下次访问同一页面时可以直接从本地缓存中读取,而不需要重新从服务器请求资源,从而提升访问速度和用户体验。
浏览器的缓存机制主要有两种类型:强缓存和协商缓存。
-
强缓存:通过在响应头中添加Expires或Cache-Control来实现。Expires是过期时间,表示资源在何时过期,需要重新请求服务器;Cache-Control是缓存控制,可以设置max-age来表示资源在多长时间内有效,如果还在有效期内,浏览器会直接使用缓存。
-
协商缓存:通过在响应头中添加Last-Modified或ETag来实现。Last-Modified是资源的最后修改时间,浏览器在下次请求资源时会在请求头中添加If-Modified-Since字段,服务器会找到最后修改时间和If-Modified-Since进行比较,如果未发生改变则服务器返回304状态码,浏览器直接使用缓存;ETag则是根据资源内容生成的唯一字符串,浏览器在下次请求资源时会在请求头中添加If-None-Match字段,服务器会找到新的ETag和If-None-Match进行比较,如果未发生改变服务器返回304状态码,浏览器直接使用缓存。
需要注意的是,浏览器缓存的使用需要根据具体情况进行调整,对于不需要缓存的内容需要通过设置响应头来禁用缓存。另外在开发中,也需要使用一些方法来防止浏览器缓存影响调试效率,如禁用缓存、使用F12工具禁用缓存等。
17.TCP三次握手流程
TCP三次握手是建立一个TCP连接的过程,主要步骤如下:
-
客户端向服务端发送SYN请求报文:客户端(Client)向服务端(Server)请求建立连接,客户端先发送一个SYN(Synchronize Sequence Number)标志的TCP报文段(可以理解为一种数据格式),其中包含一个随机生成的序列号(Sequence Number)作为本次连接的起始序列号,以及一些其他的控制信息,如最大段大小等。
-
服务端返回ACK和SYN的响应报文:服务端收到客户端发出的SYN请求报文之后,需要对其进行确认,故服务端发送一个SYN和ACK(Acknowledgment)标志的TCP报文段,其中SYN用于表示确认客户端发来的SYN报文段,ACK用于表示服务端收到客户端的SYN请求报文段并确认,同时也包含一个服务端随机生成的序列号,并将客户端发来的序列号+1作为其ACK的序号。
-
客户端返回ACK的响应报文:客户端接收到服务端返回的SYN和ACK响应报文之后,需要再次确认,故客户端发送一个ACK标志的TCP报文段,其中ACK用于表示客户端已经收到服务端的SYN和ACK报文段并确认,同时将服务端发来的序列号+1作为其ACK的序号。
四个报文段中,前三个报文段均为标识建立连接的控制信息,而最后一个ACK分组内可以包含业务数据。通过三次握手,建立起TCP连接后双方就可以互相传输数据了。
注意,三次握手确保了双方能够互相收发数据,并且每方都知道对方已经准备好发送和接收数据。但并不能保证双方之间所有数据包都能够正确传输,仍然需要后续的确认/应答机制进行校验和重传,来保证数据的准确性和完整性。
17.TCP的四次挥手过程
TCP (传输控制协议) 通信的四次挥手如下:
-
主动方(Client)发送 FIN 报文:主动方发送一个 FIN 报文指明自己已经没有数据要发送了。
-
被动方(Server)收到 FIN 报文后发送 ACK 报文:被动方接收到 FIN 报文后,发送 ACK 报文表示已收到主动方的请求。此时,被动方服务器会进入 CLOSE_WAIT 状态,等待最后的数据传输完成。
-
被动方(Server)发送 FIN 报文:被动方在数据传输完成后,也会发送一个 FIN 报文。这个 FIN 报文表示被动方已经没有数据要发送了。
-
主动方(Client)收到 FIN 报文后发送 ACK 报文:主动方收到 FIN 报文后,发送 ACK 报文表示已经收到了被动方的 FIN 报文。此时,主动方会进入 TIME_WAIT 状态,等待 2MSL 之后再关闭连接。
在进行四次挥手的过程中,主动方发送 FIN 报文后,被动方可以立即发送 ACK 报文来确认收到了 FIN 报文,但被动方发送 FIN 报文后,主动方必须先发送 ACK 报文来确认收到了 FIN 报文,然后才能发送 FIN 报文。
四次挥手过程的目的是为了双方都能确认连接已经关闭。在最后一次 ACK 报文被发送后,双方的连接将正式关闭。
需要注意的是,四次挥手过程只是用于终止已经建立的连接。建立连接时,客户端和服务端需要进行三次握手的过程。
18.为什么不能采用两次握手
TCP协议采用三次握手来建立连接的机制,其原因有以下几点:
-
可靠性:在TCP协议中,三次握手建立连接的机制可以确保连接的可靠性。如果采用两次握手建立连接的话,由于无法确认另一端是否真的已经准备好发送和接收数据,因此可能会出现连接建立异常或者数据不完整的情况,从而影响数据传输的正常进行。
-
防止历史连接的影响:在TCP协议中,三次握手建立连接的机制可以防止历史连接对当前连接的影响。如果采用两次握手建立连接,由于无法确认另一端是否已经关闭了历史连接,从而可能会出现历史连接对数据传输造成的影响。
-
防止黑客攻击:在TCP协议中,三次握手建立连接的机制可以防止黑客攻击。如果采用两次握手建立连接,黑客可以通过发送伪造的连接请求从而突破系统的安全措施,从而进行非法的访问和攻击。
因此,TCP协议采用三次握手的机制来建立连接,可以保证连接的可靠性、防止历史连接的影响,以及防止黑客攻击等。
18.TCP和UDP的区别
tcp连接就像打电话,两者之间必须有一条不间断的通路,数据不到达对方,对方 就一直在等待,除非对方直接挂电话。先说的话先到,后说的话后到,有顺序。
udp就象寄一封信,发信者只管发,不管到。但是你的信封上必须写明对方的地址。 发信者和收信者之间没有通路,靠邮电局联系。信发到时可能已经过了很久,也可 能根本没有发到。先发的信未必先到,后发的也未必后到。
19.如何检测SYN攻击
SYN攻击是一种常见的DoS攻击,其主要原理是攻击者发送大量的SYN请求,但不进行后续的ACK确认操作,从而占用服务器的资源,导致正常请求无法响应。检测SYN攻击可以采用以下方法:
-
监控网络流量:通过对服务器上的网络流量进行监控,可以发现是否存在异常的SYN请求。如果发现SYN请求的数量远远高于正常情况下的请求量,就有可能存在SYN攻击。
-
监听端口状态:通过在服务器上监听端口状态,可以发现是否存在大量的半连接状态。如果存在大量的半连接状态,就有可能存在SYN攻击。
-
IP过滤:可以通过使用IP过滤软件和硬件来限制来自不信任IP地址的SYN请求。这样可以防止攻击者通过多个IP地址发起SYN攻击。
-
加入SYN cookie防范:SYN cookie是一种防范SYN攻击的技术手段,其主要原理是在服务器上加入一个cookie,并将cookie的值作为SYN响应的一部分返回给客户端。如果客户端能够正确解析SYN响应并返回相应的ACK,则证明客户端是合法的。如果客户端无法处理SYN响应,则证明其可能是攻击者,服务器可以拒绝其连接请求。
-
设置TCP最大半连接数:可以通过设置TCP最大半连接数来限制服务器上允许存在的半连接数。这样可以有效地防止SYN攻击。
综上所述,检测SYN攻击需要对网络流量进行监控、监听端口状态、采用IP过滤和SYN cookie等技术手段,并且需要建立一套完善的防范机制来防范SYN攻击。
20.HTTPs如何保证安全
HTTPS(HyperText Transfer Protocol Secure)是一种安全的HTTP协议,通过使用SSL/TLS协议对HTTP的传输过程进行加密和认证,从而保证数据传输的安全性和完整性。HTTPS能够保证安全的主要原因有以下几点:
-
数据加密:HTTPS使用SSL/TLS协议对HTTP的数据进行加密,使得数据在传输过程中无法被窃取和篡改。而且SSL/TLS协议使用非对称加密和对称加密的组合方式来对数据进行加密,从而保证了数据的机密性。
-
客户端/服务器身份认证:HTTPS使用数字证书来对服务器和客户端进行身份认证,从而避免了中间人攻击等安全问题。数字证书包括公钥、证书申请者信息、证书颁发者信息等,可以有效地保证通信双方的身份合法性。
-
防止篡改:HTTPS使用数字签名来验证数据的完整性,防止数据在传输过程中被篡改。
-
防御恶意攻击:HTTPS还包含一些安全机制,如CSRF保护、XSS防范等,可以有效地防御一些Web应用程序的安全漏洞和恶意攻击。
总之,HTTPS通过使用SSL/TLS协议对数据进行加密和身份认证、数字签名保证数据的完整性、各种安全机制防御恶意攻击等手段,可以有效地保证数据在传输过程中的安全性,为用户提供了更安全、可靠的网络应用服务。
21.HTTPS的加密过程/HTTPS建立连接的过程
HTTPS采用SSL/TLS协议对HTTP的数据进行加密和认证。SSL/TLS协议使用非对称加密和对称加密的组合方式来对数据进行加密,加密过程主要包括以下几个步骤:
-
客户端发送请求:客户端向服务器发送加密通信请求,协商使用的加密套件(包括加密算法、密钥长度等)。
-
服务器响应请求:服务器返回自己的数字证书以及协商使用的加密套件。
-
客户端验证服务器:客户端使用内置的证书颁发机构列表来验证服务器端的数字证书,确认身份合法性。
-
客户端生成随机密钥:客户端生成一个随机密钥(称为"对称密钥"或"会话密钥"),用于后续的对称加密通信。
-
服务器使用公钥加密密钥:服务器使用数字证书中包含的公钥来对生成的随机密钥进行加密,确保只有客户端能够解密。
-
客户端使用私钥解密密钥:客户端使用浏览器内置的私钥解密服务器发来的密钥。
-
客户端向服务器发送加密请求:客户端使用解密后的随机密钥来对请求报文进行对称加密,然后将加密后的请求报文发送到服务器端。
-
服务器使用密钥解密请求:服务器使用同样的随机密钥来对客户端发送的请求报文进行解密,得到原始请求。
-
服务器发送响应:服务器使用同样的方式加密响应报文并发送到客户端。
总之,HTTPS采用SSL/TLS协议对数据进行加密和认证的过程中,客户端和服务器通过采用非对称加密和对称加密的组合方式来达到安全通信的目的,从而可以保证安全的数据传输。
22.cookie,session,localstorage,sessionstorage有什么区别
cookie、session、localStorage和sessionStorage都是用来在客户端存储和处理数据的技术,但它们之间有以下差别:
-
cookie:是由服务器生成的键值对,通过HTTP响应设置到客户端浏览器上,客户端浏览器会将其存储并在每次发出HTTP请求时自动携带。可以通过设置过期时间来控制其存储时间,不同的浏览器支持的大小也不一样。cookie的作用是在浏览器与服务器之间维持状态,常用于用户登录状态的维护。
-
session:也是一种用于维持状态的技术,但是它的实现与cookie不同。session数据是存储在服务器端的,服务器在创建session并将其唯一标识(称为session ID)返回给客户端浏览器,浏览器存储这个ID并在每次请求时携带给服务器,服务器根据这个ID来获取session数据。与cookie相比,session的存储量更大,安全性更高,但需要占用服务器资源,还需要解决session ID劫持等问题。
-
localStorage:是HTML5引入的客户端存储技术,常用于存储Web应用的状态和用户的个人设置。localStorage只能存储字符串类型的数据,大约可以存储5MB左右的数据,且不受浏览器关闭的影响。
-
sessionStorage:与localStorage相似,但是其数据仅在浏览器会话期间(也就是窗口关闭前)有效,也只能存储字符串类型的数据。
总之,cookie、session、localStorage和sessionStorage都有其特点和应用场景,需要根据实际需求来选择和使用。
23.虚拟dom是什么? 原理? 优缺点?
虚拟DOM(Virtual DOM)是一个JavaScript对象,它是对真实DOM对象的抽象表示。它是由React框架首次提出,用于解决频繁的DOM操作引起的性能问题。
虚拟DOM的原理如下:
-
组件状态(state)发生了改变。
-
React会根据新的状态生成一棵虚拟DOM树。
-
比较新旧两棵虚拟DOM树的差异,找出需要改变的部分。
-
仅对需要改变的部分进行真正的DOM操作,更新视图。
虚拟DOM的优点:
-
性能优化:相比较直接操作DOM,虚拟DOM的更新操作更加高效,可以减少大量重绘和重排等DOM操作,提升应用的性能表现。
-
渲染速度:虚拟DOM使用Diff算法在内存中对比新旧DOM树,只更新需要更新的节点,而不是全部重绘重排。
-
跨平台:虚拟DOM可以在不同平台上使用,比如浏览器、Node.js、原生应用开发等。
虚拟DOM的缺点:
-
学习成本:虚拟DOM的使用需要学习新的语法和相关概念,需要一定的学习成本。
-
内存占用:虚拟DOM需要维护一个虚拟DOM树,需要占用一定的内存空间。
-
实现复杂:虚拟DOM本身的实现和维护是一个较为复杂的过程,需要在高效性和可维护性之间寻找平衡。
在实际应用中,虚拟DOM的优缺点需要在具体场景下进行综合考虑和取舍。
24.常见http状态码
HTTP状态码是由3个数字构成的代码,表示客户端HTTP请求的处理结果。HTTP状态码的分类有5个范围,每个范围表示不同的含义。常见的HTTP状态码有以下几种:
1xx(信息状态码):表示请求已经被接收,需要继续处理。
- 100(Continue):服务器已接收到请求头,并且客户端应该继续发送请求主体。
2xx(成功状态码):表示请求已经被成功接收、理解、接受。
-
200(OK):请求已成功,请求所希望的响应头或实体都随着该状态返回。
-
201(Created):请求成功并创建了一个新的资源。
3xx(重定向状态码):表示需要客户端采取进一步的操作才能完成请求。
-
301(Moved Permanently):请求的资源已永久移动到新位置,将来的请求应使用新的URI。
-
302(Found):请求的资源临时从不同的URI响应请求,但将来可能还会使用原来的URI。
-
304(Not Modified):客户端发送了一个带条件的GET请求,并且该资源已被缓存,该响应响应于判断该条件为真。
4xx(客户端错误状态码):表示客户端请求存在问题,服务器禁止了该请求。
-
400(Bad Request):请求无效,服务器无法处理。
-
401(Unauthorized):请求需要身份验证。
-
403(Forbidden):服务器拒绝了客户端的请求。
-
404(Not Found):请求的资源无法被找到。
5xx(服务器错误状态码):表示服务器在处理客户端请求时发生了错误。
-
500(Internal Server Error):服务器遇到了意外情况,不能完成客户请求。
-
503(Service Unavailable):服务器当前无法处理请求,可能是由于过载或维护。
25.vue的数据绑定机制是如何实现的
Vue的数据绑定主要基于以下两个机制:
- 数据劫持
Vue使用了ES5的Object.defineProperty()方法来实现数据劫持,通过此方法拦截对属性的读取和修改,从而实现数据的响应式。当一个数据被设置为响应式时,Vue会在内部创建一个Dep对象,用于存放所有依赖于该数据的Watcher对象。当该数据变化时,会触发所有依赖该数据的Watcher对象的update()方法,从而更新视图。
- 模板编译
Vue的模板编译器会将视图模板解析成一棵抽象语法树(AST),并根据AST生成render函数,该函数相当于一个VNode的工厂函数。VNode是一种虚拟DOM节点的抽象,它描述了一个节点的所有属性信息,包括节点类型、属性、子节点等。当数据发生变化时,Vue会重新执行render函数,比较新旧VNode树的差异,并仅对差异部分进行重绘操作,从而实现高效的视图更新。
综上所述,Vue的数据绑定机制通过数据劫持和模板编译相结合的方式来实现数据的响应式和视图的高效更新,从而带来了更加流畅的用户体验。
26.网站首页有大量的图片,加载很慢,如何去优化呢?
网站首页加载慢的原因可能有很多,其中图片占据了很大一部分。以下是一些图片优化的建议:
-
压缩图片大小:使用压缩工具(如TinyPNG)压缩图片,减小图片的文件大小,从而减小加载时间。
-
减少图片数量:尽量避免在首页放置过多的图片,仅保留必要的图片。
-
使用合适的图片格式:选择合适的图片格式,例如对于图像,常用的格式为jpg, png格式。
-
图片懒加载:只有当浏览器滚动到图片可见的地方才加载图片。
-
CDN加速:使用CDN(内容分发网络)将图片部署到离用户较近的节点上,从而提高图片加载的速度。
-
图片预加载:使用预加载技术,提前加载页面中需要的图片,当用户访问时,图片可以快速的从本地加载出来。
-
图片瘦身处理:有一些工具可以对图片进行瘦身处理,减小图片的体积,从而加快图片的加载速度。
综上所述,对于网站加载缓慢,特别是图片加载缓慢的问题,可以使用图片压缩,减少图片数量,合适的图片格式,图片懒加载等方式来优化。
27.双飞翼布局和圣杯布局
双飞翼布局和圣杯布局都是三栏布局中比较常用的方式,它们有以下特点:
-
有定宽的中间列,两侧列宽度自适应。
-
中间列在文档流中排列最前面。
-
左右两栏可以任意调整顺序。
-
可以保证三栏的高度相等。
下面分别介绍一下双飞翼布局和圣杯布局的实现方法。
- 双飞翼布局
双飞翼布局的核心是使用外层div容器撑开整个页面,中间的内容通过一个子div实现,左右两边通过负margin和相对定位去定位左右两栏。
HTML结构:
<div class="container">
<div class="main">main content</div>
<div class="left">left</div>
<div class="right">right</div>
</div>
CSS样式:
.container {
width: 100%;
}
.left,.right {
width: 200px;
float: left;
position: relative;
margin-left: -200px;
}
.left{
left: -100%;
}
.right{
right: -100%;
}
.main {
margin-left: 200px;
margin-right: 200px;
background-color: lightgray;
}
- 圣杯布局
圣杯布局也是使用子div作为中间内容容器,左右两栏同样采用负margin和相对定位进行定位,唯一不同的是左右两边的顺序,因为要保证中间内容在文档流中排列最前面,所以左侧的列要放在中间列之前。
HTML结构:
<div class="container">
<div class="main">main content</div>
<div class="left">left</div>
<div class="right">right</div>
</div>
CSS样式:
.container {
width: 100%;
padding-left: 200px;
padding-right: 200px;
}
.left,.right {
width: 200px;
float: left;
position: relative;
margin-left: -100%;
}
.left{
left: -200px;
}
.right{
right: -200px;
}
.main {
float: left;
width: 100%;
background-color: lightgray;
}
以上就是双飞翼布局和圣杯布局的实现方法。
28.深浅拷贝
JavaScript中的深拷贝和浅拷贝是针对复杂数据类型中引用类型数据的复制方式而言的。
-
浅拷贝:复制对象引用,新的对象指向原始对象,当原始对象发生改变时,新的对象也会发生改变。
-
深拷贝:将对象从堆中完整地复制一份到栈中,新的对象和原来的对象是完全独立的,当原始对象发生改变时,新的对象不会发生改变。
浅拷贝的实现方式有:
-
Object.assign()方法。它可以将一个或多个源对象的属性复制到目标对象中,如果遇到同名属性,后者会覆盖前者。
-
Array.prototype.concat()方法。它可以将一个或多个数组合并成一个新数组。
-
展开运算符 "..."。它可以将一个数组或对象展开成多个参数或多个元素。
深拷贝的实现方式有:
-
递归拷贝。递归遍历原始对象的每一层,对每一个属性进行复制。
-
JSON.parse()和JSON.stringify()方法。将对象序列化成JSON字符串再反序列化回来,这种方式只能处理能够被JSON序列化的对象。
需要注意的是,使用以上方法可能存在一些问题,例如对象循环引用、Function类型的属性、原型链上的属性等情况,需要特殊处理。所以,在实际开发中,我们需要根据具体的场景选择合适的方式,对于复杂数据类型的拷贝,建议使用专业的库,比如lodash、jquery等库中提供的深拷贝方法。
实现深拷贝的思路是递归遍历对象和数组的每一个属性或元素,对所有引用类型变量创建一个新的指针或引用。下面是手动实现深拷贝的一种方式:
function deepClone(target) {
// 如果是值类型或 null,则直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 根据 target 的类型初始化一个空对象或数组
const newObj = Array.isArray(target) ? [] : {};
// 遍历 target 的所有属性或元素
for (let key in target) {
// 如果 target 对象当前属性或元素是自身属性,跳过当前循环
if (!target.hasOwnProperty(key)) {
continue;
}
// 如果 target 对象当前属性或元素是引用类型,递归调用 deepClone 函数进行深拷贝
newObj[key] = typeof target[key] === 'object' && target[key] !== null ?
deepClone(target[key]) :
target[key];
}
return newObj;
}
这个函数可以接受任意类型的对象或数组作为参数,返回该对象或数组的深拷贝副本。需要注意的是,这种方式可能存在一些问题,例如对象循环引用时可能导致函数爆栈,因此在实际开发中,建议使用专业的库进行深拷贝操作。
29.css的flex属性
Flex布局是CSS3新增的一种布局模式,通过flex属性可以轻松地实现弹性布局,使得元素间可以自由灵活地伸缩。flex布局是基于容器-项目的模型,容器内包含一组项目,可以通过设置容器的样式来影响其内部项目的布局方式。
常用的flex属性包括以下几个:
-
display:设置容器为flex布局。通过设置display属性值为"flex",可以将父级容器变为flex容器。
-
flex-direction:定义了项目在主轴上的排列方式。默认值为"row",表示从左往右排列。其他取值包括"column"(从上到下排列)、"row-reverse"(从右往左排列)和"column-reverse"(从下到上排列)。
-
justify-content:定义了项目在主轴上的对齐方式。默认值为"flex-start",表示左对齐。其他取值包括"center"(居中对齐)、"flex-end"(右对齐)、"space-between"(两端对齐,项目间间隔相等)、"space-around"(每个项目两侧间隔相等)等。
-
align-items:定义了项目在交叉轴上的对齐方式。默认值为"stretch",表示拉伸对齐。其他取值包括"center"(居中对齐)、"flex-start"(顶部对齐)、"flex-end"(底部对齐)和"baseline"(基线对齐)。
-
align-content:定义了多行项目在交叉轴上的对齐方式。默认值为"stretch",表示拉伸对齐。其他取值包括"center"(居中对齐)、"flex-start"(顶部对齐)、"flex-end"(底部对齐)和"space-between"(两端对齐,项目间间隔相等)、"space-around"(每个项目两侧间隔相等)等。
-
flex-wrap:定义了项目是否换行。默认值为"nowrap",表示不换行。其他取值包括"wrap"(换行,第一行在上面)和"wrap-reverse"(换行,第一行在下面)。
-
flex-flow:是flex-direction和flex-wrap两个属性的缩写。
-
flex:是flex-grow、flex-shrink和flex-basis三个属性的缩写。flex-grow表示项目的放大比例,flex-shrink表示项目的缩小比例,flex-basis表示在分配多余空间之前,项目占据的主轴空间(默认值为0)。
Flex布局具有以下优点:
-
等分布局。通过设置flex属性可以将容器内的项目按照一定比例分配空间,实现等分布局。
-
自适应布局。当容器尺寸变化时,容器内部的项目可以根据一定的规则自适应布局。
-
方便的对齐方式。Flex布局提供了多种对齐方式,可以实现多种布局效果。
总之,Flex布局是一种灵活而强大的布局模式,可以实现很多常见布局需求,更好地满足不同场景下的开发需求。
30.实现一个节流函数? 如果想要最后一次必须执行的话怎么实现?
节流函数可以控制函数的触发频率,常见的实现方式有时间戳和定时器两种。其中时间戳方式是指限制函数触发的间隔时间,直到该时间间隔内没有函数被触发再执行处理函数;而定时器方式则是每隔一段时间执行一次函数,也就是让函数在固定的时间段内被反复触发。
下面是一个基于定时器的实现方式:
function throttle(fn, wait) {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, wait);
}
};
}
这个函数接受一个处理函数 fn 和一个时间间隔 wait 作为参数,并返回一个新的函数。该函数内部定义了一个变量 timer,记录定时器的状态。当执行 throttle 返回的新函数时,如果定时器不存在,则设置一个定时器,并在 wait 时间之后执行 fn,并清除定时器;否则跳过当前循环,继续等待定时器时间到。
实现最后一次必须执行时,需要根据时间戳方式实现,即直到等待时间结束才立即执行。具体实现如下:
function throttle(fn, wait, immediate) {
let timer = null;
let lastTime = 0;
return function() {
const now = Date.now();
if (immediate && !lastTime) {
lastTime = now;
fn.apply(this, arguments);
} else if (now - lastTime > wait) {
lastTime = now;
fn.apply(this, arguments);
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
lastTime = Date.now();
timer = null;
}, wait - (now - lastTime));
}
};
}
这个函数新增了一个布尔值参数 immediate,表示是否立即执行;同时修改了定时器部分的逻辑,使用时间戳方式实现。如果 immediate 为 true,则在第一次触发时立即执行 fn,否则要等待 wait 时间后才执行函数。
31.vue的computed和watch的区别
Vue.js 中的 computed 和 watch 都是观察与响应 Vue 实例的数据变化,但是它们针对的数据类型和响应方式不同。
computed 计算属性
计算属性是用来监听并响应 Vue 实例中数据的变化,其本质是一个函数,会根据依赖的响应式数据的值进行计算,返回计算结果。详细特点如下:
- 计算属性基于它们的依赖进行缓存,即只有当计算属性的相关依赖发生改变时才会重新计算,未改变时直接返回缓存结果,所以计算属性是具有记忆功能的。
- 计算属性默认情况下只有 getter 方法,需要自己设置 setter 方法才能支持双向数据绑定。
- 计算属性本质是在输出结果前所进行的一个计算操作,不会修改原始数据。
watch 监听器
让我们可以在数据变化时执行异步或开销较大的操作,并且比起计算属性可以做更多的事情,包括执行异步操作、操作计时器等等。详细特点如下:
- 更加复杂、更有自由度,内部对被监听的属性进行操作都有一定的灵活性。
- 一个属性的变化需要经过 watch 一个时间垫的手续才会被触发,相对较慢。
- watch 能执行携带参数,并且能执行一些函数防抖操作。
- 使用 watch 监听数据变化时代码通常比计算属性代码可读性差些。
从实现上看,computed 本质是使用 Object.defineProperty() 函数在 Vue 实例上定义一个新的计算属性,并且依赖于响应式数据;而 watch 本质上是通过调用 Vue 实例的 $watch 方法来监听数据变化。
在使用上,应优先选用 computed ,只有当 watch 能够比 computed 更好地完成工作时,才使用 watch。通常情况下,计算属性适用于多个组件统一使用的数据模型变化的监听,并根据模型变化计算得出具体的组件渲染数据;而 watch 则适用于一些逻辑上复杂的业务场景中,满足需要对某个数据多个维度进行变化后的操作需求。
32.let var const 有什么区别
let、var、const 都是 JavaScript 中声明变量的关键字,它们有以下区别:
- 变量提升
在使用 var 声明变量时,变量会被提升到函数的作用域或全局作用域的顶部,这就意味着可以在变量声明之前访问到该变量。而使用 let 和 const 声明变量时,变量是不存在提升的。因此,在使用 let 和 const 声明的变量之前必须先进行声明,否则会报错。
- 作用域
var 声明的变量存在函数作用域,let 和 const 声明的变量存在块级作用域。块级作用域指的是一对花括号 {} 内的范围,例如:if 语句、for 循环等。在块级作用域内使用 let 和 const 声明的变量不会泄漏到全局作用域,也不会引起变量覆盖问题。
- 可变性
使用 var 声明的变量存在变量提升和函数作用域的特性,同时也可以被重新赋值和修改。而使用 let 声明的变量和 const 声明的常量(不可修改)均不能被重新定义,而且在第一次赋值后也不能被重新赋值。
- 临时性死区
使用 let 和 const 声明的变量存在“临时性死区”,即在变量被声明之前,使用该变量会报错。这是因为变量的声明被限制在块级作用域或函数作用域中,并且不存在变量提升。而 let 和 const 的区别在于,对于 const 声明的变量,必须在声明同时进行初始化,而且初始化后的变量值不能被修改。
总的来说,使用 let 和 const 声明变量可以避免常见的 JavaScript 变量声明问题,如变量提升和作用域问题。而且在代码中使用 const 声明常量可以使代码更加安全和易于维护。
33.vue原理(手写代码,实现数据劫持)
Vue 的核心是一个响应式的数据绑定系统,它采用了数据劫持的方式,通过 Object.defineProperty() 来劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调函数。以下是手写实现数据劫持的代码:
// 定义一个 Observer 类,实现数据劫持
class Observer {
constructor(data) {
this.walk(data);
}
// 遍历对象的所有属性,进行数据劫持
walk(data) {
// 判断数据是否为对象类型,如果不是则不能劫持
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
// 定义响应式数据
defineReactive(obj, key, val) {
// 创建 dep 实例,用于收集 watcher
const dep = new Dep();
// 递归对子属性进行数据劫持
this.walk(val);
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 将 watcher 添加到订阅者列表中
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 通知所有订阅者进行更新
dep.notify();
}
});
}
}
// 定义一个 Dep 类,用于管理订阅者列表
class Dep {
constructor() {
this.subs = [];
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub);
}
// 通知所有订阅者进行更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
// 定义一个 Watcher 类,用于在订阅者列表中订阅更新
class Watcher {
constructor(obj, key, cb) {
// 记录当前 watch 实例所对应的对象、属性名和回调函数
this.obj = obj;
this.key = key;
this.cb = cb;
// 将 Dep.target 设置为当前 watch 实例,以便于添加 watcher 时进行订阅
Dep.target = this;
// 读取属性值,触发 getter,将当前 watcher 添加到 dep 的订阅列表中
this.value = obj[key];
// 置空 Dep.target,防止重复添加
Dep.target = null;
}
// 订阅者的更新回调函数,进行数据更新
update() {
const newValue = this.obj[this.key];
if (this.value !== newValue) {
this.value = newValue;
this.cb(newValue);
}
}
}
// 使用
const data = {
name: "张三",
age: 18,
hobby: {
sports: "篮球"
}
};
// 响应式数据劫持
new Observer(data);
// 添加订阅者,触发响应式更新
new Watcher(data, "hobby", val => {
console.log(`我的爱好是${val.sports}`);
});
data.hobby.sports = "足球"; // 输出:我的爱好是足球
以上代码中,Observer 类用于实现对数据的劫持和监听,Dep 类用于收集订阅者和发布更新消息,Watcher 类用于实现具体的订阅和响应更新。待数据变动时,触发 setter 时将通知所有订阅者更新。这就是 Vue 响应式数据绑定的核心实现原理。
34.uglify原理的是什么
Uglify 是一个 JavaScript 压缩工具,用于压缩 JavaScript 代码,减小代码体积,提高网页的加载速度。Uglify 能够删除不必要的空格和注释、对变量名进行简单的重命名、合并代码等操作。其实现原理主要分为以下几个步骤:
- 词法分析
Uglify 把原始的 JavaScript 代码转换成一个个 token,这个过程叫做词法分析。词法分析器会将 JavaScript 代码拆分成最小的语法单元 (token):关键字、标识符、运算符、符号、数字、字符串等。它使用正则表达式来匹配这些 token,并生成对应的语法树。在这个过程中,Uglify 会保留注释、字符串等信息。
- 语法分析
词法分析器把代码转换成 token 后,语法分析器会将 token 组合成一个个语句,形成一个抽象语法树(AST)。这个过程叫做语法分析。在这个过程中,Uglify 会移除不必要的语句,简化代码结构,比如删除空语句、删除未使用的变量、删除未用到的函数等。
- 代码优化
Uglify 会对代码进行一些优化,例如将一些常量表达式求值,将多个语句合并到一起,删除不必要的分号等。这个过程叫做代码优化。
- 代码生成
代码优化之后,Uglify 会按照一定格式将代码重新生成为字符串。在这个过程中,Uglify 会删除多余的空白符和注释,并对变量名、函数名进行简单的重命名等。最终生成的压缩代码,可以更加高效地传输和执行。
Uglify 的压缩过程是一个循序渐进的过程,每一步都对代码进行了必要的处理,保证了代码压缩的效果。虽然在压缩过程中可能会损失一些信息,例如注释、代码可读性等,但是这些都是可以通过配置进行控制的。
35.说下vue的keep alive
Vue 的 keep-alive 是一个抽象组件,它可以将内部的组件进行缓存并在需要时直接渲染缓存的组件,提高了组件的重用性和渲染效率。当一个组件被包裹在 keep-alive 组件内部时,这个组件就可以被缓存下来,当组件再次渲染时,会直接从缓存中取出组件进行渲染,避免了重新创建和销毁组件的开销。
KeepAlive 组件本质上是一个抽象组件,它并不会渲染任何元素,它会把它的第一个子组件缓存起来。当第二次渲染时,直接从缓存中获取子组件并进行渲染。如果第二次渲染时,缓存中不存在对应的子组件,则会使用普通的渲染流程,创建并渲染新的子组件。
KeepAlive 组件的使用非常简单,只需要将需要被缓存的组件包裹在它的内部即可。比如:
<template>
<div>
<keep-alive>
<component :is="componentName"></component>
</keep-alive>
</div>
</template>
在这个例子中,使用了动态组件,根据 componentName 的值来动态渲染对应的组件,并将组件包裹在 keep-alive 组件内,使组件被缓存起来。
除了包裹在组件中使用之外,KeepAlive 还可以在路由中使用,实现页面缓存,通过设置路由的 meta 属性 keepAlive 为 true 来启用页面缓存,例如:
const routes = [
{
path: '/detail/:id',
name: 'Detail',
component: Detail,
meta: {
keepAlive: true
}
}
]
需要注意的是,KeepAlive 缓存的组件会被保存在内存中,如果缓存的组件过多,会占用大量内存资源,所以需要根据实际情况合理使用 KeepAlive。另外,由于 KeepAlive 是一个抽象组件,它不会保持任何状态或者临时数据。如果需要缓存的组件需要保持一些状态或临时数据,需要像普通组件一样使用 Vuex 或者 props 等方式进行状态管理。
36.什么是立即执行函数
立即执行函数(Immediately-invoked function expression,IIFE)是一种 JavaScript 函数执行方式,其特点是函数定义后立刻执行,然后立即销毁,不会在全局作用域中留下任何变量,防止变量污染全局作用域。
IIFE 的语法格式如下:
(function() {
// 函数体
})();
或者
(function() {
// 函数体
}());
其中,用括号包裹函数表达式,然后在末尾再加上一个括号,这样就可以立即执行函数。
IIFE 的作用:
- 实现模块化
由于 IIFE 内部的变量不会暴露在全局作用域中,因此可以用它来实现模块化,避免变量名冲突、全局变量污染等问题。比如:
var module = (function() {
var count = 0;
var increaseCount = function() {
count++;
};
var getCount = function() {
return count;
};
return {
increaseCount: increaseCount,
getCount: getCount
};
})();
module.increaseCount();
console.log(module.getCount()); // 1
- 防止变量污染全局作用域
IIFE 可以防止函数内部的变量污染全局作用域,用于保护全局命名空间,避免变量名冲突等问题。
- 优化性能
IIFE 可以减少全局变量的使用,从而优化性能。因为 JavaScript 的作用域链和垃圾回收机制都是基于词法作用域的,而词法作用域是由函数创建的,所以 IIFE 可以带来性能优化的效果。
37. http请求跨域问题,你都知道哪些解决跨域的方法
在 Web 开发中,由于浏览器的同源策略,导致浏览器不能直接访问不同域下的资源,这就产生了跨域问题。常见的解决跨域问题的方法有以下几种:
- JSONP
JSONP(JSON with Padding)是一种解决跨域问题的简单方法,在服务端的响应中返回一个回调函数的调用,并将数据作为参数传递给回调函数。浏览器发现响应是一个 script 标签,就会把响应内容解析为 JavaScript 代码并立即执行。由于浏览器对 script 标签的 src 属性的访问并不受同源策略的限制,因此可以实现跨域。例如:
function jsonpCallback(data) {
// 处理响应数据
}
const script = document.createElement('script');
script.src = 'https://example.com/api?callback=jsonpCallback';
document.body.appendChild(script);
- CORS
CORS(Cross-Origin Resource Sharing)是一种官方的解决跨域问题的方式,它通过在服务端设置 Access-Control-Allow-Origin 来允许某个域名下的页面访问资源。具体实现方式是,浏览器在发送跨域请求时会加上一个 Origin 头部,服务端在接收到这个请求时可以根据 Origin 来判断是否允许访问,然后在响应头部设置 Access-Control-Allow-Origin 和其它的 CORS 相关头部来告诉浏览器允许访问。
- 代理服务器
利用服务器代理请求也是一种常用的解决跨域问题的方法。具体实现方式是,在客户端将请求发送给自己的服务器,服务器再将请求发给目标服务器,获取到响应后再将响应返回给客户端,这样客户端就可以避免直接请求目标服务器,从而避免跨域问题。
- postMessage 方法
在页面中通过 postMessage 方法传递数据,可以跨域传递数据。它的原理是在对方页面中监听 message 事件,当接收到数据时进行处理。
总体来说,解决跨域问题的方法很多,需要根据具体情况选择适合的方式来解决。
38.进程通信方式有哪些
进程通信是指操作系统在运行多个进程时,为它们提供相互通信的机制。常见的进程通信方式包括:
- 管道(Pipe)
管道是一种半双工的通信方式,只能实现单向通信。数据在通信时会被存储在内核缓冲区中。管道可以分为有名管道和无名管道,有名管道可以让无关进程间相互通信,而无名管道只能在有亲缘关系的进程间通信。
- 共享内存(Shared Memory)
共享内存是指两个或多个进程共享同一块物理内存,进程之间可以直接读写对方的内存空间,这种通信方式的速度非常快。需要由程序员专门负责同步访问共享内存的进程。
- 消息队列(Message Queue)
消息队列是一种通过记录和发送消息来进行进程通信的机制。每个消息都被赋予一个优先级,并根据先进先出的原则进行处理。进程通过系统调用来向消息队列中添加消息或者删除消息。
- 信号量(Semaphore)
信号量通常用于控制进程对于共享资源的访问,对于信号量的访问需要进行加锁和解锁操作,来保证同步,控制共享资源的互斥提供的信号量可以是具有之间关联性的进程。
- 套接字(Socket)
套接字是进程间通信机制中最通用和最强大的一种方式,其支持多种不同格式和大小的数据,还可以实现进程之间的双向通信。套接字通常用于不同计算机之间或者同一计算机上不同进程之间的通信。
总结起来,不同的进程通信方式各有各的优缺点,需要根据具体情况选择合适的方式来实现进程间通信。
39.tcp滑动窗口是什么
TCP 滑动窗口是一种用来控制发送方发送数据量的机制。它通过协商接收缓存区的大小和当前网络拥塞程度,动态地调整发送方的窗口大小,以保证在不破坏可靠传输的前提下,控制发送方发送数据的速度,从而避免网络拥塞和数据丢失的问题。
TCP 滑动窗口的窗口大小是指发送方允许等待确认的字节数的最大值,在 TCP 会话的建立过程中会协商双方的窗口大小。发送方在每次发送数据段时都会等待接收方的确认(ACK),接收方对前面接收到的所有数据段的 ACK 作出响应后,窗口大小会随之增加,发送方的窗口就可以增加,可以发送更多的数据,从而提高传输速度。如果发送方接收到 ACK 超时或者接收到重复 ACK,则会认为接收方的窗口较小,会适当降低发送速率,避免网络拥塞。
因此,TCP 滑动窗口协议是一种基于反馈的拥塞控制策略,在网络拥塞、延迟等场景下能够适应动态的网络环境,保证传输的可靠性和效率。
40.vue 是怎么解析template的? template会变成什么?
Vue 的模板(template) 是以 HTML 代码形式书写的 Vue 实例的视图模板。当 Vue 实例挂载到 DOM 中后,Vue 会对模板进行编译和解析,把数据和虚拟 DOM 合并得到渲染后的 DOM,最后将 DOM 渲染在浏览器中。
Vue 的模板解析过程主要分为以下几个步骤:
- 模板编译
当 Vue 实例挂载到 DOM 中后,Vue 会首先对模板进行编译,将模板中的指令、变量、事件绑定等表达式解析成一段渲染函数,提高渲染效率。在编译过程中,Vue 会解析模板中的指令,生成虚拟 DOM 树。
- 虚拟 DOM
在模板编译的过程中,Vue 会生成一份模板对应的虚拟 DOM 树。这棵树包含了模板中所有的 DOM 元素和属性,以及相应的事件和指令信息。
- 渲染函数
Vue 编译模板时,会生成一段渲染函数,这个函数将由 Vue 调用以生成虚拟 DOM。对于动态数据的更新,Vue 会重新调用这个渲染函数,生成新的虚拟 DOM。
- 生成渲染后的 DOM
Vue 通过视图同步线程监听虚拟 DOM 的变化,并在数据发生变化时重新生成渲染函数,然后调用该函数将数据渲染成真正的 DOM 元素。
经过以上的处理,模板会被解析成渲染函数和虚拟 DOM 树,最终渲染成真实的 DOM 元素展示在浏览器中。随着数据的变化,Vue 会重新生成渲染函数,重新生成虚拟 DOM,再进行 DOM 操作,有效减少了 DOM 操作的开销,提高了页面的渲染效率。
41.vue 双向绑定的原理是什么?
Vue 双向数据绑定的原理基于对象劫持(Object.defineProperty),通过劫持数据对象的 setter 和 getter 方法,在数据发生变化时自动更新对应的视图,同时在视图变化时自动更新对应的数据。
具体而言,当 Vue 实例创建时,Vue 将数据对象通过 Object.defineProperty 方法转化为 getter 和 setter,即劫持数据对象的 getter 和 setter 方法,这样可以监听数据的变化。每次访问数据对象的属性时,Vue 会将其添加到依赖列表中。
在组件渲染时,Vue 创建了一个虚拟 DOM 树(Virtual DOM),在该树上绑定了依赖。当数据对象发生变化时,setter 方法会被调用,Vue 提前从依赖列表中找到绑定该属性的所有 Watcher(观察者),并通知它们进行更新。同时,Vue 会根据新的数据对象和旧的虚拟 DOM 树生成新的虚拟 DOM 树,通过比对新旧虚拟 DOM 树的差异,只更新实际需要更新的部分。
当用户输入表单数据时,Vue 会将这些数据通过 v-model 指令进行双向数据绑定。v-model 会利用 input 事件和 change 事件来更新数据对象,达到实时更新数据的目的。
总之,Vue 的双向数据绑定机制是依赖于对象劫持实现的,通过劫持数据对象的 getter 和 setter 方法,实现对数据的监听。同时 Vue 利用虚拟 DOM 树和差异比对算法来尽量减少 DOM 的操作,提高性能。
42.用过vue 的render吗? render和template有什么关系
Vue 中的 render 函数是 Vue 组件的核心函数,用于完成组件的渲染和功能实现。与之相对的是 template,template 是一种声明式的模板语法,它只是描述了组件应该长什么样子,但它本身不是函数,在运行时需要被编译成 render 函数。render 函数和 template 一起使用会提高 Vue 应用的性能,也更加灵活。
在Vue 中,当组件渲染时,Vue 会首先检查是否存在 render 函数,如果存在,则使用 render 函数渲染组件,否则 Vue 会将组件的 template 模板编译成 render 函数并渲染。render 函数和 template 的关系是:render 函数是由 template 模板编译而来的,它们都是用来生成 DOM 树的。
相对于 template,使用 render 函数编写组件可以更加灵活,可以动态调整组件的渲染逻辑,实现一些比较复杂的组件。使用 render 函数编写组件的过程也更加直接,可以直接操作组件的状态和属性,甚至可以使用 JavaScript 的语法来写模板。由于 render 函数是真正的 JavaScript 代码,它可以在编译时进行静态分析,从而实现更高效的渲染过程。而 template 模板则需要在运行时进行动态编译,性能相对较低。
总之,render 函数和 template 都是用来生成组件的 DOM 树的方式。使用哪种方式主要取决于具体项目和需求。在一些小的应用中,使用 template 可以更快速地开发出页面,而在一些大型应用中,使用 render 函数更加灵活,可以提高渲染性能。
43.typescript你都用过哪些类型?typescript中type和interface的区别
在使用 TypeScript 中,我主要使用了以下类型:
- string:字符串类型
- number:数字类型
- boolean:布尔类型
- any:任意类型
- void:无任何返回值类型
- null 和 undefined: 空类型
- never:表示永远不存在的值的类型
- object:表示非原始类型的类型
下面是 type 和 interface 的区别:
- 声明语法不同。
type 的声明语法为:
type MyType = {
name: string
age: number
}
而 interface 的声明语法为:
interface MyInterface {
name: string
age: number
}
- type 可以声明基本类型,联合类型,元组类型等。
type Status = 'success' | 'fail' // 声明联合类型
type User = [string, number] // 声明元组类型
interface 无法声明具体的值类型。
- type 可以为现有的类型起别名。
type MyNumber = number
而 interface 无法给已有类型起别名。
- 当出现类似类型重复的需求时,type 可以结合联合类型和交叉类型来解决问题。
type UserAccount = {
id: number,
name: string,
email: string
}
type OptionalUserAccount = {
id?: number,
name?: string,
email?: string
}
// 交叉类型
type User = UserAccount & OptionalUserAccount
// 类型声明为:{ id: number; name: string; email: string; }
而 interface 并没有类似交叉类型的语法。
总之,type 和 interface 的主要区别在于声明语法,能否声明具体的值类型,以及在复杂类型声明中的灵活程度。在具体使用时,应根据具体场景来选择使用哪种方式。
44. 谈下webpack loader机制
Webpack Loader 机制是非常重要的一部分,它使得我们可以轻松地在我们的项目中使用各种类型的文件。Webpack Loader允许在导入模块时预处理文件,例如把 Sass 编译成 CSS、把 ES6/7/8 编译成 ES5等。
Webpack Loader 是一个 Node.js 模块,遵循 JavaScript 函数的最佳实践。该模块导出一个函数,该函数接收待处理的资源文件,并返回转换后的结果。
Loader 有两部分,执行时返回的对象和对应的配置项。其中执行时返回的对象可以有以下方法:
pitch用于动态更改得到资源的加载顺序。 pitch 函数被执行之后,Webpack 会作为 pitch 的返回值和普通的 loader 的返回值之间进行区别,并打断其它 loader 的执行;raw和buffer是两种加载资源的方式,可以使得 Loader 返回 Buffer 类型的结果,或者通过 encoding 等选项多种方式返回结果;loader的返回结果可以是相对地址甚至是远程地址,但是最好能够使用绝对路径。
Loader 的配置项可以有以下选项:
test路径测试,也可以是正则表达式,满足条件的文件会进入 loader 的处理管线;exclude路径测试,满足条件的文件不会进入 loader 的处理管线;include路径测试,只有满足条件的文件才会进入 loader 的处理管线;loaders引用 loader,可以通过这种方式在编译时使用一个以上的 loader;options配置选项。
总之,Webpack Loader 机制在项目中的作用非常重要,它让我们可以方便地在项目中使用各种类型的文件,并且通过自定义的方式实现对不同类型文件的处理,提高了开发效率和代码质量。
45.线程和进程的关系
进程是计算机中运行程序的基本单位,而线程则是进程中独立运行单位
1.线程在进程中进行(单纯的车厢)
2.一个进程可以包含多个线程(一辆火车可以有多个车厢)
3.不同进程间数据很难共享(一辆火车上的乘客很难换到另一辆火车上)
4.同一进程下不同线程间数据很容易共享(A车厢换到B车厢很容易)
5.进程要比线程消耗更多的计算机资源(采用多列火车比多个车厢更耗资源)
6.进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响其他火车,但是一节车厢着火了会影响整个火车)
7.进程可以拓展到多机,进程最适合多核
8.进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等他结束,才能使用这块内存。(火车上的洗手间)——‘互斥锁’
9.进程使用的内存地址可以限定使用量(比如火车上的餐厅)——‘信号量’
46.js继承方式
JavaScript 的继承实现有多种方式,本文解释其中的几种实现方式。
原型链继承
缺点:原型链继承的最大问题在于子类实例共享父类实例的引用属性,可以通过在子类实例上修改该属性,影响到其它子类实例。这也是为什么在某些情况下,不推荐使用原型链继承的原因。
原型链是 JavaScript 中实现对象继承的一种机制,每个对象都具有一个原型对象(prototype),并从中继承方法和属性。当访问对象的一个属性时,如果该对象本身没有定义该属性,则会沿着原型链向上查找,直到找到该属性为止。
如果要将一个对象作为另一个对象的模板,实现继承,可以使用原型链。具体而言,可以通过将一个对象的原型对象设置为另一个对象来实现继承。在 JavaScript 中,可以使用 Object.create() 方法来创建一个新对象,该对象的原型对象为指定的对象。例如:
const parent = {
name: 'John',
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
};
const child = Object.create(parent);
child.name = 'Alice';
child.sayHello(); // 输出:Hello, my name is Alice
在上面的例子中,我们创建了一个 parent 对象,并为其定义了一个 sayHello() 方法。然后,我们使用 Object.create() 方法创建了一个新对象 child,并将其原型对象设置为 parent 对象。这意味着当从 child 对象中访问一个属性或方法时,如果该对象本身没有定义该属性或方法,就会沿着原型链向上查找,最终会找到 parent 对象中的该属性或方法。
总之,原型链是 JavaScript 中实现继承的一种机制,可以让一个对象从其原型对象中继承方法和属性,并可以通过将一个对象的原型对象设置为另一个对象来实现继承。在访问对象属性时,会沿着原型链向上查找,直到找到该属性为止。
构造函数继承
构造函数继承是一种基于 JavaScript 构造函数的继承方式。它是通过在子类构造函数内部调用父类构造函数来实现继承,通过这种方式,子类可以继承到父类的属性,因为父类构造函数会在子类构造函数内部执行,从而给子类的 this 对象添加上父类的属性。
缺点:构造函数继承只能继承父类的属性,无法继承父类原型对象上的属性和方法。
组合继承
组合继承是一种结合了原型链继承和构造函数继承的继承方式。它是通过在子类构造函数内部调用父类构造函数来继承父类的属性,并在子类原型上设置一个父类实例来继承父类的原型方法和属性,通过这种方式,子类可以继承到父类的所有属性和方法。
缺点:组合继承会调用两次父类的构造函数,一次在子类构造函数内部调用,一次在设置子类原型的时候调用,从而导致一些不必要的浪费。
寄生组合继承
寄生组合继承是一种修正了组合继承中不必要的父类构造函数调用的继承方式。它是通过创建一个空的构造函数来复制父类原型并将其设置为子类原型,从而实现继承。
function inherit(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
Class 继承
ES6 的 Class 继承是一种非常简洁和易于理解的继承方式,它是基于原型链和构造函数继承的实现。通过使用 class 和 extends 语法糖,就可以声明一个类并继承另一个类。子类可以通过调用 super 来访问父类的属性和方法,从而实现继承。
class Animal {
constructor(name, color) {
this.name = name;
this.color = color;
}
run() {
console.log('I can run');
}
}
class Dog extends Animal {
constructor(name, color) {
super(name, color);
}
say() {
console.log(`My name is ${this.name}`);
}
}
let dog = new Dog('wangcai', 'white');
dog.run(); // I can run
dog.say(); // My name is wangcai
不同于前面的继承方式,Class 继承是 ES6 中添加的一种特性,它使用了更简洁、更易于理解的语法糖,让原型链继承和构造函数继承更为直观,能够更加符合开发者的编码习惯。
47.箭头函数中的this
箭头函数中的 this 是词法作用域,它的作用域与声明时的父级作用域相同,与函数被调用的方式无关。在箭头函数中,this 始终指向当前函数所在的作用域中的 this。也就是说,它的指向是在定义时就已经确定了,而不是在运行时确定的。
这种行为与普通函数是不同的。普通函数中的 this 的指向,取决于函数被调用的方式,如果是作为函数调用,this 指向全局对象,如果是作为对象的方法调用,this 指向该对象。
例如,使用箭头函数的方式定义一个对象的方法,this 就会指向该对象:
const obj = {
name: 'Tom',
getName: function () {
return this.name;
},
getThisName: () => {
return this.name;
}
};
console.log(obj.getName()); // Tom
console.log(obj.getThisName()); // undefined
在上面的代码中,getName 方法使用普通函数定义,getThisName 方法使用箭头函数定义,由于箭头函数中的 this 指向父级作用域中的 this,因此 getThisName 方法中的 this 指向全局对象,返回 undefined。
需要注意的是,在没有父级作用域的情况下(比如在全局作用域下),箭头函数的 this 仍然指向全局对象,这种行为与普通函数一致:
const name = 'global';
const fn1 = function () {
console.log(this.name);
};
const fn2 = () => {
console.log(this.name);
};
fn1(); // global
fn2(); // global
因此,在使用箭头函数时,需要注意它的特殊行为,以避免出现意外的结果。通常来说,箭头函数更适合作为简单函数或回调函数使用,而不适合作为对象的方法使用。
48.说一下this
this 是 JavaScript 中的一个关键字,表示函数执行过程中的上下文对象。它的值在函数被调用时确定,并且取决于函数的调用方式。
在全局作用域中,this 指向全局对象,例如在浏览器中,this 指向 window 对象。在函数中,this 的值可能不同,具体取决于函数被调用的方式。
以下是常见的几种调用方式和对应的 this 值:
- 作为函数调用:
this指向全局对象,在浏览器中是window对象。
function foo() {
console.log(this);
}
foo(); // Window 对象
- 作为方法调用,
this指向调用该方法的对象。
const obj = {
name: 'Tom',
getName: function() {
console.log(this.name);
}
};
obj.getName(); // Tom
- 使用
call或apply调用,this指向传入的第一个参数。
function foo() {
console.log(this);
}
foo.call('Tom'); // 'Tom'
- 使用
bind返回一个新的函数,this指向绑定的第一个参数。
const obj = {
name: 'Tom',
getName: function() {
console.log(this.name);
}
};
const getName = obj.getName.bind(obj);
getName(); // Tom
- 构造函数中使用
this,this指向新创建的实例对象。
function Person(name) {
this.name = name;
}
const person = new Person('Tom');
console.log(person.name); // Tom
需要注意的是,在箭头函数中,this 是词法作用域,与函数被调用的方式无关,其 this 始终指向声明时所在的作用域,而不是执行时所在的作用域。
49.输出是啥
function foo() {
setTimeout(() => {
console.log('id;', this.id)
}, 100);
}
var id = 21;
foo.call({ id: 42 })
function foo() {
setTimeout(() => {
console.log('id:', this.id)
}, 100)
};
var id = 21;
foo();输出什么
第一个输出为:id: 42,因为使用了 call 方法改变了 foo 函数中的 this 指向,指向了传入的 {id: 42} 对象。
第二个输出为:id: 21,因为 foo 函数中的 this 指向全局作用域中的 id 变量,而在全局作用域中已经定义了 id 变量,并且在调用 foo 函数时并没有传入对象改变 this 指向,所以输出结果为全局作用域中的 id 变量值。
50.垃圾回收机制
在 JavaScript 中,垃圾回收是自动进行的机制,其主要任务是控制那些动态分配的内存的分配与回收。JavaScript 的垃圾回收算法是基于标记清除(Mark and Sweep)的。
标记清除算法是建立在对象是否可达的基础上的,所谓可达就是对象可通过某种方式访问到。当一个对象不再被访问时,它就可以被回收了。
标记清除算法主要分为两个阶段:标记阶段和清除阶段。
- 标记阶段:垃圾回收器会从根对象(通常是全局对象)出发,遍历所有引用对象,并标记它们。未被标记的对象就是垃圾对象,可以被清除。
- 清除阶段:垃圾回收器会遍历整个堆,把未被标记的对象清除掉。在清除对象的同时,垃圾回收器会把被清除对象所占用的内存释放出来,以便后续的内存分配。
JavaScript 的垃圾回收器会周期性地执行标记清除算法,以回收不再被引用的对象所占用的内存。其中周期性执行的间隔时间通常是不确定的,由具体实现决定。在现代浏览器中,还包括一些优化策略,例如增量标记和垃圾压缩等机制,以提高垃圾回收的效率。
51. 说一下map和weakmap
Map 和 WeakMap 都是 JavaScript 中的数据结构,用于存储键值对。它们的主要区别在于对键的引用类型处理方式的不同。
Map 可以使用任何类型作为键,而在 WeakMap 中,键必须是一个引用类型,即对象类型。在 Map 中,当使用非基本类型的键作为键值对中的键时,Map 实例会对这个键创建一个强引用,这意味着只有在这个键没有其他引用时才会被垃圾回收。而在 WeakMap 中,当使用对象作为键时,WeakMap 只会对其进行弱引用,即在没有其他强引用指向这个对象时,WeakMap 自动删除与之关联的键值对,从而释放内存。
除了对键引用类型处理方式的差异之外,Map 和 WeakMap 的使用方式基本相同。它们都有类似于 get、set、has 等方法,可以使用这些方法对键值对进行操作。
由于 WeakMap 可以自动回收被垃圾的对象及该对象对应的键值对,因此在一些特定的场景下,使用 WeakMap 可以减少内存泄漏的风险。但是需要注意的是,在使用 WeakMap 时,需要保证使用的键是引用类型,因为对于非引用类型的键,WeakMap 会抛出 TypeError 异常。
总之,Map 和 WeakMap 都是 JavaScript 中的数据结构,它们用于存储键值对,并且区别在于对键引用类型处理方式的不同。在没有内存泄漏的风险时,推荐使用 Map,而在需要防止内存泄漏时,可以使用 WeakMap。