1. 首屏加载和白屏加载时间的计算
- 白屏时间:
- 概念:是指浏览器开始显示内容的时间。一般认为浏览器开始渲染< body>标签或者解析完head标签的时刻就是页面白屏结束的时间点。
- 原因:
- 弱网络下,网络延迟,js加载延迟会阻塞页面
- 客户端存在bug,缓存模块错乱,不缓存js等
- 优化方法:
- 加快js执行速度,可以使用js先渲染一个屏幕范围内的东西
- 减少文件加载体积,如html压缩,js压缩
- 在首屏直接同步渲染html,后续的滚屏等再采用异步请求数据和渲染html
- 统计白屏数量
- 监听某个主div的变化(单页面的应用,总会有个入口div来监听),白屏就是该div没有在规定时间内放入东西,所以只要在规定时间内该div没有变化,那就可以进行白屏统计。
- 计算白屏时间
responseStart - navigationStart- responseStart:为浏览器从服务器、缓存或者本地资源接收到响应的第一个字节之时的 Unix时间戳
- navigationStart:表征了从同一个浏览器上下文的上一个文档卸载(unload)结束时的UNIX时间戳
let timing=performance.timing; let start=timing.navigationStart; let firstPainttime=0; firstPainttime=timing.responseStart-start; console.log(firstPainttime)
- 首屏时间:
- 概念:是指用户看到的第一屏。就是网页顶部大小为当前窗口的区域显示完整的时间。首屏时间是用户对一个网站的重要体验因素。
- 计算首屏时间:
- 页面标签法:适用首屏内容不需要拉取数据才能生存以及页面不考虑图片等资源的加载。
- 首屏高度内图片加载法:可以把首屏内加载最慢的图片的加载时间当作首屏时间。这样的化可以在DOM树构建完成后去
遍历首屏内所有的图片标签
,并监听所有图片的onload事件
,最终获取到遍历的图片标签中加载时间的最大值
,用这个最大值减去navigationStart
就可获取近似的首屏时间。
- 优化方法
通常的页面加载模式:将css放在head标签里,为了避免阻塞将js放在页面底部,这样的话页面渲染完还要等待js的加载,js拉取数据及js的渲染,比较慢。
首屏优先模式:- 首屏数据拉取逻辑置于顶部。为了让数据第一时间返回相比起将数据放在外部资源会少一个js加载资源的往返时间
- 首屏渲染css及js逻辑优先内联html。是为了当html文档返回时css和js能够立即执行。
- 次屏逻辑延后处理和执行。各种数据上报最好延后处理,这样可以减少阻塞。
- DOM树构建时间
指浏览器开始对基础页文件内容进行解析到文本中构建出一个内部数据结构(DOM树)
的时间。开发人员创建domready事件,domready在DOM加载之后及资源加载之前被触发,在本地浏览器中以DOMContentLoaded事件的形式被使用。 - 整页时间
整页时间是指页面完成整个加载过程
的时刻。
loadEventEnd - navigationStart
在传统方法采集中,会使用window对象的loaded事件来记录时间戳,表示浏览器认定该页面已经载入完全了。
2. html常用文本标签
p / span / i / br / strong / h1 / a / sub / sup / hr / code
3. 浏览器兼容性问题及处理方法
-
浏览器兼容性问题是指:网页在不同浏览器上的显示效果不一致。一般分为两种:
- 浏览器不识别内容
- 浏览器虽然识别了,但是自身定义的范围不一致或者默认的参数不同。(浏览器内核的不同)
- webkit:Chrome / Safari
- gecko:FF
- trident:IE
- Presto:Opera7及以上
-
如何处理兼容
CSS Hack
:针对不同浏览器写不同code。主要分为三类- IE6:识别的hack是
_
*
- IE7/遨游:识别的是
*
。#id{*display:block;} - 其他(IE8 Chrome FF Safari Opera等):
- IE8:#id{margin-top:10px 9;/* IE8 */}
- FF:@-moz-document url-prefix(){ #id{ display: block; } }
- Safari:@media screen and (-webkit-min-device-pixel-ratio:0){#id { display: block; }}
- IE6:识别的hack是
- 不同浏览器标签默认的
外补丁和内补丁的不同
(随便写的标签,不加样式控制的情况下,各自的margin和padding差异较大)- *{margin:0;padding:0;} 通配符 * 来设置各个标签的内外补丁是0
IE6双边距问题
。块属性标签float之后,又加了横向的margin
的情况下,ie6显示的margin比设置的大。- 在float的标签属性里面加入
display: inline
;将其转换为行内属性。(比如:在给 div 加 float后,用 margin 实现横向间距就会出现这个问题,这是可以加 display: inline)
- 在float的标签属性里面加入
- 当
标签的高度设置小于10px,在IE6 / IE7 中超出自己的高度
。- 给
超出高度的标签设置overflow: hidden
; 或者设置行高line-height小于你的设置高度
- 给
图片默认有间距
- 使用
float给图片布局
- 使用
边距重叠问题
:当相邻的两个元素都设置了margin时,margin会取最大值- 为了不重叠,可以给子元素外
增加一个父元素
,并给父元素加 overflow:hidden
- 为了不重叠,可以给子元素外
- cursor:hand显示的手型在Safari上面不支持
- 统一使用
cursor:pointer
- 统一使用
- 两个块级元素,父元素设置了overflow:hidden,子元素设置了position:relative;且高度大于父元素,在IE6 / IE7中会被隐藏而不是溢出
- 父级元素设置
position:relative
- 父级元素设置
4. 数组去重
-
[…new set(arr)]
let a = [1, 2, 2, 3, 4, 4, 5, 6, 6]; let b = [...new Set(a)]; console.log(b); // [1, 2, 3, 4, 5, 6]
-
Array.from + Set
- Set是ES6新提供的数据结构。类似于数组,但是本身没有重复值。
- Array.from方法用于将两类对象转为真正的数组:
- 类似数组的对象
- 可遍历的对象
let a = [1, 2, 2, 3, 4, 4, 5, 6, 6]; let b = Array.from(new Set(a)); console.log(b); // [1, 2, 3, 4, 5, 6]
-
Map对象和filter方法
- Map对象是ES6提供的一个新的数据结构,其中has的办法是返回一个布尔值,表示某个值是否存在当前的Map对象之中,set的办法是给Map对象设置key/value。
- filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
let arr = [1, 2, 2, 3, 4, 4, 5, 6, 6]; let res = new Map(); let b = arr.filter((a) => !res.has(a) && res.set(a, 1)); console.log(b); // [1, 2, 3, 4, 5, 6]
-
for循环嵌套,利用splice去重。es5
var arr = [1, 2, 2, 3, 4, 4, 5, 6, 6]; for(var i = 0; i < arr.length; i++){ for(var j = i+1; j < arr.length; j++){ if(arr[i] === arr[j]){ arr.splice(j, 1); j--; } } } console.log(arr); // [1, 2, 3, 4, 5, 6]
-
建新数组,利用indexOf去重。es5
var arr = [1, 2, 2, 3, 4, 4, 5, 6, 6]; var newArr = []; for(var i = 0; i < arr.length; i++){ // 如果newArr中没有arr[i] if(newArr.indexOf(arr[i]) === -1){ newArr.push(arr[i]); } } console.log(newArr); // [1, 2, 3, 4, 5, 6]
5. 深拷贝与浅拷贝
区别:如果B复制了A,当修改A时,B发生了变化,就是浅拷贝,拿人手短。如果B没变,就是深拷贝,自食其力。
浅拷贝只复制对象的第一层属性,深层次的对象级别就拷贝引用,深拷贝可以对对象的属性进行递归复制。
原理解析:
比如说基本数据类型(string,number,boolean,null,undefined,symbol)的 name 和 value 都存在于 栈内存中。像下面一样,如果b要拷贝a的话,栈内会给b新开辟一个内存,所以此时修改a的值,对b不会产生影响。当然这种也不算是深拷贝,因为深拷贝只是针对object类型数据。


再比如说引用数据类型(对象{a: 1},数组[1, 2, 3],函数)的name是存在栈中,value是存在堆中,但是栈内存会提供一个引用地址指向堆内存的值。

当b要拷贝a时,其实复制的是a的引用地址,而并非堆里面的值
所以当要改变a[0]的值=1时,由于a和b指向的都是同一个地址,所以a变了的话b自然也就受到了影响,这就是浅拷贝了。
而像上面举例到的基本数据类型那样,为引用数据类型在堆内存中也开辟了一个新的内存来存放b的值,这样的话就达到深拷贝的效果了。

深拷贝和浅拷贝都只是针对引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创建一个一模一样的对象,新对象跟旧对象不共享内存,修改了新对象后不会修改到原对象。
- 浅拷贝方法
- 只复制第一层
function a(){
let obj1 = {
a: 1,
b: 2,
c: {
d: 3
}
}
var obj2 = Array.isArray(obj1)? [] : {};
for(let i in obj1){
obj2[i] = obj1[i];
}
obj2.a = 5;
obj2.c.d = 6;
console.log(obj1.a); // 1
console.log(obj2.a); // 5
console.log(obj1.c.d); // 6
console.log(obj2.c.d); // 6
}
- object.assign
function a() {
let obj1 = {
a: {
b: 1
},
c: 2
};
let obj2 = Object.assign({}, obj1);
obj2.a.b = 3;
obj2.c = 3;
console.log(obj1.a.b); // 3
console.log(obj2.a.b); // 3
console.log(obj1.c); // 2
console.log(obj2.c); // 3
}
- 直接用=赋值
function a() {
let a = [0, 1, 2, 3, 4],
b = a;
console.log(a === b); // true
a[0] = 1;
console.log(a, b);
}

-
深拷贝
- JSON.Stringify & JSON.parse(但是这种方法会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。)
function deepClone(obj1){ let _obj = JSON.stringify(obj1); let obj2 = JSON.parse(_obj); return obj2; } var a = [1, [2, 3], 4, 5]; var b = deepClone(a); console.log(a); console.log(b);
- 递归实现深拷贝
function deepClone(obj){ let objClone = Array.isArray(obj)?[]:{}; if(obj && typeof obj === 'object'){ for(var key in obj){ // hasOwnProperty判断对象是否包含特定的自身(非继承)属性 if(obj.hasOwnProperty(key)){ // 判断obj[key]是否为对象,如果是,递归复制 if(obj[key] && typeof obj[key] === 'object'){ objClone[key] = deepClone(obj[key]); }else{ // 如果不是,简单复制 objClone[key] = obj[key]; } } } } return objClone; } var a = [1, [2, 3], 4, 5]; var b = deepClone(a); a[0] = 9; console.log(a, b); a[1][0] = 8; console.log(a, b);
6. SEO
合理的title,description,keywords
。搜索对这三项的权重逐渐减小,title强调重点,description把页面内容高度概括,keywords列举出重要关键词。- < title>网站标题< /title>
- < meta name=”Description” Content=”你网页的简述”>
- < meta name=”Keywords” Content=”关键词1,关键词2,关键词3,关键词4″>
语义化的html代码,符合W3C规范
。语义化代码让搜索引擎容易理解网页。非装饰性的img必须有alt
。重要的html代码放在最前
。搜索引擎是从上到下抓取html的,所以要保证重要内容一定要被抓取到。少用iframe
。搜索引擎不会抓取iframe中的内容。提高网站速度
。网站速度是搜索引擎排序的一个重要指标。
7. JS为什么是单线程?
javascript是单线程,与它的用途有关。作为浏览器脚本语言,js的主要作用是与用户互动以及操作DOM,这决定了它只能是单线程,否则会带来很多复杂的问题。比如说js如果同时有两个线程的话,一个线程在某个DOM节点上面添加内容,而另一个线程则是删除了这个节点,这时浏览器应该以哪个线程为主?
所以,为了避免这种复杂性,从一诞生,javascript就是单线程。
但是为了利用多核CPU的计算能力,HTML5提供了web worker标准,允许js脚本同时创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个新的标准并没有改变js单线程的本质。
8. JS是单线程的,那它是如何实现异步操作的?
JS是单线程的,但是JS是在浏览器中运行的脚本语言。它的宿主,浏览器可不是单线程。
9. 事件轮询 Event Loop
先看一段代码
console.log('hi');
setTimeOut(function cb1(){
console.log('cb1')
})
console.log('bye')
执行后的打印顺序就像下面图中的结果一样。

- 同步代码,一行一行放在call stack 中执行
- 遇到异步代码,会先记录下,等待时机(定时,网络请求等)
- 时机到了,就移动到callback queue
- 如果call stack为空(即同步代码执行完), event loop开始工作。(
每次call stack清空(即每次轮询结束),即同步任务执行完。都是DOM重新渲染的机会,DOM结构如有改变则重新渲染。然后去触发下一次Event Loop
) - 轮询查找callback queue,有则移动到call stack中执行
- 然后继续轮询查找(永动机一样)
DOM事件和event loop
- 异步(setTimeOut, ajax等)使用回调,基于event loop
- dom事件也使用回调,基于event loop
- js是单线程的,而且和DOM渲染公用同一个线程。所以js执行的时候,得留一些时机供DOM渲染
10. 宏任务(macroTask)和微任务(microTask)
- 宏任务:DOM渲染后触发。宏任务(Web APIs)是由浏览器规定的。
- setTimeOut
- setInterval
- ajax
- DOM事件
- 微任务:DOM渲染前触发。微任务是由ES6语法规定的。
- Promise
- async/await
微任务执行比宏任务要早
demo👇
console.log(100);
setTimeOut(() => {
console.log(200);
})
Promise.resolve().then(() => {
console.log(300);
})
console.log(400);
// 100
// 400
// 300
// 200

test
-
1 首先执行console.log(1),因为输出没抛出error,所以就不会触发catch,直接是.then
Promise.resolve() .then(() => { console.log(1); }) .catch(() => { console.log(2) }) .then(() => { console.log(3) }) // 1 // 3
-
2 首先执行console.log(1),抛出error,触发catch执行console.log(2),执行完之后没有抛error,所以接着.then执行console.log(3)
Promise.resolve() .then(() => { console.log(1); throw new Error("error1"); }) .catch(() => { console.log(2); }) .then(() => { console.log(3); }) // 1 // 2 // 3
-
3 首先执行console.log(1),抛出error,触发catch执行console.log(2),执行完之后没有抛error,所以不会触发下面的catch
Promise.resolve() .then(() => { console.log(1); throw new Error("error1"); }) .catch(() => { console.log(2); }) .catch(() => { console.log(3); }) // 1 // 2
-
4
async function fn(){ return 100; } (async function () { const a = fn(); const b = await fn(); })(); // promise // 100
-
5 c这块接不住await传过来的失败状态的结果,因为失败的话要用catch来接收,而这块的 const c = await Promise.reject(300); 是相当于用.then来接收了,所以会报错
(async function () { console.log('start'); const a = await 100; console.log('a', a); const b = await Promise.resolve(200); console.log('b', b); const c = await Promise.reject(300); console.log('c', c); console.log('end') })(); // start // a, 100 // b, 200 // 报错
-
6
console.log(100); setTimeOut(() => { console.log(200); }) Promise.resolve().then(() => { console.log(300); }) console.log(400); // 100 // 400 // 300 // 200
-
7
- 1-5时,所有的同步代码执行完毕(event loop – call stack 被清空)
- 执行微任务 6 / 7
- 尝试触发DOM渲染
- 触发event loop 执行宏任务 8
async function async1(){ console.log("async start"); // 2 await async2(); // 先执行然后await // await下面作为回调内容 =》微任务 console.log("async1 end"); // 6 } async function async2(){ console.log("async2"); // 3 } console.log("script start"); // 1 // 宏任务 setTimeout(function(){ console.log("settimeOut"); // 8 }, 0) async1(); // 初始化promise传入的函数会立刻执行 new Promise(function (resolve) { console.log("promise 1"); // 4 resolve(); }).then(function(){ // 微任务 console.log("promise 2"); // 7 }); console.log("script end"); // 5

-
8 执行结果为 1 3 6 4 5 2
console.log(1); setTimeout(function(){ // 宏任务欧 console.log(2); }, 0) new Promise((resolve) => { console.log(3); // 初始化promise传入的函数会立即执行 resolve(); }).then(function(){ // 微任务 console.log(4); }).then(function(){ // 微任务 console.log(5); }) console.log(6);
-
9 闭包。执行结果 0 1 0。f1,f2是两个函数,拥有两个不同的函数作用域。第二次调用f1()时,因为是闭包,第一次变量i没有被销毁,还保存在内存中,所以第二次调用的时候打印出来1
function foo() {
var i = 0
return function () {
document.write(i++);
}
}
var f1 = foo();
var f2 = foo();
f1(), f1(), f2()
- cookie相关
-
概念
cookie是纯文本,没有可执行的代码。是指某些网站为了识别用户身份、进行session跟踪而存储在本地终端(浏览器)上面的数据(通常会加密)。默认为临时存储,浏览器关闭时会被销毁。要想长时间存储一个cookie,就需要设置cookie的过期时间。- 当用户访问了某个网站的时候,我们就可以通过cookie在访问者电脑上存储数据。
- 或者某些网站为了辨别用户身份,进行session跟踪而将数据存储在用户本地终端上(通常经过加密)。
-
cookie的工作过程
当网页发送http请求时,浏览器会先检查是否有相应的cookie,有的话就自动添加到request header中的cookie字段中
。这是浏览器自动帮我们做的,而且是每一次http请求浏览器都会自动帮我们做。
存储在cookie中的数据,每次都会被浏览器自动放在http请求中。这对于那种每次请求都需要携带的信息(如身份验证)就很适合。
过程:- 假设当前域名下还是没有 Cookie
- 接下来,浏览器发送了一个请求给服务器(这个请求是还没带上 Cookie 的)
- 服务器设置 Cookie 并发送给浏览器(当然也可以不设置)
- 浏览器将 Cookie 保存下来
- 接下来,以后的每一次请求,都会带上这些 Cookie,发送给服务器
-
cookie的特征
- 不同浏览器存放的cookie位置不同,也是不能通用的
- cookie的存储是以域名形式进行区分的,在同一浏览器下不同的域名存储的cookie是独立的
- cookie也可以设置过期时间,默认是会话结束时,时间到期后会自动销毁
- 一个浏览器能创建的cookie最多为300个,并且每个不超过4kb,每个web站点能设置的cookie总数不能超过20个
- 我们可以设置cookie生效的域(当前设置cookie所在域的子域),也就是说我们可以操作当前域及当前域下面的所有子域
- cookie必须在html文件的内容输出之前设置。如果用户在浏览器上设置了禁止cookie,则cookie不能建立
-
cookie的设置
- 客户端设置
document.cookie = '名字=值'; document.cookie = 'username=doris;domain=baike.baidu.com'; //并且设置了生效域
- 服务端设置:不管是请求一个资源文件(html/css/js/图片),还是发送一个ajax请求,服务端都会返回response,而response header中有一项叫
set-cookie
,是服务端专门用来设置cookie的。
//Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的): Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
注意:一个set-cookie只能设置一个cookie,如果你想设置多个cookie,就需要添加同样多的set-cookie字段。服务端可以设置cookie的所有选项:expires、domain、path、secure、HttpOnly。通过set-cookie指定这些选项只会在浏览器端使用,而不会被发送到服务器端。
-
cookie的domain(域概念)
domain指定了cookie要被发送到哪个或者哪些域中。默认情况下,domain会被设置为创建该cookie的页面所在的域名,所以当给相同域名发送请求时,该cookie会被发送至服务器。浏览器会把domain的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将domain能匹配到的域名的cookie发送至服务器。- 客户端设置
document.cookie = "username=doris;path=/;domain=qq.com"
上面的domain设置为qq.com,表示访问域名尾部是qq.com的网站时浏览器都会将该cookie带上,path值为"/",表示访问qq.com域名下的根目录下的都将能带上该cookie
- 服务端设置
Set-Cookie: username=doris;path=/;domain=qq.com // 注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。
-
cookie的路径概念
因为安全方面的考虑,默认情况下,只有与创建cookie的页面在同一个目录或者子目录下的网页才能访问。但path属性可以为cookie指定路径,domain和path加起来构成了URL,表示当浏览器在访问URL下的网站或者URL带有这个前缀的网站时都将会带上该cookie。- 客户端设置
最常用的例子就是让cookie在根目录下,这样不管在哪个子页面创建的cookie,该域名下的所有页面都能访问到
document.cookie = "username=doris; path=/"
- 服务端设置
Set-Cookie:name=doris; path=/blog
如上设置:path选项会与/blog,/blogrool等相匹配,任何以/blog开头的选项都是合法的。需要注意的是,只有在domain选项核实完毕之后,才会对path属性进行比较,path属性的默认值是发送set-cookie消息头所对应的url中的path部分。
- 客户端设置
-
domain与path总结
domain是域名,path是路径。两者加起来就构成了URL。domain和path一起来限制cookie能被哪些URL访问,所以domain和path两个选项共同决定了cookie何时能被浏览器自动添加到请求头中发送出去。如果没有设置这两个选项,则会使用默认,domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。
js操作cookie
JavaScript 可以使用 document.cookie
属性来创建 、读取、及删除 cookie。
-
创建cookie
document.cookie="username=John Doe"; // 添加过期时间(以 UTC 或 GMT 时间)。默认情况下,cookie 在浏览器关闭时删除 document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT"; // 添加浏览器 cookie 的路径。默认情况下,cookie 属于当前页面 document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
-
读取cookie
通过document.cookie来获取当前网站下的cookie的时候,得到的是字符串形式的值,它包含了当前网站下所有的cookie,这个方法只能获取非 HttpOnly 类型的cookie。它会把所有的cookie通过一个分号+空格的形式串联起来,例如:username=chenfangxu; job=codingvar x = document.cookie;
-
修改cookie
要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。document.cookie="username=John Smith; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
-
删除cookie
把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
- 如何用原生js实现一个图片的下载?
- 创建一个a标签
- 将a标签的href属性赋值为图片的base64编码
- 指定a标签的download属性,作为下载文件的名称
- 触发a标签的点击事件
// 这里是获取到的图片base64编码,这里只是个例子哈,要自行编码图片替换这里才能测试看到效果
const imgUrl = 'data:image/png;base64,...'
// 如果浏览器支持msSaveOrOpenBlob方法(也就是使用IE浏览器的时候),那么调用该方法去下载图片
if (window.navigator.msSaveOrOpenBlob) {
var bstr = atob(imgUrl.split(',')[1])
var n = bstr.length
var u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
var blob = new Blob([u8arr])
window.navigator.msSaveOrOpenBlob(blob, 'chart-download' + '.' + 'png')
} else {
// 这里就按照chrome等新版浏览器来处理
const a = document.createElement('a')
a.href = imgUrl
a.setAttribute('download', 'chart-download')
a.click()
}