前台、中台、后台
- 前台:面向用户、客户可以感知的,如商城
- 中台:可以看着对前台的补充,公共服务功能,如支付系统、搜索系统、客服
- 后台:面向运营、比如商品管理、物流管理
1、如何将数据非常大的数据渲染而不卡顿 / js完成一个无限循环的动画
- 定时器setInterval,做数据分割,批次渲染
- 高级进阶:requestAnimationFrame (H5新特性)cancalAnimationFrame
- 帧动画,每一帧会执行一次的定时器,一般每秒60帧(1000/60)
- 优势
- requestAnimationFrame会把每一帧中的DOM操作集中起来,完成一次重回或回流,接着就刷新浏览器频率
- 隐藏和不可见的元素中,requestAnimationFrame将不会进行重绘和回流,这意味减少CPU、 GPU、内存的消耗
2、性能优化
- 网络
- DNS预解析 dns-prefetch,文档顶部插入DNS预解析,先查询IP,用的时候可以直接拿对应的IP
- <link rel="dns-prefetch" href="https://www.baidu.com">
- 缓存
- 强缓存
- expires 受限本地时间,如果修改本地时间,可能会造成缓存过期
- cache-control
- no-catch
- 使用缓存前,会向服务器发起请求,
- 确认资源是否更改(Etag),返回304,表示可以使用缓存
- 通常配合 private 属性, 表示内容只缓存到私有缓存中
- no-store 绝对禁止缓存,所用内容都不被缓存
- max-age=30 (1 属性) 30秒后过去
- cache-control的优先级要高于expires
- no-catch
- 协商缓存(缓存校验)1 客户端和服务端共同实现 last-modified 、Etag
- 1
- Last-Modified 服务器检查缓存时间与服务器资源最后修改的时间是否一致
- 一致返回 304,表示可以使用浏览器缓存
- 不一致返回200,表示使用最新的资源
- Etag 标识符,弥补last-modified 缺陷,只要内容被修改,Etag就会改变。避免修改时间改变,内容没变情况,服务器对资源进行hash运算
- Last-Modified 服务器检查缓存时间与服务器资源最后修改的时间是否一致
- 请求发送的时候会在缓存中寻找是否有对应的请求
- 如果没有,则重新向服务器请求数据
- 如果有,则有俩中情况
- 如果有请求没有过期,则使用缓存结果
- 如果请求过期,则带上缓存的标识,重新请求
- 如果服务器返回304,表示请求资源没有更新,客户端则使用缓存
- 如果服务器返回200,表示请求资源有更新,使用新的请求结果,并和请求标识重新保持到缓存中。
- 频繁变动的资源,可以使用cache- control: no-cache;
- 总结:
- cache-control / expires 第一层,以绝对时间和相对时间校验资源
- last-modified / etag 第二层,浏览器和服务器通讯,两者校验资源是否更新
- 还有缓存 service Worker独立线程,离线缓存、消息推送、网络代理 push cache 属于http2内容
- 响应文件采用GZIP压缩
- 预加载 preload
- <link rel="preload" href="">
- 降低首屏的加载时间
- 预渲染 prerend
- <link rel="prerend" href="">
- 提高页面的加载速度,但要确保用户一定会打开,比如首页
- 懒执行
- 让某些逻辑延迟到使用时候计算,用于首屏优化
- 懒加载
- 不关键的资源延后加载,可见区域,按需加载 observe,或者图片loading=“lazy”属性
- 路由懒加载
- const hello = () => import('./hello.vue');
- conponent: relsove => require(['@/view/hello.vue'], relsove)
- 组件懒加载
- const hello = () => import(/* webpackChunkName: "hello" */, './hello.vue')
- 打包后的懒加载文件,路由访问时,通过script标签引入对应的JS文件,prefetch 空闲加载css
- 文件优化
- 图片优化
- 一些修饰性的小图标可以用css替代
- 小图片可以转换成base64
- 多个小图标可以合成一张雪碧图
- 支持webp的可以使用webp
- 根据不同屏幕选择不同尺寸的图片
- CSS放在head中
- JS放在body下方,避免堵塞HTML渲染
- 在JS引用可以加async或 defer进行异步加载
- defer 是有序加载和执行,等DOM加载完才执行
- async是无序的,加载完即执行,不与DOM操作有关,一般用于第三方
- CDN
- 静态资源尽量使用CDN加载,因为浏览器对单个域名并发请求有上限。
- 静态资源的CDN避免和主域名重名,因为相同域名在请求时会带上cookie参数,有安全风险。
- webpack优化
- 打包使用mode: production模式,productSourceMap: false; 这样会自动开启代码压缩
- ES6模块开始tree-shaking,移除没有使用的代码
- 打包时添加哈希值,实现浏览器缓存文件
- 代码分割splitchunk.catchgroup,插件 splitCkunkPlugin
- UI 框架组件,按需引入模块
- SSR服务器渲染
- 利用服务器优选渲染部分重要内容,其他内容可以懒加载,如js \ next.js
- nuxtjs对应的是vue
- 基于js的通用应用框架
- SSR服务器渲染静态页面,在服务端生成HTML,然后发送给客户端
- 异步加载数据,中间件支持,布局支持
- 有利于SEO,加载速度快,自动配置路由
- 依赖node和npm npx
- 优势:
- 有利于SEO:不同爬虫工作原理只爬取源码,不执行网站的脚本,除了google的高级爬虫会执行脚本后渲染的页面。而单页面SPA大多数页面是JS动态生成,因此抓取的页面是空白页面
- 更有利于首屏渲染:因为客户端直接从服务端获取完整HTML内容并解析渲染,无需请求数据,等待响应渲染,而且SPA单页面应用首次加载打包后的文件比较大,会一个较长的等待时间。
- 缺点:
- 服务端压力大
- 学习成本相较高
- 图片优化
- 1
- 强缓存
- DNS预解析 dns-prefetch,文档顶部插入DNS预解析,先查询IP,用的时候可以直接拿对应的IP
3、安全
同源策略,协议、域名、端口号必须都相同,否则视为跨域,这是一种保护网页不受入侵的策略。
- XSS
- 跨站脚步攻击
- 原理:
- 攻击者在网站注入恶意的代码,利用恶意的代码对客户端进行篡改,从而在用户访问页面时,控制浏览器行为或获取用户的私密数据的一种攻击方式。
- 共同行为:将一些私密数据,如cookie、session发给攻击者,或将受害者重定向到一个有攻击者控制的网站,从而在受害者机器是进行操作。
- 方式
- 存储型XSS
- 将恶意代码保存到目标网站的服务器中,如博客、社交论坛等
- 行为
- 攻击者将一条包含XSS代码的留言后者其他数据提交到服务器
- 当目标用户浏览时,XSS内容会从服务器解析之后加载出来。
- 浏览器将恶意代码当着正常脚本来执行,从而获取浏览器所具有的权限命令
- 反射行XSS
- 通过引诱用户点击以一个恶意代码链接跳到目标网站来实现。
- 行为
- A给B发送一个恶意的URL
- B点击URL跳转到具有恶意的网站
- 网站在B的浏览器中执行JS,可以执行B具有的权限命令
- 防御
- 输入检查:检查输入不合法的特殊字符
- 输出检查 :使用HTML、URL编码,转义输入的内容
- 使用httpOnly:禁止页面的JS带有httpOnly属性的cookie,主要对抗XSS之后防止cookie被劫持,服务器向浏览器设置cookie,Set-Cookie: HttpOnly
- 输入长度限制
- 开启CSP(Content Security Policy)安全策略:指定可信的内容来源,如脚本、图片、style等远程资源,实质是白名单制度。
- 在HTTPS头信息的Content-Security-Policy字段设置
- 在网页中设置 <meta>标签 <meta http-equiv="Content-Security-Policy" content="">
- CSRF
- 跨站伪造请求:攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户校验,冒充用户执行某些操作
- 原理:
- 受害者登录正常A站,并保留了登录凭证
- 攻击者诱使受害者访问B站
- B站向A站发起请求,浏览器默认会携带A点的Cookie
- A站接收请求后会对其进行验证,且误认为是受害者发起的请求,同时以受害者身份执行对应的操作
- 方式
- GET类型
- 一般利用 img 标签发起,在受害者访问恶意网站时,浏览器会自动向src指向的地址发送请求
- POST类型
- 通常是构造一个自动提交的表单在页面上,模拟用户去完成一次POST操作
- 链接类型
- 通常是需要诱骗用户点击才会触发
- 防御:
- 请求时附带验证信息,如验证码或token
- 客户端使用账号和密码登陆后,服务器接收到请求,验证账号密码,正确后会生成一个token,将token返回给客户端,客户端将token存在cookie或者localStorage中,以后每次请求都会带上token作为身份识别。如果放在session Storage中,那么每次关掉会话重新打开都要频繁认证。
- 一定要在https中加密传输token
- 设置 sameSite 属性,设置Cookie不随跨域请求发送
- 设置Referer,检验请求发送的域名是不是第三方网站发起的
- get请求不对数据进行修改
- 不让第三方网站访问到用户cookie
- 阻止第三方网站请求接口
- 请求时附带验证信息,如验证码或token
- 点击劫持
- 在页面中覆盖一层透明化的内容层,用底层的内容诱导用户去点击,实际上是触发透明层的点击事件,从而执行用户不知情的操作
- 特点
- 盗取用户资金
- 获取用户敏感信息
- 与XSS和CSRF相结合,诱骗用户点击恶意链接
- 防御
- 服务器可设置X-Frame-Options
- Session 和 Token:都是服务端存储,解决两个无状态的请求关联问题
- session 是空间换时间,存储在服务端,消耗服务端内存,对比session是否一致
- Token 是时间换空间,就是每次请求都要经过 token 算法,比较计算的token和携带的token是否一致
- GET类型
- 存储型XSS
3、跨域
- 协议、域名、端口号,只要有一个不同就是跨域
- JSONP
- 利用script标签没有跨域限制实现,但是它只能使用get请求
- 非正式传输协议,允许用户传递一个callback给服务端,然后服务端返回数据时将这个JSON数据作为参数放在callback中,这样用户的就可以通过callback调用自定函数,使用传入的JSON数据。
- 不是正真的AJAX
- ajax的核心是通过XMLHttpRequest来实现
-
var xhr = new XMLHttpRequest(); xhr.open('POST', '/server', true); // 异步 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') xhr.send(data); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { ... } }
-
-
- jsonp的核心是调用服务器提供的js脚本
- 只支持get,ajax支持get和post
-
- 跨域资源共享CORSaccess-control-allow-origin: *,需要服务器配置运行跨域请求
- domain= 'test.com' 表示子域名相通可以实现跨域
- postMessage通常用于获取潜入页面的第三方页面数据
- nginx代理跨域
-
// 发送消息 window.parent.postMessage('message', 'https://test.com') // 接收信息 var mc = new MessageChannel() mc.addEventListener('message', event => { var origin = event.origin || event.originalEvent.origin; if (origin === 'https://test.com') { ... } })
4、浏览器渲染机制
- 解析HTML文件并构建DOM树
- 解析CSS并构建CSSOM树
- 将DOM和CSSOM合并成渲染树(render tree)
- 根据渲染树来布局,计算每个节点的位置
- 调用GPU绘制,合成图层,显示在屏幕上
- 构建CSSOM树会阻塞渲染,十分消耗性能,层级越多,执行速度越慢
- CSSOM树构建会阻塞到JS执行和DOM渲染
5、Load和DOMContentLoaded
- load事件触发代表页面中的DOM、CSS、JS、图片都加载完毕
- DOMContentLoaded 事件触发代表初始的HTML被完全加载和解析,不需要等待CSS、JS、图片等
6、重绘与回流
- 重绘是当节点需要更改外观而不影响布局,比如改变color,background-color
- 回流是布局或者几何属性需要改变
- 添加或删除可见的DOM
- 改变元素的位置,尺寸
- 页面初始化、浏览器窗口resize变化
- 获取某些属性:Top、scrollTop、width、height
- 回流必定重绘,重绘不一定发生回流,回流所需的成本比重绘要高
- 避免回流和重绘:
- 尽可能在DOM树的最末端改变class,这样对前面DOM不产生影响
- 避免设置多层内联样式
- 动画效果尽量应用到脱离文档元素,如absolute / fixed
- 避免使用table,改动一点,全部要回流
- 使用CSS3硬件加速,让transform、opacity、filters等动画效果不引起回流重绘
- 避免用JS去直接修改样式,使用添加和删除 class
- 避免频繁操作DOM,使用虚拟DOM
- 元素的隐藏和现实可以使用display来实现
7、算法
- 1+2+3+...+n 递归
-
// 求和 function sum(n) { if (n === 1) return 1; return sum(n - 1) + n; } // 求阶乘 function fn(n) { if (n === 1) return 1; return fn(n-1)*n } // 求斐波那契数列 1,1,2,3,5 function fb(n) { if (n === 1 || n === 2) return 1; return fb(n-1) + fb(n-2); } // 递归处理导航菜单数据
- 深度拷贝,递归
- 递归:函数内部调调其本身,必须有return,否则死循环
- 如下:
// 深拷贝:JSON.parse(JSON.stringify(obj)) // 浅拷贝:Object.assign(obj), 只拷贝指针,object只拷贝第一层 var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for(var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; }
- 多维数组转为一维数组
-
如下:
// [1, [2],[3, [4, 5]]] 转 [1,2,3,4,5] const flatten = (arr) => { let res = []; arr.forEach((item, i, curArr) => { if (Array.isArray(item)) { res = res.concat(flatten(item)) } else { res.push(item) } }) return res; }
- 数组去重ld
-
如下:
// 1、Set [...new Set(arr)] // 高效率 利用obj唯一key,Object.keys(obj) function unique(arr) { var result = {}; for (var i = 0; i < arr.length; i++) { if (!result[arr[i]]) { result[arr[i]] = true; } } return Object.keys(arr); } // 2、forEach + indexOf (缺点 arr.indexOf(NaN) === -1) // 3、filter + indexOf (缺点 arr.indexOf(NaN) === -1) arr.filter((val, index, item) => { return arr.indexOf(val) === index; // indexOf返回第一个匹配元素的下标 }) // 4、reduce + includes arr.reduce((per, cur) => { return per.includes(cur) ? per : per.concat(cur); // concat可以并入非数组 }, []) // 5、forEach + includes
8、闭包机制
- 外部函数可以访问函数内部的变量,本质就是在一个函数内部创建另一个函数
- 特点
- 函数的嵌套函数,子函数的对象可以访问父函数的对象,反之不可以
- 能够读取其他函数内部私有化变量,
- 参数和变量不被垃圾回收机制回收
- 垃圾回收机制:为了防止内存泄露,垃圾回收机制不断的寻找那些不再使用的变量,并释放它所指向的内存。
- 解释:一个对象不被引用,或者两个对象相互引用,但不被第三个函数引用,这两个情况下的对象都会被垃圾回收机制回收
- 标记清除:大部分浏览器使用这种方式
- 当变量进入执行环境时,垃圾回收器会对该变量进行打标,当该变量离开环境时,则会再次打标,随之清除
- 引用计数:低版本常常引起内存泄露
- 追踪一个引用类型的引用次数,当一个变量赋值该引用类型时,引用次数加1,当该变量赋值其他值时,该引用类型减1,当引用次数为0,则对引用类型进行垃圾回收。
- 好处:
- 可以读取函数内部变量
- 可以让这些变量的值始终保持在内存中,即缓存。
- 缺点:
- 因为函数内的变量无法被垃圾回收机制所回收,因此会消耗内存,如果变量很多,则有可能会造成内存泄露
- 两种情况
- 如果函数没有别外部变量引用,而直接执行,那么垃圾回收机制会回收变量
- 如果函数被外部变量引用,那么垃圾回收机制不会回收闭包中的变量
- 将变量赋值为 null 可消除占用内存
-
// 闭包的实际应用,私有变量始终保存在内存中,可以改变它的值,并不立即释放,因为内部变量赋值给了外部全局变量,外部全局变量不知道什么时候会被引用。 function fun() { var i = 0; return function() { console.log(i++); } } fun(); // 没有打印,因为返回的是函数,垃圾机制会将变量 i 会被回收 fun()(); // 0,因为i++,先赋值再加, ++i,先加再赋值,i=1存在内存中 fun()(); // 0,重新运行fun导致i重新赋值为0 var f1 = fun(), f2 = fun(); // 先取出return的匿名函数,重新赋值i=0 f1(); // 0,i++的原因,此时内存中 i = 1; f1(); // 1,此时没有重新运行fun,而是return的匿名函数,读取内存中的i f2(); // 0,f2是重新运行了fun导致i重新赋值为0
- 标记清除:大部分浏览器使用这种方式
- 特点
9、HTTP 状态码
- 200 请求成功
- 301 资源永久被转移到了其他URL
- 302 临时转移
- 304 客户端有缓存可以使用
- 400 客户端请求的语法错误,无法理解
- 401 请求要求用户的身份认证
- 403 服务器理解客户端请求,但拒绝执行此请求
- 404 请求资源不存在,找不到了
- 500 内部服务器错误
- 502 网关或代理服务器接收到无效响应
- 503 超载或系统维护,服务器暂时无法处理请求
11、常见的内存泄露
- 函数中的全局变量:每次执行该函数时都会生成该变量,并且不会随着函数执行结束后而释放
- 未清除的定时器:它内部引用变量不会释放
- 脱离DOM的元素引用:一个DOM容器删除之后,变量未设置null,则其内部的dom元素不会释放
- 持续绑定的事件:函数中addEventListener绑定事件,函数执行多次,绑定便会产生多次,产生内存泄露
- 闭包
- log:console.log的对象是不会被垃圾回收
12、HTTP与HTTPS
- http默认端口号:80/8080,是HTTP协议运行在TCP之上。所有传输内容都是明文,客户端与服务器端都无法验证对方的身份
- https默认端口号:443,是HTTP运行在SSL之上,所有传输的内容都经过加密。
- HTTP1、0和HTTP3.0的区别
- 0
- 线程阻塞,在同一时间,同一域名的请求有一定的数量限制,超过则会阻塞
- 1 (目前最常用 TCP链接)
- 缺陷:TCP 按顺序发送,排队堵塞
- 改进:
- 持久连接,建立TCP连接,需要加 Connection: keep-alive
- 管道机制,一个TCP中客户端可以发送多个请求
- 分块传输编码
- 新增请求方式:Put、Delete、Options、connect、trace
- 0 - 过渡协议
- 特点:
- 采用二进制格式而非文本格式,
- 服务器推送
- 完全多路复用,只需一个连接即可实现并行
- 使用报头压缩,降低开销
- 0
- 彻底解决排队阻塞问题
- 采用UDP协议,不需要3次握手和4次挥手,传输速度更快
- TLS支持,加密性更好
- QUIC协议自定义了连接机制和重传机制
- 自定义流量控制
- TCP/IP是一个四层协议系统,数据链路层、网络层、传输层、应用层
- IP 为上层协议提供无状态、无连接、不可靠的服务。
- 无状态:IP双方不同步传输数据行,数据包的传输和接受是相互独立
- 无连接:IP双方不能长时间维持对方的任何信息,每次发送数据都要指明对方的IP地址
- 不可靠:IP协议不能确保IP数据包准确的到达接受端。
- TCP:面向连接、可靠的、面向字节流传输,三次握手,四次挥手,耗时长
- UDP:无连接、不可靠、面向报文传输。
- IP 为上层协议提供无状态、无连接、不可靠的服务。
- WebSocke:H5的一种新协议,允许服务端向客户端传递信息,实现双通信。
- 建立在TCP协议之上,与http协议良好兼容
- 数据比较轻量级、性能开销小,通讯高效
- 可发送文本或二进制
- 没有同源限制,可与任意服务器通信
- 特点:
- 0
13、优雅降级和渐进增强
- 优雅降级:是从一个复杂的现状开始,试图减少用户体验,来兼容不同环境的正常运行
- 渐进增强:是从一个基础的现状开始,逐步扩充功能,适应未来更新的需求。
14、typeof、instanceOf、constructer
- typeof会返回一个变量的基本类型(null除外),不能准确的判断引用数据的类型(’object‘),除了function
- instanceOf返回一个布尔值,可以准确的判断引用数据类型,但不能正确判断基础数据的类型
- 原理:判断右边参数的原型(prototype)是否在左边参数(__proto__)的原型链上
- isArray区别
- prototype 也是Array Array.isArray(Array.prototype) // true Array.prototype instanceof Array // false
- isArray({__proto__: Array.prototype}) // false {__proto__: Array.prototype} instanceof Array // true
- instanceof 不能跨iframe工作,而isArray可以
- const a = {}; a instanceof Object; // true
- constructor
- 除了undefined和null以外都能判断 , 比instanceof更为准确
- const a = {}; a.constructor === Object; // true
-
// instanceof 和 Array.isArray // instanceof 判断右边参数的原型(prototype)是否在左边参数(__proto__)的原型链上 Array.isArray(Array.prototype) // true Array.prototype instanceof Array // false const a = { __proto__: Array.prototype }; Array.isArray(a); // false; a instanceof Array; // true a.__proto__ === Array.prototype;
15、GET和POST区别
- get请求不安全,post安全
- get请求数据有大小限制,受浏览器URL的长度限制,chrome最大长度8182byte,超过服务器返回414标识
- get参数显示在URL中,容易被别人窃取,POST在请求体中
- post需要设置请求体
- get是从服务器请求数据,post是向服务器发送数据
16、箭头函数和普通函数
- 普通函数
- 可以通过bind、call、apply改变this方向
- 可以使用new,因为有原型
- 与构造函数的区别
- 构造函数首字母大写
- 构造函数必须要用new实例化,普通函数可以直接调用
- 构造函数创建时,内部会创建一个新的对象,并返回这个对象
- 构造函数内部this指向其实例,普通函数this执行调用的对象,没有则指向window
- 箭头函数
- 不可以使用new,因为箭头函数没有constructor
- 不可以通过bind、call、apply改变this方向
- 本身没有this,因为没有原型
- 它的this是其包裹它的外部普通函数,如果没有则指向window,返回undefined
- 如果包裹它的外部普通函数的this发生改变,那么箭头函数中的this也会发生改变
- call、apply、bind的相同点和区别
- 相同点
- 都能改变this指向
- 第一个参数就是this要指向的对象
- 都可以传入参数
- 不同点
- call、bind的参数是依次传入,而apply只有两个参数,第二个参数是数组
- call和apply是函数调用返回结果,而bind方法返回的仍是一个函数,需要再执行一次
- 如果传入的第一个值是this,则表示预设初始参数
-
// call实现 Function.prototype.call = function(context) { const ctx = context || window; ctx.fn = this; // this相当于将Function的实例,即调用的函数,作为ctx的方法 // 获取实参, 类数组转数组,从下标1开始去参数 const args = Array.from(arguments).slice(1); // 调用ctx的实例,传入参数,如果没有则在ctx找 const res = arguments.length > 1 ? ctx.fn(..args) : ctx.fn(); // 删除该方法,防止污染 delete cxt.func return res; } // apply实现 Function.prototype.apply = function(context) { const ctx = context || window; ctx.fn = this; // 绑定实例 // 获取实参 const args = arguments[1]; // argunments是获取不定的参数 // ctx调用实参方法,传入参数 const res = argunments.length > 1 ? ctx.fn(...args) : ctx.fn(); // 删除方法 delete cxt.func return res; // 返回运行结果 } // bind实现,注意返回的是函数 Function.prototype.bind = function(context) { const fn = this; const ctx = context || window; const args = Array.from(arguments).slice(1); // Array.from对类数组或可迭代的对象新建一个浅拷贝的数组,arguments类数组对象 return function proxy() { ctx.fn = fn; const res = ctx.fn(...args); delete ctx.fn; return res; } }
- 相同点
- 与构造函数的区别
17、同步和异步
- 同步是阻塞模式,如果一个请求需要等待回调,那么后面代码会一直等待下去,知道返回结果
- 异步是非阻塞模式,无需等前面代码回调,即可执行下去
18、事件循环机制,宏任务和微任务
- JS运行过程中主要执行事件循环:同步任务和异步任务
- 主程序从上而下执行同步任务
- 异步任务会被放入异步任务队列中
- 当同步任务执行完成后,才会去异步任务队列中执行异步事件
- 同步任务 -》微任务 -》宏任务
- 微任务:在DOM渲染之前触发
- Promise 、 async/await、nextTick 、Object.observe
- 宏任务:在DOM渲染之后触发
- script标签内运行代码、DOM事件、AJAX、setTimeout、setInterval
19、Cookie、sessionStorage、localStorage、indexdb、Service Work
- service work 是运行在浏览器后台的独立线程,一般用来实现缓存功能
- 传输协议必须是HTTPS
- service work的缓存与浏览器其他内建的缓存机制不同,他可以控制缓存文件、匹配缓存、读取缓存,并且缓存是连续性
- 存储大小:cookie只有4kb,sessionStoarge和localStorage是5M
- 有效期:
- cookie可以设置有效期,超过有效期就会自动清除,如:标记用户与跟踪用户行为
- 会话状态管理:如登录状态,购物车信息,
- 个性化设置:如自定主体、设置
- 浏览器行为跟踪:如历史脚印,关注的类目
- sessionStorage是会话缓存,关闭选项卡或者关闭浏览器就会清除,如:敏感账号一次性登录
- localStorage是永久存储,除非手动清除,如:大量商品数据
- cookie可以设置有效期,超过有效期就会自动清除,如:标记用户与跟踪用户行为
- 存储位置:cookie不设置有效期,存储在内存中,设置有效期存储在硬盘中,Storage只存储在浏览器端
- 作用域:
- sessionStorage:同一个浏览器手动输入地址的新tab是不共享的,
- cookie和LocalStorage同个浏览器同源页面是共享的
- indexDB存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexDB
- service worker 归功于PWA的流行
- 是运行在浏览器后台的独立线程,一般用来实现缓存功能
- 传输协议必须是HTTPS,本地也可以通过http://localhost跑起来
- service work的缓存与浏览器其他内建的缓存机制不同,他可以控制缓存文件、匹配缓存、读取缓存,并且缓存是连续性
42、Worker 实现多线程
- worker,它能向浏览器申请开辟一个线程来处理js,
一般用来优化UI展示、交互的任务,用来完成耗时操作,
如大量操作DOM,复杂的计算、大文件上传、主流程加载速度优化,
- var work = new Worker('js文件的路径');
- postMessage('hello')
- onmessage = function (event) {}
20、async/await、Promise
- async/await代码简洁,使异步代码看起来像同步
- async/await和Promise都是非阻塞式
- async/await是基于Promise实现,是解决地狱回调,async是Generator的语法糖,*转换async,yield转换await
- Promise有三种状态(pending等待,fulfilled已完成,reject已拒绝),不可逆的
- pending:不会触发then和catch
- fulfilled:会触发后续的then
- reject: 会触发catch
- then正常返回resolve,报错则返回reject
- catch正常返回resolve,报错则返回reject
- promise 三个方法: all、any、race,将所有promise组成一个promise
- all 只有所有promise都成功才算成功
- any 只要有一个成功就算成功,除非全部拒绝
- race 看第一个完成的promise的状态
- 优点:
- 解决地狱回调问题
- 对象不受外界影响
- 状态一旦发生改变就不能更改,不可逆
- 缺点:
- 一旦执行,不可终止
- 内部报错,需要回调抛出,用try / catch 捕获
- Promise有三种状态(pending等待,fulfilled已完成,reject已拒绝),不可逆的
21、document.ready 和 document.onload
- ready表示文件结构已经加载完毕(不包含图片),可以多次
- onload表示页面包括图片在内的所有元素都加载完成,只能有一个,多个会覆盖
22、原生(内置)对象和宿主(浏览器)对象
- 所有内置对象都是原生对象,Date 、Array、Object、RegExp、Number
- 宿主对象是宿主环境,如浏览器的对象,Location、Navgator、 Document
23、New 一个对象的过程
- 创建一个新对象,新对象的原型__proto__指向new的构造函数的原型prototype
- 将this指向这个新对象,并执行构造函数的代码,为新对象添加属性
- 最后 隐式 return this 返回新的对象,如果return其他结果,则返回其他
- 对象创建的方法:
- 字面量的方式创建 {}
- 通过 new Object() 创建
- 通过构造函数 new Person() 创建
24、原型和原型链
- 原型
- 每个构造函数都一个原型对象,实例化出来的对象都有一个原型,指向的是构造函数的原型对象,原型对象里面有个指针constructor,它指向构造函数。
- 所有原型对象都是Object构造函数的实例,实例的原型(__proto__)都是指向Object的原型对象(prototype)
- Object的原型对象(prototype)的原型(__proto__)是null
- 原型链
- 当实例化的对象访问一个属性时,首先会在该对象内部寻找,如果找不到,则会向其(__proto__)指向的原型(构造函数的原型对象)中寻找,如果还未找到,则继续向原型中__proto__指向的原型中寻找,直到找到或者null(prototype.__protot__)为止,这样形成的链式就是原型链
- 原型作用:实现属性和方法的继承,共享数据,节约内存空间
- 获取原型的方法
- __proto__
- getPrototypeOf()
- prototype
-
function Foo(){} const foo = new Foo(); foo.__proto__ === Foo.prototype Foo._proto__ === Function.prototype Foo.prototype.__proto__ === Object.prototype
25、for...in for...of 和 object.keys
- ..in 会遍历自身属性和继承的原型属性,使用hasOwnProperty可以判断自身属性
- ..of ES6新增,遍历所有数据结构的统一方法,遍历数组,Set和Map、某些类似数组的对象,如arguments对象,DOM对象,Generator对象
- keys 只遍历自身属性,不会遍历继承的原型属性
26、set 、 map 、object
- Map是键值对,键和值可以是任何类型,object的键只能是String、number或symbol,Set是值的集合
- Map可以通过get获取值,set设置值,size获取长度,..of遍历,Object可以通过属性获取值,通过遍历获取长度。
- Set通过has来判断是否有值,且值是唯一性,用于去重
- from(arr):可将一个类数组的对象或者可遍历对象转换成一个正真的数组
- [...new Set(arr)]
- Map可以作为存储数据
27、防抖和节流
- 防抖:一段时间后执行,如果有重复触发,则重新计时
- 场景:登陆按钮、窗口调整大小resize、输入框实时保存
- 节流:一段时间内只执行一次,不论触发多少次
- scroll滚动事件,一定时间内只执行一次
- 浏览器播放时间,一定时间内读取一次后续数据
28、require 和 import
- require 对应导出的方法是export,import对应的方法是export 、export default
- require 对应是CommonJs的语法,commonjs是nodejs中的模块化规范,import是ES6的语法
- require是动态加载,运行时加载模块的所有方法,import是静态加载,编译的时候调用,代码提升顶部
- require 引入的是整个模块里面的对象,import按需加载模块中的对象
- require 导出的是值的拷贝,import导出的是值的引用
export {} 对应 import {}
export default xx 对应 import xx
29、谈谈你对ES6的理解
- 新增模版字符串
- 箭头函数
- ..of
- .in 循环出来的是key,for...of 循环出来的是value,forEach 不能使用break、continue、return语句
- arguments 指向不定参数和默认参数替代
- 提供promise
- 增加 let 和 const
- 增加块级作用域
- 新增API:from Array.map Array.reduce Objec.keys Object.vuales Object.assign
- 引入module模块的概念
30、事件冒泡、事件捕获
- 事件冒泡:事件按照从最特定的事件目标到最不特定的事件目标的顺序触发,
- 一般都是把原本需要绑定到子元素上面的事件委托给父元素,让父元素去监听事件。
- 事件委托:利用事件冒泡指定一个事件处理程序,来处理其内部的某一类型的所有事件。
- 事件冒泡三个阶段:事件捕获、目标阶段、事件冒泡
- 阻止事件冒泡
- stopPropagation()
- 事件捕获:事件从最不特定的事件目标到最特定的事件目标的顺序触发
- 阻止事件默认
- preventDefault()
- 添加事件时用addEventListener(event, fn, useCapture),第三个参数时bool,用来设置事件时捕获时执行,还是事件冒泡时执行
30、H5新特性
- 语义化标签
- header、nav、footer、section、aside、article等
- 增强型表单
- type:number 、email、time、tel、data、color等
- 元素:datalist、keygen、output
- 属性:placeholder、required、min、max、multipe、autofocus、step
- 新增标签 audio、video
- canvas
- 地理定位getCurrentPosition
- 拖拽 drag
- dragstart 开始拖拽被拖拽元素时触发, 作用被拖拽元素
- drag 正被拖拽元素时触发, 作用被拖拽元素
- dragend 被拖拽元素结束时触发,作用被拖拽元素
- 通过dataTransfer.setData() getData() 来传值。
- dragenter 被拖拽元素移进入目标元素时触发, 作用目标元素
- dragover 被拖拽元素在目标元素内移动时触发, 作用目标元素
- dragleave 被拖拽元素移出目标元素时触发, 作用目标元素
- drop 目标元素接受被拖拽元素时触发, 作用目标元素
- 本地存储 sessionStorage 、localStorage
- 新事件:onresize、ondrage、onscroll、onerror、onplay、onpause
- webSocket
- 单个TCP链接上进行全双工通讯的协议,
- 下一代 H5通讯协议,持久化协议,实现服务器与客户端全双工通信,
- 更好的节省服务器资源和宽带,打到实时通讯目的。
- 块级元素和行内元素
31、JSON.stringify() 特性
- 对 undefined、函数和symbol的值
- 对象:stirngify({a: undefined})序列化时会忽略跳过
- 数组:stirngify([undefined、Fn、symbol]) === null 序列化后变成 null
- 单独:stirngify(undefined/Fn/symbol) === undefined 序列化后变成 undefined
32、数组方法
- 改变数组:
- push:从后面添加元素并返回新数组的长度
- pop:从后面删除元素并返回删除的元素
- shift:从前面删除元素并返回删除的元素
- unshift:从前面添加元素并返回新数组的长度
- splice(i, len, newVal, newVal1):删除或添加元素,i下标,len删除的长度,newVal是添加的参数,返回删除元素的数组,没有删除则返回空数组
- sort:对数组进行排序,返回排序后的数组,注意数字都是转换字符,按位比较,而不是大小
- 顺序,数字排序 -> 字母排序 -> null -> undefined
- reverse:对数组进行反转,返回反转后的数组
- 不改变数组:
- slice(start, end):从下标start开始截取,到下标end前,不包括end,如果是负数,则表示从取最后几位,返回截取的数组
- slice(-3) 截取最后三位
- slice(-3, -1) 截取倒数第三和第二
- join(undefined):将数组的元素取出来,按照传入的字符组合成字符串,返回组合的字符串
- 如果传入空 或者 undefined,则默认逗号隔开
- 如果数组中有undefined或者null,会被重置为空,但是间隔符号还在。
- concat:合并数组,重复数据不覆盖,返回合并后的数组
- indexof:读取元素下标,如果没有则返回-1
- includes:判断数组是否有该元素,返回boolean值
- forEach:遍历数组,执行函数,不能break、continue,可以if return
- map:遍历数组,执行函数,返回新的数组
- filter:遍历数组,最满足条件的元素进行重组,生成新的数组。
- reduce:求和
- every: 所以都满足才返回true
- some: 一项满足就返回true
-
arr.reduce((pre, cur, index, arr) => { // pre表示上一个求和值,开始则表示传入的init // cur 当期要处理的元素,index表示当前元素的下标,arr表示原数组 }, init)
- slice(start, end):从下标start开始截取,到下标end前,不包括end,如果是负数,则表示从取最后几位,返回截取的数组
33、数据类型
- 基础数据类型:Number、String、Unll、Undefined、Boolean、Symbol、BigInt
- Number(null) 返回 0,被定义了,但是空值
- Number(undefined) 返回 NaN,未被定义
- typeof NaN 是 number typeof null === 'object'
- 判断方法
- typeof NaN === 'number' 且 isNaN判断
- 特性,NaN !== NaN
- is() 两个值是否相等,不会隐士转换,利用NaN特性,自身不全等自身
- is(-0, +0); // false
- is(0, -0); // false
- is(0, +0); // true
- is(NaN, NaN); // true
- is(NaN, 0/0); // true
- 引用数据类型:Object(array,function、object、Date、regExp、Math)
- 判断方法
- typeof NaN 是 number typeof null === 'object'
34、v-html 和 @click、class不生效
- @click改为onclick,将method里的点击事件赋值到window方法上
- class不生效:可以说使用 /deep/ 或 >>> 穿透,新建style,不含scoped
35、Vue 子与子传值
- 使用bus
- vuex
- 通过父组件传递 $ref,storage 、provide / inject
36、nextTick使用场景
- 在created操作dom,此时DOM并没有渲染
- 某个操作改变数据,并改变dom
37、关于富文本图片放大
- 使用v-html,点击事件@click,传入$event
- 通过比较target.tagName === 'IMG'判断,然后将e.traget.src赋值给data,最后渲染出来
38、首屏白屏优化
- 减少文件体积,HTML,CSS压缩
- css放在头部,js放在body后,异步加载
- 图片合成雪碧图,图片压缩,图片CDN加载,减小服务器并发
- 懒加载,不可视区域不加载
- 尽量减少异步数据容量
- 使用本地缓存
- 服务端渲染
39、ref作用,获取真实DOM
40、JS中获取位置间距
- clientHeight 可见区域的高度,不包含滚动条和border
- offsetHeight 可见区域的高度,包含滚动条和border
- scrollHeight 整个文档高度,包含滚动隐藏的部分
- clientTop border的高度,一般是0
- scrollTop 滚动隐藏的高度,相对于文档顶部的距离
41、输入一个URL到页面过程发生了什么
- 首选在浏览器输入URL
- 查找缓存,看看浏览器缓存-系统缓存-路由缓存中是否该地址资源
- DNS解析,向DNS服务器发送请求,DNS服务器解析成IP,DNS基于UDP协议
- 建立TCP连接
- 发起HTTP请求:将请求报文作为三次握手第三次数据发送给服务器
- 服务器发送响应的数据
- 关闭TCP连接:四次挥手释放TCP
- 浏览器渲染
- 构建DOM树
- 构建CSS规则树
- 结合DOM树和CSS规则树,构建渲染树
- 计算每个节点在页面的位置
- 通过GPU进行渲染绘制
43、SEO优化
- head中的title
- js 服务端渲染
- h1标签,只能存在一个
- keywords 网页关键字,网页的关键字
- description 告诉搜索引擎站点的主要内容
- H5语义化标签: header、nav、aside、footer、section、article
- 隐藏SEO关键字,网络爬虫识别文字,如:logo不用图标,而是用a标签的背景图,标签内SEO关键字,设置font-size: 0;
- 集中权重,不重要的链接设置 rel="nofollow",让爬虫忽略跟踪。
- 扁平化结果,尽量减少网站的结构层,目录结构不超过3层,有利爬取
- 提高加载速度,一旦超时爬虫就放弃抓取
- og:富媒体对象
- og:title,og:type,og:url,image、og.site_name
- 便于社交媒体分享抓取关键信息
44、工作中遇到什么问题(技术)
- 跨域问题
- 本地开发接口,本地代理 (proxy, tableproxy)
- 资源未更新,加hash值,清缓存
- 地狱回调,promise或async await
- 数据更新不及时,nextTick
- 首屏加载慢或白屏时间过长
- 移动端bug调试,抓包工具whistle
- 数据去重,Set
- 多重嵌套表单数据校验
- 浏览器兼容,样式兼容
45、看过那些书,推荐一些
- JavaScript高级程序设计(红宝书)
- JavaScript权威指南 (犀牛书)
- 你不知道的JavaScript
- JavaScript DOM编程艺术
- 锋利的jQuery
- 深入浅出js
- js权威指南
46、如果学习一个新的框架要用多久
- 如果只是拿来实现一些常用的功能的话,一天就可以了
- 如果是掌握框架的每个要点,可能要4、5天
47、怎么实现0.5px边框
- 使用伪元素,相对定位absolute,设置1pxborder,在缩小5,scale(0.5)
- 使用border,渐变色,黑色渐变透明色,视觉效果
48、关注并想学习的框架
论坛 gitHub的issue 、stackoverflow、google、官方文档 、CSDN、MDN、codeProject
- Vue3、vite、ts、小程序
- Electron、Tauri 应用桌面端
- Svelte 、solidjs,相对React、Vue
- js,React服务器渲染框架
- Remix,React服务器渲染框架
- js,Vue服务器渲染框架
49、H5的兼容性问题
- IOS端键盘收起来后页面不归位
- 问题:输入内容,软键盘弹起,页面整体内容上移,但是收起键盘,页面内容不滑下
- 分析:软键盘占位并没有随着收起而消失
- 解决:在失去焦点时给了事件,重置滚到到当前的位置scrollTo(0, 100)
- IOS端H5页面上下滑动时卡顿,页面缺失
- 问题:IOS滑动到下一屏时会有时会出现卡顿,页面部分显示不全
- 解决:*{ -webkit-overflow-scrolling: touch; },控制元素在移动设备是否有滚动回弹效果
- IOS移动端click事件300ms的延迟
- 解决:
- js可以解决手机点击延迟
- zepto的touch模块
- tap事件
- ontouchstart事件
- IOS端绑定click事件到非button元素上,不触发事件
- 给元素添加样式: cursor: pointer;
- 解决:
50、计算页面白屏事件
在header中和body后打印一个时间,两者相减即可
51、rem计算原理
- 将屏幕的宽度分为10等分,每一等分即为1rem,整个屏幕即为10rem
- rem以 HTML 中的font-size为准,1rem = fontSize px
-
window.resize = function() { var width = document.doucmentElement.clientWidth || document.body.clientWidth; document.documentElement.style.fontSize = width / 10 + 'px'; }
52、H5的混合开发项目:H5与原生的通信技术 jsbridge
53、CSS布局:table布局、float布局、flxe布局、响应式布局、grid布局
- 静态布局
- 流式布局(栅栏、百分比、vw、vh):宽高随着屏幕大小等比例缩放
- 自适应布局(@media):不同尺寸屏幕布局会改变,但元素大小不会变
- 响应式布局(@media 和 流式布局):不同尺寸屏幕布局和元素大小都会改变
- 弹性布局(rem / em)
1、屏幕自适应 大屏:1200px pad:1023px-768px,手机 :480px - 767px
改变一般是导航栏、头部和尾部,中间主体采用flex响应式布局
- CSS @media
- @media screen and (max-width: 980px) { }
- 在head 中使用 link 链接,media属性用于查询
- <link rel="stylesheet" type="text/css" href="moxie.css" media="screen and (max-width=980px)" />
- 在vue css中 @import 导入css文件
- @import url('css/moxie.css') screen and (max-width: 980px);
2、Css优先级:important > 内联样式 > ID选择器 》 类、属性、伪类(:hover,:actived)选择器 > 标签 、伪元素(::before, ::after)选择器 > * 通配符
3、Css sprites: 为了应对多次http请求造成服务器并发运行负担,因此将多个小图片合成一张雪碧图,极好的减少了网页的http请求,从而大大的提高页面的性能。然后通过background-image/repeate/position来实现图标渲染
54、IE和标准和模型的区别
- IE:width包括 content、border和padding
- 标准:width只有 content
55、垂直居中布局
- position:absolute + transform:translate(-50%, -50%);
- position: absolute + margin;
- display: flex; jusitify-content: center; align-items: center;
- gird布局
- table-cell
56、Git和SVN区别
- Git 分布式管理,由本地仓库,可离线本提交保存
- 可以独立开发自己的需求
- 不需要等待其他人的项目,可以发布上线
- SVN 集中式管理,没有本地仓库,必须联网才能提交
- 版本控制系统管理所有文件的修订版本,所有协同开发都能获取到最新的代码,必须在规定的时间保质保量的完成所有需求才能统一发布上线。
- 管理员可以管理所有开发人员的权限
- 后提交代码可能会与前面其他人提交的代码有冲突,需要解决
- 如果服务器出现故障,所有开发者都不能提交和拉取
57、说说JS实现继承的几种方法
- 原型继承
- 继承父类的实例对象:
- prototype = new Animal();
- constructor重新指回 Cart :prototype.constructor = Cart;
- 直接继承父类原型:
- prototype = Animal.prototype;
- constructor重新指回 Cart :prototype.constructor = Cart;
- 经典继承
- 构造函数改变this:在其中的一个函数中,执行 apply/call(this, arguments)
- 组合继承方式(原型继承 + 经典继承)
-
Cat 函数中执行 Animal.apply(this, arguments); Cat.prototype = new Animal(); Cat.prototype.constructor = Cart
-
- 继承父类的实例对象:
-
- 寄生组合方式
-
Cat 函数中执行 Animal.apply(this, arguments); Cat.prototype = Object.create(Animal.prototype); Cat.prototype.constructor = Cart
-
- 寄生组合方式
58、异步图片加载的方法
loadImageAsync('./loading.jpg').then(image => document.body.appendChild(image));
function loadImageAsync(url) {
return new Promise(function(res, rej) {
const image = new Image();
image.src = url;
image.onload = function() {
res(image)
}
image.onerror = function() {
rej(new Error('没有加载图片'))
}
})
}
59、BFC盒子模型
- 看作隔离了的独立容器,容器里面的元素不会在布局上影响外部元素
- 如:overflow,position, dispaly: inline-block、flex
60、对象创建的方式
- 字面量: var obj = { a: 1 };
- Object构造函数: var obj = new Object();
- 工厂模式:函数封装字面量,传参,然后return对象
- 构造函数:new 构造函数来生成对象
- new 方法:先建一个新对象,然后将新对象的__proto__指向构造函数的原型prototype
- 将 this 指向新对象,然后执行构造函数代码,为新对象添加属性
- 最后隐式 return this 对象
62、[] == ![] true
- ![] 转成boolean
- == 类型转换number ,boolean转换0
- []转换number 也是 0
63、可视化拖拽布局思路
- 首先中间是个布局主体,它的数据是一个数组componentData,这个数组是来渲染主体
- 其次要封装一些常用组件,每个组件都要有唯一的标识符ID
- 遍历这些组件,将其渲染到左侧的组件库列表中,添加draggeable 属性, 提供拖拽功能
- 将组件拖入主体,通过 dragstart、drop 事件 getData('id') 获取对应的组件,并渲染到主体容器中。
- 通过遍历 componentData 循环 <component :is="name" />
- 再通过右侧该组件的相关的配置项进行数据配置
- 保持配置的数据后,该数据会被写入 componentData 对应的该组件数据中
- 其他组件同样操作,组成完整的 componentData ,遍历渲染出整个页面。
1、主体容器中移动组件
-
- 容器设置relative,组件设置 absolute
- 根据 mousedown 记录当前坐标,mousemove 每次鼠标移动,计算xy距离进行改变组件位置,mouseup 鼠标抬起,结束移动
2、组件排序和删除
-
- 排序可以通过拖拽的位置来判断
- 记录每个组件垂直中心距离文档顶部高度,集合一个数组;
- 然后拖拽组件所距离文档顶部高度,和数组中数据比较,处于哪两个之间
- 最后获取第一个下标,然后通 splice 将该组件的数据删除、插入重新排序
- 删除可以通过 当前组件的ID,遍历整个模板的数据数组进行删除即可
- 排序可以通过拖拽的位置来判断
难点:
1、怎么将每个组装的组件渲染出来
-
- 使用 动态组件 <template :is="tplname" :data="data">
2.、组件排序拖拽怎么实现
-
- 记录每个组件垂直中心距离文档顶部高度,集合一个数组;
- 然后拖拽组件所距离文档顶部高度,和数组中数据比较,处于哪两个之间
- 最后获取第一个下标,然后通 splice 将该组件的数据删除、插入重新排序
3、吸附功能怎么做
-
- 假设一个组件位置已经确定好了,然后移动另一个组件
- 移动组件的时候,动态获取该组件的(x/y offset())坐标和宽高
- 判断移动组件在目标组件的方位,方法:目标组件坐标 + 目标组件宽/高 > 移动组件对应的坐标
- 这差值如果 小于3 就可以吸附,将左侧总和值赋值给右边值
4、组件库数量过多,打包后文件很大
-
- 按需加载: import 引入当下需要渲染的组件
5、新老版本组件同类型同名问题
-
- 在组件数据中加上 版本号字段
- 循环遍历 import 的路径带上 版本号标识 作为路径加载组件
64、平时遇到的问题
- 1、秒杀活动怎么处理高并发和数据请求失败
- 页面数据缓存
- 静态页面处理,放到CDN上,动态数据通过ajax请求
- 安全性能,防刷、限流
- 2、页面不可视区域接口提前加载
- observe 监听
- 3、数据量大的渲染操作
- 将数据进行分割,按需渲染
补充问题:
https://juejin.cn/post/7127673540379148318#heading-9
https://www.cnblogs.com/haixiaozh/p/16589466.html
v-html放大图片
深拷贝写
ref场景使用
304
首屏白屏优化
vue子与子传值
数据类型划分
nexttick使用场景
多重对象递归,地狱回调
标签:面试题,缓存,请求,Javascript,数组,2023,元素,prototype,加载 From: https://www.cnblogs.com/cp-cookie/p/17129230.html