一文看懂前端八股文

178 阅读9分钟

JavaScript执行机制(Event Loop)

JavaScript是一门单线程语言,执行程序都需要排队。当遇到定时器或异步请求的时候经常被卡住,这时候就需要暂停手头的任务去等待卡住的任务完成,体验极差。
为了解决这个问题,将任务分为同步任务和异步任务两种类型。同步任务专门用来处理那些不需要等待的任务,比如let a = 1;异步任务专门用来处理需要专门等待一会的任务,比如setTimeout、网络请求等等。
其中有的任务是需要连贯执行的,有的不需要连贯执行,异步任务也是这样,所以把异步任务又分为微任务和宏任务。不需要立即连贯执行的异步任务属于宏任务,比如script、setTimeout等会进入宏任务Event Table中,需要立即连贯执行的异步任务属于微任务,比如promise.then(),process.nextTick等包裹代码块会进去微任务Event Table中,他们一般需要等下一个宏任务执行前完成。

完整的JS执行机制:

搞懂flex布局

flex布局是W3C新提出的一种布局方案。
控制方向:flex-direction: row/row-reverse/column/column-reverse;
换行:flex-wrap: wrap/nowrap/wrap-reverse;
主轴方向排列:justify-content: flex-start/flex-end/center/space-between/space-around;
交叉轴方向排列:align-item: flex-start/flex-end/center/baseline/stretch;
行与行之间排列分布:align-content: flex-start/flex-end/center/space-between/space-around/stretch;
单独控制每一项:
单独排序:order: 1。
分配剩余空间,分配比例:flex-grow: 1。
收缩空间,收缩比例:flex-shrink: 1。
单独设置顺序:align-self: auto/flex-start/flex-end/center/baseline/strech;

HTTP缓存

使用HTTP缓存使页面打开速度更快,服务器压力更小,但是会出现一个问题就是浏览器没办法及时拿到新的资源。
强缓存:在缓存没有过期,浏览器可以决定直接使用缓存。
Expires:过期时间。浏览器根据这个时间判断是不是需要使用缓存。
Cache-Control:过期时长,在HTTP1.1提出。max-age(单位是秒)/no-cache(不使用缓存)/no-store(禁止缓存)/private(只有浏览器使用缓存)/public(浏览器、服务器、代理服务器都可以缓存)
协商缓存:在缓存过期后,需要咨询服务器是否可以使用缓存。
Last-Modified:最后修改时间。浏览器请求时,服务器返回Last-Modified,以后每次请求都会带上上一次的返回的最后修改时间If-Modified-Since,服务器进行比对,如果比对结果是没有变化,就返回304状态,如果有更新就返回200状态。
ETag:文件内容的唯一标识。浏览器请求时,服务器返回ETag,以后每次请求都会带上上一次返回的值If-None-Match,服务器进行比对,比对没有变化返回304,更新则返回新的资源。

DNS域名解析

用户输入一个网址后,浏览器不能理解网址,需要转为对应的IP地址。
DNS域名解析分为两大类,首先进行本地解析,如果找到了就返回对应的IP地址,没找到就进行互联网域名服务器解析返回IP地址。
本地解析:首先从浏览器缓存里找如果找到了就返回IP地址。没有找到就在系统缓存里找,找到了返回IP地址。没有找到就在本地HOST文件找,找到了就返回IP地址,没有就行下一阶段。
互联网域名服务器解析:先从本地DNS服务器缓存里查找,没有就去根服务器找,然后在顶级域名服务器,再到权威域名服务器找,最后返回对应的IP地址。

TCP三次握手

字段含义解读:
seq(sequence number):序列号,随机数。相当于随机验证码。
ack(acknowledgement number):确认号。ack=seq+1。确认发送方的seq值,确认收到了对面发送的seq。
TCP标志位:ACK(acknowledgement)确认序号有效。SYN(synchronous)发起一个新连接。
过程:
客户端主动开启结束CLOSED状态,服务端被动开启结束CLOSE状态,进入LISTEN阶段。
客户端向服务端发送一个TCP报文,携带数据SYN=1,seq=x。客户端进入SYN-SEND阶段。
服务端回复SYN=1,ACK=1,seq=y,ack=x+1。服务端进入SYN-RCVD阶段。
客户端收到后回复ACK=1,seq=x+1,ack=y+1。
服务端收到后,双方都进入ESTAB-LISHED阶段,然后开始数据传输。

