前端进阶知识

一、你不知道的HTML标签

meta 标签:自动刷新/跳转

1
2
<meta http-equiv="Refresh" content="5; URL=page2.html">
<meta http-equiv="Refresh" content="60">

title 标签与 Hack 手段:消息提醒

1
2
3
4
5
6
7
8
9
10
11
12
13
let msgNum = 1 // 消息条数
let cnt = 0 // 计数器
const inerval = setInterval(() => {
cnt = (cnt + 1) % 2
if(msgNum===0) {
// 通过DOM修改title
document.title += `聊天页面`
clearInterval(interval)
return
}
const prefix = cnt % 2 ? `新消息(${msgNum})` : ''
document.title = `${prefix}聊天页面`
}, 1000)

性能优化:script 标签:调整加载顺序提升渲染速度

  • async 属性。立即请求文件,但不阻塞渲染引擎,而是文件加载完毕后阻塞渲染引擎并立即执行文件内容。
  • defer 属性。立即请求文件,但不阻塞渲染引擎,等到解析完 HTML 之后再执行文件内容。
  • HTML5 标准 type 属性,对应值为“module”。让浏览器按照 ECMA Script 6 标准将文件当作模块进行解析,默认阻塞效果同 defer,也可以配合 async 在请求完成后立即执行。
  • dns-prefetch。当 link 标签的 rel 属性值为“dns-prefetch”时,浏览器会对某个域名预先进行 DNS 解析并缓存。这样,当浏览器在请求同域名资源的时候,能省去从域名查询 IP 的过程,从而减少时间损耗。下图是淘宝网设置的 DNS 预解析。
  • preconnect。让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析、TLS 协商、TCP 握手,通过消除往返延迟来为用户节省时间。
  • prefetch/preload。两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch 可能会在浏览器忙时被忽略,而 preload 则是一定会被预先下载。
  • prerender。浏览器不仅会加载资源,还会解析执行页面,进行预渲染。

搜索优化:meta 标签:提取关键信息

1
<meta content="拉勾,拉勾网,拉勾招聘,拉钩, 拉钩网 ,互联网招聘,拉勾互联网招聘, 移动互联网招聘, 垂直互联网招聘, 微信招聘, 微博招聘, 拉勾官网, 拉勾百科,跳槽, 高薪职位, 互联网圈子, IT招聘, 职场招聘, 猎头招聘,O2O招聘, LBS招聘, 社交招聘, 校园招聘, 校招,社会招聘,社招" name="keywords">

有时候为了用户访问方便或者出于历史原因,对于同一个页面会有多个网址,又或者存在某些重定向页面,比如:https://lagou.com/a.html https://lagou.com/detail?id=%22abcd%22

1
<link href="https://lagou.com/a.html" rel="canonical">

二、如何高效操作 DOM 元素?

在循环外操作元素

1
2
3
4
5
6
7
8
9
10
11
12
const times = 10000;
console.time('switch')
for (let i = 0; i < times; i++) {
document.body === 1 ? console.log(1) : void 0;
}
console.timeEnd('switch') // 1.873046875ms
var body = JSON.stringify(document.body)
console.time('batch')
for (let i = 0; i < times; i++) {
body === 1 ? console.log(1) : void 0;
}
console.timeEnd('batch') // 0.846923828125ms

批量操作元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const times = 10000;
console.time('createElement')
for (let i = 0; i < times; i++) {
const div = document.createElement('div')
document.body.appendChild(div)
}
console.timeEnd('createElement')// 54.964111328125ms
console.time('innerHTML')
let html=''
for (let i = 0; i < times; i++) {
html+='<div></div>'
}
document.body.innerHTML += html // 31.919921875ms
console.timeEnd('innerHTML')


const times = 20000;
let html = ''
for (let i = 0; i < times; i++) {
html = `<div>${i}${html}</div>`
}
document.body.innerHTML += html
const div = document.querySelector('div')
for (let i = 0; i < times; i++) {
div.style.fontSize = (i % 12) + 12 + 'px'
div.style.color = i % 2 ? 'red' : 'green'
div.style.margin = (i % 12) + 12 + 'px'
}

缓存元素集合

1
2
3
4
5
6
7
8
9
for (let i = 0; i < document.querySelectorAll('div').length; i++) {
document.querySelectorAll(`div`)[i].innerText = i
}

