记录一下3月底到4月的前端开发工程师面经

9,065 阅读49分钟

b840c72e59dd5b880ed68bb8d10b6c62.jpeg

文章会持续更新

1.https 原理(加密 证书)

  1. 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
  2. web服务器收到客户端请求后,会将网站的证书(包含公钥)传送一份给客户端
  3. 客户端收到网站证书后会检查证书的颁发机构以及过期时间,如果没有问题就随机产生一个秘钥
  4. 客户端利用公钥将会话秘钥加密,并传送给服务端,服务端利用自己的私钥解密出会话秘钥
  5. 之后服务器与客户端使用秘钥加密传输

2. 网络安全相关 (XSS攻击和CSRF攻击原理及防御措施;token认证;cookie和session)

XSS

  1. XSS: Cross Site Scripting攻击,全称跨站脚本攻击.

    XSS指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些 代码,嵌入到web页面中去。使别的用户访问都会执行相应的嵌入代码。 从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。

  2. XSS攻击的危害包括:

    1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号

    2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力

    3、盗窃企业重要的具有商业价值的资料

    4、非法转账

    5、强制发送电子邮件

    6、网站挂马

    7、控制受害者机器向其它网站发起攻击

  3. 原因解析

    主要原因:过于信任客户端提交的数据!

    解决办法: 不信任客户端提交的数据,只要是客户端提交的数据就应该先进行相应的过滤处理后方可进行下一步的操作.

    进一步分析: 客户端提交的数据本身就是应用所需的,但是恶心攻击者利用网站对客户端提提交数据的信任,在数据中插入一些符号以及JavaScript代码,那么这些数据就会成为应用代码中给的一部分了,那么攻击者就可以肆无忌惮的展开攻击

    因此我们绝对不可信任任何客户端提交的数据

  4. 类型

    持久性(存储型)和非持久性(反射型)

    持久型: 持久型也就是攻击的代码被写入数据库中,这个攻击危害性很大,如果网站访问量很大 的话,就会导致大量正常访问的页面,就会导致大量正常访问页面的用户都受到攻击。最典型的 就是留言板的XSS攻击

    非持久型: 非持久型XSS是指发送请求时,XSS代码出现在请求的URL中,作为参数提交到服务 器,服务器解析并响应。响应结果中包含XSS代码,最后浏览器解析并执行,从概念上可以看出, 反射型XSS代码首先是出现在URL中,然后需要服务端解析,最后需要浏览器之后XSS代码才能攻 击

  5. 防御方法

    两种方式用来防御

    1. 转义字符

    • 首先,对于用户的输入应该是永远不信任的,最普遍的做法就是转义输入输出的内容,对于括号,尖括号,斜杠进行转义

    2. CSP

    • 内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段

      CSP 的主要目标是减少和报告 XSS 攻击 ,XSS 攻击利用了浏览器对于从服务器所获取的内容的信任。恶意脚本在受害者的浏览器中得以运行,因为浏览器信任其内容来源,即使有的时候这些脚本并非来自于它本该来的地方。

      CSP通过指定有效域——即浏览器认可的可执行脚本的有效来源——使服务器管理者有能力减少或消除XSS攻击所依赖的载体。一个CSP兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和HTML的事件处理属性)。

      作为一种终极防护形式,始终不允许执行脚本的站点可以选择全面禁止脚本执行

      CSP本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以进行加载和执行,我们只需要配置规则,如何拦截是由浏览器自己实现的,我们可以通过这种方式来尽量减少XSS攻击

    开启CSP的方式(如果使用CSP)

      1. 你可以使用 Content-Security-Policy HTTP头部 来指定你的策略,像这样:
    设置HTTP Header中的Content-Security-Policy: policy
    
      1. 设置meta标签的方式
    meta http-equiv=“Content-Security-Policy” content=“default-src ‘self’; img-src https://*; child-src ‘none’;”>
    

    常见用例(设置HTTP Header来举例)

    • 一个网站管理者想要所有内容均来自站点的同一个源 (不包括其子域名) 只允许加载本站资源
    Content-Security-Policy: default-src ‘self
    • 一个网站管理者允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)
    Content-Security-Policy: default-src ‘self’ *.trusted.com
    
    • 一个网站管理者允许网页应用的用户在他们自己的内容中包含来自任何源的图片, 但是限制音频或视频需从信任的资源提供者(获得),所有脚本必须从特定主机服务器获取可信的代码.
    Content-Security-Policy: default-src ‘self’; img-src *; media-src media1.com media2.com; script-src userscripts.example.com
    
    • 只允许加载HTTPS协议图片
    Content-Security-Policy: img-src https://*
    
    • 允许加载任何来源框架
    Content-Security-Policy: child-src ‘none’
    

    对于这种方式来说,这要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,而且CSP的兼容性不错.

CSRF

1 基本概念

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

2 原理

原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑

也就是完成一次CSRF攻击,受害者必须依次完成两个步骤: 1 登录受信任的网站,并在本地生成Cookie 2 在不登出信任网站的情况下,访问危险网站

你也许有疑问:如果我不满足以上两个条件中的一个,我就不会收到CSRF攻击。 的确如此,但是你不能保证以下情况的发生: 1 你不能保证你登录了一个网站后,不再打开一个tab页面并访问其他页面 2 你不能保证你关闭浏览器后,你的本地Cookie立刻过期,你的上次会话已经结束.(事实上,关闭浏览器不能结束一个会话,)

3 危害(CSRF可以做什么)

攻击者盗用了你身份,以你的名义发送恶意请求,CSRF能够做的事情:以你的名义发送邮件,盗取你的账号甚至是购买商品,虚拟货币的转账,个人隐私泄露和财产安全

CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!

4 如何防御

防范CSRF攻击可以遵循以下几种规则:

  • GET请求不对数据进行修改
  • 不让第三方网站访问到Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者Token

SameSite

  • 可以对Cookie设置SameSite属性,该属性表示Cookie不随着跨域请求发送,可以很大程度上减少CSRF的攻击,但是该属性目前并不是所有浏览器都兼容

验证 Referer HTTP头部

  • 对于需要防范CSRF的请求,我们可以通过验证Referer来判断请求是否为第三方网站发起的.

Token服务端核对令牌

  • 服务器下发一个服务端核对令牌随机Token,每次发送请求时将Token携带上,服务器验证Token是否有效

验证码

  • 这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,厄…这个方案可以完全解决CSRF,但个人觉得在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。

3.缓存方式