vue的diff算法

首先需要了解什么是虚拟DOM,虚拟DOM是表示真实DOM的JS对象。
diff算法能把新旧两个虚拟DOM进行对比,快速找到两个之间的差异就可以最小化的更新视图。
比对过程:
当数据改变的时候就会触发getter方法,进一步触发dep.notify方法,通知订阅者patch(oldvnode, newvnode)。
首先看是不是同类标签isSameVnode,不是同类标签直接替换,是同类标签进一步比对,就是patchVnode方法。
然后看新旧节点是否相等,相等直接return,不相等分情况比对,原则以新虚拟节点为准。如果新虚拟节点和旧虚拟节点都有文本节点则新直接替换旧;旧的没有子节点,新的有子节点,直接添加新的子节点;新的没有子节点,旧的有子节点,则删除子节点;新旧都有子节点则比对他们的子节点updateChildren。
updateChildren方法:原则以新节点为准。采用同级比较,首尾指针方式进行比对。旧节点的start和新的start和end比较,旧的end和新的start和end比较。依次比较,当比较成功后退出当前比较。渲染结果以newVnode为准。每次比较成功后start和end向中间靠拢。当新旧节点其中一个start到end后,终止比较。如果都匹配不到,则旧虚拟DOM key值去比对新虚拟DOM key,相同则复用,并移动到新虚拟DOM位置。

了解promise

为了解决回调地狱,promise就诞生了,它的使用就是优雅的表示异步回调。
状态:pending、fulfilled、rejected。状态不可逆。
方法:.then、.catch、.finally(不管成功和失败都执行)、.all(所有都成功才执行成功,任何一个失败则执行失败)、.allSettled(所有结果都执行,不管成功还是失败)、.any(任意一个成功则执行)、.race(任意一个成功或失败则执行)、.reject(返回一个状态为rejected的promise对象)、.resolve(四种参数:promise对象,直接返回promise对象;具有.then方法的对象,将这个对象转为promise并立即执行.then方法;参数不是对象或对象没有.then方法,状态为resolved的新的promise对象并将参数传给下一个then;不带参数,状态为resolved的promise对象)。链式调用。
手写:

class MyPromise {
    constructor(exector) {
        this.status = 'pending';
        this.result = null;
        this.reason = null;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
        try {
            exector(this.resolve.bind(this), this.reject.bind(this))
        } catch(e) {
            this.reject(e)
        }
    }

    resolve(result) {
        if (this.status == 'pending') {
            this.status = 'fulfilled';
            this.result = result;
            this.onFulfilledCallbacks.forEach((callback) => {
                callback(result)
            })
        }
    }

    reject(reason) {
        if (this.status == 'pending') {
            this.status = 'rejected';
            this.reason = reason;
            this.onRejectedCallbacks.forEach((callback) => {
                callback(reason)
            })
        }
    }

    then(onFulfilled, onRejected) {
        let promise2 = new MyPromise((resolve, reject) => {

            onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : value => value;
            onRejected = typeof onRejected == 'function' ? onRejected : reason => {throw reason};
    
            if (this.status == 'fulfilled') {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.result)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch(e) {
                        reject(e)
                    }
                })
            } else if (this.status == 'rejected') {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch(e) {
                        reject(e)
                    }
                })
            } else if (this.status == 'pending') {
                this.onFulfilledCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(this.result)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch(e) {
                            reject(e)
                        }
                    })
                })
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch(e) {
                            reject(e)
                        }
                    })
                })
            }
        })

        return promise2
    }
}


function resolvePromise(promise2, x, resolve, reject) {
    if (x == promise2) {
        throw new TypeError('promise error');
    }
    // A+规范的promise
    if (x instanceof MyPromise) {
        x.then(y => {
            resolvePromise(promise2, y, resolve, reject)
        }, reject)
    } else if (x !== null && ((typeof x == 'object') || (typeof x == 'function'))) {
        try {
            let then = x.then;
        } catch(e) {
            return reject(e)
        }
        if (typeof then == 'function') {
            let called = false;
            try {
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    if (called) return;
                    called = true;
                    reject(r)
                })
            } catch(e) {
                if (called) return;
                called = true;
                reject(e)
            }
        } else {
            resolve(x);
        }
    } else {
        resolve(x)
    }
}