//如果能够将元素集合赋值给 JavaScript 变量,每次通过变量去修改元素,那么性能将会得到不小的提升。
const divs = document.querySelectorAll('div')
for (let i = 0; i < divs.length; i++) {
divs[i].innerText = i
}

总结

  • 尽量不要使用复杂的匹配规则和复杂的样式,从而减少渲染引擎计算样式规则生成 CSSOM 树的时间;
  • 尽量减少重排和重绘影响的区域;
  • 使用 CSS3 特性来实现动画效果。

三、3 个使用场景助你用好 DOM 事件

防抖

有一个搜索输入框,为了提升用户体验,希望在用户输入后可以立即展现搜索结果,而不是每次输入完后还要点击搜索按钮。最基本的实现方式应该很容易想到,那就是绑定 input 元素的键盘事件,然后在监听函数中发送 AJAX 请求。

l la lag lago 前面进行了 4 次无效查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 代码2
const debounce = (func, wait = 0) => {
let timeout = null
let args
function debounced(...arg) {
args = arg
if(timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise((res, rej) => {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch(e) {
rej(e)
}
}, wait)
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}

节流

实现节流函数的过程和防抖函数有些类似,只是对于节流函数而言,有两种执行方式,在调用函数时执行最先一次调用还是最近一次调用,所以需要设置时间戳加以判断。

节流与防抖都是通过延迟执行,减少调用次数,来优化频繁调用函数时的性能。不同的是,对于一段时间内的频繁调用,防抖是延迟执行后一次调用,节流是延迟定时多次调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const throttle = (func, wait = 0, execFirstCall) => {
let timeout = null
let args
let firstCallTimestamp


function throttled(...arg) {
if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
if (!execFirstCall || !args) {
console.log('set args:', arg)
args = arg
}
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise(async(res, rej) => {
if (new Date().getTime() - firstCallTimestamp >= wait) {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
} else {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
}, firstCallTimestamp + wait - new Date().getTime())
}
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
args = null
timeout = null
firstCallTimestamp = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
throttled.cancel = cancel
throttled.flush = flush
return throttled
}

代理

事件代理的实现原理就是利用上述 DOM 事件的触发流程来对一类事件进行统一处理。比如对于上面的列表,我们在 ul 元素上绑定事件统一处理,通过得到的事件对象来获取参数,调用对应的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ul = document.querySelector('.list')
ul.addEventListener('click', e => {
const t = e.target || e.srcElement
if (t.classList.contains('item')) {
getInfo(t.id)
} else {
id = t.parentElement.id
if (t.classList.contains('edit')) {
edit(id)
} else if (t.classList.contains('delete')) {
del(id)
}
}
})

四、掌握 CSS 精髓:布局

单列布局
单列布局是最常用的一种布局,它的实现效果就是将一个元素作为布局容器,通常设置一个较小的(最大)宽度来保证不同像素宽度屏幕下显示一致。

2 列布局
2 列布局使用频率也非常的高,实现效果就是将页面分割成左右宽度不等的两列,宽度较小的列设置为固定宽度,剩余宽度由另一列撑满。为了描述方便,我们暂且称宽度较小的列父元素为次要布局容器,宽度较大的列父元素为主要布局容器。

3 列布局
3 列布局按照左中右的顺序进行排列,通常中间列最宽,左右两列次之。

五、如何管理你的 CSS 代码?

1
2
3
4
5
6
7
base/,模板代码,比如默认标签样式重置;
components/,组件相关样式;
layout/,布局相关,包括头部、尾部、导航栏、侧边栏等;
pages/,页面相关样式;
themes/,主题样式,即使有的项目没有多个主题,也可以进行预留;
abstracts/,其他样式文件生成的依赖函数及 mixin,不能直接生成 css 样式;
vendors/,第三方样式文件。

六、浏览器如何渲染页面?

  1. 字节流解码
  2. 输入流预处理
  3. 令牌化
  4. 构建 DOM 树
  5. 构建渲染树
  6. 布局
  7. 绘制

七、JavaScript 的数据类型,你知多少?

JavaScript 的数据类型可以分为 7 种:空(Null)、未定义(Undefined)、数字(Number)、字符串(String)、布尔值(Boolean)、符号(Symbol)、对象(Object)。

Undefined

Undefined 是一个很特殊的数据类型,它只有一个值,也就是 undefined。可以通过下面几种方式来得到 undefined:

1
if(typeof key === 'undefined'){} // 判断正解

Null

Null 数据类型和 Undefined 类似,只有唯一的一个值 null,都可以表示空值,甚至我们通过 “==” 来比较它们是否相等的时候得到的结果都是 true,但 null 是 JavaScript 保留关键字,而 undefined 只是一个常量。

Number

两个重要值
Number 是数值类型,有 2 个特殊数值得注意一下,即 NaN 和 Infinity。

  • NaN(Not a Number)通常在计算失败的时候会得到该值。要判断一个变量是否为 NaN,则可以通过 Number.isNaN 函数进行判断。
  • Infinity 是无穷大,加上负号 “-” 会变成无穷小,在某些场景下比较有用,比如通过数值来表示权重或者优先级,Infinity 可以表示最高优先级或最大权重。

八、一等公民

this 关键字

this 是 JavaScript 的一个关键字,一般指向调用它的对象。

这句话其实有两层意思,首先 this 指向的应该是一个对象,更具体地说是函数执行的“上下文对象”。其次这个对象指向的是“调用它”的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。

箭头函数和普通函数相比,有以下几个区别,在开发中应特别注意:

  • 不绑定 arguments 对象,也就是说在箭头函数内访问 arguments 对象会报错
  • 不能用作构造器,也就是说不能通过关键字 new 来创建实例;
  • 默认不会创建 prototype 原型属性;
  • 不能用作 Generator() 函数,不能使用 yeild 关键字。

原型

原型是 JavaScript 的重要特性之一,可以让对象从其他对象继承功能特性,所以 JavaScript 也被称为“基于原型的语言”。

什么是原型和原型链?
简单地理解,原型就是对象的属性,包括被称为隐式原型的 proto 属性和被称为显式原型的 prototype 属性。

隐式原型通常在创建实例的时候就会自动指向构造函数的显式原型。例如,在下面的示例代码中,当创建对象 a 时,a 的隐式原型会指向构造函数 Object() 的显式原型。

new 操作符实现了什么?

  • 创建一个临时的空对象,为了表述方便,我们命名为 fn,让对象 fn 的隐式原型指向函数 F 的显式原型
  • 执行函数 F(),将 this 指向对象 fn,并传入参数 args,得到执行结果 result
  • 判断上一步的执行结果 result,如果 result 为非空对象,则返回 result,否则返回 fn。

作用域

作用域是指赋值、取值操作的执行范围,通过作用域机制可以有效地防止变量、函数的重复定义,以及控制它们的可访问性。

命名提升

对于使用 var 关键字声明的变量以及创建命名函数的时候,JavaScript 在解释执行的时候都会将其声明内容提升到作用域顶部,这种机制称为“命名提升”。

闭包

在函数内部访问外部函数作用域时就会产生闭包。闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这种关联不只是跨作用域引用,也可以实现数据与函数的隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过闭包来实现单例模式
var SingleStudent = (function ()
    function Student() {}
    var _student; 
    return function () {
        if (_student) return _student;
        _student = new Student()
        return _student;
    }
}())
var s = new SingleStudent()
var s2 = new SingleStudent()
s === s2 // true

九、执行顺序

异步和同步

同步:程序在同步调用函数的时候,会立即执行操作并等待得到返回结果后再继续运行,也就是说同步执行是阻塞的。

异步:而异步会将操作和结果在时间上分隔开来,在当下执行操作,在未来某个时刻返回结果,在这个等待返回结果的过程中,程序将继续执行后面的代码。也就是说异步执行是非阻塞的。

异步与回调

异步操作都采用回调函数的形式

异步原理

事件循环

对于大多数语言而言,实现异步会通过启动额外的进程、线程或协程来实现,而我们在前面已经提到过,JavaScript 是单线程的。

事件队列

数组 eventLoop 表示事件队列(也有称作“任务队列”),用来存放需要执行的任务事件(可以理解为回调函数),对象 event 变量表示当前需要执行的任务事件。

处理异步

异步串行

将异步转化为串行执行是非常常见的操作,先通过一个简单的例子来回顾基本实现方式。

异步并行

(1)Promise.all([promise1 …… promiseN])

调用函数 Promise.all 会返回一个新的 Promise 实例,该实例在参数内所有的 promise 都完成 (resolved) 时回调完成 (resolve);如果参数中 promise 有一个失败(rejected),那么此实例返回第一个失败 promise 的结果。

当执行的异步函数具有强一致性时可以使用它,比如要更新一个较大的表单数据,会发送多个请求分别更新不同的数据,如果一个请求更新失败则放弃本次提交。

(2)Promise.allSettled([promise1……promiseN])

调用函数 Promise.allsellted 会返回一个新的 Promise 实例,该实例会在所有给定的 promise 已经执行完成时返回一个对象数组,每个对象表示对应的 promise 结果。

这个函数适用于需要并发执行多个异步函数,这些异步函数的执行结果相互独立。比如同时发送多个 AJAX 请求来分别更新多条数据。

(3)Promise.race([promise1……promiseN])

调用函数 Promise.race 会返回一个新的 promise 实例,一旦参数中的某个 promise 执行完成,新的 promise 实例就会返回对应 promise 的执行结果。

这个函数会让多个并发函数产生“竞争”,从而挑选出最先执行完成的。比如尝试从多个网址加载图片资源。

异常处理

Promise 有一个隐藏的“坑”,那就是内部的异常不能在外部通过 try/catch 所捕获,当内部发生异常时,会自动进入失败状态(rejected)。

十、区分浏览器中进程与线程

  • 进程是操作系统进行资源分配和调度的基本单位
  • 线程是操作系统进行运算的最小单位
  • 一个程序至少有一个进程,一个进程至少有一个线程
  • 线程需要由进程来启动和管理

十一、TCP 是怎样建立/断开连接的

三次握手

在建立 TCP 连接之前,客户端和服务器之间会发送三次数据,以确认双方的接收和发送能力,这个过程称为三次握手(Three-way Handshake)。

三次握手的具体过程如下所示。

第一次握手:

刚开始客户端处于 CLOSED 的状态,服务端处于 LISTEN 状态。客户端给服务端发送一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。

第二次握手:

当服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

第三次握手:

当客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也同样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方成功建立起了连接。

四次挥手

当客户端和服务端断开连接时要发送四次数据,这个过程称之为四次挥手。

四次挥手的具体过程如下所示。

第一次挥手:

在挥手之前服务端与客户端都处于 ESTABLISHED 状态。客户端发送一个 FIN 报文,用来关闭客户端到服务器的数据传输,此时客户端处于 FIN_WAIT_1 状态。

第二次挥手:

当服务端收到 FIN 之后,会发送 ACK 报文,并且把客户端的序列号值加 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。

第三次挥手:

如果服务端同意关闭连接,则会向客户端发送一个 FIN 报文,并且指定一个序列号,此时服务端处于 LAST_ACK 的状态。

第四次挥手:

当客户端收到 ACK 之后,处于 FIN_WAIT_2 状态。待收到 FIN 报文时发送一个 ACK 报文作为应答,并且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。等待一段时间后会进入 CLOSED 状态,当服务端收到 ACK 报文之后,也会变为 CLOSED 状态,此时连接正式关闭。

十二、multirepo && monorepo

multirepo
multirepo 就是将项目中的模块拆分出来,放在不同的仓库中进行独立管理。例如,用于 Node.js 的 Web 框架 Koa,它依赖的模块 koa-convert 和 koa-compose 分别拆分成了两个仓库进行管理。

这种方式的好处是保证仓库的独立性,方便不同团队维护对应的仓库代码,可以根据团队情况选择擅长的工具、工作流等。

但这种方式也会存在一些问题,具体如下。

开发调试及版本更新效率低下。比如在仓库 A 用到的仓库 B 中发现了一个 bug,就必须到仓库 B 里修复它、打包、发版本,然后再回到仓库 A 继续工作。在不同的仓库间,你不仅需要处理不同的代码、工具,甚至是不同的工作流程;还有,你只能去问维护这个仓库的人,能不能为你做出改变,然后等着他们去解决。

团队技术选型分散。不同的库实现风格可能存在较大差异(比如有的库依赖 Vue,有的依赖 React),还有可能会采用不同的测试库及校验规则,维护起来比较困难。

而 monorepo 方式恰好就能解决这些问题。

monorepo
monorepo 就是将所有相关的模块放在同一个项目仓库中。这种方式在管理上会更加方便,项目所有代码可以使用统一的规范及构建、测试、发布流程。

很多著名的开源项目都采取了这种管理方式,比如开源项目 babel,它依赖的模块都放在了 packages 目录下。

十三、前端性能优化思路

以从两个方向入手:加载性能优化和渲染性能优化。

虽然不同方向的优化手段不同,但大体上都遵循两个思路:做减法和做除法。做减法是直接减少耗时操作或资源体积,做除法是在耗时操作和资源体积无法减少的情况下,对其进行拆分处理或者对不可拆分的内容进行顺序调换。

下面来进行举例分析。加载性能的优化手段中,做减法的有:

  • 采用 gzip 压缩,典型的减少资源的传输体积;
  • 使用缓存,强制缓存可以减少浏览器请求次数,而协商缓存可以减少传输体积;
  • 使用雪碧图,减少浏览器请求次数。

做除法的有:

  • HTTP2 多路复用,把多个请求拆分成二进制帧,并发传输;

  • 懒加载,将 Web 应用拆分成不同的模块或文件,按需加载;

  • 把 script 标签放到 body 底部,通过调整顺序来控制渲染时间。

而在渲染性能优化的手段中,做减法的有:

  • 避免重排与重绘,减少渲染引擎的绘制;

  • 防抖操作,减少函数调用或请求次数;

  • 减少 DOM 操作,减少渲染引擎和脚本引擎的切换,同时也减少渲染引擎绘制。

做除法的有:

  • 骨架屏,将页面内容进行拆分,调整不同部分的显示顺序;

  • 使用 Web Worker,将一些长任务拆分出来,放到 Web Worker 中执行;

  • React Fiber,将同步视图的任务进行拆分,可调换顺序,可暂停。

十四、安全

跨站脚本(Cross Site Scripting,XSS)

XSS 攻击示例
一般我们把 XSS 分为反射型、存储型、DOM 型 3 种类型。反射型 XSS 也叫非持久型 XSS,是指攻击者将恶意代码拼写在 URL 中提交给服务端,服务端返回的内容,也带上了这段 XSS 代码,最后导致浏览器执行了这段恶意代码。

XSS 防御手段

  • 参数校验。对于 HTTP 请求的 URL 参数和请求体 payload 的数据进行校验,比如我们接收的数据是用户年龄,那么在后端,需要判断一下数据是否是 Number,对于不符合校验规则的数据及时抛出错误。

  • 字符转义。对于一些特殊符号,比如“<”“>”“&”“””“’”“/”,我们需要对其进行转义,后端接收这些代码时候的转义存储,前端在显示的时候,再把它们转成原来的字符串进行显示。

跨站请求伪造(Cross-site Request Forgery,CSRF/XSRF)

CSRF 攻击就是在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作。和 XSS 攻击方式相比,CSRF 并不需要直接获取用户信息,只需要“借用”用户的登录信息相关操作即可,隐蔽性更强。

CSRF 防御手段

通过前面的例子可以看到 CSRF 大多来自第三方网站,所以浏览器会在请求头带上 Refer 字段,服务器可以判断 Refer 来拒绝不受信任的源发出的请求。

由于攻击者在大多数情况下利用 cookie 来通过验证,所以可以在请求地址中添加其他头部字段,比如 token,服务端只有接收到正确的 token 后才响应正确的内容。

攻击者是在不知情的情况下,自动发起恶意的请求,那么可以通过用户确认来防御攻击,比如加入图形或短信验证码让用户输入,确认该操作是用户本人发起的。但是加入验证码会影响用户的体验,所以验证码不能频繁使用。

点击劫持(C lickJacking )

攻击者创建一个网页利用 iframe 包含目标网站,然后通过设置透明度等方式隐藏目标网站,使用户无法察觉目标网站的存在,并且把它遮罩在网页上。在网页中诱导用户点击特定的按钮,而这个按钮的位置和目标网站的某个按钮重合,当用户点击网页上的按钮时,实际上是点击目标网站的按钮。

ClickJacking 防御

通过例子可以看到 ClickJacking 的攻击原理主要是利用了 iframe,所以可以通过设置响应头部字段 X-Frame-Options HTTP 来告诉浏览器允许哪些域名引用当前页面。X-Frame-Options 的值有 3 个,具体如下。

  • DENY:表示页面不允许在 iframe 中引用,即便是在相同域名的页面中嵌套也不允许,GitHub 首页响应头部使用的就是这个值。

  • SAMEORIGIN:表示该页面可以在相同域名页面的 iframe 中引用,知乎网站首页响应头部使用的就是这个值。

  • ALLOW-FROM [URL]:表示该页面可以在指定来源的 iframe 中引用。

十五、综合能力

  • 数据结构
  • 算法
  • 编程方式
  • 设计模式
  • Serverless