目录
浏览器跨域的时候会发生什么,和访问不跨域的浏览器有什么区别?怎样算做跨域?如何规避跨域问题?
利用 reduce 给数组实现一个 myMap 方法,和 map 一样的效果
通过await和setTimeout来设计一个函数使其完成延迟一秒的功能
常见的Webpack Loader有哪些? 如何实现一个Webpack Loader?
常见的Webpack Plugin有哪些? 如何实现一个Webpack Plugin?
HTTP1.0、1.1和2.0都有什么区别?1.1的持久连接和2.0的多路复用有什么区别?
小冰龙镇楼
Q:CSS相关问题
A:
盒模型有哪些?区别是什么?box-sizing的作用
盒模型分为:W3C盒模型 和 IE盒模型
W3C盒模型,也就是标准盒子模型
宽度=内容的宽度(content)+ border + padding + margin
宽度=内容宽度(content+border+padding)+ margin
box-sizing 属性是用来控制元素的盒子模型的解析模式,默认为content-box。互换模型格式。
content-box:W3C的标准盒子模型,设置元素的 height/width 属性指的是content部分的高/宽
border-box:IE传统盒子模型。设置元素的height/width属性指的是border + padding + content部分的高/宽
什么是BFC?如何创建?有何应用?
BFC 即块格式化上下文,BFC内的元素不会影响外面的元素。
创建:
- float不为none
- position为absolute或fixed
- overflow不为visible
- display为inline-block、flex、inline-flex等
应用:
- 防止margin重叠
- 清除内部浮动
- 自适应多栏布局
什么是重绘和回流?如何优化?
回流(重排):当DOM的变化影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做回流(重排)。
触发:添加或者删除可见的DOM元素、元素尺寸改变——边距、填充、边框、宽度和高度
重绘:当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
触发:改变元素的color、background、box-shadow等属性
优化建议
- 尽量用class修改样式:样式表越简单,回流和重绘就越快,减少使用style 一条条改变样式
- 尽量在DOM树的末端改变class:回流和重绘的DOM元素层级越高,成本就越高,在DOM树的末端改变class可以使其影响尽可能少的节点。
- 使用虚拟DOM(如react)的脚本库:虚拟DOM具有批处理和高效的Diff算法,可以确保非常高效的渲染。它不会立即进行排版与重绘操作,而是对虚拟DOM进行频繁修改后,最后一次性比较并修改真实DOM中需要改的部分
- 使用CSS 预处理器:如sass 和 less,可以方便地进行变量和混合(mixins)等操作,在编译时将它们转换为CSS,这样可以减少代码的冗余和复杂性,进而减少重绘和回流。
- 使用CSS3动画或transform:使用CSS3的动画属性或transform属性来实现动画效果,这些属性通常会在GPU层面处理,而不会引起回流,性能更好。
Q:JS相关问题
对Promise的理解
Promise 是为了解决callback造成的 “回调地狱” 问题而提出的
当一个回调函数嵌套一个回调函数的时候就会出现一个嵌套结构,当嵌套的多了就会出现回调地狱的情况。
Promise 是 异步编程的一种解决方案 ,可以更好的处理链式调用。
Promise 对象
Promise 对象有以下两个特点:
-
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
-
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected 。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是:如果你错过了它,再去监听,是得不到结果的。
Promise 语法代码
new Promise(function (resolve, reject) {
// function 是构造函数的回调函数
// resolve 表示成功的回调,reject 表示失败的回调
// resolve 和 reject 是两个函数
}).then(function (res) {
// 成功的函数
}).catch(function (err) {
// 失败的函数
})
Promise 实例生成以后,可以用 then 方法分别指定 resolve 状态和 reject 状态的回调函数。
代码示例:
//Promise是一个构造函数,new之后等于说调用了构造函数
const promise = new Promise((resolve,reject)=>{
//构造函数中传的参数是一个函数
//这个函数内的两个参数(resolve,reject))分别又是两个函数
//异步代码
setTimeout(()=>{
// resolve(['111','222','333'])
reject('error')
},2000)
})
promise.then((res)=>{
//兑现承诺,这个函数被执行
console.log('success',res);
}).catch((err)=>{
//拒绝承诺,这个函数就会被执行
console.log('fail',err);
})
Promise链式,then 方法返回一个新的Promise 实例(注意:不是原先的Promise 实例),可以通过then 方法实现链式调用
Promise 方法
1. all:all 方法是与 then 同级的另一个方法,该方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后并且执行结果都是成功的时候才执行回调。
Promise
.all([promiseClick3(), promiseClick2(), promiseClick1()])
.then(function(results){
console.log(results);
});
//all接收一个数组参数,这组参数为需要执行异步操作的所有方法,里面的值最终都算返回Promise对象。
//这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。
//那么,三个异步操作返回的数据哪里去了呢?都在then里面,all会把所有异步操作的结果放进一个数组中传给then,然后再执行then方法的成功回调将结果接收
Promise.all () 返回的 Promise 异步地变成完成状态, 其 resolve 回调的结果是一个数组,包含了所以Promise 的 resolve 回调的结果。
然而,只要任何一个输入的 Promise 变为拒绝状态(即失败),Promise.all () 返回的 Promise 就会立刻中断执行变为拒绝状态,其 reject 回调执行,并且 reject 的是第一个抛出的错误信息。
2. allSetted: allSetted区别于 all方法的是,不会中断执行,而是等待所有 Promise 都解决或拒绝,然后返回结果。
3. race:race 与 allSetted方法相反,谁先执行完成就先执行回调。先执行完的不管是进行了race 的成功回调还是失败回调,其余的将不会再进入race 的任何回调。
原型和原型链
原型是什么? 原型链是什么?
- 每一个类(构造函数)都有一个显示原型prototype(本质就是个对象)
- 每一个实例都有一个隐式原型__proto__
=====================================================================
注:__proto__ 属性已经从 Web 标准中删除,现在更推荐使用 Object.getPrototypeOf()来获取参数对象的原型
- 类显式原型的 prototype 等于其创建的实例的隐式原型 __proto__
- 查找对象实例的方法和属性时,先在自身找,找不到则沿着__proto__向上查找,我们把 __proto__ 形成的链条关系称
原型链
(实现了js继承)var arr = []; arr.__proto__ === Array.prototype
二者有什么作用?
- 实现了js的继承
- 实现了实例的公用属性和方法(实现类的实例方法扩展)
更多详情可参考:原型和原型链 【js】prototype和__proto__的区别和联系_js prototype和__proto__-CSDN博客
new关键字都做了什么
- 创建一个新的空对象:首先,JavaScript引擎会创建一个新的空对象。这个对象会继承自构造函数的
prototype
对象。这个对象会被用作构造函数的上下文(即this
)。 -
设置原型链:新创建的对象的_proto_属性(在ES6中,通过Object.getPrototypeOf()访问)会被设置为构造函数的prototype对象。这样,新创建的对象就可以继承构造函数原型上的属性和方法
- 执行构造函数中的代码:接下来,构造函数中的代码会被执行。这通常包括给 this 添加属性和方法,也就是给新创建的对象添加属性和方法。
- 返回新对象:如果构造函数没有显式地返回一个对象,那么
new
表达式的结果就是新创建的对象。如果构造函数显式地返回了一个对象,那么new
表达式的结果就是这个返回的对象。需要注意的是,如果构造函数返回了一个非对象类型的值(比如null
、undefined
、number
、string
、boolean
等),那么这个非对象类型的值会被忽略,new
表达式的结果仍然是新创建的对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
// 使用new关键字创建Person对象
const john = new Person('John', 30);
console.log(john.name); // 输出: John
console.log(john.age); // 输出: 30
john.greet(); // 输出: Hello, my name is John and I'm 30 years old.
// 检查原型链
console.log(john.__proto__ === Person.prototype); // 输出: true
防抖和节流
防抖(Debounce)
防抖的核心思想是:在一定时间内,事件处理函数只执行一次,如果在这个时间内又触发了该事件,则重新计算执行时间。
例如,如果你有一个搜索框,并且希望在用户停止输入一段时间后才发送搜索请求,那么防抖就非常有用。在用户持续输入的过程中,防抖函数会不断重置执行时间,只有当用户停止输入一段时间后,才会真正执行事件处理函数。
代码实现 ( react ):
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
节流(Throttle)
节流的核心思想是:每隔一定的时间就执行一次事件处理函数,不管事件触发得有多频繁。
以滚动事件为例,如果你希望在用户滚动页面时,每隔一段时间就执行一些操作(比如更新某个元素的位置),那么节流就很合适。无论用户滚动得有多快,事件处理函数都会按照设定的时间间隔执行。
代码实现 ( react ):
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
总结
- 防抖更侧重于延迟执行:在事件被触发后n秒内函数只能执行一次,如果在这n秒内又被重新触发,则重新计算执行时间。
- 节流更侧重于间隔执行:无论事件触发多么频繁,都会保证在一定时间间隔内只执行一次事件处理函数。
闭包问题
闭包时,如何修改函数内部变量
这篇文章介绍的蛮好: 闭包--修改函数内部变量_js __proto__修改闭包内的值-CSDN博客
Q:对浏览器的理解
浏览器事件循环(Event Loop)
是JavaScript运行时环境(如浏览器)处理异步操作和回调机制的核心部分。它确保了在单线程环境中,即使存在大量的异步操作,代码也能有序且高效地执行。
浏览器事件循环的主要步骤和机制:
- 执行栈(Execution Stack):
- 当浏览器加载一个页面时,JavaScript引擎会创建一个全局执行上下文,并将其推入执行栈。
- 当调用函数时,会为该函数创建一个新的执行上下文,并将其推入执行栈。
- 执行栈中的代码同步执行,直到执行完毕或遇到异步操作。
- 任务队列(Task Queue):
- 浏览器为异步操作维护了一个或多个任务队列。这些队列用于存放待处理的任务(通常是回调函数)。
- 当一个异步操作(如setTimeout、AJAX请求、用户交互等)完成时,它的回调函数会被放入任务队列中等待执行。
- 事件循环:
- 当执行栈为空时(即没有更多的同步代码需要执行),事件循环会查看任务队列。
- 如果任务队列中有任务,事件循环会取出一个任务并将其放入执行栈中执行。
- 执行完毕后,事件循环再次查看任务队列,取出下一个任务执行,如此循环往复。
- 宏任务(MacroTask)与微任务(MicroTask):
- 任务队列实际上分为宏任务队列和微任务队列。
- 宏任务包括script(整体代码)、setTimeout、setInterval、I/O、UI渲染等。
- 微任务包括Promise的回调、process.nextTick(Node.js中)、MutationObserver(使用方式查看这里)等。
- 在每个事件循环的迭代中,首先执行栈中的同步代码,然后执行所有的微任务,最后才执行一个宏任务。在下一个迭代中,再次执行所有的微任务,然后再执行下一个宏任务,依此类推。
代码示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
//log打印为:
script start、
script end、
promise1、
promise2、
setTimeout
以上代码执行帧动画可以查看Tasks, microtasks, queues and schedules
对于process.nextTick
和Promise
的执行顺序:
- process.nextTick:
process.nextTick
允许你将一个回调函数推迟到当前事件循环的迭代结束之前执行。这意味着,无论当前正在处理什么操作(例如 I/O 操作或定时器回调),process.nextTick
注册的回调函数都会在当前事件循环迭代结束之前优先执行。因此,它的执行优先级非常高。- Promise:Promise 是另一种处理异步操作的方式。当一个 Promise 被 resolve 或 reject 时,它的回调函数会被放入微任务队列(microtask queue)中。微任务队列会在每个事件循环迭代的末尾被清空,这意味着 Promise 的回调函数会在当前事件循环迭代的所有同步代码和
process.nextTick
回调之后执行。因此,根据这些机制的特点,我们可以得出以下执行顺序:
- 首先执行同步代码。
- 然后执行
process.nextTick
注册的回调函数。- 接着执行微任务队列中的 Promise 回调函数。
- 最后,当当前事件循环迭代结束时,进入下一个事件循环迭代,并重复上述过程。
这个执行顺序确保了
process.nextTick
的回调函数总是优先于 Promise 的回调函数执行
浏览器本地存储中 cookie、localStorage 和 SessionStorage的区别
1. 存储容量不同
cookie的大小一般为4K, localStorage 和 sessionStorage的大小一般为5M。
2. 有效期不同(过期时间)
cookie可以设置过期时间,一旦过期就会被浏览器删除,
localStorage存储的内容不会过期,需要手动清除才会消失(调用api或者清除缓存)。
sessionStorage在当前会话下有效,关闭页面或者浏览器时会被清空。
3. 与服务端的通信
cookie参与服务器端通信,每次都会携带http的头信息中。(如果使用cookie保存过多数据会带来性能问题)
localStorage 和 sessionStorage不参与服务器端的通信,仅在客户端(即浏览器)中保存。
4. 生成方式
cookie由服务器生成。可设置失效时间。服务器通过响应头“Set-Cookie”传给前端。
localStorage 和 sessionStorage 由前端生成。
(1)xxxStorage.setItem(‘key’, ‘value’);
//该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
(2)xxxStorage.getItem(‘key’);
//该方法接受一个键名作为参数,返回键名对应的值。
(3)xxxStorage.removeItem(‘key’);
//该方法接受一个键名作为参数,并把该键名从存储中删除。
(4)xxxStorage.clear();
//该方法会清空存储中的所有数据。
5. 数据共享及访问权限
cookie 可以设置访问路径和访问域名,以控制哪些页面可以访问 cookie,在相同浏览器不同页面可共享数据。
localStorage 存储的数据可以被同一域名下的所有页面访问。
sessionStorage 存储的数据可以被同一域名下的同一个页面可以共享,不同页面不可共享(同源页面)。
6.安全性
cookie会被发送到服务器端,数据有可能额被劫持和篡改,相比较 localStorage 和 sessionStorage 只存储在浏览器端相对来说更安全。
cookie:浏览器发起http请求,服务器会进行cookie设置,也就是set-cookie,服务器会把name和value的值填充完整,发送给浏览器后,浏览器会保存起来,这样浏览器以后发送的每一个请求都会自动附上这个cookie。
浏览器跨域的时候会发生什么,和访问不跨域的浏览器有什么区别?怎样算做跨域?如何规避跨域问题?
浏览器跨域时,会出现同源策略的限制,导致从一个域名的网页无法直接请求另一个域名的资源。这种限制是由浏览器为了提高网站的安全性对JavaScript施加的。
跨域与访问不跨域的浏览器的主要区别在于权限和限制。在不跨域的情况下,浏览器可以自由地访问和操作同源的页面和资源。然而,在跨域的情况下,浏览器会受到同源策略的限制,无法直接访问和操作不同源的页面和资源。
具体地说,当协议、域名或端口中任何一个不同时,就视为跨域。
规避跨域问题的方法有多种,以下是一些常见的策略:
JSONP:
利用 <script> 标签不受浏览器同源策略的限制,通过和后端配合来解决跨域问题。
具体来说,客户端创建一个 <script> 标签,将请求后端的接口拼接一个回调函数名称作为参数传给后端,并将此<script> 标签添加到页面中。后端接收到请求后,会解析得到回调函数名称,然后把数据和回调函数名称拼接成函数调用的形式返回。客户端解析后会调用定义好的回调函数,获取后端返回的数据。
CORS(跨源资源共享):
服务端配置响应头Access-Control-Allow-Origin,允许指定的源进行跨域访问。前端无需配置,实现简单。
搭建Node代理服务器:
由于同源策略是浏览器限制的,服务端请求服务器是不受浏览器同源策略的限制的。
webpack-dev-server(
仅适用于开发环境)
通过在本地启动一个代理服务器,当前端代码发起一个跨域请求时,这个请求不是直接发送到后端 API 服务器的,而是首先发送到了 webpack-dev-server
这个代理服务器。
因此,可以搭建一个Node代理服务器来代理访问服务器。客户端请求自己的Node代理服务器,代理服务器转发客户端的请求访问目标服务器,处理请求后返回数据给客户端。( 需要注意的是,客户端和代理服务器之间也可能存在跨域问题,所以需要在代理服务器中设置CORS。)
从输入url开始到渲染页面浏览器都做了什么
从输入URL开始到渲染页面,浏览器主要进行了以下步骤:
- 解析URL:当用户在浏览器的地址栏输入URL 后,浏览器会首先解析这个URL,判断其是否合法。
- 查找缓存:浏览器会查看自己的缓存,判断是否有之前访问过的这个URL的缓存页面。如果有,那么浏览器会直接显示这个缓存页面,而不会再去服务器请求。(这个过程非常快,所以用户会感觉到页面加载速度很快。
- DNS解析:如果浏览器缓存中没有找到对应的页面,那么浏览器会开始DNS解析过程,将URL 中的域名解析成对应的IP 地址。DNS 解析的过程就是寻找哪个服务器上有请求的资源,这个过程可能涉及浏览器缓存、操作系统缓存、路由缓存、ISP 的DNS 服务器,甚至根服务器的递归查询。
- 建立连接与发送请求:浏览器利用TCP协议通过三次握手与服务器建立连接,并发送HTTP请求。HTTP请求包括header和body,body中有请求的内容。
- 接收并解析响应:服务器接收到请求后,会发送HTML、CSS、JavaScript等资源文件作为响应。浏览器接收到这些资源后,会开始解析HTML代码。
- 渲染页面:解析HTML中的标签,生成DOM树;解析CSS样式,生成CSSOM树;将DOM树和CSSOM树合并生成渲染树;然后遍历渲染树进行布局和绘制等操作。在渲染页面的过程中,浏览器可能会发现页面中还包含一些异步请求,比如AJAX请求、图片加载等,这些请求会按照相应的机制进行处理。
- Http请求结束,TCP断开连接:浏览器与服务器进行四次挥手断开链接。
三次握手过程:
- 建立连接时,客户端发送SYN报文(SYN=1;seq=x)到服务器,并进入SYN_SENT状态,等待服务器确认
- 服务器收到SYN包,确认SYN,同时发送一个SYN包,即SYN+ACK包(SYN=1;ACK=1;seq=y;ack=x+1),此时服务器进入SYN_RECV状态。第 2 次握手实际上是分两部分来完成的,即 SYN+ACK(请求和确认)报文。服务器收到了客户端的请求,向客户端回复一个确认信息(ACK=x+1)。服务器再向客户端发送一个 SYN 包(seq=y)建立连接的请求,此时服务器进入 SYN_RECV 状态,
- 客户端收到服务器的SYN+ACK包,向服务器发确认包ACK,(ACK=1;seq=x+1;ack=y+1)客户端和服务端进入Established状态,TCP连接建立成功
SYN:同步序列编号
SEQ 表示请求序列号
ACK: 确认序列号,表示响应
RST:连接重置 复位标志
URG:紧急标志
RSH:推标志,表示有DaTa数据传输
FIN:结束标志SYN建立连接 -> SYN+ACK 响应 -> PSH(TCP数据包传递) -> FIN关闭连接 -> RST连接重置
四次挥手过程:
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
- 第一次挥手: 客户端会发送一个 FIN 报文,报文中会指定一个序列号。此时客户端停止再发送数据,主动关闭TCP连接,处于 FIN_WAIT1(终止等待1) 状态。(FIN=1;seq=u)
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。(ACK=1,确认号ack=u+1,序号seq=v)
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。(FIN=1,ACK=1,序号seq=w,确认号ack=u+1)
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。(ACK=1,seq=u+1,ack=w+1)
服务器只要收到了客户端发出的确认,立即进入CLOSED状态,服务器结束TCP连接的时间要比客户端早一些。
Q:Js常见算法题
利用 reduce 给数组实现一个 myMap 方法,和 map 一样的效果
Array.prototype.myMap = function(callback) {
return this.reduce((acc, curr, index, arr) => {
acc.push(callback(curr, index, arr));
return acc;
}, []);
};
通过await和setTimeout来设计一个函数使其完成延迟一秒的功能
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedAction() {
console.log('Starting action...');
await delay(1000); // 等待1秒
console.log('Action completed after 1 second.');
}
delayedAction();
数组扁平化
const nestedArray = [1, 2, [3, 4, [5, 6]]];
const flatArray = (arr) =>{
return arr.reduce((pre,cur)=>{
return pre.concat(Array.isArray(cur)? flatArray(cur) :cur)
},[])
}
console.log( flatArray(nestedArray)); // 输出: [1, 2, 3, 4, 5, 6]
不使用reduce的方法
function flattenArray(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flattenArray(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
const nestedArray = [1, 2, [3, 4, [5, 6]]];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // 输出: [1, 2, 3, 4, 5, 6]
JS处理多个HTTP请求限制最大并发数(并发限制)
import axios from 'axios';
class ConcurrentController {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent;
this.currentConcurrent = 0;
this.pendingPromises = [];
}
// 发起请求并控制并发数
fetch(url) {
return new Promise((resolve, reject) => {
// 如果当前并发数未达到限制,则发起请求
if (this.currentConcurrent < this.maxConcurrent) {
this.currentConcurrent++;
axios.get(url)
.then(response => {
resolve(response.data);
})
.catch(error => {
reject(error);
})
.finally(() => {
this.currentConcurrent--;
this.executeNext(); // 执行下一个待处理的请求
});
} else {
// 如果达到并发限制,则将请求加入待处理队列
this.pendingPromises.push({ resolve, reject, url });
}
});
}
// 执行待处理队列中的下一个请求(如果有)
executeNext() {
if (this.pendingPromises.length > 0) {
const { resolve, reject, url } = this.pendingPromises.shift();
this.fetch(url)
.then(resolve)
.catch(reject);
}
}
}
// 使用示例
const concurrentController = new ConcurrentController(3); // 限制最大并发数为3
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3',
];
const fetchAllData = async () => {
const promises = urls.map(url => concurrentController.fetch(url));
try {
const results = await Promise.all(promises);
console.log(results); // 打印所有请求的结果
} catch (error) {
console.error('Failed to fetch all data', error);
}
};
fetchAllData();
Js 实现大数相加
const fn = (num) => {
let arr = num.split('').reverse()
return arr
}
let arr1 = fn(num1)
let arr2 = fn(num2)
let arr3 = []
let maxLen = arr1.length > arr2.length ? arr1.length : arr2.length
let isAdd = false // 是否需要进位
for (let i = 0; i < maxLen; i++) {
arr3[i] = (arr1[i] || 0) * 1 + (arr2[i] || 0) * 1 + (isAdd ? 1 : 0)
if (arr3[i] > 9) {
isAdd = true
arr3[i] = arr3[i] % 10
} else {
isAdd = false
}
}
let str = arr3.reverse().join('')
console.log('str =', str);
数组转树
/**
*@param { Array } list 需要转的数组
*@param { String } rootValue 根节点的id
*/
//pid则用于标识该节点的父节点。一个节点的pid值等于其父节点的id值。
function transListToTreeData(list, rootValue) {
var arr = []
list.forEach(item => {
if (item.pid === rootValue) {
const children = transListToTreeData(list, item.id)
if (children.length) {
item.children = children
}
arr.push(item)
}
})
return arr
}
transListToTreeData(dataList,'') // 调用方法
两个栈模拟队列
核心是:通过把第一个栈1中元素反向存储到栈2中,实现先进先出的队列特性
var QueueWithTwoStacks = function() {
this.stack1 = []
this.stack2 = []
};
// 入队操作
QueueWithTwoStacks.prototype.enqueue = function(value) {
// 入栈
this.stack1.push(value)
};
// 出队操作
CQueue.prototype.deleteHead = function() {
// 如果第二个栈为空,则将第一个栈的所有元素弹出并压入第二个栈
if (this.stack2.length === 0) {
while (this.stack1.length > 0) {
this.stack2.push(this.stack1.pop());
}
}
// 如果第二个栈仍然为空,说明队列为空,返回null或抛出异常
if (this.stack2.length === 0) {
return null; // 或者抛出异常:throw new Error('Queue is empty');
}
// 弹出并返回第二个栈的顶部元素
return this.stack2.pop();
};
}
排序问题
归并排序
//归并排序
function mergeSort(arr) {
if (arr.length === 1) return arr;
const midIdx = Math.floor(arr.length / 2);
return merge(mergeSort(arr.slice(0, midIdx)), mergeSort(arr.slice(midIdx)))
}
function merge(leftArr, rightArr) {
let temp = [];
while (leftArr.length > 0 && rightArr.length > 0) {
if (leftArr[0] < rightArr[0]) {
temp.push(leftArr.shift());
} else {
temp.push(rightArr.shift());
}
}
return temp.concat(leftArr).concat(rightArr);
}
快速排序
//快速排序
function quickSort(arr) {
// 基线条件:小于或等于1个元素的数组是有序的
if (arr.length <= 1) return arr;
// 选择一个基准值,这里我们选择数组的中间值作为基准值
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0];
// 定义两个子数组,一个用于存放比基准值小的元素,另一个用于存放比基准值大的元素
let left = [];
let right = [];
// 遍历数组,将比基准值小的元素放入左边的子数组,比基准值大的元素放入右边的子数组
for (let i = 0; i < arr.length; i++) {
if (arr[i] > pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 递归地对左右两个子数组进行快速排序,然后连接这两个子数组和基准值,得到排序后的数组
return quickSort(left).concat([pivot], quickSort(right));
}
// 时间复杂度:平均情况下为O(n log n),最坏情况下为O(n^2),但在实际应用中表现良好。
Q: 前端工程化
webpack配置
代码示例
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'main.js', // 打包后的文件名
path: path.resolve(__dirname, 'dist'), // 打包后的文件存放的位置
},
plugins: [],
module: {
rules: [
{
test: /\.js$/, // 使用正则来匹配 .js 结尾的文件
exclude: /node_modules/, // 排除 node_modules 目录
use: {
loader: 'babel-loader', // 转换文件,使用的 loader
},
},
],
},
};
常见的Webpack Loader有哪些? 如何实现一个Webpack Loader?
Loader: 将其他类型的文件转换为js文件,根据rule匹配文件扩展名,处理文件的转换器。用于对模块的源代码进行转换,以便webpack能够理解和打包
Webpack-Loader:
- 样式处理:
css-loader
:解析CSS文件中的代码。style-loader
:将CSS模块作为样式导出到DOM中。sass-loader
和less-loader
:加载和转义sass或less文件,将它们转换为CSS。postcss-loader
:使用postcss加载和转义CSS/SSS文件。
- 脚本转换编译:
babel-loader
:将ES6+的代码转换为向后兼容的JavaScript版本。coffee-loader
:将CoffeeScript转换为JavaScript。
- 资源处理 (处理图片和字体):
file-loader
:复制文件到输出目录,并返回(相对)URL,在代码中通过相对 URL 去引用输出的文件。url-loader
:类似file-loader,区别是用户可以设置一个阈值,大于阈值会交给file-loader处理,小于阈值时返回文件base64 形式编码。
- 模板处理:
html-loader
:将HTML文件导出编译为字符串,可供js识别的其中一个模块。pug-loader
、jade-loader
(jade是pug的前身):加载pug或jade模板。ejs-loader
:加载ejs模板。handlebars-loader
:将Handlebars模板转移为HTML。
- 其他:
vue-loader
:处理.vue文件,这是Vue.js单文件组件的格式。raw-loader
:将文件已字符串的形式返回。imports-loader
和exports-loader
:向模块注入变量或者提供导出模块功能。
要实现一个Webpack Loader,你需要编写一个Node.js模块,该模块导出一个函数。这个函数将接受Webpack传递给它的源代码,并返回转换后的源代码。
- 创建一个Node.js模块:首先,你需要创建一个Node.js文件,这个文件就是你的loader。
- 编写loader函数:在模块中,你需要导出一个函数。这个函数应该接受两个参数:
source
(源代码)和sourceMap
(可选的源代码映射)。- 转换源代码:在loader函数中,你可以对源代码进行任何必要的转换。这可能包括解析、转换语法、添加前缀等。
- 返回转换后的源代码:最后,loader函数应该返回转换后的源代码。
// uppercase-loader.js module.exports = function(source) { // 将源代码转换为大写 const uppercasedSource = source.toUpperCase(); // 返回转换后的源代码 return uppercasedSource; }; //要使用这个loader,需要在Webpack的配置文件中的module.rules数组中添加相应的规则: // webpack.config.js module.exports = { // ... module: { rules: [ { test: /\.txt$/, // 匹配所有.txt文件 use: 'uppercase-loader', // 使用uppercase-loader }, ], }, // ... };
常见的Webpack Plugin有哪些? 如何实现一个Webpack Plugin?
Plugin是用于在打包过程中执行一些额外的操作,如文件压缩、代码分离、资源优化、生成HTML文件等。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Webpack-plugin:
-
Html-Webpack-Plugin
:依据一个简单的 index .html模板,生成一个自动引用你打包后的 JavaScript文件的、新的 index.html文件。 -
Mini-Css-Extract-Plugin
:分离CSS到单独的文件中。 -
Clean-Webpack-Plugin
:在每次构建之前清理 /dist文件夹,确保只包含最新的构建文件。 -
Define-Plugin
:允许在编译时创建配置的全局常量,这可以对开发模式和发布模式的构建允许不同的行为非常有用。 -
Copy-Webpack-Plugin
:将单个文件或整个目录复制到构建目录中。 -
Compression-Plugin
:为资源文件生成gzip或Brotli压缩版本。 -
Optimize-CSS-Assets-Plugin
:压缩CSS资源。 -
Hot-Module-Replacement-Plugin
:实现模块热替换功能,可以在不重新加载整个页面的情况下更新各种模块。
要实现一个Webpack-Plugin,你需要遵循Webpack-Plugin API。以下是创建一个简单Webpack-Plugin的基本步骤:
创建一个Node.js模块:首先,你需要创建一个Node.js文件,这个文件就是你的插件。
实现
apply
方法:在插件中,你需要实现一个apply
方法。这个方法会被Webpack compiler调用,并且可以在这个函数中访问compiler对象。使用compiler对象:compiler对象暴露了很多钩子(hooks),你可以在这些钩子上挂载自定义函数来扩展Webpack的功能。
返回插件实例:最后,你需要导出一个插件实例,这样Webpack才能使用它。
// my-custom-plugin.js class MyCustomPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('MyCustomPlugin', (compilation, callback) => { console.log('My custom plugin is working!'); // 异步插件使用 callback() 来通知Webpack已经完成工作 callback(); }); } } module.exports = MyCustomPlugin; //在你的webpack.config.js文件中,你需要添加这个插件到你的配置中: // webpack.config.js const MyCustomPlugin = require('./my-custom-plugin'); module.exports = { // ... plugins: [ new MyCustomPlugin(), // ... ], // ... };
loader和plugin的区别
Loader 主要用于对模块的源代码进行转换。在 rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin 负责在打包过程中执行额外的操作,优化输出结果。在 plugins 中单独配置,类型为数组,每一项是一个Plugin的实例,参数都通过构造函数传入。
webpack4.0有哪些优化点
- 性能优化:Webpack 4.0在性能优化方面做了大量的工作,包括更快的构建速度,更小的输出体积,以及更好的缓存机制。这些改进都极大地提升了开发者的体验。
- 零配置:Webpack 4.0引入了零配置的概念,对于许多常见的项目类型,Webpack 4.0可以自动推断出配置,无需开发者手动设置。这大大简化了Webpack的使用过程。
- 更好的模块联邦:Webpack 4.0引入了模块联邦的概念,允许不同的Webpack构建在运行时共享代码。这有助于减少代码的冗余,提升代码的复用性。
- 动态导入和代码拆分:Webpack 4.0支持ES6的动态导入语法,允许开发者将代码拆分成多个小块,按需加载。这有助于提升应用的启动速度,减少用户的等待时间。
- 插件系统的改进:Webpack 4.0的插件系统也得到了改进,使得插件的编写和使用都更加简单和直观。
说说 WabPack打包的流程。
WebPack是一个打包工具, WebPack可以将项目中使用的脚本开发语言CoffeeScript、Type Script、样式开发语言Less或者Sass“编译”成浏览器能识别的 JavaScript和CSS文件。
具体流程如下:
(1)通过 entry配置入口文件。
(2)通过 output指定输出的文件。
(3)使用各种 loader处理CSS、 JavaScript、 image等资源,并将它们编译与打包成浏览器可以解析的内容等。
Q:HTTP相关问题
HTTP1.0、1.1和2.0都有什么区别?1.1的持久连接和2.0的多路复用有什么区别?
在连接处理、性能优化、安全性以及报文传输四个方面来看:
从连接处理的角度来看:
- HTTP1.0使用短连接,即每个请求/响应后都会关闭连接。这意味着每次客户端发送请求时,都需要与服务器建立一个新的TCP连接,并在请求完成后断开连接。
- 相比之下,HTTP1.1引入了持久连接的概念,允许在同一个连接上发送多个请求和响应,从而减少了连接建立和关闭的开销。
- HTTP2.0则进一步提升了性能,它使用多路复用技术,允许在一个单一的连接上并行处理多个请求和响应,极大地提高了网络资源的利用率。
在性能优化方面:
- HTTP1.1相对于HTTP1.0做出了多项改进。例如,它支持请求管道化,即在一个持久连接上可以同时发送多个请求,从而提高了请求的并发处理能力。此外,HTTP1.1还引入了更多的缓存控制机制,如ETag和If-None-Match等,使得缓存管理更为灵活和高效。
- 而HTTP2.0则更进一步,它通过二进制分帧、头部压缩、服务器推送等技术,进一步提升了网络传输的效率和性能。
在安全性方面:
- HTTP2.0基于HTTPS,天然具有安全特性,可以避免单纯使用HTTPS的性能下降。这使得HTTP2.0在保护用户数据安全的同时,也保证了网络传输的高效性。
从报文传输的角度来看:
- HTTP1.x的报文是基于文本的,
- HTTP2.0则将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。这种二进制格式使得协议具有更多的扩展性,同时也提高了报文传输的效率。
持久连接和多路复用的区别:
HTTP/1.1的持续连接通过减少连接建立和关闭的开销提高了网络性能,但请求仍然是按顺序进行的;
而HTTP/2.0的多路复用则通过在一个连接上并行处理多个请求和响应,请求和响应可以按照任意的顺序进行传输和处理。
HTTP的请求报文和响应报文包含哪些部分?
请求报文包含3部分:
(1)请求行,包含请求方法、URI、HTTP版本信息。
(2)请求首部字段。
(3)请求内容实体。
响应报文包含3部分:
(1)状态行,包含HTTP版本、状态码、状态码的原因短语。
(2)响应首部字段。
(3)响应内容实体。
HTTP中有哪些请求方式?
- GET:(查)请求指定的页面信息,并返回实体主体。get的参数都是放在url中的,可以被缓存,截取直接就能获取数据,get请求的数据会放在url后面,使用?A=B格式(A是名称,B是参数)发送,这个url有长度限制,一般为1024字节
- POST:(增)向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中(传输的数据量不受限制)。。post的数据都是放在RequestBody,可以进行一次加密,相对安全些。
- PUT:(改)从客户端向服务器传送的数据取代指定的文档内容
- DELETE:(删)删除文件,与PUT方法相反,删除对应URL位置的文件。
put和post的区别:
- put请求:(幂等)如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来修改资源)。
- Post请求:(非幂等)后一个请求不会把第一个请求覆盖掉。(所以Post用来增加资源)。