2021年前端开发面试题总结

222 阅读16分钟

- ES6

- 数组方法总结

- 前端性能优化

- VUE2

- VUE3

- VUE3.2

- BFC

- 浏览器渲染原理

- vue项目实现权限控制的几种思路

- 遇到过什么问题,怎么解决的

项目构建内存溢出(V8引擎限制,node只能使用部分内存)
ElementUI和vue版本兼容问题
深拷贝 正反序列化date对象和正则(lodash库)
接手外包项目(开发的人多,页面多嵌套深,风格不同)

- 说说你对 SPA 单页面的理解,它的优缺点分别是什么?

SPA仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现一系列的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 因此服务器压力变小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理; 缺点:
  • 初次加载耗时多、SEO 难度较大

- 输入一个URL

(同一域名,TCP连接数限制,chrome6个)
DNS解析=》TCP连接=》发送HTTP请求=》服务器处理请求并返回HTTP报文=》浏览器解析渲染页面=》连接结束
1.浏览器查询并读取自身的 DNS 缓存
2.否则进行DNS 解析,将域名解析成 IP 地址
3.TCP 三次握手,确认双端的收发能力是否正常
4.发送 HTTP 请求
5.服务器处理请求并返回 HTTP 报文
6.浏览器解析渲染页面
根据 HTML 解析出 DOM 树
根据 CSS 解析生成 CSS 规则树
结合 DOM 树和 CSS 规则树,生成渲染树
根据渲染树计算每一个节点的信息
根据计算好的信息绘制页面
7.TCP 四次挥手,断开连接

参考:segmentfault.com/a/119000000…

- 三次握手四次挥手

三次握手是为了确认双方的收发能力是否正常以及指定自己的初始化序列号,为后续的传输做准备。 一发一指(SYN报文,ISN)=》》两发一指(SYN报文,ACK报文,ISN)=》》一发(ACK报文) 一发一指(FIN报文,序列号)=》》两发一指(ACK报文,序列号=》FIN报文)=》》一发(ACK报文) mp.weixin.qq.com/s/Yx4rIgaIc…

- HTTP 1.0 1.1 2.0

1.1长连接(Connection: keep-alive)、缓存处理(缓存判断If-Modified-Since,Expires)、错误处理和宽带优化
2.0 新的二进制格式、多路复用、头部压缩、服务端推送
juejin.im/post/5b3c87…

- HTTPS

超文本传输安全协议 一句话:https在http通信基础上,在应用层和传输层增加了一层ssl协议来加密数据包,是为了解决http存在的一些问题,起到身份验证和保护数据的功能

四次握手:(重点:第三个随机数服务端通过解密也知道,就能各自生成会话密钥)

1.客户端请求建立SSL链接,并向服务端发送一个随机数(client random)和客户端支持的加密方法(比如RSA),此时是明文传输的。 2.服务端选择客户端支持的一种加密算法并生成另一个随机数(server random),并将授信的服务端证书和公钥下发给客户端。 3.客户端收到服务端的回复,会校验服务端证书的合法性,若合法,则生成一个新的随机数premaster secret并通过服务端下发的公钥及加密方法进行加密,然后发送给服务端。 4.服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用三个随机数通过一定算法生成会话密钥。 握手阶段结束了,客户端与服务器就可以使用普通的HTTP协议,只不过用"会话密钥"加密内容。

http的问题 明文传输相对容易被窃听,(抓包工具) 服务端客户端伪装后不确认双方身份,认证存在风险 因此,请求和响应传输过程中,内容可能会遭受攻击者拦截和篡改

https的问题 缺点:安全性是相对http来说提高了很多,但是范围还是比较有限,黑客该攻击还是攻击 不是免费的,证书需要花钱 增加服务器负担,本来TCP三次握手就能确定链接,现在多了一层开销, 也影响到了缓存效率

UDP和TCP UDP适合性能要求高的场景,不需要建立连接头部开销少,想发啥发啥,因此不安全,而且没有拥塞机制会有丢包的风险

对称加密: 加密和解密时使用相同的密钥 非对称加密: 是使用一对密钥,一个加密一个解密