XSS攻击与防范

XSS,全称Cross-site scripting,跨站脚本攻击。注重于注入恶意脚本。
CSRF,全称Cross-site request forgery,跨站请求伪造。注重于请求伪造。
XSS危害:窃取cookie、劫持流量、插入广告、置入木马、获取用户信息。一切用户输入都是不安全的,分为反射型(浏览器提交恶意代码到服务端,服务端将恶意代码传回客户端)、存储型(浏览器将恶意代码提交到服务端,服务端将恶意代码存到数据库中)、DOM型(恶意代码仅在客户端运行)。
预防:对输入进行过滤,对输出进行转义。校验、过滤、编码转义、限制。

ES6新特性

let/const:块级作用域。
模块化:export和import。
解构:let {a} = obj。
扩展运算符:...
includes、isArray()、指数操作符、Object.keys、Object.values、Object.entries、null传导运算符?.、模板字符串等等。

防抖和节流

防抖:操作时不执行,不操作时执行。举例:连续点击按钮,只执行最后一次的点击事件。
节流:每个一段时间执行一次。监听鼠标移动,鼠标一秒移动一万次,控制执行性10次,100ms执行一次。

async/await原理

特征:能暂停执行并恢复执行,类似generator函数,yield暂停执行,next恢复执行。

function* gen() {
    yield 1
    yield 2
    yield 3
}

const g = gen();
console.log(g.next()) // {value: 1, done: false}
console.log(g.next()) // {value: 2, done: false}
console.log(g.next()) // {value: 3, done: false}
console.log(g.next()) // {value: undefined, done: true}

async/await用yield解读:

async function gen() {
    const data1 = await getData(1) // 1秒输出2
    const data2 = await getData(data1) // 2秒输出4
    return `success: ${data2}`
}

function* gen() {
    const data1 = yield getData(1)
    const data2 = yield getData(data1)
    return `success: ${data2}`
}
const g = gen();
const next1 = g.next() // {value: Promise {<fulfilled>}, done: false}
next1.value.then(data1 => {
    console.log(data1); // 1秒输出2
    const next2 = g.next(data1); // {value: Promise {<fulfilled>}, done: false
    next2.value.then(data2 => {
        console.log(data2) // 2秒输出4
        console.log(g.next(data2)) // {value: 'success: 4, done: true
    })
})

实现一个生成器generator转async:

function* gen() {
    const data1 = yield getData(1)
    const data2 = yield getData(data1)
    return `success: ${data2}`
}
function generatorToAsync(generatorFun) {
    return function() {
        return new Promise((resolve, reject) => {
            const g = generatorFun();
            const next1 = g.next();
            next1.value.then((data1) => {
                const next2 = g.next(data1);
                next2.value.then(data2 => {
                    resolve(g.next(data2).value)
                })
            })
        })
    }
}

const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res))

改良,支持多个yield调用:

function generatorToAsync(generatorFun) {
    return function() {
        const gen = generatorFun.call(this, arguments)

        return new Promise((resolve, reject) => {
            function step(key, arg) {
                let res;
                try {
                    res = gen[key](arg) // 每次执行next
                } catch(e) {
                    return reject(e)
                }

                const {value, done} = res;
                if (done) {
                    return resolve(value)
                } else {
                    return Promise.resolve(value).then(val => step('next', val), err => step('throe', err))
                }
            }

            step('next') // 首次执行
        })
    }
}

var/let/const,for循环面试

es6之前都是var生命变量。
var:变量提升。
let、const:块级作用域。临时死区:当在作用域内用let/const声明变量之后,在声明之前不能使用。const声明变量必须赋值。禁止重复声明。

for (var i = 1; i < 6; i++) {
    setTimeout(function() {
        console.log(i) // 5个 i = 6,用的同一个i
    }, 0)
}

// 解决:立即执行函数或者var改let
for (var i  = 1; i < 6; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j)
        }, 0)
    })(i)
}