这个其实就是强缓存和协商缓存的问题,强缓存会直接去取缓存的文件,而协商缓存会去像服务器发送一次确认文档是否有效的请求。

详细: mp.weixin.qq.com/s/G5FIrWOts…

一般js、css等静态资源 走强缓存, html走协商缓存(没有hash)

4.跨域方式(proxy代理、CORS策略、jsonp脚本跨域、websoket...)

  1. 具体实现
  2. 分别在什么场景下使用

1. JSONP跨域

jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

jsonp的缺点:只能发送get一种请求。

2、跨域资源共享(CORS)

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

浏览器将CORS跨域请求分为简单请求和非简单请求。

只要同时满足一下两个条件,就属于简单请求

(1)使用下列方法之一:

  • head
  • get
  • post

(2)请求的Heder是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件,就属于非简单请求。浏览器对这两种的处理,是不一样的。

简单请求

  对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

CORS请求设置的响应头字段,都以 Access-Control-开头:

1)Access-Control-Allow-Origin:必选

  它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

2)Access-Control-Allow-Credentials:可选

  它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

3)Access-Control-Expose-Headers:可选

  CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

非简单请求

  非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

预检请求

  预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

1)Access-Control-Request-Method:必选

  用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

2)Access-Control-Request-Headers:可选

  该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的回应   服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

  HTTP回应中,除了关键的是Access-Control-Allow-Origin字段,其他CORS相关字段如下:

1)Access-Control-Allow-Methods:必选

  它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

2)Access-Control-Allow-Headers

  如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

3)Access-Control-Allow-Credentials:可选

  该字段与简单请求时的含义相同。

4)Access-Control-Max-Age:可选

  用来指定本次预检请求的有效期,单位为秒。

3、nginx代理跨域

 nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。

1)nginx配置解决iconfont跨域

  浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2)nginx反向代理接口跨域

跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。

实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

nginx具体配置:

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

4、nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1)非vue框架的跨域

  使用node + express + http-proxy-middleware搭建一个proxy服务器。

  • 前端代码:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • 中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

2)vue框架的跨域

  node + vue + webpack + webpack-dev-server搭建的项目,跨域请求接口,直接修改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

5、document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

6、location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

  具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

7、window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

8、postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数:

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

9、WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。 原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

小结

以上就是9种常见的跨域解决方案,jsonp(只支持get请求,支持老的IE浏览器)适合加载不同域名的js、css,img等静态资源;CORS(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;Nginx代理跨域和nodejs中间件跨域原理都相似,都是搭建一个服务器,直接在服务器端请求HTTP接口,这适合前后端分离的前端项目调后端接口。document.domain+iframe适合主域名相同,子域名不同的跨域请求。postMessage、websocket都是HTML5新特性,兼容性不是很好,只适用于主流浏览器和IE10+。

5. TCP三次握手和四次挥手的理解

一、三次握手讲解

  • 客户端发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,服务器由SYN=1知道客户端要求建立联机(客户端:我要连接你)
  • 服务器收到请求后要确认联机信息,向A发送ack number=(客户端的seq+1),syn=1,ack=1,随机产生seq=7654321的包(服务器:好的,你来连吧)
  • 客户端收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,客户端会再发送ack number=(服务器的seq+1),ack=1,服务器收到后确认seq值与ack=1则连接建立成功。(客户端:好的,我来了)

二、为什么http建立连接需要三次握手,不是两次或四次?

答:三次是最少的安全次数,两次不安全,四次浪费资源;

三、TCP关闭连接过程

  1. Client向Server发送FIN包,表示Client主动要关闭连接,然后进入FIN_WAIT_1状态,等待Server返回ACK包。此后Client不能再向Server发送数据,但能读取数据。

  2. Server收到FIN包后向Client发送ACK包,然后进入CLOSE_WAIT状态,此后Server不能再读取数据,但可以继续向Client发送数据。

  3. Client收到Server返回的ACK包后进入FIN_WAIT_2状态,等待Server发送FIN包。

  4. Server完成数据的发送后,将FIN包发送给Client,然后进入LAST_ACK状态,等待Client返回ACK包,此后Server既不能读取数据,也不能发送数据。

  5. Client收到FIN包后向Server发送ACK包,然后进入TIME_WAIT状态,接着等待足够长的时间(2MSL)以确保Server接收到ACK包,最后回到CLOSED状态,释放网络资源。

  6. Server收到Client返回的ACK包后便回到CLOSED状态,释放网络资源

四、为什么要四次挥手?

TCP是全双工信道,何为全双工就是客户端与服务端建立两条通道,通道1:客户端的输出连接服务端的输入;通道2:客户端的输入连接服务端的输出。两个通道可以同时工作:客户端向服务端发送信号的同时服务端也可以向客户端发送信号。所以关闭双通道的时候就是这样:

客户端:我要关闭输入通道了。

服务端:好的,你关闭吧,我这边也关闭这个通道。

服务端:我也要关闭输入通道了。

客户端:好的你关闭吧,我也把这个通道关闭。

6. 斐波那契数列

1.递归

function f(n) {
	if (n === 1 || n === 2) {
    	return 1;
    } else {
    	return f(n-1) + f(n-2);
    }
}

时间复杂度:O(2^N)

空间复杂度:O(N)

时间复杂度是指数阶,属于爆炸增量函数,在程序设计中我们应该避免这样的复杂度。

用递归法实现斐波那契数列代码实现比较简洁,但在n比较大时会引起栈溢出,无法实现所需功能。

  1. 尾调用
function f(n, ac1=1, ac2=1) {
	if (n<=2) {
    	return ac2;
    } 
    return f(n-1, ac2, ac1+ac2);
}
  1. 迭代器
function* f(){
	let [prev, curr] = [0, 1];
    // for(;;)相当于死循环 等于while(1)
    for(;;) {
    	yield curr;
        [prev, curr] = [curr, prev + curr];
    }
}
for(let n of f()) {
	if (n > 1000) break;
    console.log(n);
}
  1. 带有缓存的递归
function memozi(fn) {
	var r = {};
    return function(n) {
    	if (r[n] == null) {
        	r[n] = fn(n);
            return r[n];
        } else {
        	return r[n];
        }
    }
}

var fibfn = memozi(function(n) {
	if (n==0) {
    	return 0;
    } else if (n==1) {
    	return 1;
    } else {
    	return fibfn(n-1) + fibfn(n-2)
    }
})

这里用到闭包相关知识,所以问了“闭包”相关知识(详情见第15题)。

7. 编程题

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串

var isValid = function(s) {
	const n = s.length;
    if (n % 2 === 1) {
    	return false;
    }
    const pairs = new Map([
    	[')','('],
        [']','['],
        ['}','{']
    ]);
    const stk = [];
    for(let i=0; i<s.length; i++) {
    	if (pairs.has(s[i])) {
        	if (!stk.length || stk[stk.length-1] !== pairs.get(s[i])) {
            	return false;
            }
            stk.pop();
        } else {
        	stk.push(s[i]);
        }
    }
    return !stk.length;
}

8. 提交数据两种数据结构

URL传值和form表单提交的区别和原理

区别:

1、url传值就是get ,from表单就是post ,get是从服务器获取数据,post是向服务器传送数据

2、 对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。

3、get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。

4、get安全性非常低,post安全性较高

9.性能优化

    1. 减少 HTTP 请求
    1. 使用 HTTP2
    1. 使用服务端渲染
    1. 静态资源使用 CDN
    1. 将 CSS 放在文件头部,JavaScript 文件放在底部
    1. 使用字体图标 iconfont 代替图片图标
    1. 善用缓存,不重复加载相同的资源
    1. 压缩文件
    1. 图片优化
    1. 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码
    1. 减少重绘重排
    1. 使用事件委托
    1. 注意程序的局部性
    1. if-else 对比 switch
    1. 查找表
    1. 避免页面卡顿
    1. 使用 requestAnimationFrame 来实现视觉变化
    1. 使用 Web Workers
    1. 使用位操作
    1. 不要覆盖原生方法
    1. 降低 CSS 选择器的复杂性
    1. 使用 flexbox 而不是较早的布局模型
    1. 使用 transform 和 opacity 属性更改来实现动画
    1. 合理使用规则,避免过度优化

10.浏览器从输入到页面呈现内容的过程 及 优化

  • 1.DNS解析
  • 2.TCP阶段
  • 3.HTTP阶段
  • 4.解析 / 渲染阶段
  • 5.布局layout / 渲染页面

优化:

1.通过 DNS 预解析来告诉浏览器未来我们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就可以尽快地完成 DNS 解析。

第一步:打开或关闭DNS预解析

你可以通过在服务器端发送 X-DNS-Prefetch-Control 报头。或是在文档中使用值为 http-equiv 的meta标签:

<meta http-equiv="x-dns-prefetch-control" content="on">

需要说明的是,在一些高级浏览器中,页面中所有的超链接(<a>标签),默认打开了DNS预解析。但是,如果页面中采用的https协议,很多浏览器是默认关闭了超链接的DNS预解析。如果加了上面这行代码,则表明强制打开浏览器的预解析。(如果你能在面试中把这句话说出来,则一定是你出彩的地方)

第二步:对指定的域名进行DNS预解析

如果我们将来可能从 smyhvae.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容:

<link rel="dns-prefetch" href="http://www.smyhvae.com/">

当我们从该 URL 请求一个资源时,就不再需要等待 DNS 解析的过程。该技术对使用第三方资源特别有用。

11.防抖与节流函数

1.防抖 (debounce)

function debounce (f, wait) {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      f(...args)
    }, wait)
  }
}