七层协议 物理、链路、网络、传输、应用(会话、表示)

- cdn原理

当用户发起访问时,他的访问请求被DNS定位到较近的缓存服务器, 如果用户请求的内容刚好在缓存服务器里面,则直接把内容返还给用户; 如果用户请求的内容没有被缓存,则缓存服务器向邻近缓存服务器或者直接向源站抓取内容,然后返还用户。

- 缓存

强缓存

强缓存根据请求头的ExpiresCache-Control判断是否命中强缓存,命中强缓存的资源直接从本地加载,不会发起任何网络请求。服务器响应时的Cache-Control略有不同,其中有两个需要注意下:1. public, public 表明这个请求可以被任何对象缓存,代理/CDN等中间商。2. private,private 表明这个请求只能被终端缓存,不允许代理或者CDN等中间商缓存。

Expires是一个具体的日期,到了那个日期就会让这个缓存失活,优先级较低,存在max-age的情况下会被忽略,和本地时间绑定,修改本地时间可以绕过。

Cache-Control 的值有很多,常用的有max-ageno-cacheno-storeno-cache 会让浏览器缓存这个文件到本地但是不用,Network中disable-cache勾中的话就会在请求时带上这个haader,会在下一次新鲜度验证通过后使用这个缓存。no-store 会完全放弃缓存这个文件。

另外,如果你的服务器的返回内容中不存在ExpiresCache-Control: max-age,或 Cache-Control:s-maxage但是存在Last-Modified时,那么浏览器默认会采用一个启发式的算法,即启发式缓存。通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间,之后浏览器仍然会按强缓存来对待这个资源一段时间,如果你不想要缓存的话务必确保有no-cacheno-store在响应头中。

协商缓存

协商缓存一般会在强缓存新鲜度过期后发起,向服务器确认是否需要更新本地的缓存文件,如果不需要更新,服务器会返回304否则会重新返回整个文件。

服务器响应中会携带ETagLast-ModifiedLast-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。

但是如果在本地打开缓存文件,就会造成Last-Modified被修改,所以在HTTP / 1.1 出现了ETag

Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的

If-None-Match的header会将上次返回的ETag发送给服务器,询问该资源的ETag是否有更新,有变动就会发送新的资源回来

ETag(If-None-Match)的优先级高于Last-Modified(If-Modified-Since),优先使用ETag进行确认。

协商缓存比强缓存稍慢,因为还是会发送请求到服务器进行确认。

- 跨域(同源策略)

同源策略是一种约定,它是浏览器最核心也是最基本的安全策略。所谓同源是指“协议 + 域名 + 端口”相同。 方法:nginx反向代理、CORS(Access-Control-Allow-Origin)、Jsonp、Node中间件代理 CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案 JSONP只支持GET请求, Node中间件代理和nginx反向代理,主要是通过同源策略对服务器不加限制。 日常工作中,用得比较多的跨域方案是cors和nginx反向代理

- 路由

location.hash的值实际就是URL中#后面的东西 history实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()

兼容IE8(IE10)、无需服务端配合处理、锚点失效、相同Hash不会进历史栈 juejin.im/post/5d2d19… juejin.im/post/5ac61d…

- cookie、session、token、JWT、sessionStorage、localStorage

cookie、session区别:安全性、存取值的类型、有效期、存储大小

cookie、token区别:安全性、移动端、跨域

token、JWT区别:token需要服务端查询校验,jwt只需解密校验

Cookie、sessionStorage、localStorage区别:http携带、存储大小、有效期

www.ruanyifeng.com/blog/2018/0… juejin.im/post/5e055d…

- 柯里化

将一个多参数的函数转换成多个一个参数的函数,同时提高自由度和适用性。

function _check (regExp,string) { return regExp.test(string)}
let checkCellPhone = _check(/^1\d{10}$/);
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

juejin.im/post/5d2299…

- 正则

*重复零次或更多次
? 重复零次或一次
www.cnblogs.com/xuqiang7/p/…

- 作用域链

函数内部属性 [[Scope]] 包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的 作用域链,它能决定哪些数据能被访问到。 而且作用域链初始化执行上下文环境,已经确定了

