1、手撕防抖与节流、树与对象的转换、递归调用,链表头插法
1.1、防抖
防抖函数用于延迟执行某个函数,直到过了一定的间隔时间(例如等待用户停止输入)后再执行。
即后一次点击事件发生时间距离一次点击事件至少间隔一定时间。
javascript
function debounce(fn, wait) {
let timer = null
return function () {
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.call(this, arguments)
}, wait)
}
}
1.2、节流
节流函数用于限制函数的执行频率,确保一定时间内只执行一次。
javascript
//时间戳版
function throttle(fn, wait) {
let date = Date.now()
return function () {
let now = Date.now()
if (now - date > wait) {
fn.call(this, arguments)
date = now
}
}
}
//定时器版
function throttle(fn, wait) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.call(this, arguments)
timer = null
}, wait)
}
}
}
1.3、树与对象的转换
参考作者之前的文章
2、水平垂直居中方法
①flex布局,父元素display:flex; justify-content:center; align-items:center;
②父元素position:relative; 子元素position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
③父元素position:relative; 子元素position:absolute; left:0; top:0; bottom:0; right:0; margin:auto;
④文字的话, text-align:center; line-height 和 height 相等
3、 手写ajax(使用promise封装)
javascript
function getJSON(url) {
let promise = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open("GET", url, true)
xhr.onreadystatechange = function () {
if (this.readyState !== 4) return
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.onerror = function () {
reject(new Error(this.statusText))
}
xhr.responseType = "json"
xhr.setRequestHeader("Accept", "application/json")
xhr.send(null)
})
return promise
}
4、扁平数组
javascript
const flatten = (arr) => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
};
arr1 = [1, [2, 3], [4, [5, [6, 7], 8]]];
5、setTimeout实现setInterval
javascript
function myInterval(func, time) {
let ids = [];
function fn() {
let id = setTimeout(() => {
func();
fn();
}, time);
ids.push(id);
}
fn();
return ids;
}
let id = myInterval(() => {
console.log("Hello World");
}, 500);
function clearMyInterval(idList) {
idList.forEach((id) => {
clearTimeout(id);
});
}
setTimeout(() => {
clearMyInterval(id);
}, 3000);
6、输入url发生了什么?
-
URL 解析: 浏览器解析输入的 URL。
-
DNS 解析: 如果域名需要解析,进行 DNS 解析。如果已经有缓存的 DNS 记录,可以跳过此步骤。
-
检查缓存: 浏览器检查缓存,看是否已经有了之前请求过的资源的副本。这包括检查浏览器缓存和可能存在的代理服务器缓存。
-
有缓存: 如果资源已经存在于缓存中,并且没有过期,浏览器可以跳过后续的步骤,直接使用缓存中的资源渲染页面。
-
无缓存或缓存过期: 如果资源不存在于缓存中,或者缓存已经过期,浏览器将按照正常的流程发起网络请求,
-
TCP 连接:拿到IP地址后,三次握手建立TCP连接,https的话还需要进行TLS加密协议的握手过程
-
**发送请求,获取响应:**连接建立成功之后,浏览器会构建请求行、cookie等数据附加到请求头中,发给服务器,服务器接受请求并解析,如果没有对应的资源就404;否则检查HTTP请求头有没有包含协商缓存信息(前面命中强缓存且已过期的话就会走这个步骤),如果验证缓存没有更新,过期的缓存依然可以使用,就返回304和空响应体;如果没有缓存或者资源更新了,就读取完整请求并准备http响应,进行查询数据库等操作,返回200和查询到的资源
-
TCP 连接: 浏览器接收到响应数据之后,如果是http1.1以下则直接关闭连接,否则双方都可以根据情况选择关闭TCP连接或者保留重用,现在浏览器默认都会保持连接(keep-alive)
-
浏览器渲染: 浏览器使用获取到的资源渲染页面。
缓存是一种重要的性能优化手段,可以减少网络请求,加快页面加载速度。缓存策略通常由服务器端和浏览器端一起决定,可以通过 HTTP 头部信息来进行配置。例如,使用 Cache-Control
头部可以控制缓存的行为,而 ETag
和 Last-Modified
头部可以用于验证缓存是否过期。
7、加载js和css会不会阻塞页面渲染
css用link和@import的情况
link标签引入css资源时在火狐浏览器中是异步加载的,在谷歌浏览器中是同步加载的。
但如果是通过style标签引入样式,则不论何种浏览器,均为同步加载。
@import是在网页完全载入后才加载,在关键路径上创造了更多的网络请求,阻塞渲染时间,影响浏览器的并行下载,多个@import导致下载顺序紊乱。
8、重排重绘
重排:当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
①添加、删除可见的dom
②元素的位置改变
③元素的尺寸改变(外边距、内边距、边框厚度、宽高等几何属性)
④页面渲染初始化
⑤浏览器窗口尺寸改变
重绘:是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
如何减少reflow、repaint?
①不要一条一条的修改DOM的样式,可以先定义好css的class,然后修改DOM的className。
②不要把DOM结点的属性值放在一个循环里当成循环里的变量。
③为动画的HTML元件适用fixed或absolute的position,那么修改他们的css是不会reflow
9、深浅拷贝
浅拷贝:基本数据类型、扩展运算符()、slice()、concat()、Object.assign()
深拷贝:JSON.parse(JSON.stringify())、手写深拷贝、lodash
手撕深拷贝
javascript
let obj = {
lili: { name: "lili", person: ["lisan", "zhangsan"] },
arr: [1, 2, 3, 4],
fruit: "apple",
};
function deepclone(obj) {
// 检查是否是基本数据类型,如果是则直接返回
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
let newArray = [];
for (let i = 0; i < obj.length; i++) {
newArray[i] = deepclone(obj[i]);
}
return newArray;
} else {
let newObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepclone(obj[key]);
}
}
return newObj;
}
}
console.log(deepclone(obj));
10、手撕合并函数
javascript
function deepMerge(target, source) {
// 检查参数类型
if (typeof target !== "object" || typeof source !== "object") {
throw new Error("Both target and source must be objects");
}
// 遍历source对象的属性
for (const key in source) {
if (source.hasOwnProperty(key)) {
// 如果属性是对象且存在于target中,递归深度合并
if (
typeof source[key] === "object" &&
source[key] !== null &&
target.hasOwnProperty(key) &&
typeof target[key] === "object" &&
target[key] !== null
) {
if (Array.isArray(target[key])) {
target[key] = [].concat(target[key], source[key]);
} else {
target[key] = deepMerge(target[key], source[key]);
}
} else {
// 否则直接赋值
// target[key] = source[key];
target[key] = [].concat(target[key], source[key]);
}
}
}
return target;
}
// 使用例子
const targetObject = {
name: "John",
age: 30,
address: {
city: "New York",
zip: "10001",
people: { class: "1班" },
},
hobbies: ["shopping"],
};
const sourceObject = {
age: 31,
address: {
zip: "10002",
},
hobbies: ["reading", "traveling"],
};
const resultObject = deepMerge(targetObject, sourceObject);
console.log(resultObject);
11、Object和map
共同点:键值对的动态集合,支持增删
不同点:
①构造方式不同
//map
const map = new Map()
const map1 = new Map([['a',1],['b',2]])
//obj
const obj = new Object()
const obj1 = Object.create()
②object键的类型必须是String或者Symbol、map键的类型可以是任意类型
③object中key是无序的,map中可以是有序的,按照插入的顺序返回
④object只能通过Object.key()方法或for in统计数量,map有map.size
⑤object可以通过点或中括号访问属性,map用map.get()
⑥object不具备Iterator特性,不能for of遍历,map的keys()、values()、entries()都具有迭代器
⑦object可以用JSON.stringify()进行序列化,map只能转化成JSON,不能被parse解析
⑧应用场景:object做数据存储,需要序列化时使用;map频繁更新键值对,key类型未知时使用
12、http和https的区别
①https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。
②http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议。
③http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
④http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
13、堆和栈
13.1、区别
①堆栈空间分配区别(操作系统):
栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
堆 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
②堆栈缓存方式区别:
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
③堆栈数据结构区别:
栈是一种先进后出(FILO)的数据结构;
堆可以被看成是一棵树,如:堆排序。
13.2、最大堆和最小堆?
最大堆:根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。
最小堆:根结点的键值是所有堆结点键值中最小者,且每个结点的值都比其孩子的值小。
13.3、 堆栈溢出
- JavaScript 的函数调用栈有一定大小限制,当函数调用的嵌套层数过多时,会导致栈溢出错误。
javascript
function recursive() {// 递归出现栈溢出
recursive();
}
recursive();
- JavaScript 的堆也有大小限制。堆是用来存储变量和对象等数据的一段内存空间,当我们创建了大量数据或者数据太大而超过了堆的容量时,就会触发堆溢出错误。
javascript
let arr = [];
while (true) {// 堆溢出
arr.push('a');
}
14、进程和线程
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
进程和线程的区别:
①进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
②进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
③线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC) 进行。
④但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
15、promise.all,promise.any,promise.race,promise allsettled
Promise.allSettled
不会在其中一个 Promise 失败时立即 reject,而是等待所有 Promise 完成后再返回结果。
javascript
const promise1 = Promise.resolve(42);
const promise2 = Promise.reject("Oops!");
const promise3 = new Promise((resolve) => setTimeout(() => resolve("Done!"), 1000));
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
console.log(results);
// results 包含了每个 Promise 的状态和结果
// [{ status: 'fulfilled', value: 42 }, { status: 'rejected', reason: 'Oops!' }, { status: 'fulfilled', value: 'Done!' }]
})
.catch((error) => {
console.error("Error:", error);
});
Promise.all
在所有 Promise 全部成功(resolved)时才会成功,但只要有一个 Promise 失败(rejected),整个 Promise.all
就会立即失败。这种行为被称为"一败俱败"。
传递给 Promise.all
的数组为空: 如果传递给 Promise.all
的 Promise 数组为空,返回的 Promise 会立即被解决为一个空数组。
如果在某一个promise中reject后,那一个函数捕捉catch,就不会报错。不然就会走all的catch
16、场景题:100000条数据渲染
javascript
const renderList = async () => {
const list = await getList()
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)
const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
17、你做的项目如何部署到服务器
javascript
function myNew(constructor, ...args) {
// 步骤 1:创建一个新的空对象
const obj = {};
// 步骤 2:将新对象的 __proto__ 指向构造函数的 prototype 属性
obj.__proto__ = constructor.prototype;
// 步骤 3:将构造函数的上下文传递给构造函数,并执行构造函数
const result = constructor.apply(obj, args);
// 步骤 4:如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
return result instanceof Object ? result : obj;
}
18、路由鉴权
18.1、请求数据
-
用户登录: 用户在登录时,通过用户名和密码等方式向后端发起登录请求。后端验证用户身份,并在登录成功后生成一个 Token。
-
Token 存储: 将生成的 Token 存储在前端,通常是通过 localStorage。axios请求拦截,如果存在token,在每次请求时自动附加到请求头。
-
请求时携带 Token: 在每次请求后端受保护资源时,将 Token 携带在请求头中,通常是通过 Authorization 头或自定义头。
-
后端验证 Token: 后端接收到请求后,通过解析 Token 验证用户身份和权限。如果 Token 有效且权限足够,则返回相应资源;否则,返回错误状态。
18.2、路由守卫
根据权限决定跳转: 如果用户拥有访问权限,正常放行,让用户访问受保护页面。如果用户没有权限,可以将其重定向到登录页或其他提示页面,或者显示相应的提示信息。
javascript
// 路由表
const routes = [
{ path: '/', component: Home, meta: { requiresAuth: true } },
{ path: '/admin', component: Admin, meta: { requiresAuth: true, requiresAdmin: true } },
{ path: '/login', component: Login }
];
// 创建路由实例
const router = new VueRouter({
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = /* 根据用户身份信息判断用户是否已登录 */;
const isAdmin = /* 根据用户身份信息判断用户是否是管理员 */;
if (to.meta.requiresAuth && !isAuthenticated) {
// 用户未登录,重定向到登录页
next('/login');
} else if (to.meta.requiresAdmin && !isAdmin) {
// 用户不是管理员,可以根据需要进行处理,例如重定向到首页
next('/');
} else {
// 用户有权限,放行
next();
}
});
// 在 Vue 实例中使用路由
new Vue({
el: '#app',
router,
render: h => h(App)
});
18.3、不同等级的权限控制
-
获取用户权限信息: 在用户登录成功后,获取token与用户的权限信息,存储在Vuex或者localStorage
-
定义菜单权限配置: 在前端定义一个菜单权限配置,包含不同权限下允许访问的菜单项。这可以是一个简单的 JSON 对象或数组,其中每个菜单项都包含一个权限属性,表示需要的用户权限。
-
根据用户权限过滤菜单项: 根据用户拥有的权限,从菜单权限配置中筛选出符合条件的菜单项。
-
动态生成菜单: 使用框架或库的路由功能,根据过滤后的菜单项动态生成菜单。这可以在页面加载时或用户登录成功后执行。
javascript
// 菜单权限配置
const menuConfig = [
{ path: '/dashboard', name: 'Dashboard', permission: 'view_dashboard' },
{ path: '/users', name: 'Users', permission: 'manage_users' },
// 其他菜单项
];
// 获取用户权限信息(模拟)
const userPermissions = ['view_dashboard', 'manage_users']; // 实际中应该根据登录成功后的用户信息获取权限
// 根据用户权限过滤菜单项
const userMenu = menuConfig.filter(item => userPermissions.includes(item.permission));
// 在 Vue 实例中使用路由
const router = new VueRouter({
routes: userMenu,
});
// 示例组件中动态生成菜单
Vue.component('Sidebar', {
template: `
<div>
<router-link v-for="item in userMenu" :to="item.path" :key="item.path">
{{ item.name }}
</router-link>
</div>
`,
data() {
return {
userMenu,
};
},
});
// 在 Vue 实例中使用路由和菜单组件
new Vue({
el: '#app',
router,
render: h => h(App),
});