使用场景

  • 1.登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
  • 2.调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  • 3.文本编辑器实时保存,当无任何更改操作一秒后进行保存

2.节流

function throttle (f, wait) {
  let timer
  return (...args) => {
    if (timer) { return }
    timer = setTimeout(() => {
      f(...args)
      timer = null
    }, wait)
  }
}

使用场景

  • 1.scroll 事件,每隔一秒计算一次位置信息等
  • 2.浏览器播放事件,每个一秒计算一次进度信息等
  • 3.input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)

总结 (简要答案)

  • 防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout。防抖可以比作等电梯,只要有一个人进来,就需要再等一会儿。业务场景有避免登录按钮多次点击的重复提交。
  • 节流:控制流量,单位时间内事件只能触发一次,与服务器端的限流 (Rate Limit) 类似。代码实现重在开锁关锁 timer=timeout; timer=null。节流可以比作过红绿灯,每等一个红灯时间就可以过一批。

12. ES6 promise的原理,越细致越好,与Generator的区别

手写Promise(简易版)

function myPromise(fn) {
    this.cbs = [];
    const resolve = (value) => {
        setTimeout(() => {
            this.data = value;
            this.cbs.forEach((cb) => cb(value));
        })
    }
    fn(resolve);
}
myPromise.prototype.then = function (onResolved) {
    return new myPromise((resolve) => {        
        this.cbs.push(() => {
            const res = onResolved(this.data);
            if (res instanceof myPromise) {
                res.then(resolve);
            } else {
                resolve(res);
            }
        });
    });
}
export default myPromise;

promise详细知识点 juejin.cn/post/690555…

13. 怎么禁止js访问cookie

Set-Cookie: name=value; HttpOnly

14. 实现左侧固定,右侧自适应两栏布局的方法

HTML布局:

<div class="outer">
   <div class="sidebar">固定宽度区(sideBar)</div>
    <div class="content">自适应区(content)</div>
</div>
<div class="footer">footer</div>

方法:

1、将左侧div浮动,右侧div设置margin-left