- 原型链

每个对象都可以有一个原型_proto_,这个原型还可以有它自己的原型,以此类推,形成一个原型链。 查找特定属性的时候,我们先去这个对象里去找, 如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找...... 这个操作被委托在整个原型链上 juejin.im/post/583585…

- 垃圾回收

segmentfault.com/a/119000001…

- 闭包

闭包:函数嵌套函数,内部函数引用外部函数变量 应用:设计私有的方法和变量。 juejin.im/post/5dc644…

- 事件循环eventloop

从js加载代码到执行栈开始,依次执行主线程中的全部同步任务, 当遇到异步API则添加回调事件异步队列,其中微任务事件添加到微任务队列,宏任务事件添加到宏任务队列, 开始执行当前所有微任务队列中的微任务回调事件(process.nextTick由于Promise.then) 根据先进先出原则,取出宏任务第一条放到执行栈中执行,执行过程中遇到微任务和宏任务则继续把它们推到各自的队列, 然后本轮宏任务执行完后再把本轮产生的微任务一次清空 juejin.im/post/5b97d2…

- ES6

类、箭头函数、结构赋值、扩展运算符、let/const命令、函数参数默认值、模板字符串、对象简写 juejin.im/post/5ca2e1… es6.ruanyifeng.com/#docs/destr…

- 箭头函数和普通函数区别

普通函数的this指向调用它的对象,箭头函数this永远指向上下文;箭头函数是匿名函数,没有原型属性,不能用new不能作为构造函数;箭头函数不绑定arguments,取而代之用rest参数...解决; 箭头有简略写法,普通函数没有;

- 水平垂直居中

flex(justify-content/align-items)、定位(四种)

image.png www.jianshu.com/p/b4934c3d4…

- link与@import的区别

参考答案

  1. link是 HTML 方式, @import是 CSS 方式
  2. link最大限度支持并行下载,@import过多嵌套导致串行下载,出现FOUC
  3. link可以通过rel="alternate stylesheet"指定候选样式
  4. 浏览器对link支持早于@import,可以使用@import对老浏览器隐藏样式
  5. @import必须在样式规则之前,可以在 css 文件中引用其他文件
  6. 总体来说:link 优于@import

- JS的四种设计模式

参考答案

工厂模式

简单的工厂模式可以理解为解决多个相似的问题;

function CreatePerson(name,age,sex) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sex = sex;
    obj.sayName = function(){
        return this.name;
    }
    return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age);  // 28
console.log(p1.sex);  // 男
console.log(p1.sayName()); // longen

console.log(p2.name);  // tugenhua
console.log(p2.age);   // 27
console.log(p2.sex);   // 女
console.log(p2.sayName()); // tugenhua  

单例模式

只能被实例化(构造函数给实例添加属性与方法)一次

// 单体模式
var Singleton = function(name){
    this.name = name;
};
Singleton.prototype.getName = function(){
    return this.name;
}
// 获取实例对象
var getInstance = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {//相当于一个一次性阀门,只能实例化一次
            instance = new Singleton(name);
        }
        return instance;
    }
})();
// 测试单体模式的实例,所以a===b
var a = getInstance("aa");
var b = getInstance("bb");  

沙箱模式

将一些函数放到自执行函数里面,但要用闭包暴露接口,用变量接收暴露的接口,再调用里面的值,否则无法使用里面的值

let sandboxModel=(function(){
    function sayName(){};
    function sayAge(){};
    return{
        sayName:sayName,
        sayAge:sayAge
    }
})()

发布者订阅模式

就例如如我们关注了某一个公众号,然后他对应的有新的消息就会给你推送,

//发布者与订阅模式
    var shoeObj = {}; // 定义发布者
    shoeObj.list = []; // 缓存列表 存放订阅者回调函数

    // 增加订阅者
    shoeObj.listen = function(fn) {
        shoeObj.list.push(fn); // 订阅消息添加到缓存列表
    }

    // 发布消息
    shoeObj.trigger = function() {
            for (var i = 0, fn; fn = this.list[i++];) {
                fn.apply(thisarguments);//第一个参数只是改变fn的this,
            }
        }
     // 小红订阅如下消息
    shoeObj.listen(function(color, size) {
        console.log("颜色是:" + color);
        console.log("尺码是:" + size);
    });

    // 小花订阅如下消息
    shoeObj.listen(function(color, size) {
        console.log("再次打印颜色是:" + color);
        console.log("再次打印尺码是:" + size);
    });
    shoeObj.trigger("红色"40);
    shoeObj.trigger("黑色"42);  

