面试复盘|虾皮前端
一面
-
自我介绍
-
项目MDB文件解析怎么做的
-
如何使用的geoserver(2、3都是自己项目相关的)
-
HTML5有哪些更新
-
新增语义化标签:
-
header:定义文档的页眉(头部);
-
nav:定义导航链接的部分;
-
footer:定义文档或节的页脚(底部);
-
article:定义文章内容;
-
section:定义文档中的节(section、区段);
-
aside:定义其所处内容之外的内容(侧边);
-
-
音频、视频标签:audio、video
-
数据存储:localStorage、sessionStorage
-
canvas(画布)、Geolocation(地理定位)、websocket(通信协议)
-
input标签新增属性:placeholder、autocomplete、autofocus、required
-
history API:go、forward、back、pushstate
-
移除的元素有:
-
纯表现的元素:basefont,big,center,font, s,strike,tt,u;
-
对可用性产生负面影响的元素:frame,frameset,noframes;
-
-
-
script标签中defer和async的区别
defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:
-
执行顺序:多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;
-
脚本是否并行执行:async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行进行的,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。
-
使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。
-
-
CSS中display:none与visibility:hidden的区别
这两个属性都是让元素隐藏,不可见。两者区别如下:
-
在渲染树中
-
display:none会让元素完全从渲染树中消失,渲染时不会占据任何空间;
-
visibility:hidden不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。
-
-
是否是继承属性
-
display:none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点的属性也无法显示;
-
visibility:hidden是继承属性,子孙节点消失是由于继承了hidden,通过设置visibility:visible可以让子孙节点显示;
-
-
修改常规文档流中元素的 display 通常会造成文档的重排,但是修改visibility属性只会造成本元素的重绘;
-
如果使用读屏器,设置为display:none的内容不会被读取,设置为visibility:hidden的内容会被读取。
-
-
JS中变量提升
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
-
var的创建和初始化被提升,赋值不会被提升。
-
let的创建被提升,初始化和赋值不会被提升。
-
function的创建、初始化和赋值均会被提升。
-
-
new的时候都做了哪些操作
new操作符的执行过程:
-
首先创建了一个新的空对象
-
设置原型,将对象的原型设置为函数的 prototype 对象。
-
让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
-
判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
具体实现:
function objectFactory() { let newObject = null; let constructor = Array.prototype.shift.call(arguments); let result = null; // 判断参数是否是一个函数 if (typeof constructor !== "function") { console.error("type error"); return; } // 新建一个空对象,对象的原型为构造函数的 prototype 对象 newObject = Object.create(constructor.prototype); // 将 this 指向新建对象,并执行函数 result = constructor.apply(newObject, arguments); // 判断返回对象 let flag = result && (typeof result === "object" || typeof result === "function"); // 判断返回结果 return flag ? result : newObject; } // 使用方法 objectFactory(构造函数, 初始化参数);
-
-
判断对象身上有没有某个属性?判断某个属性仅仅是原型对象上的属性?
hasOwnProperty和in属性操作 : 判断对象身上有没有某个属性
-
in判断一个对象, 是否拥有某个属性(如果对象身上没有, 会到原型对象里面查找)
-
hasOwnProperty只到对象自身查找
-
判断某个属性仅仅是原型对象上的属性
("属性名" in p) && !p.hasOwnProperty("属性名")
-
-
JS中数据经常不准确,比如:为什么0.1+0.2 ! == 0.3,如何让其相等?
let n1 = 0.1, n2 = 0.2 console.log(n1 + n2) // 0.30000000000000004
要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004。
如何实现0.1+0.2=0.3呢?
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){ return Math.abs(arg1 - arg2) < Number.EPSILON; } console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
-
对原型、原型链的理解
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
-
原型链的终点是什么?如何打印出原型链的终点?
由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__。
-
异步编程的实现方式?
-
回调函数
-
Promise
-
generator
-
async/await
-
-
HTTP、HTTPS的区别
-
HTTPS协议需要CA证书,费用较高;而HTTP协议不需要;
-
HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;
-
使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;
-
HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。
-
-
HTTPS通信(握手)过程
-
客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
-
服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
-
客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。
-
服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。
-
客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。
-
-
讲一下CSRF
-
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
-
攻击类型,常见的 CSRF 攻击有三种:
-
GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
-
POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
-
链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。
-
-
防范 : 跨站请求伪造主要在服务器端做
-
进行同源检测,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
-
使用 CSRF Token 进行验证,服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
-
对 Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
-
在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。
-
加验证码,手机验证码等等
-
-
二面
-
自我介绍
-
使用过node、django、C#、java,为什么要学这么多后端语言
-
TCP、UDP的区别于使用场景
-
区别
UDP TCP 是否连接 无连接 面向连接 是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信 传输方式 面向报文 面向字节流 首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节 适用场景 适用于实时应用,例如视频会议、直播 适用于要求可靠传输的应用,例如文件传输
-
使用场景
-
TCP应用场景: 效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有UDP高。例如:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。
-
UDP应用场景: 效率要求相对高,对准确性要求相对低的场景。例如:QQ聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)。
-
-
-
讲一下HTTP三次握手
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
-
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
-
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y
-
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
-
-
为什么要三次?
-
为了确认双方的接收能力和发送能力都正常
-
如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
-
-
为什么UDP不会粘包
-
TCP协议是⾯向流的协议,UDP是⾯向消息的协议。UDP段都是⼀条消息,应⽤程序必须以消息为单位提取数据,不能⼀次提取任意字节的数据
-
UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端⼝等信息),这样对于接收端来说就容易进⾏区分处理了。传输协议把数据当作⼀条独⽴的消息在⽹上传输,接收端只能接收独⽴的消息。接收端⼀次只能接收发送端发出的⼀个数据包,如果⼀次接受数据的⼤⼩⼩于发送端⼀次发送的数据⼤⼩,就会丢失⼀部分数据,即使丢失,接受端也不会分两次去接收。
-
-
如何解决跨域
-
JSONP解决跨域:只能解决GET跨域
-
CORS操作:Cross-origin resource sharing 跨域资源共享:允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服跨域问题,他需要浏览器和服务器的同时支持
-
nginx代理跨域
-
nodejs 中间件代理跨域
-
WebSocket协议跨域
-
window.name + iframe跨域
-
-
实现JSONP
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script> (function (w) { /** * jsonp的实现 * @param {Object}option */ function jsonp(option) { // 0. 产生不同的函数名 var callBackName = 'itLike' + Math.random().toString().substr(2) + Math.random().toString().substr(2); // console.log(callBackName); // 1. 函数挂载在全局 w[callBackName] = function (data) { option.success(data); // 删除script标签 document.body.removeChild(scriptEle); }; // 2. 处理url链接 option.url = option.url + '?callback=' + callBackName; // 3. 创建script标签插入body var scriptEle = document.createElement('script'); scriptEle.src = option.url; document.body.appendChild(scriptEle); } w.jsonp = jsonp; })(window); </script> <script> // 调用 jsonp({ url: 'http://localhost:3000/', success: function (data) { console.log(data); alert('111'); } }); jsonp({ url: 'http://localhost:3000/', success: function (data) { console.log(data); alert('222'); } }); jsonp({ url: 'http://localhost:3000/', success: function (data) { console.log(data); alert('333'); } }); </script> </body> </html>
-
浏览器事件循环
因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
Event Loop 执行顺序如下所示:
-
首先执行同步代码,这属于宏任务
-
当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
-
执行所有微任务
-
当执行完所有微任务后,如有必要会渲染页面
-
然后开始下一轮 Event Loop,执行宏任务中的异步代码
-
-
Vue数据双向绑定原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
-
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
-
实现深拷贝
// 深拷贝的实现 function deepCopy(object) { if (!object || typeof object !== "object") return; let newObject = Array.isArray(object) ? [] : {}; for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } } return newObject; }
-
手写节流防抖
-
节流
// 函数节流的实现; function throttle(fn, delay) { let curTime = Date.now(); return function() { let context = this, args = arguments, nowTime = Date.now(); // 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } }; }
-
防抖
// 函数防抖的实现 function debounce(fn, wait) { let timer = null; return function() { let context = this, args = arguments; // 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; }
-
-
算法:
-
爬楼梯:递归、数组缓存、动态规划
-
快速排序及时间复杂度分析
-
HR面
-
自我介绍
-
项目中遇到的困难
-
项目是实习还是自己接的
-
学习成绩
-
工作地点
-
为什么选择虾皮
- ···
总结:
- 虾皮是比较看重学历的,感觉简历筛选是一道门槛。
- 进入面试后感觉虾皮问的都很基础,会结合项目,从项目入手,然后对相关知识点进行深度提问。
- 感觉面试就是一个循序渐进的过程,不断面试对遗漏的知识点进行查漏补缺。
- 如果简历写了Vue,感觉Vue肯定会问到一些原理方面的问题,要着重准备。
- 浏览器原理、网络、性能优化也比较重要。
小问题:
- Vue在双向数据绑定的时候,getter/setter是什么时候进行绑定设置的呢?