/*方法1*/
.outer{overflow: hidden; border: 1px solid red;}
.sidebar{float: left;width:200px;height: 150px; background: #BCE8F1;}
.content{margin-left:200px;height:100px;background: #F0AD4E;}
  1. flex
/*方法2*/
.outer7{display: flex; border: 1px solid red;}
.sidebar7{flex:0 0 200px;height:150px;background: #BCE8F1;}
.content7{flex: 1;height:100px;background: #F0AD4E;}

flex可以说是最好的方案了,代码少,使用简单。但存在兼容性,有朝一日,大家都改用现代浏览器,就可以使用了。

需要注意的是,flex容器的一个默认属性值:align-items: stretch;。这个属性导致了列等高的效果。 为了让两个盒子高度自动,需要设置: align-items: flex-start;

  1. float + BFC方法
/*方法3*/
.outer6{overflow: auto; border: 1px solid red;}
.sidebar6{float: left;height:150px;background: #BCE8F1;}
.content6{overflow:auto;height:100px;background: #F0AD4E;}

这个方案同样是利用了左侧浮动,但是右侧盒子通过overflow: auto;形成了BFC,因此右侧盒子不会与浮动的元素重叠。

延伸问题:因为代码中用了flex布局, 问了flex布局相关知识

Flex是Flexible Box的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。 任何一个容器都可以指定为Flex布局。容器分为两种,块flex和行内flex.

.box{
	display: flex; /*webkit需要加前缀*/
    /*display:inline-flex;*/
}

Flex布局有两层,采用flex布局的元素称为flex容器,其子元素则自动成flex item,即项目. 注:flex不同于block,flex容器的子元素的float,clear,vertical-align属性将失效.

Flex布局:

  1. flex容器有两根轴:水平主轴就是x轴(main axis)和竖直轴也是y轴(cross axis),两轴相关位置标识如下:

  2. flex容器属性:

  • flex-direction:决定项目的排列方向。

  • flex-wrap:即一条轴线排不下时如何换行。

  • flex-flow:是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。

  • justify-content:定义了项目在主轴上的对齐方式。(justify)

  • align-items:定义项目在交叉轴上如何对齐。

  • align-content:定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。(换行会产生多轴)

Flex item属性:

  • order:定义项目的排列顺序。数值越小,排列越靠前,默认为0。

  • flex-grow:定义项目的放大比例,如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

  • flex-shrink:定义了项目的缩小比例,默认为1,如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。

  • flex-basis:定义了在分配多余空间之前,项目占据的主轴空间(main size)。

  • flex:是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。

  • align-self:允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

15. 闭包

什么是「闭包」。

「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。

「闭包」的作用是什么。

闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」。

关于闭包的谣言

闭包会造成内存泄露?

错。

说这话的人根本不知道什么是内存泄露。内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。

闭包里面的变量明明就是我们需要的变量(lives),凭什么说是内存泄露?

这个谣言是如何来的?

因为 IE。IE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。

这是 IE 的问题,不是闭包的问题。

详细讲解: zhuanlan.zhihu.com/p/22486908

16.VUE双向数据绑定原理

1.vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现的, 也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变;

2.核心:关于VUE双向数据绑定,其核心是 Object.defineProperty()方法;

3.介绍一下Object.defineProperty()方法

(1)Object.defineProperty(obj, prop, descriptor) ,这个语法内有三个参数,分别为 obj (要定义其上属性的对象) prop (要定义或修改的属性) descriptor (具体的改变方法)

(2)简单地说,就是用这个方法来定义一个值。当调用时我们使用了它里面的get方法,当我们给这个属性赋值时,又用到了它里面的set方法;

17. webpack打包过程

webpack打包流程概括

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 1.初始化参
  • 2.开始编译 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通 过执行对象的run方法开始执行编译
  • 3.确定入口 根据配置中的 Entry 找出所有入口文件
  • 4.编译模块 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 5.完成模块编译 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系
  • 6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  • 7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。

在以上过程中, Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。其实以上7个步骤,可以简单归纳为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。

18. Map 和 Object 的区别

  • 在 Object 中, key 必须是简单数据类型(整数,字符串或者是 symbol),而在 Map 中则可以是 JavaScript 支持的所有数据类型,也就是说可以用一个 Object 来当做一个Map元素的 key。

  • Map 元素的顺序遵循插入的顺序,而 Object 的则没有这一特性。

  • Map 继承自 Object 对象。

  • 新建实例

    • Object 支持以下几种方法来创建新的实例:
    var obj = {...};
    
    var obj = new Object();
    
    var obj = Object.create(null);
    
    • Map 仅支持下面这一种构建方法:
    var map = new Map([[1, 2], [2, 3]]); // map = {1 => 2, 2 => 3}
    
  • 数据访问

    • Map 想要访问元素,可以使用 Map 本身的原生方法:
    map.get(1) // 2
    
    • Object 可以通过 . 和 [ ] 来访问
    obj.id;
    obj['id'];
    
    • 判断某个元素是否在 Map 中可以使用
    map.has(1);
    
    • 判断某个元素是不是在 Object 中需要以下操作:
    obj.id === undefined;
    // 或者
    'id' in obj;
    

    另外需要注意的一点是,Object 可以使用 Object.prototype.hasOwnProperty() 来判断某个key是否是这个对象本身的属性,从原型链继承的属性不包括在内。

  • 新增一个数据

    • Map 可以使用 set() 操作:
    map.set(key, value)       // 当传入的 key 已经存在的时候,Map 会覆盖之前的值
    
    • Object 新增一个属性可以使用:
    obj['key'] = value;
    obj.key = value;
    // object也会覆盖
    
  • 删除数据

    • 在 Object 中没有原生的删除方法,我们可以使用如下方式:
    delete obj.id;
    // 下面这种做法效率更高
    obj.id = undefined
    

    需要注意的是,使用 delete 会真正的将属性从对象中删除,而使用赋值 undefined 的方式,仅仅是值变成了 undefined。属性仍然在对象上,也就意味着 在使用 for … in… 去遍历的时候,仍然会访问到该属性。

    • Map 有原生的 delete 方法来删除元素:
    var isDeleteSucceeded = map.delete(1);
    console.log(isDeleteSucceeded ); // true
    // 全部删除
    map.clear();
    
  • 获取size

    • Map 自身有 size 属性,可以自己维持 size 的变化。

    • Object 则需要借助 Object.keys() 来计算

    console.log(Object.keys(obj).length); 
    
  • Iterating

    Map 自身支持迭代,Object 不支持。

    如何确定一个类型是不是支持迭代呢? 可以使用以下方法:

    console.log(typeof obj[Symbol.iterator]); // undefined
    console.log(typeof map[Symbol.iterator]); // function
    

何时使用 Map ,何时使用 Object?

  • 当所要存储的是简单数据类型,并且 key 都为字符串或者整数或者 Symbol 的时候,优先使用 Object ,因为Object可以使用 字符变量 的方式创建,更加高效。

  • 当需要在单独的逻辑中访问属性或者元素的时候,应该使用 Object

  • JSON 直接支持 Object,但不支持 Map

  • Map 是纯粹的 hash, 而 Object 还存在一些其他内在逻辑,所以在执行 delete 的时候会有性能问题。所以写入删除密集的情况应该使用 Map。

  • Map 会按照插入顺序保持元素的顺序,而Object做不到。

  • Map 在存储大量元素的时候性能表现更好,特别是在代码执行时不能确定 key 的类型的情况

19. new 一个函数发生了什么

构造调用:

  • 创造一个全新的对象
  • 这个对象会被执行 [[Prototype]] 连接,将这个新对象的 [[Prototype]] 链接到这个构造函数.prototype 所指向的对象
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象

如果函数返回一个对象,那么new 这个函数调用返回这个函数的返回对象,否则返回 new 创建的新对象

20.数组去重

Array.from(new Set([1, 1, 2, 2]))

21.数组扁平化

function flatten(arr) {
  let result = [];

  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result = result.concat(arr[i]);
    }
  }

  return result;
}

const a = [1, [2, [3, 4]]];
console.log(flatten(a));

22.事件循环机制 (Event Loop)

事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。 先执行宏任务队列,然后执行微任务队列,然后开始下一轮事件循环,继续先执行宏任务队列,再执行微任务队列。

  • 宏任务:script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
  • 微任务:process.nextTick()/Promise  

上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。

优先级

  • setTimeout = setInterval 一个队列
  • setTimeout > setImmediate 
  • process.nextTick > Promise

执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行

23 .函数中的arguments是数组吗?类数组转数组的方法了解一下?

是类数组,是属于鸭子类型的范畴,长得像数组,

  • ... 运算符
  • Array.from
  • Array.prototype.slice.apply(arguments)

24. 编写自定义webpack插件

一个典型的Webpack插件代码如下:

class MyWebpackPlugin {
  constructor(options) {
  }
  
  apply(compiler) {
    // 插入钩子函数
    compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => {});
  }
}

module.exports = MyWebpackPlugin;

接下来需要在webpack.config.js中引入这个插件。

module.exports = {
  plugins:[
    // 传入插件实例
    new MyWebpackPlugin({
      param:'paramValue'
    }),
  ]
};

Webpack在启动时会实例化插件对象,在初始化compiler对象之后会调用插件实例的apply方法,传入compiler对象,插件实例在apply方法中会注册感兴趣的钩子,Webpack在执行过程中会根据构建阶段回调相应的钩子。

详细知识点请google

25.实现一个 EventEmitter

function EventEmitter() {
  this.__events = {};
}
EventEmitter.VERSION = '1.0.0';

EventEmitter.prototype.on = function(eventName, listener) {
  if (!eventName || !listener) return;
  // 判断回调的 listener(或listener.listener) 是否为函数
  if (!isValidListener(listener)) {
    throw new TypeError('listener must be a function');
  }
  var events = this.__events;
  var listeners = events[eventName] = events[eventName] || [];
  var listenerIsWrapped = typeof listener === 'object';
  // 不重复添加事件, 判断是否有一样的
  if (indexOf(listeners, listener) === -1) {
    listeners.push(listenerIsWrapped ? listener : {
      listener: listener,
      once: false
    })
  }
  return this;
}

EventEmitter.prototype.emit = function(eventName, args) {
  // 通过内部对象获取对应自定义事件的回调函数
  var listeners = this.__events[eventName];
  if (!listeners) return;
  // 考虑多个 listener 的情况
  for (var i = 0; i < listeners.length; i++) {
    var listener = listeners[i];
    if (listener) {
      listener.listener.apply(this, args || []);
      // listener 中 once 为true 的进行特殊处理
      if (listener.once) {
        this.off(eventName, listener.listener)
      }
    }
  }
  return this;
}

EventEmitter.prototype.off = function(eventName, listener) {
  var listeners = this.__events[eventName];
  if (!listeners) return;
  var index;
  for (var i = 0, len = listeners.length; i < len; i++) {
    if (listeners[i] && listeners[i].listener === listener) {
      index = i;
      break;
    }
  }
  if (typeof index !== 'undefined') {
    listeners.splice(index, 1, null);
  }
  return this;
}

EventEmitter.prototype.once = function(eventName, listener) {
  // 调用 on 方法, once 参数传入 true,待执行之后进行 once 处理
  return this.on(eventName, {
    listener: listener,
    once: true
  })
}

EventEmitter.prototype.alloOff = function(eventName) {
  // 如果该 eventName 存在,则将其对应的 listeners 的数组直接清空
  if (eventName && this.__events[eventName]) {
    this.__events[eventName] = [];
  } else {
    this.__events = {};
  }
}

// 判断是否是合法的 listener
function isValidListener(listener) {
  if (typeof listener === 'function') {
    return true;
  } else if (listener && typeof listener === 'object') {
    return isValidListener(listener.listener);
  } else {
    return false;
  }
}

// 判断新增自定义事件是否存在
function indexOf(array, item) {
  var result = -1;
  item = typeof item === 'object' ? item.listener : item;
  for(var i = 0, len = array.length; i<len; i++) {
    if (array[i].listener === item) {
      result = i;
      break;
    }
  }
  return result;
}

在 Vue 框架中不同组件之间的通讯里,有一种解决方案叫 EventBus。和 EventEmitter的思路类似,它的基本用途是将 EventBus 作为组件传递数据的桥梁,所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所有组件都可以收到通知,使用起来非常便利,其核心其实就是发布-订阅模式的落地实现

26. 垃圾回收:释放内存,提升浏览器页面性能

1. JavaScript 的内存管理

栈内存中的基本类型,可以通过操作系统直接处理;而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理

2.Chrome 内存回收机制

  1. 新生代内存回收: Scavenge 算法

  2. 老生代内存回收: Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)

老生代内存的管理方式和新生代的内存管理方式区别还是比较大的。Scavenge 算法比较适合内存较小的情况处理;而对于老生代内存较大、变量较多的时候,还是需要采用“标记-清除”结合“标记-整理”这样的方式处理内存问题,并尽量避免内存碎片的产生

3.内存泄漏与优化

场景

  • 1.过多的缓存未释放;

  • 2.闭包太多未释放;

  • 3.定时器或者回调太多未释放;

  • 4.太多无效的 DOM 未释放;

  • 5.全局变量太多未被发现。

27. 浅谈前端模块化规范

CommonJs和Es Module的区别

1.CommonJs

  • CommonJs可以动态加载语句,代码发生在运行时

  • CommonJs混合导出,还是一种语法,只不过不用声明前面对象而已,当我导出引用对象时之前的导出就被覆盖了

  • CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染

2.Es Module

  • Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
  • Es Module混合导出,单个导出,默认导出,完全互不影响
  • Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改

28. css div 垂直水平居中,且 div 高度永远是宽度的一半(宽度可以不指定)

    html,
      body {
        width: 100%;
        height: 100%;
      }

      .outer {
        width: 400px;
        height: 100%;
        background: blue;
        margin: 0 auto;

        display: flex;
        align-items: center;
      }

      .inner {
        position: relative;
        width: 100%;
        height: 0;
        padding-bottom: 50%;
        background: red;
      }

      .box {
        position: absolute;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
      }

    <div class="outer">
      <div class="inner">
        <div class="box">hello</div>
      </div>
    </div>

padding-bottom 值为%时,是基于父元素宽度的百分比下内边距.

29. visibility 和 display 的差别(还有opacity)

  • visibility 设置 hidden 会隐藏元素,但是其位置还存在与页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘
  • display 设置了 none 属性会隐藏元素,且其位置也不会被保留下来,所以会触发浏览器渲染引擎的回流和重绘。
  • opacity 会将元素设置为透明,但是其位置也在页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘

30.js脚本加载问题,async、defer问题

  • 如果依赖其他脚本和 DOM 结果,使用 defer
  • 如果与 DOM 和其他脚本依赖不强时,使用 async

31.深拷贝

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}

32.Vue 的父组件和子组件生命周期钩子执行顺序是什么

  • 1.加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  • 2.子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
  • 3.父组件更新过程 父beforeUpdate->父updated
  • 4.销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

总结:从外到内,再从内到外

33. 下面代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
 	conso.log(1);
}