代码实现逻辑是用数组存贮订阅者, 发布者回调函数里面通知的方式是遍历订阅者数组,并将发布者内容传入订阅者数组

- 说说bind、call、apply 区别?

参考答案

call 和 apply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。

除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。

let a = {
    value1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck''24')
getValue.apply(a, ['yck''24'])

bind和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind实现柯里化。

- 数组扁平化

对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,我们如何将其扁平化为[1, 1, 2, 1, 2, 3]这样的一维数组呢:

1.ES6的flat()

const arr = [1, [1,2], [1,2,3]]arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]

2.join

const arr = [1, [1,2], [1,2,3]]

const str = arr.join().split(',')

3.递归

对于树状结构的数据,最直接的处理方式就是递归

const arr = [1, [1,2], [1,2,3]]

function flat(arr) {  
    let result = []  
    for (const item of arr) {    
        item instanceof Array ? result = result.concat(flat(item)) :result.push(item)  
    }  
    return result
}
flat(arr) // [1, 1, 2, 1, 2, 3]

4.reduce()递归

const arr = [1, [1,2], [1,2,3]]

function flat(arr) {  
    return arr.reduce((prev, cur) => {    
        return prev.concat(cur instanceof Array ? flat(cur) : cur)  }, [])
    }
flat(arr)  // [1, 1, 2, 1, 2, 3]

5.迭代+展开运算符

let arr = [1, [1,2], [1,2,3]]
while (arr.some(Array.isArray)) {  
    arr = [].concat(...arr);
}
console.log(arr)  // [1, 1, 2, 1, 2, 3]

- GET请求和POST请求有何区别

标准答案: •GET请求参数放在URL上,POST请求参数放在请求体里

•GET请求参数长度有限制,POST请求参数长度可以非常大

•POST请求相较于GET请求安全一点点,因为GET请求的参数在URL上,且有历史记录

•GET请求能缓存,POST不能

更进一步:

其实HTTP协议并没有要求GET/POST请求参数必须放在URL上或请求体里,也没有规定GET请求的长度,目前对URL的长度限制,是各家浏览器设置的限制。GET和POST的根本区别在于:GET请求是幂等性的,而POST请求不是

幂等性,指的是对某一资源进行一次或多次请求都具有相同的副作用。例如搜索就是一个幂等的操作,而删除、新增则不是一个幂等操作。

由于GET请求是幂等的,在网络不好的环境中,GET请求可能会重复尝试,造成重复操作数据的风险,因此,GET请求用于无副作用的操作(如搜索),新增/删除等操作适合用POST

参考资料:HTTP|GET 和 POST 区别?网上多数答案都是错的[11]

- 手写冒泡排序

冒泡排序总会执行(N-1)+(N-2)+(N-3)+..+2+1趟,但如果运行到当中某一趟时排序已经完成,或者输入的是一个有序数组,那么后边的比较就都是多余的,为了避免这种情况,我们增加一个flag,判断排序是否在中途就已经完成(也就是判断有无发生元素交换)

function bubbleSort(arr){  
    let flag = true  
    for(let i = 0; i < arr.length; i++) {    
        for(let j = 0; j < arr.length - i - 1; j++) {      
            if(arr[j] > arr[j+1]) {        
                flag = false        
                let temp = arr[j]        
                arr[j] = arr[j+1]        
                arr[j+1] = temp      
            }    
        }    
        if(flag) break;  
    }  
    return arr
}

- 下面代码输出的结果是什么?

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x)     
console.log(b.x)
首先,ab同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。\
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。

其它参考资料

[1] 你知道的前端优化手段

[2] stalled和Initial connection偶尔请求时间长