回流、重绘

回流:计算元素的形状、位置、大小。对性能影响较大。
重绘:转换为屏幕上的实际像素。对性能影响较小。
浏览器解析HTML生成DOM树,解析CSS生成CSSDOM树,然后合并构成渲染树。在经过回流,在经过重绘展现页面。
浏览器对回流和重绘做了一些优化,浏览器维护着一个重绘和回流的队列,将需要重绘和回流的操作放到这个队列里边,当队列中的重绘和回流操作达到一定数量,浏览器会清空这个队列,执行操作。但是当访问以下的属性或方法时,浏览器会立刻清空这个队列,所以要减少使用这些属性或方法:clientTop、clientLeft、clientWidth、clientHeight、offsetTop、scrollTop、getComputedStyle()、getBoundingClientReact()等。
避免回流:避免频繁操作样式、避免频繁操作DOM、复杂动画、触发css3硬件加速。

TCP四次挥手

FIN:释放链接。
过程:
客户端和服务端都是数据传输阶段。ESTAB-LISHED阶段。客户端主动断开方,服务端被动断开方。
客户端发送FIN=1,seq=u,客户端进入FIN-WAIT-1半关闭状态。
服务端回复ACK=1,seq=v,ack=u+1,服务端进入CLOSE-WAIT关闭整理阶段。客户端进入FIN-WAIT-2阶段。
服务端在发送一条报文FIN=1,seq=w,ack=u+1,ACK=1,服务端进入LAST-ACK阶段。
客户端回复ACK=1,seq=u+1,ack=w+1,客户端进入TIME-WAIT阶段,之后等待4分钟,用来确认服务端收到发的报文。
服务端收到后进入CLOSE阶段,客户端等4分钟后也进入CLOSE阶段。

https安全传输原理

最开始用的是对称加密方案,加密和解密用的同一把钥匙。缺点:如果钥匙被别人截取就会不安全。加密算法:AES、DES、3DES等。
非对称加密:每个人都有公匙和私匙,数据用公匙加密,用私匙解密。缺点:加解密流程很复杂,所以很慢。加密算法:RSA。
非对称加密和对称加密相结合:非对称加密来加密钥匙,用钥匙加密解密数据。传输的时候用公匙加密钥匙,双方用私匙解密后,得到钥匙用这个加密解密数据。缺点:中间人攻击,中间人分别与双方协定钥匙,在分别与双方通信。
引入权威第三方机构CA数字证书:网站公匙,数字签名。
hash验证:一段消息使用随机key加密,再hash运算得到消息摘要,通过服务端的私匙进行加密得到数字签名,客户端收到数字签名后通过公匙进行解密,得到消息摘要1,把消息hash运算得到消息摘要2,对比两个消息摘要。
https传输过程:
客户端发出请求,服务端准备数字证书包括公匙和其他信息,与公匙配对的私匙保留在服务端。服务端把数字证书返回服务端。
客户端拿到证书,解析证书,验证证书的合法性(对比两个摘要),不合法发出https警告,合法提取公匙生成随机key,再用公匙机密随机key,加密后将随机key密文发送服务端。
服务端收到后使用私匙机密,得到随机key。以后用这个随机key将消息进行对称加密通信。

输入URL到页面呈现发生了什么

用户输入一个网址,一个url包括协议、域名、端口、路径、参数等信息。
首先判断浏览器缓存包括强缓存和协商缓存。
进行DNS域名解析,拿到域名对应的ip地址。
拿到ip地址后,进行TCP三次握手建立连接。
客户端发送http请求,如果是https还涉及到加解密过程。
服务器处理请求返回资源。
四次挥手断开连接。
浏览器渲染页面,涉及到重绘和回流,加载JS涉及到JS的执行机制。

JS垃圾回收机制