答案解析 因为==会进行隐式类型转换 所以我们重写toString方法就可以了

var a = {
  i: 1,
  toString() {
    return a.i++;
  }
}

if( a == 1 && a == 2 && a == 3 ) {
  console.log(1);
}

34.考察作用域

下面代码输出什么

var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()

依次输出:undefined -> 10 -> 20

解析:

在立即执行函数中,var a = 20; 语句定义了一个局部变量 a,由于js的变量声明提升机制,局部变量a的声明会被提升至立即执行函数的函数体最上方,且由于这样的提升并不包括赋值,因此第一条打印语句会打印undefined,最后一条语句会打印20。

由于变量声明提升,a = 5; 这条语句执行时,局部的变量a已经声明,因此它产生的效果是对局部的变量a赋值,此时window.a 依旧是最开始赋值的10,

35.在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的

1.子组件为何不可以修改父组件传递的 Prop

单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。

2.如果修改了,Vue 是如何监控到属性的修改并给出警告的

if (process.env.NODE_ENV !== 'production') {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }

在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。

需要特别注意的是,当你从子组件修改的prop属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。

36.cookie 和 token 都存放在 header 中,为什么不会劫持 token?

1、首先token不是防止XSS的,而是为了防止CSRF的;

2、CSRF攻击的原因是浏览器会自动带上cookie,而浏览器不会自动带上token

