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

b840c72e59dd5b880ed68bb8d10b6c62.jpeg

文章会持续更新css

1.https 原理(加密 证书)

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

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

XSS

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

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

  2. XSS攻击的危害包括:vue

    一、盗取各种用户账号,如机器登陆账号、用户网银账号、各种管理员账号html5

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

    三、盗窃企业重要的具备商业价值的资料node

    四、非法转帐webpack

    五、强制发送电子邮件nginx

    六、网站挂马git

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

  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一种请求。

二、跨域资源共享(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:可选

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

三、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;
    }
}
复制代码

四、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
    }
}
复制代码

五、document.domain + iframe跨域

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

六、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页面全部对象。

七、window.name + iframe跨域

window.name属性的独特之处:name值在不一样的页面(甚至不一样域名)加载后依旧存在,而且能够支持很是长的 name 值(2MB)。

八、postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数很少能够跨域操做的window属性之一,它可用于解决如下方面的问题:

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

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

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

九、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三次握手和四次挥手的理解

1、三次握手讲解

  • 客户端发送位码为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则链接创建成功。(客户端:好的,我来了)

2、为何http创建链接须要三次握手,不是两次或四次?

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

3、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状态,释放网络资源

4、为何要四次挥手?

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表单提交的区别和原理

区别:

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

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

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

四、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>
复制代码

方法:

一、将左侧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?

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

二、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. e m i t / 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 是两个对象, a t t r s 里存放的是父组件中绑定的非 P r o p s 属性, attrs 里存放的是父组件中绑定的非 Props 属性, listeners里存放的是父组件中绑定的非原生事件。

  1. provide/inject

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

  2. p a r e n t / 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.最后:分享下本身整理的部分知识点文章连接

相关文章
相关标签/搜索