本文笔记内容来自视频:爪哇教育 2021大厂前端核心面试题详解(二)-路白
跟着视频敲一遍,加强记忆,整理,方便复盘
手动搬砖,有不对的地方欢迎大佬指出
一、有做过前端加载优化相关的工作吗?都做过哪些努力。
做性能优化的目的是什么?
- 首屏时间
- 首次可交互时间
- 首次有意义内容渲染时间
常见的优化手段?
- 只请求当前需要的资源
- 异步加载、懒加载、polyfill的优化
- 缩减资源体积
- 打包压缩 webpack 4
- gzip
- 图片格式的优化、压缩,根据屏幕分辨率展示不同分辨率的图片
- 尽量控制cookie的大小
- 时序优化
- js promise.all 并发发送请求
- ssr 把打包放在服务端,由服务端做渲染输出,方便做不同缓存;不是放在CDN 上;方便seo
- prefetch, prerender, preload
// 加载遇到xxx.com时立即进行dns的预解析
<link rel="dns-prefetch" href="xxx1.com" />
<link rel="dns-prefetch" href="xxx2.com" />
<link rel="preconnect" href="xxx1.com" />
// 预加载图片资源
<link rel="preload" as="image" href="https://aaa.com/xxx.png" />
- 合理利用缓存
- cdn cdn预热 cdn刷新
- http缓存
- localStorage, sessionStorage
思考题🤔:如果一段js执行时间长,怎么去分析?(装饰器计算函数执行时间)
// 装饰器decorator
export function measure(target: any, name: string, descriptor: any) {
const oldValue = descriptor.value;
descriptor.value = async function () {
console.time(name);
const ret = await oldValue.apply(this, arguments);
console.timeEnd(name);
return ret;
}
return descriptor;
}
// 验证
export default class Home extends Vue {
public longTimefn(){
return new Promise((resolve) => setTimeout(resove, 3000));
}
// 通过measure 可以知道在控制台created输出执行多长时间
@measure
public async created(){
await this.longTimefn();
}
}
思考题🤔:阿里云oss支持通过链接后面拼参数来做图片的格式转换,尝试写一下,把任意图片格式转换为webp,需要注意什么?
- 判断浏览器是否兼容webp:caniuse.com/ & webp格式转换
- 注意考虑一些边界问题
function checkWebp() {
try {
return (
document.createElement('canvas')//创建canvas元素,可把图片转成base64格式(base64开头是带图片格式的)
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0
) // 判断浏览器是否支持webp
} catch (e) {
return false;
}
}
const supportWebp = checkWebp();
export function getWebpImageUrl(url) {
if(!url){
throw Error('url 不能为空')
}
// 是否是base64格式
if(url.startsWith('data:')){
return url;
}
//是否支持webp
if(!supportWebp) {
return url;
}
return url + '?x-oss-processxxxx';//进行字符串拼接
}
思考题🤔:如果有巨量的图片需要展示,除了懒加载的方式,有没有其他方法限制一下同时加载图片数量? (实质:代码题,实现promise并发控制)
- Promise.race()返回第一个完成的结果,结果可以是resolves,也可以是rejects
function limitLoad(urls, handler, limit){
const sequence = [].concat(urls);
let promises = [];
promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(()=>{
retuen index;
})
});
let p = Promise.race(promises);
for (let i = 0; i < sequence.length; i++) {
p = p.then((res) => {
promises[res] = handler(sequence[i]).then(() => {
return res;
});
return Promise.race(promises);
})//链式完成顺序推入
}
}
二、平时有关注过前端的内存处理吗?
1. 你了解js中的内存管理吗?什么情况会导致内存泄露?
-
内存的生命周期
- 内存分配:声明变量,函数,对象的时候,系统会自动分配内存。
- 内存使用:即读写内存,也就是调用,使用变量、函数等的时候。
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存。
-
Js中的内存分配
const n = 123; // 给数值变量分配内存 const s = “azerty”; // 给字符串分配内存 const o = { a: 1, b: null }; // 给对象及其包含的值分配内存 -
Js中的内存使用
使用值的过程实际上是对分配内存进行读取与写入操作。读取与写⼊可能是写⼊⼀个变量或者⼀个对象的属性值,甚⾄传递函数的参数。var a = 10; // 分配内存 console.log(a); // 对内存的使⽤ -
js中的垃圾回收机制
垃圾回收算法主要依赖于引用的概念。
在内存管理的环境中,⼀个对象如果有访问另⼀个对象的权限(隐式或者显式),叫做⼀个对象引⽤另⼀个对象。
例如:⼀个Javascript对象具有对它原型的引⽤(隐式引⽤)和对它属性的引⽤(显式引⽤) 在这⾥,“对象”的概念不仅特指 JavaScript 对象,还包括函数作⽤域(或者全局词法作⽤域)。
- 4.1 引用计数算法。垃圾回收。缺陷:循环引用,内存泄露
- 引⽤计数算法定义“内存不再使⽤”的标准很简单,就是看⼀个对象是否有指向它的引⽤。 如果没有其他对象指向它了,说明该对象已经不再需了。
- 但它却存在⼀个致命的问题:循环引⽤。
- 如果两个对象相互引⽤,尽管他们已不再使⽤,垃圾回收不会进⾏回收,导致内存泄露。
- 4.2 标记清除算法。
- 标记清除算法将“不再使⽤的对象”定义为“⽆法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使⽤的。 那些⽆法由根部出发触及到的对象被标记为不再使⽤,稍后进⾏回收。
- 在运行的时候给存储在内存的所有变量加上标记;
- 从根部触发,能触及的对象,把标记清除;
- 那些还存在有标记的就被视为将要删除的变量;
- 最后垃圾收集器会执⾏最后⼀步内存清除的⼯作,销毁那些带标记的值并回收它们所占⽤的内存空间。
- 标记清除算法将“不再使⽤的对象”定义为“⽆法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使⽤的。 那些⽆法由根部出发触及到的对象被标记为不再使⽤,稍后进⾏回收。
- js中,有哪些常见的内存泄露?
- 全局变量
function foo() { bar1 = ‘some text’; // 没有声明变量 实际上是全局变量 => window.bar1 this.bar2 = ‘some text’ // 全局变量 => window.bar2 } foo(); window.bar1 = null;//不用之后一定释放内存- 未被清除的定时器和回调 如果后续 renderer 元素被移除,整个定时器实际上没有任何作⽤。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器⽆法被内存回收, 定时器函数中的依赖也⽆法回收。在这个案例中的 serverData 也⽆法被回收。
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById(‘renderer’); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 每 5 秒调⽤⼀次 //清除定时器 clearTimeout(); clearInterval();- 闭包 一个内部函数,有权访问包含其的外部函数中的变量。下面这种情况下,闭包也会造成内存泄露
这段代码,每次调⽤ replaceThing 时,theThing 获得了包含⼀个巨⼤的数组和⼀个对于新闭包 someMethod 的对象。 同时 unused 是⼀个引⽤了 originalThing 的闭包。这个范例的关键在于,闭包之间是共享作⽤域的,尽管 unused 可能⼀直没有被调⽤,但是someMethod 可能会被调⽤,就会导致⽆法对其内存进⾏回收。 当这段代码被反复执⾏时,内存会持续增⻓。var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 对于 ‘originalThing’的引⽤ console.log(“hi”); }; theThing = { longStr: new Array(1000000).join(‘*’), someMethod: function () { console.log(“message”); } }; }; setInterval(replaceThing, 1000);- DOM的引用
很多时候,我们对DOM的操作,会把DOM的引用保存在一个数组或者Map中。
下面的案例中,即使我们对于image元素进行了移除,但是仍然有对image元素的引用,依然无法对其进行垃圾回收。此时,
elements.image = null;就很有必要了
const elements = { image: document.getElementById('image‘) } document.body.removeChild(document.getElementById('image‘)); elements.image = null;//对象的属性仍然存在,记得释放 - 如何避免内存泄露
- 减少不必要的全局变量,使⽤严格模式避免意外创建全局变量。
- 使用完数据后,及时解除引用(闭包中的变量,dom引⽤,定时器清除)
- 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
思考题🤔:实现sizeOf函数,传入一个参数object,计算这个object占用了多少bytes?
考察:1. 对于计算机基础,js内存基础的考察; 2. 递归; 3. 细心程度
- number: 64位存储,8个字节
- string: 每个长度2字节
- boolean: 4字节
const xxx = {};
const testData = {
a: 111, // 8字节 + key:2字节 = 10
b: 'aaa', // 6字节 + key:2字节 = 8
2222: false, // 4字节 + key:2字节 = 6
c: xxx,//注意:相同引用的内存是不占用额外的内存空间的 2+2 = 4
d: xxx // 2字节 + 2 = 4
}//共32字节
const seen = new WeakSet();
// 对对象的处理
function sizeOfObject(object) {
if(object === null){
return 0;
}
let bytes = 0;
// 注意:对象里的key也是占用内存空间的!
const properties = Object.keys(object);
for(let i = 0; i < properties.length; i++){
const key = properties[i];
bytes += calculator(key);//无论是什么类型,key都应该算上!
if(typeof object[key] === 'object' && object[key] !== null) {
if(seen.has(object[key])){
continue;// 有坑
}
seen.add(object[key]);
}
// bytes += calculator(key);// 有坑
bytes += calculator(object[key]);
}
}
function calculator(object) {
//判断类型,还可以:Object.prototype.toString.call(object) === '[object Object]'
const objectType = typeof object;
switch(objectType){
case 'string':{
return object.length * 2;
}
case 'boolean':{
return 4;
}
case 'number':{
return 8;
}
case 'object':{
if(Array.isArray(object)) {
//对数组的处理,递归处理
// [1,2,3,4]
// [{a:1},{b:2}]
// 通过map把每个元素都经过calculator再处理一次,一直递归到不是object类型为止,再通过reduce做加法处理。
return object.map(calculator).reduce((res,current) => res + current,0)
}else{
// 对对象的处理
return sizeOfObject(object);
}
}
}
}
console.log(calculator(testData));//32
参考自 object-sizeof