37.下面的代码打印什么内容,为什么

var b = 10;
(function b(){
    b = 20;
    console.log(b); 
})();

1打印结果内容如下:

ƒ b() {
b = 20;
console.log(b)
}

原因:

作用域:执行上下文中包含作用于链: 在理解作用域链之前,先介绍一下作用域,作用域可以理解为执行上下文中申明的变量和作用的范围;包括块级作用域/函数作用域;

特性:声明提前:一个声明在函数体内都是可见的,函数声明优先于变量声明; 在非匿名自执行函数中,函数变量为只读状态无法修改;

38.实现 Promise.race()

Promise._race = promises => new Promise((resolve, reject) => {
	promises.forEach(promise => {
		promise.then(resolve, reject)
	})
})

39. 输出以下代码执行的结果并解释为什么

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

结果:

截屏2021-03-14 上午11.37.43.png

  • push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
  • 根据MDN的说法理解,push方法应该是根据数组的length来根据参数给数组创建一个下标为length的属性,
  • push方法影响了数组的length属性和对应下标的值

在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。

题目的解释应该是:

  • 1.使用第一次push,obj对象的push方法设置 obj[2]=1;obj.length+=1
  • 2.使用第二次push,obj对象的push方法设置 obj[3]=2;obj.length+=1
  • 3.使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印
  • 4.打印时因为数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined

第一第二步还可以具体解释为:因为每次push只传入了一个参数,所以 obj.length 的长度只增加了 1。push方法本身还可以增加更多参数

40.for in 和 for of的区别, for of怎么遍历对象

  • for in适合遍历对象
  • for of适合遍历数组

for-of循环不支持普通对象,但如果你想迭代一个对象的属性,以下方法可以实现

方法1:
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

方法2:
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

所有拥有Symbol.iterator的对象被称为可迭代的

41. webpack 打包构建流程,用过哪些类

  • 1.Webpack CLI 启动打包流程;
  • 2.载入 Webpack 核心模块,创建 Compiler 对象;
  • 3.使用 Compiler 对象开始编译整个项目;
  • 4.从入口文件开始,解析模块依赖,形成依赖关系树;
  • 5.递归依赖树,将每个模块交给对应的 Loader 处理;
  • 6.合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

42. 原型链,继承

43.vue响应式原理 ?基本都会问

链接: www.bilibili.com/video/BV1G5…

44.TS相关知识

45.vue性能优化

  • 1.函数式组件(Functional components)

    优化前的组件代码如下:

    <template>
      <div class="cell">
        <div v-if="value" class="on"></div>
        <section v-else class="off"></section>
      </div>
    </template>
    
    <script>
    export default {
      props: ['value'],
    }
    </script>
    
    

    优化后的组件代码如下:

    <template functional>
      <div class="cell">
        <div v-if="props.value" class="on"></div>
        <section v-else class="off"></section>
      </div>
    </template>
    
    

    函数式组件和普通的对象类型的组件不同,它不会被看作成一个真正的组件,我们知道在 patch 过程中,如果遇到一个节点是组件 vnode,会递归执行子组件的初始化过程;而函数式组件的 render 生成的是普通的 vnode,不会有递归子组件的过程,因此渲染开销会低很多。因此,函数式组件也不会有状态,不会有响应式数据,生命周期钩子函数这些东西。你可以把它当成把普通组件模板中的一部分 DOM 剥离出来,通过函数的方式渲染出来,是一种在 DOM 层面的复用。

    1. Child component splitting

46.数组扁平化(depth几成扁平)

47. spa单页应用, 怎么避免内存泄露

简介

如果你在用 Vue 开发应用,那么就要当心内存泄漏的问题。这个问题在单页应用 (SPA) 中尤为重要,因为在 SPA 的设计中,用户使用它时是不需要刷新浏览器的,所以 JavaScript 应用需要自行清理组件来确保垃圾回收以预期的方式生效。

内存泄漏在 Vue 应用中通常不是来自 Vue 自身的,更多地发生于把其它库集成到应用中的时候。