目标:清除不再使用的对象,腾退所占用的内存。
策略:引用计数法和标记清除法。
引用计数法:当对象引用一次引用数加一,取消引用一次,引用数减一,引用数为0,垃圾回收机制回收。问题:循环引用会无法回收。
标记清除法:所有对象标记为0,从根对象遍历,存活的对象标记为1,最后清除所有标记为0的对象,最后将所有标记为1的对象重新标记为0,方便下一次垃圾回收。缺点:不连续,新的对象找位置是个问题。
找位置策略:First-fit:找到能放下对象的第一个块位置。Best-fit:找到能放下新对象的最小块。Worst-fit:找到最大块,切一块给新对象。
V8对垃圾回收机制的优化:
有的对象需要频繁回收比如:小、新、存活时间短的,有的对象不需要频繁回收,比如老、大、存活时间长的。堆内存分出两个区域:新生代和老生代。
新生代有两个空间from空间(使用空间)和to空间(闲置空间)。当from空间要满的时候就开始标记,标记存活的对象,标记好复制到to空间,清除from空间,再交换他们的名称。
老生代所有标记为0,存活的标记为1,清除标记为0的,把标记为1的变为0,再用标记压缩算法整理好。
JS是单线程的,在执行垃圾回收机制的时候会暂停,称为全停顿。V8机制对这种进行优化,垃圾回收机制支持多线程并行回收。
增量标记:垃圾回收机制可以执行一段JS代码执行一段垃圾回收,交叉执行。
为了记录上一次标记的位置,诞生了三色标记法,将上一次标记的位置标记为灰色,下次执行开始从灰色开始标记。
V8还支持并发回收,主线程执行JS,辅助线程执行垃圾回收。

npm run xxx 发生了什么

当我们执行npm run xxx时,就是相当于执行了package.json下的scripts里的指令。
就是执行了node_modules目录下的.bin下的对应的软链接脚本文件。
软链接脚本文件在找到package-lock.json,里面对应的JS去执行。

前端路由实现方式hash/history

hash:网址上带有#,#后边是路径,切换路径页面更新。其实是触发了hashChange事件,在这个事件回调中执行无刷新页面跳转。
history:history.back()、history.go()、history.forward()、history.pushState()、history.replaceState()、window.onpopstate。
history返回404的原因:服务端没设置当前URL的静态资源或API接口,服务端需要做相应处理,没有资源定位到根位置。

函数柯里化

将一个接收多个参数的函数,转为一系列只接收一个参数的函数。

add(2)(3)(5)(6)
add(2)(3, 5)(6)
function add(a, b, c, d) {
    return a*b*c*d
}

function curry(fn, ...args1) {
  let len = fn.length;

  return function (...args2) {
    const newArgs = args1.concat(args2);
    if (newArgs.length >= len) {
      return fn.call(this, newArgs);
    } else {
      return curry(fn, newArgs);
    }
  };
}

正则表达式

匹配什么?
\小写字母 表示类型,\大写字母表示相反过程。
\d 数字。\D 非数字。digit,0-9任意数字。
\w 数字、字母。\W 非数字、字母。word。
\s 空格。\S 非空格。 space。
. 任意字符。i 忽略大小写(ignore)。g 全局匹配(global)。
匹配几个?
*零次或多次。+ 一次或多次。?0次或1次。{n}:n次。{n, m} 至少n次至多m次。{n, }至少n次。
匹配什么位置?
^ 匹配开头。 $ 匹配结尾。\b 匹配边界。
分组与集合?
() 将多个字符或子表达式作为一个整体来匹配 /(abc)+/
[]匹配该集合中任意一个字符 /[abc]/
逻辑运算
或:|, 举例:/hello | world/ 。
与:拼接多个条件表示与。/\d{3}\s\w+/
非:零宽度断言表示非,/(?<!\d)\w+/
零宽度断言(匹配邻居)?
正向先行断言:(?=...),匹配内容后边挨着什么,/\d(?=[a-z])/
负向先行断言:(?!...),匹配内容后边不挨着什么,/\d(?![a-z])/
正向后行断言:(?<=...),匹配内容前边挨着什么,/(?<=[a-z])\d/
负向后行断言:(?<!...),匹配内容前边不挨着什么,/(?<![a-z])\d/

JS闭包

两个特点:函数嵌套函数;内层函数可以访问外层函数的变量和参数。
两个作用:防止变量和参数被垃圾回收机制回收,变量持久化;防止变量和参数被外部变量污染。
风险:滥用会造成内存泄漏。
应用:实现JS模块化、缓存函数、封装私有变量、实现函数柯里化、防抖节流。