一个更常见的实际的场景是使用 Vue Router 在一个单页应用中路由到不同的组件。当一个用户在你的应用中导航时,Vue Router 从虚拟 DOM 中移除了元素,并替换为了新的元素。Vue 的 beforeDestroy() 生命周期钩子是一个解决基于 Vue Router 的应用中的这类问题的好地方。 我们可以将清理工作放入 beforeDestroy() 钩子,像这样:

beforeDestroy: function () {
  this.choicesSelect.destroy()
}

总结

Vue 让开发非常棒的响应式的 JavaScript 应用程序变得非常简单,但是你仍然需要警惕内存泄漏。这些内存泄漏往往会发生在使用 Vue 之外的其它进行 DOM 操作的三方库时。请确保测试应用的内存泄漏问题并在适当的时机做必要的组件清理。

48. vue中 solt和slot-scope的原理

以下讲解基于Vue 2.6

  1. slot 和 solt-scope在组件内部被统一整合成了函数

  2. 他们的渲染作用域都是 子组件

  3. 并且都能通过 this.$scopedSlots去访问

父组件经过初始化时的一系列处理,每个插槽会转换成一个key(插槽名,未命名时是default)对应的函数(有作用域参数的话,会传作用越参数). 子组件的实例 this.$scopedSlots 就可以访问到 父组件里的 ‘插槽函数’。 如果是 普通插槽, 就直接调用函数生成 vnode, 如果是 作用域插槽, 就带着 props 去调用函数生成 vnode.

总结: Vue 2.6 版本后对 slot 和 slot-scope 做了一次统一的整合,让它们全部都变为函数的形式,所有的插槽都可以在 this.$scopedSlots 上直接访问,这让我们在开发高级组件的时候变得更加方便。在优化上,Vue 2.6 也尽可能的让 slot 的更新不触发父组件的渲染,通过一系列巧妙的判断和算法去尽可能避免不必要的渲染。在 2.5 的版本中,由于生成 slot 的作用域是在父组件中,所以明明是子组件的插槽 slot 的更新是会带着父组件一起更新的)

具体文章链接: juejin.cn/post/684490…

49.路由钩子在Vue生命周期的体现

1.完整的路由导航解析流程(不包括其他生命周期)

    1. 导航被触发
    1. 在失活的组件里调用 beforeRouteLeave 守卫
    1. 调用全局的 beforeEach 守卫
    1. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)
    1. 在路由配置里调用 beforeEnter
    1. 解析异步路由组件
    1. 在被激活的组件里调用 beforeRouterEnter
    1. 调用全局的 beforeResolve 守卫(2.5+)
    1. 导航被确认
    1. 调用全局的 afterEach 钩子
    1. 触发 DOM 更新
    1. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

2.触发钩子的完整顺序

路由导航、keep-alive、和组件生命周期钩子结合起来的, 触发顺序, 假设是从a组件离开, 第一次进入b组件:

  • beforeRouteLeave:路由组件的组件离开路由前钩子, 可取消路由离开
  • beforeEach: 路由全局前置守卫, 可用于登录验证、全局路由loadding等
  • beforeEnter: 路由独享守卫
  • beforeRouteEnter: 路由组件的组件进入路由前钩子
  • beforeResolve: 路由全局解析守卫
  • afterEach: 路由全局后置钩子
  • beforeCreate: 组件生命周期, 不能访问this
  • created: 组件生命周期, 可以访问this, 不能访问dom
  • beforeMount: 组件生命周期
  • deactivated: 离开缓存组件a, 或触发a的beforeDestroy和destroyed组件销毁钩子
  • mounted: 访问/操作dom
  • activated: 进入缓存组件,进入a的嵌套子组件(如果有的话)
  • 执行beforeRouterEnte回调函数next

50. 生命周期?那个生命周期可以获取到真实DOM?修改data里面的数据,会触发什么生命周期?

  1. 总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。
  • 创建期间的生命周期函数
    • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好 data 和 methods 属性

    • created:实例已经在内存中创建OK,此时 data 和 methods 已经创建OK,此时还没有开始 编译模板

    • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中

    • mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示

  • 运行期间的生命周期函数:
    • beforeUpdate:状态更新之前执行此函数, 此时 data 中的状态值是最新的,但是界面上显示的 数据还是旧的,因为此时还没有开始重新渲染DOM节点

    • updated:实例更新完毕之后调用此函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了!

  • 销毁期间的生命周期函数:
    • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
    • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
  1. mounted生命周期可以获取到真实DOM

  2. 修改data里面的数据,会触发 beforeUpdate、updated生命周期

51.Vue组件data为什么是一个函数

data是一个函数的时候,每一个实例的data属性都是独立的,不会相互影响

52. vue 组件通信?一般说了vuex,就会问vuex用法?action和mutations区别?实现原理等?

  1. props/$emit

  2. emit/emit/on

  3. vuex

      1. Vue Components: Vue组件。HTML页面上, 负责接收用户操作等交互行为, 执行dispatch方法触发对应的action进行回应
      1. dispatch: 操作行为触发方法,是唯一能执行action的方法
      1. actions: 操作行为处理模块,有组件中的$store.dispatch('action 名称', datal) 来触发。 然后由commit()来触发mutation的调用,间接更新 state. 负责处理Vue Components接收到的所有交互行为。包含同步/异步操作, 支持多个同名方法, 按照注册的顺序依次触发。 向后台API 请求的操作就在这个模块中进行, 包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
      1. commit: 状态改变提交操作方法。对mutation进行提交, 是唯一能执行mutation的方法
      1. mutations: 状态改变操作方法, 有actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来, 以进行state的监控等。
      1. state: 页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
      1. state对象读取方法
  4. $attrs/$listeners

    • $attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(class 和 style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

简单来说:$attrs与$listeners 是两个对象,attrs里存放的是父组件中绑定的非Props属性,attrs 里存放的是父组件中绑定的非 Props 属性,listeners里存放的是父组件中绑定的非原生事件。

  1. provide/inject

    Vue2.2.0新增API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

  2. parent/parent / children与 ref

53.$nextTick 作用?实现原理?微任务向宏任务的降级处理,经常被问到说出几种宏任务,微任务。

$nextTick 作用:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

实现原理:

1.能力检测

这一块其实很简单,众所周知,Event Loop分为宏任务(macro task)以及微任务( micro task),不管执行宏任务还是微任务,完成后都会进入下一个tick,并在两个tick之间执行UI渲染。 但是,宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务。

2.根据能力检测以不同方式执行回调队列

降级处理: Promise -> MutationObserver -> setImmediate -> setTimeout

  • 宏任务:
    • I/O,事件队列中的每一个事件都是一个macrotask

    • setTimeout / setInterval

    • MessageChannel是通信渠道API,ie11以上和其它浏览器支持。

    • setImmediate 目前只有IE10以上实现了该方法其它浏览器不支持.作用回调功能,node支持。

    • requestAnimationFrame 也算宏任务 node不支持。

  • 微任务
    • Promise.then catch finally
    • MutationObserver 浏览器支持 IE11以上 node不支持,它会在指定的DOM发生变化时被调用
    • process.nextTick 浏览器不支持 node支持
实现一个简易的nextTick

let callbacks = []
let pending = false

function nextTick (cb) {
    callbacks.push(cb)

    if (!pending) {
        pending = true
        setTimeout(flushCallback, 0)
    }
}

function flushCallback () {
    pending = false
    let copies = callbacks.slice()
    callbacks.length = 0
    copies.forEach(copy => {
        copy()
    })
}

54.vue scoped属性作用?实现原理?

当style标签具有该scoped属性时,其CSS将仅应用于当前组件的元素

<style scoped>
.example {
 color: red;
}
</style>
<template>
 <div class="example">hi</div>
</template>

<style>
.example[data-v-5558831a] {
 color: red;
}
</style>
<template>
 <div class="example" data-v-5558831a>hi</div>
</template>

实现原理: PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom

55.vue router有几种模式?实现方式?

  1. hash模式

hash模式的工作原理是hashchange事件,可以在window监听hash的变化。我们在url后面随便添加一个#xx触发这个事件。

HashHistory的push和replace()

  window.onhashchange = function(event){
    console.log(event);
  }
  1. history模式(对应HTML5History)

HTML5History.pushState()和HTML5History.replaceState()

在HTML5History中添加对修改浏览器地址栏URL的监听是直接在构造函数中执行的,对HTML5History的popstate 事件进行监听:

constructor (router: Router, base: ?string) {
  
 window.addEventListener('popstate', e => {
 const current = this.current
 this.transitionTo(getLocation(this.base), route => {
 if (expectScroll) {
 handleScroll(router, route, current, true)
 }
 })
 })
}

56. key的作用?没有key的情况,vue会怎么做?会引出diff的问题

  1. key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素, 使得整个patch过程更加高效, 减少DOM操作, 提高性能。

  2. 另外, 若不设置key还可能在列表更新时引发一些隐藏的bug

  3. vue中在使用相同标签名元素的过度切换时,也会使用到key属性, 其目的也是为了让vue可以区分它们, 否则vue只会替换其内部属性而不会触发过度效果。

57. vue diff过程

58.vue 2.x defineProperty缺陷?业务代码里面怎么处理?$set原理?vue是怎么重写数组方法的?考察你是不是真的看过源码

1.vue 2.x defineProperty缺陷

  • Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;

  • Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。

  • 不能监听动态length的变化.

    例如,arr = [1],直接更改arr[10] = 20,这样就监听不到了,因为length在规范不允许重写,而arr[0]直接更改是可以监听到的。

  • 数组方法使用不能监听到数组的变更,例如push

    这也是为什么vue重写这些方法的原因

  • 数据的变化是通过getter/setter来追踪的。因为这种追踪方式,有些语法中,即便是数据发生了变化,vue也检查不到。比如 向Object添加属性/ 删除Object的属性。

  • 检测数组的变化,因为只是拦截了unshift shift push pop splice sort reverse 这几个方法,所以像

    list[0] = 4
    list.length = 0
    

    检测不到

业务代码处理:this.$set(this.data,”key”,value’)

Object.defineProperty本身有一定的监控到数组下标变化的能力:Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。

2.$set原理

在 set 方法中,对 target 是数组和对象做了分别的处理, target 是数组时,会调用重写过的 splice 方法进行手动 Observe 。

对于对象,如果 key 本来就是对象的属性,则直接修改值触发更新,否则调用 defineReactive 方法重新定义响应式对象。

3. vue是怎么重写数组方法的

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {  
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

重写了数组中的那些方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化,然后手动调用notify,通知渲染watcher,执行update

59.vue 3.0 proxy优缺点?怎么处理vue3不支持IE?

  • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

60.computed 和 watch 的区别和运用的场景?除了基本的,看你能不能说出三种watcher的区别

computed:计算属性

计算属性是由data中的已知值,得到的一个新值。 这个新值只会根据已知值的变化而变化,其他不相关的数据的变化不会影响该新值。 计算属性不在data中,计算属性新值的相关已知值在data中。 别人变化影响我自己。 watch:监听数据的变化

监听data中数据的变化 监听的数据就是data中的已知值 我的变化影响别人

1.watch擅长处理的场景:一个数据影响多个数据

2.computed擅长处理的场景:一个数据受多个数据影响

61. 如何实现图片的懒加载

思路: 如何判断图片出现在了当前视口 (即如何判断我们能够看到图片) 如何控制图片的加载

方案一: 位置计算 + 滚动事件 (Scroll) + DataSet API

  • 1.位置计算: clientTop,offsetTop,clientHeight 以及 scrollTop 各种关于图片的高度作比对

  • 2.监听 window.scroll 事件

  • 3.DataSet API: <img data-src="solo.jpg">

    • 首先设置一个临时 Data 属性 data-src,控制加载时使用 src 代替 data-src,可利用 DataSet API 实现

    • img.src = img.datset.src

方案二: getBoundingClientRect API + Scroll + DataSet API

    1. Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置
      // clientHeight 代表当前视口的高度
       img.getBoundingClientRect().top < document.documentElement.clientHeight
      
  • 2.监听 window.scroll
    1. 同上

方案三: IntersectionObserver API + DataSet API

const observer = new IntersectionObserver((changes) => {
  // changes: 目标元素集合
  changes.forEach((change) => {
    // intersectionRatio
    if (change.isIntersecting) {
      const img = change.target
      img.src = img.dataset.src
      observer.unobserve(img)
    }
  })
})

observer.observe(img)

ie不支持

方案四: LazyLoading属性

<img src="shanyue.jpg" loading="lazy">

除chrome几乎都不支持

62. form表单设计

考察 element-ui 源码表单设计, 代码仓库地址: github.com/glihui/guo-…

63.最后:分享下自己整理的部分知识点文章链接