前端面试题汇总一

说一说 cookie sessionStorage localStorage 区别?

得分点:

数据存储位置、生命周期、存储大小、写入方式、数据共享、发送请求时是否携带、应用场景

标准回答:

Cookie、SessionStorage、 LocalStorage 都是浏览器的本地存储。

共同点:

都是存储在浏览器本地的

区别:

cookie 是由服务器端写入的,而 SessionStorage、 LocalStorage 都是由前端写入的,cookie 的生命周期是由服务器端在写入的时候就设置好的,LocalStorage 是写入就一直存在,除非手动清除,SessionStorage 是页面关闭的时候就会自动清除。cookie 的存储空间比较小大概 4KB,SessionStorage、 LocalStorage 存储空间比较大,大概 5M。Cookie、SessionStorage、 LocalStorage 数据共享都遵循同源原则,SessionStorage 还限制必须是同一个页面。在前端给后端发送请求的时候会自动携带 Cookie 中的数据,但是 SessionStorage、 LocalStorage 不会

加分回答

由于它们的以上区别,所以它们的应用场景也不同,Cookie 一般用于存储登录验证信息 SessionID 或者 token,LocalStorage 常用于存储不易变动的数据,减轻服务器的压力,SessionStorage 可以用来检测用户是否是刷新进入页面,如音乐播放器恢复播放进度条的功能。

说一说 JS 数据类型有哪些,区别是什么?

得分点

Number、String、Boolean、BigInt、Symbol、Null、Undefined、Object、8 种

标准回答

JS 数据类型分为两类:一类是基本数据类型,也叫简单数据类型,包含 7 种类型,分别是 Number 、String、Boolean、BigInt、Symbol、Null、Undefined。另一类是引用数据类型也叫复杂数据类型,通常用 Object 代表,普通对象,数组,正则,日期,Math 数学函数都属于 Object。

数据分成两大类的本质区别:基本数据类型和引用数据类型它们在内存中的存储方式不同。
基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。
引用数据类型是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

加分回答

Symbol 是 ES6 新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以作为 object 的 key。
数据的创建方法 Symbol(),因为它的构造函数不够完整,所以不能使用 new Symbol()创建数据。由于 Symbol()创建数据具有唯一性,所以 Symbol() !== Symbol(), 同时使用 Symbol 数据作为 key 不能使用 for 获取到这个 key,需要使用 Object.getOwnPropertySymbols(obj)获得这个 obj 对象中 key 类型是 Symbol 的 key 值。

1
2
3
4
let key = Symbol("key");
let obj = { [key]: "symbol" };
let keyArray = Object.getOwnPropertySymbols(obj); // 返回一个数组[Symbol('key')]
obj[keyArray[0]]; // 'symbol'

BigInt 也是 ES6 新出的一种数据类型,这种数据类型的特点就是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题。

使用方法: -整数末尾直接+n:647326483767797n -调用 BigInt()构造函数:BigInt(“647326483767797”)

说一说你对闭包的理解?

得分点

变量背包、作用域链、局部变量不销毁、函数体外访问函数的内部变量、内存泄漏、内存溢出、形成块级作用域、柯里化、构造函数中定义特权方法、Vue 中数据响应式 Observer

标准回答

闭包 一个函数和词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。一般就是一个函数 A,return 其内部的函数 B,被 return 出去的 B 函数能够在外部访问 A 函数内部的变量,这时候就形成了一个 B 函数的变量背包,A 函数执行结束后这个变量背包也不会被销毁,并且这个变量背包在 A 函数外部只能通过 B 函数访问。 闭包形成的原理:作用域链,当前作用域可以访问上级作用域中的变量 闭包解决的问题:能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。 闭包带来的问题:由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。 加分回答 闭包的应用,能够模仿块级作用域,能够实现柯里化,在构造函数中定义特权方法、Vue 中数据响应式 Observer 中使用闭包等。

说一说 promise 是什么与使用方法?

得分点

pendding、rejected、resolved、微任务、then、catch、Promise.resolve()、Promise.reject()、Promise.all() Promise.any()、Promise.race()

标准回答

Promise 的作用:

Promise 是异步微任务,解决了异步多层嵌套回调的问题,让代码的可读性更高,更容易维护

Promise 使用:

Promise 是 ES6 提供的一个构造函数,可以使用 Promise 构造函数 new 一个实例,Promise 构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数 resolverejectresolve将 Promise 的状态由等待变为成功,将异步操作的结果作为参数传递过去;reject则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,也可以使用 catch 捕获失败,then 和 catch 最终返回的也是一个 Promise,所以可以链式调用。

Promise 的特点:

  1. 对象的状态不受外界影响(Promise 对象代表一个异步操作,有三种状态)。 - pending(执行中) - Resolved(成功,又称 Fulfilled) - rejected(拒绝)
    其中 pending 为初始状态,fulfilled 和 rejected 为结束状态(结束状态表示 promise 的生命周期已结束)。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。 Promise 对象的状态改变,只有两种可能(状态凝固了,就不会再变了,会一直保持这个结果): - 从 Pending 变为 Resolved - 从 Pending 变为 Rejected
  3. resolve 方法的参数是 then 中回调函数的参数,reject 方法中的参数是 catch 中的参数
  4. then 方法和 catch 方法 只要不报错,返回的都是一个 fullfilled 状态的 promise

加分回答

Promise 的其他方法:

  • Promise.resolve() :返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。
  • Promise.reject():返回一个状态为失败的 Promise 对象,并将给定的失败信息传递给对应的处理方法。
  • Promise.all():返回一个新的 promise 对象,该 promise 对象在参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。
  • Promise.any():接收一个 Promise 对象的集合,当其中的一个 promise 成功,就返回那个成功的 promise 的值。
  • Promise.race():当参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。

说一说跨域是什么?如何解决跨域问题?

得分点

同源限制、协议、域名、端口、CORS、node 中间件、JSONP、postmessage

标准回答

跨域:

当前页面中的某个接口请求的地址和当前页面的地址如果协议、域名、端口其中有一项不同,就说该接口跨域了。

跨域限制的原因:

浏览器为了保证网页的安全,出的同源协议策略。

跨域解决方案

cors:目前最常用的一种解决办法,通过设置后端允许跨域实现。
res.setHeader(‘Access-Control-Allow-Origin’, ‘*‘);
res.setHeader(“Access-Control-Allow-Methods”, “GET, PUT, OPTIONS, POST”);

node 中间件、nginx 反向代理:跨域限制的时候浏览器不能跨域访问服务器,node 中间件和 nginx 反向代理,都是让请求发给代理服务器,静态页面面和代理服务器是同源的,然后代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制。

JSONP:利用的原理是 script 标签可以跨域请求资源,将回调函数作为参数拼接在 url 中。后端收到请求,调用该回调函数,并将数据作为参数返回去,注意设置响应头返回文档类型,应该设置成 javascript。

postmessage:H5 新增 API,通过发送和接收 API 实现跨域通信。

加分回答

跨域场景:前后端分离式开发、调用第三方接口

说一说 BFC

得分点

块级格式化上下文、独立的渲染区域、不会影响边界以外的元素、形成 BFC 条件、float、position、overflow、display

标准回答

BFC(Block Formatting Context)块级格式化上下文,是 Web 页面一块独立的渲染区域,内部元素的渲染不会影响边界以外的元素。

BFC 布局规则

  • 内部盒子会在垂直方向,一个接一个地放置。
  • Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠。
  • 每个盒子(块盒与行盒)的 margin box 的左边,与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  • BFC 的区域不会与 float box 重叠。
  • BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  • 计算 BFC 的高度时,浮动元素也参与计算。 BFC 形成的条件 -float 设置成 left right
  • position absolute或者fixed -overflow 不是visible,为 autoscrollhidden -displayflex或者inline-block

BFC 解决能的问题:

清除浮动

加分回答

BFC 的方式都能清除浮动,但是常使用的清除浮动的 BFC 方式只有overflow:hidden,原因是使用 float 或者 position 方式清除浮动,虽然父级盒子内部浮动被清除了,但是父级本身又脱离文档流了,会对父级后面的兄弟盒子的布局造成影响。如果设置父级为display:flex,内部的浮动就会失效。所以通常只是用overflow: hidden清除浮动。 IFC(Inline formatting contexts):内联格式上下文。IFC 的高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的 padding/margin 影响),IFC 中的 line box 一般左右都贴紧整个 IFC,但是会因为 float 元素而扰乱。 GFC(GrideLayout formatting contexts):网格布局格式化上下文。当为一个元素设置 display 值为 grid 的时候,此元素将会获得一个独立的渲染区域。 FFC(Flex formatting contexts):自适应格式上下文。display 值为 flex 或者 inline-flex 的元素将会生成自适应容器。

说一说 Vuex 是什么,每个属性是干嘛的,如何使用 ?

得分点

state、mutations、getters、actions、module、store.commit、store.dispatch

标准回答

Vuex 是集中管理项目公共数据的。Vuex 有 state、mutations 、getters、actions、module 属性。

  • state 属性用来存储公共管理的数据。
  • mutations 属性定义改变 state 中数据的方法, 注意:不要在 mutation 中的方法中写异步方法 ajax,那样数据就不可跟踪了 。
  • getters 属性可以认为是定义 store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
  • action 属性类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
  • moudle 属性是将 store 分割成模块。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块,从上至下进行同样方式的分割

使用方法:

  • state :直接以对象方式添加属性
  • mutations :通过store.commit调用
  • action:通过 store.dispatch 方法触发
  • getters:直接通过 store.getters.调用

加分回答

可以使用 mapState、mapMutations、mapAction、mapGetters 一次性获取每个属性下对应的多个方法。 VueX 在大型项目中比较常用,非关系组件传递数据比较方便。

说一说 JavaScript 有几种方法判断变量的类型?

得分点 typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)

标准回答

JavaScript 有 4 种方法判断变量的类型,分别是 typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)

  • typeof:常用于判断基本数据类型,对于引用数据类型除了 function 返回’function‘,其余全部返回’object’。
  • instanceof:主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上,用其检测出来的结果都是 true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的 undefined, null, symbol 检测不出来。
  • constructor:用于检测引用数据类型,检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。 Object.prototype.toString.call():适用于所有类型的判断检测,检测方法是 Object.prototype.
  • toString.call(数据) 返回的是该数据类型的字符串。 这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是 Object.prototype.toString.call()这种方法。

加分回答

instanceof 的实现原理:验证当前类的原型 prototype 是否会出现在实例的原型链proto上,只要在它的原型链上,则结果都为 true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回 true,未找到返回 false。 Object.prototype.toString.call()原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变 this 的指向,那么把 Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

说一说样式优先级的规则是什么?

得分点

!important、行内样式、嵌入样式、外链样式、id 选择器、类选择器、标签选择器、复合选择器、通配符、继承样式

标准回答

CSS 样式的优先级应该分成五大类

  • 第一类!important,无论引入方式是什么,选择器是什么,它的优先级都是最高的。
  • 第二类引入方式,行内样式的优先级要高于嵌入和外链,嵌入和外链如果使用的选择器相同就看他们在页面中插入的顺序,在后面插入的会覆盖前面的。
  • 第三类选择器,选择器优先级:id 选择器>(类选择器 | 伪类选择器 | 属性选择器 )> (后代选择器 | 伪元素选择器 )> (子选择器 | 相邻选择器) > 通配符选择器 。
  • 第四类继承样式,是所有样式中优先级比较低的。
  • 第五类浏览器默认样式优先级最低。

加分回答

使用!important 要谨慎 - 一定要优先考虑使用样式规则的优先级来解决问题而不是 !important - 只有在需要覆盖全站或外部 CSS 的特定页面中使用 !important - 永远不要在你的插件中使用 !important - 永远不要在全站范围的 CSS 代码中使用 !important 优先级的比较指的是相同的样式属性,不同样式属性优先级比较失效,比如:在设置max-width时注意,已经给元素的max-width设置了!important但是还不生效,很有可能就是被 width 覆盖了

说一说 JS 实现异步的方法?

得分点

回调函数、事件监听、setTimeout、Promise、生成器 Generators/yield、async/awt

标准回答

所有异步任务都是在同步任务执行结束之后,从任务队列中依次取出执行。

  • 回调函数是异步操作最基本的方法,比如 AJAX 回调,回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return
  • Promise 包装了一个异步调用并生成一个 Promise 实例,当异步调用返回的时候根据调用的结果分别调用实例化时传入的 resolve 和 reject 方法,then 接收到对应的数据,做出相应的处理。Promise 不仅能够捕获错误,而且也很好地解决了回调地狱的问题,缺点是无法取消 Promise,错误需要通过回调函数捕获。
  • Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数是一个状态机,封装了多个内部状态,可暂停函数, yield 可暂停,next 方法可启动,每次返回的是 yield 后的表达式结果。优点是异步语义清晰,缺点是手动迭代Generator 函数很麻烦,实现逻辑有点绕
  • async/awt 是基于 Promise 实现的,async/awt 使得异步代码看起来像同步代码,所以优点是,使用方法清晰明了,缺点是 awt 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 awt 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。

加分回答

JS 异步编程进化史:callback -> promise -> generator/yield -> async/awt。 async/awt 函数对 Generator 函数的改进,体现在以下三点: - 内置执行器。 Generator 函数的执行必须靠执行器,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。 - 更广的适用性。 yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 awt 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 - 更好的语义。 async 和 awt,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,awt 表示紧跟在后面的表达式需要等待结果。 目前使用很广泛的就是 promise 和 async/awt

说一说 Vue2.0 双向绑定的原理与缺陷?

得分点

Object.defineProperty、getter、setter

标准回答

Vue 响应式指的是:组件的 data 发生变化,立刻触发试图的更新

原理:

Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过 Object.defineProperty 来劫持数据的 setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。 通过原生 js 提供的监听数据的 API,当数据发生变化的时候,在回调函数中修改 dom

核心 API:Object.defineProperty Object.defineProperty

作用: 用来定义对象属性

特点: 默认情况下定义的数据的属性不能修改 描述属性和存取属性不能同时使用,使用会报错 响应式原理: 获取属性值会触发 getter 方法 设置属性值会触发 setter 方法 在 setter 方法中调用修改 dom 的方法

加分回答

Object.defineProperty 的缺点 1. 一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出 2. 不能监听对象的新增属性和删除属性 3. 无法正确的监听数组的方法,当监听的下标对应的数据发生改变时

说一说数组去重都有哪些方法?

得分点

对象属性、new Set() 、indexOf、hasOwnProperty、reduce+includes、filter

标准回答

  1. 利用对象属性 key 排除重复项:遍历数组,每次判断对象中是否存在该属性,不存在就存储在新数组中,并且把数组元素作为 key,设置一个值,存储在对象中,最后返回新数组。这个方法的优点是效率较高,缺点是占用了较多空间,使用的额外空间有一个查询对象和一个新的数组
  2. 利用 Set 类型数据无重复项:new 一个 Set,参数为需要去重的数组,Set 会自动删除重复的元素,再将 Set 转为数组返回。这个方法的优点是效率更高,代码简单,思路清晰,缺点是可能会有兼容性问题
  3. filter+indexof 去重:这个方法和第一种方法类似,利用 Array 自带的 filter 方法,返回 arr.indexOf(num) 等于 index 的 num。原理就是 indexOf 会返回最先找到的数字的索引,假设数组是 [1, 1],在对第二个 1 使用 indexOf 方法时,返回的是第一个 1 的索引 0。这个方法的优点是可以在去重的时候插入对元素的操作,可拓展性强。
  4. 这个方法比较巧妙,从头遍历数组,如果元素在前面出现过,则将当前元素挪到最后面,继续遍历,直到遍历完所有元素,之后将那些被挪到后面的元素抛弃。这个方法因为是直接操作数组,占用内存较少。
  5. reduce +includes 去重:这个方法就是利用 reduce 遍历和传入一个空数组作为去重后的新数组,然后内部判断新数组中是否存在当前遍历的元素,不存在就插入到新数组中。这种方法时间消耗多,内存空间也有额外占用。

加分回答

以上五个方法中,在数据低于 10000 条的时候没有明显的差别,高于 10000 条,第一种和第二种的时间消耗最少,后面三种时间消耗依次增加,由于第一种内存空间消耗比较多,且现在很多项目不再考虑低版本浏览器的兼容性问题,所以建议使用第二种去重方法,简洁方便。

说一说 null 和 undefined 的区别,如何让一个属性变为 null

得分点

操作的变量没有被赋值、全局对象的一个属性、函数没有 return 返回值、值 null 特指对象的值未设置 undefined == null、undefined !== null

标准回答

undefind 是全局对象的一个属性,当一个变量没有被赋值或者一个函数没有返回值或者某个对象不存在某个属性却去访问或者函数定义了形参但没有传递实参,这时候都是 undefined。undefined 通过 typeof 判断类型是’undefined’。undefined == undefined undefined === undefined 。 null 代表对象的值未设置,相当于一个对象没有设置指针地址就是 null。null 通过 typeof 判断类型是’object’。null === null null == null null == undefined null !== undefined undefined 表示一个变量初始状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。在实际使用过程中,不需要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。 让一个变量为 null,直接给该变量赋值为 null 即可。

加分回答

null 其实属于自己的类型 Null,而不属于 Object 类型,typeof 之所以会判定为 Object 类型,是因为 JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。 对象被赋值了 null 以后,对象对应的堆内存中的值就是游离状态了,GC 会择机回收该值并释放内存。因此,需要释放某个对象,就将变量设置为 null,即表示该对象已经被清空,目前无效状态。

说一下浮动?

得分点 脱离文档流、盒子塌陷、 影响其他元素排版、伪元素 、overflow:hidden 、标签插入法

标准回答

浮动的作用,设置浮动的图片,可以实现文字环绕图片,设置了浮动的块级元素可以排列在同一行,设置了浮动的行内元素可以设置宽高,同时可以按照浮动设置的方向对齐排列盒子。 设置浮动元素的特点: -设置了浮动,该元素脱标。元素不占位置 -浮动可以进行模式转换(行内块元素) 浮动造成的影响,使盒子脱离文档流,如果父级盒子没有设置高度,需要被子盒子撑开,那么这时候父级盒子的高度就塌陷了,同时也会造成父级盒子后面的兄弟盒子布局受到影响。如果浮动元素后面还有其他兄弟元素,其他兄弟元素的布局也会受到影响。

清除浮动的方法:

  • 伪元素清除浮动:给浮动元素父级增加 .clearfix::after { content: ''; display: table; clear: both; } /*兼容IE低版本 */ .clearfix { *zoom: 1; }
  • overflow:hidden:给浮动元素父级增加overflow:hidden属性
  • 额外标签法:给浮动元素父级增加标签

加分回答

三种清除浮动的特点和影响

  • 伪元素清除浮动:不会新增标签,不会有其他影响,是当下清除浮动最流行的方法
  • overflow:hidden:不会新增标签,但是如果父级元素有定位元素超出父级,超出部分会隐藏,在不涉及父级元素有超出内容的情况,overflow:hidden 比较常用,毕竟写法方便简洁
  • 标签插入法:清除浮动的语法加在新增标签上,由于新增标签会造成不必要的渲染,所以这种方法目前不建议使用

说一说 es6 中箭头函数?

得分点

没有 this、this 是从外部获取、不能使用 new、没有 arguments、没有原型和 super

标准回答

箭头函数相当于匿名函数,简化了函数定义。箭头函数有两种写法,当函数体是单条语句的时候可以省略{}和 return。另一种是包含多条语句,不可以省略{}和 return。 箭头函数最大的特点就是没有 this,所以 this 是从外部获取,就是继承外部的执行上下文中的 this,由于没有 this 关键字所以箭头函数也不能作为构造函数, 同时通过 call()apply() 方法调用一个函数时,只能传递参数(不能绑定 this),第一个参数会被忽略。箭头函数也没有原型和 super。不能使用 yield 关键字,因此箭头函数不能用作 Generator 函数。不能返回直接对象字面量。

加分回答

箭头函数的不适用场景:

  • 定义对象上的方法 当调用 dog.jumps 时,lives 并没有递减。因为 this 没有绑定值,而继承父级作用域。 var dog = { lives: 20, jumps: () => { this.lives--; } }
  • 不适合做事件处理程序 此时触发点击事件,this 不是 button,无法进行 class 切换 var button = document.querySelector('button'); button.addEventListener('click', () => { this.classList.toggle('on'); }); 箭头函数函数适用场景:
  • 简单的函数表达式,内部没有 this 引用,没有递归、事件绑定、解绑定,适用于 map、filter 等方法中,写法简洁 var arr = [1,2,3]; var newArr = arr.map((num)=>num*num)
  • 内层函数表达式,需要调用 this,且 this 应与外层函数一致时 let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList();

说一说 call apply bind 的作用和区别?

得分点

bind 改变 this 指向不直接调用、call 和 apply 改变 this 指向直接调用、apply 接收第二个参数为数组 、call 用于对象的继承 、伪数组转换成真数组、apply 用于找出数组中的最大值和最小值以及数组合并、bind 用于 vue 或者 react 框架中改变函数的 this 指向

标准回答

call、apply、bind 的作用都是改变函数运行时的 this 指向。
bind 和 call、apply 在使用上有所不同,bind 在改变 this 指向的时候,返回一个改变执行上下文的函数,不会立即执行函数,而是需要调用该函数的时候再调用即可,但是 call 和 apply 在改变 this 指向的同时执行了该函数。 bind 只接收一个参数,就是 this 指向的执行上文。 call、apply 接收多个参数,第一个参数都是 this 指向的执行上文,后面的参数都是作为改变 this 指向的函数的参数。但是 call 和 apply 参数的格式不同,call 是一个参数对应一个原函数的参数,但是 apply 第二个参数是数组,数组中每个元素代表函数接收的参数,数组有几个元素函数就接收几个元素。

加分回答

call 的应用场景: 对象的继承,在子构造函数这种调用父构造函数,但是改变 this 指向,就可以继承父的属性

1
2
3
4
5
6
7
8
9
function superClass() {
this.a = 1;
this.print = function () {
console.log(this.a);
};
}
function subClass() {
superClass.call(this); // 执行 superClass,并将 superClass 方法中的 this 指向 subClass this.print(); } subClass(); 借用 Array 原型链上的 slice 方法,把伪数组转换成真数组 let domNodes = Array.prototype.slice.call(document.getElementsByTagName("div"));
}

apply 的应用场景: Math.max,获取数组中最大、最小的一项

1
2
3
4
5
6
let max = Math.max.apply(null, array);
let min = Math.min.apply(null, array); //实现两个数组合并
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6] bind 的应用场景 在 vue 或者 react 框架中,使用 bind 将定义的方法中的 this 指向当前类

说一说 HTML 语义化?

得分点

语义化标签、利于页面内容结构化、利于无 CSS 页面可读、利于 SEO、利于代码可读

标准回答

HTML 语义化就是指在使用 HTML 标签构建页面时,避免大篇幅的使用无语义的标签,

使用 HTML 语义化标签的作用:

  • 易于用户阅读,样式文件未加载时,页面结构清晰。
  • 有利于 SEO,搜索引擎根据标签来确定上下文和各个关键字的权重。
  • 方便屏幕阅读器解析,如盲人阅读器根据语义渲染网页。
  • 有利于开发和维护,代码更具可读性,代码更好维护。

说一说 this 指向(普通函数、箭头函数)?

得分点

全局执行上下文、函数执行上下文、this 严格模式下 undefined、非严格模式 window、构造函数新对象本身、普通函数不继承 this、箭头函数无 this,可继承

标准回答

this 关键字由来:在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
this 存在的场景有三种全局执行上下文和函数执行上下文和 eval 执行上下文,eval 这种不讨论。

  • 在全局执行环境中无论是否在严格模式下,(在任何函数体外部)this 都指向全局对象。在函数执行上下文中访问 this,函数的调用方式决定了 this 的值。在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window,通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
  • 普通函数 this 指向:当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。嵌套函数中的 this 不会继承外层函数的 this 值。
  • 箭头函数 this 指向:箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。

加分回答

箭头函数因为没有 this,所以也不能作为构造函数,但是需要继承函数外部 this 的时候,使用箭头函数比较方便

说一说 CSS 尺寸设置的单位

得分点

px、rem、em、vw、vh

标准回答

  • px:pixel 像素的缩写,绝对长度单位,它的大小取决于屏幕的分辨率,是开发网页中常常使用的单位。 - em:相对长度单位,在 font-size 中使用是相对于父元素的字体大小,在其他属性中使用是相对于自身的字体大小,如 width。如当前元素的字体尺寸未设置,由于字体大小可继承的原因,可逐级向上查找,最终找不到则相对于浏览器默认字体大小。
  • rem:相对长度单位,相对于根元素的字体大小,根元素字体大小未设置,使用浏览器默认字体大小。
  • vw:相对长度单位,相对于视窗宽度的 1%。
  • vh:相对长度单位,相对于视窗高度的 1%。

加分回答

rem 应用:在移动端网页开发中,页面要做成响应式的,可使用 rem 配合媒体查询或者 flexible.js 实现。原理是通过媒体查询或者 flexible.js,能够在屏幕尺寸发生改变时,重置 html 根元素的字体大小,页面中的元素都是使用 rem 为单位设置的尺寸,因此只要改变根元素字体大小,页面中的其他元素的尺寸就自动跟着修改 vw 应用:由于 vw 被更多浏览器兼容之后,在做移动端响应式页面时,通常使用 vw 配合 rem。原理是使用 vw 设置根元素 html 字体的大小,当窗口大小发生改变,vw 代表的尺寸随着修改,无需加入媒体查询和 flexible.js,页面中的其他元素仍使用 rem 为单位,就可实现响应式。

说几个未知宽高元素水平垂直居中方法

得分点

position
transform
flex justify-content align-items
vertical-align
text-align

标准回答

  1. 设置元素相对父级定位position:absolute;left:50%;right:50%,让自身平移自身高度 50% transform: translate(-50%,-50%);,这种方式兼容性好,被广泛使用的一种方式
  2. 设置元素的父级为弹性盒子display:flex,设置父级和盒子内部子元素水平垂直都居中justify-content:center; align-items:center ,这种方式代码简洁,但是兼容性 ie 11 以上支持,由于目前 ie 版本都已经很高,很多网站现在也使用这种方式实现水平垂直居中
  3. 设置元素的父级为网格元素display: grid,设置父级和盒子内部子元素水平垂直都居中justify-content:center; align-items:center ,这种方式代码简介,但是兼容性 ie 10 以上支持
  4. 设置元素的父级为表格元素display: table-cell,其内部元素水平垂直都居中text-align: center;vertical-align: middle; ,设置子元素为行内块display: inline-block; ,这种方式兼容性较好

说一说 JS 变量提升?

得分点

Var 声明的变量声明提升、函数声明提升、let 和 const 变量不提升

标准回答

变量提升是指 JS 的变量和函数声明会在代码编译期,提升到代码的最前面。
变量提升成立的前提是使用 Var 关键字进行声明的变量,并且变量提升的时候只有声明被提升,赋值并不会被提升,同时函数的声明提升会比变量的提升优先。
变量提升的结果,可以在变量初始化之前访问该变量,返回的是 undefined。在函数声明前可以调用该函数。

加分回答

使用 let 和 const 声明的变量是创建提升,形成暂时性死区,在初始化之前访问 let 和 const 创建的变量会报错。

说一说 HashRouter 和 HistoryRouter 的区别和原理?

得分点

window.onhashchange history.pushState window.onpopstate

标准回答

HashRouter 和 HistoryRouter 的区别:

  1. history 和 hash 都是利用浏览器的两种特性实现前端路由,history 是利用浏览历史记录栈的 API 实现,hash 是监听 location 对象 hash 值变化事件来实现
  2. history 的 url 没有’#’号,hash 反之
  3. 相同的 url,history 会触发添加到浏览器历史记录栈中,hash 不会触发,history 需要后端配合,如果后端不配合刷新新页面会出现 404,hash 不需要。

HashRouter 的原理:通过window.onhashchange方法获取新 URL 中 hash 值,再做进一步处理 HistoryRouter 的原理:通过history.pushState 使用它做页面跳转不会触发页面刷新,使用window.onpopstate 监听浏览器的前进和后退,再做其他处理

加分回答

hash 模式下 url 会带有#,需要 url 更优雅时,可以使用 history 模式。 需要兼容低版本的浏览器时,建议使用 hash 模式。 需要添加任意类型数据到记录时,可以使用 history 模式。

说一说 map 和 forEach 的区别?

得分点

map 创建新数组、map 返回处理后的值、forEach()不修改原数组、forEach()方法返回 undefined

标准回答

map 和 forEach 的区别:map 有返回值,可以开辟新空间,return 出来一个 length 和原数组一致的数组,即便数组元素是 undefined 或者是 null。forEach 默认无返回值,返回结果为 undefined,可以通过在函数体内部使用索引修改数组元素。

加分回答

map 的处理速度比 forEach 快,而且返回一个新的数组,方便链式调用其他数组新方法。

说一说事件循环 Event loop,宏任务与微任务?

得分点

任务挂起、同步任务执行结束执行队列中的异步任务、执行 script 标签内部代码、setTimeout/setInterval、ajax 请、postMessageMessageChannel、setImmediate、I/O(Node.js)Promise、MutonObserver、Object.observe、process.nextTick(Node.js)每个宏任务中都包含了一个微任务队列

标准回答

浏览器的事件循环:执行 js 代码的时候,遇见同步任务,直接推入调用栈中执行,遇到异步任务,将该任务挂起,等到异步任务有返回之后推入到任务队列中,当调用栈中的所有同步任务全部执行完成,将任务队列中的任务按顺序一个一个的推入并执行,重复执行这一系列的行为。 异步任务又分为宏任务和微任务。 宏任务:任务队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。 微任务:等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务 宏任务包含:执行 script 标签内部代码、setTimeout/setInterval、ajax 请、postMessageMessageChannel、setImmediate,I/O(Node.js) 微任务包含:Promise、MutonObserver、Object.observe、process.nextTick(Node.js)

加分回答

浏览器和 Node 环境下,microtask 任务队列的执行时机不同 - Node 端,microtask 在事件循环的各个阶段之间执行 - 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

说一说 Vue3.0 实现数据双向绑定的方法 ?

得分点

Proxy、数据拦截、劫持整个对象、返回一个新对象、有 13 种劫持

标准回答

Vue3.0 是通过 Proxy 实现的数据双向绑定,Proxy 是 ES6 中新增的一个特性,实现的过程是在目标对象之前设置了一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
用法: ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler)
target: 是用 Proxy 包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 是一个对象,其声明了代理 target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。

加分回答

Object.defineProperty 的问题:在 Vue 中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。目前只针对以上方法做了 hack 处理,所以数组属性是检测不到的,有局限性 Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 里,是通过递归以及遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。 Proxy 的两个优点:可以劫持整个对象,并返回一个新对象,有 13 种劫持

说一下 Diff 算法?

得分点

patch、patchVnode、updateChildren、vue 优化时间复杂度为 O(n)

标准回答 Diff 算法比较过程

  1. 第一步:patch 函数中对新老节点进行比较 如果新节点不存在就销毁老节点 如果老节点不存在,直接创建新的节点 当两个节点是相同节点的时候,进入 patctVnode 的过程,比较两个节点的内部
  2. 第二步:patchVnode 函数比较两个虚拟节点内部 如果两个虚拟节点完全相同,返回 当前 vnode 的 children 不是 textNode,再分成三种情况
    • 有新 children,没有旧 children,创建新的
    • 没有新 children,有旧 children,删除旧的
    • 新 children、旧 children 都有,执行updateChildren比较 children 的差异,这里就是 diff 算法的核心 当前 vnode 的 children 是 textNode,直接更新 text
  3. 第三步:updateChildren 函数子节点进行比较
    • 第一步 头头比较。若相似,旧头新头指针后移(即 oldStartIdx++ && newStartIdx++),真实 dom 不变,进入下一次循环;不相似,进入第二步。
    • 第二步 尾尾比较。若相似,旧尾新尾指针前移(即 oldEndIdx-- && newEndIdx--),真实 dom 不变,进入下一次循环;不相似,进入第三步。
    • 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即 oldStartIdx++ && newEndIdx--),未确认 dom 序列中的头移到尾,进入下一次循环;不相似,进入第四步。
    • 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即 oldEndIdx-- && newStartIdx++),未确认 dom 序列中的尾移到头,进入下一次循环;不相似,进入第五步。
    • 第五步 若节点有 key 且在旧子节点数组中找到 sameVnode(tag 和 key 都一致),则将其 dom 移动到当前真实 dom 序列的头部,新头指针后移(即 newStartIdx++);否则,vnode 对应的 dom(vnode[newStartIdx].elm)插入当前真实 dom 序列的头部,新头指针后移(即 newStartIdx++)。 - 但结束循环后,有两种情况需要考虑:
      • 新的字节点数组(newCh)被遍历完(newStartIdx > newEndIdx)。那就需要把多余的旧 dom(oldStartIdx -> oldEndIdx)都删除,上述例子中就是c,d
      • 新的字节点数组(oldCh)被遍历完(oldStartIdx > oldEndIdx)。那就需要把多余的新 dom(newStartIdx -> newEndIdx)都添加。

说一说三栏布局的实现方案

得分点

圣杯布局、双飞翼布局、三栏高度不定、中间栏内容多先渲染

标准回答 三栏布局,要求左右两边盒子宽度固定,中间盒子宽度自适应,盒子的高度都是随内容撑高的,一般都是中间盒子内容较多,为了保证页面渲染快,在写结构的时候,需要把中间盒子放在左右盒子的前面。 实现三栏布局的方法通常是圣杯布局和双飞翼布局。

  • 圣杯布局的实现方案:三个元素放在同一个父级元素中,代表中间盒子的元素放在最前面,父级盒子设置左右padding,三个盒子全部浮动,设置中间盒子宽度 100%,左右盒子设置固定宽度,设置左边盒子左边距-100%同时相对自身定位,右边平移自身宽度,右边盒子设置右边距-自身宽度,最后设置父级盒子清除浮动,否则父级盒子的高度无法被撑开
  • 双飞翼布局的实现方案:三个盒子对应三个元素,其中中间盒子套了两层,中间盒子内部盒子设置margin,三个盒子全部浮动,设置中间盒子宽度 100%,左右盒子设置固定宽度,设置左边盒子左边距-100%,右边盒子设置右边距-自身宽度,最后设置父级盒子清除浮动,否则父级盒子的高度无法被撑开

加分回答

圣杯布局:

  • 优点:不需要添加 dom 节点
  • 缺点:正常情况下是没有问题的,但是特殊情况下就会暴露此方案的弊端,当 middle 部分的宽小于 left 部分时就会发生布局混乱。
    双飞翼布局:
  • 优点:不会像圣杯布局那样变形,CSS 样式代码更简洁
  • 缺点:多加了一层 dom 节点

说一下浏览器垃圾回收机制?

得分点

栈垃圾回收、堆垃圾回收、新生区老生区、Scavenge 算法、标记-清除算法、标记-整理算法、全停顿、增量标记

标准回答

浏览器垃圾回收机制根据数据的存储方式分为栈垃圾回收和堆垃圾回收。

  • 栈垃圾回收的方式非常简便,当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文,遵循先进后出的原则。
  • 堆垃圾回收,当函数直接结束,栈空间处理完成了,但是堆空间的数据虽然没有被引用,但是还是存储在堆空间中,需要垃圾回收器将堆空间中的垃圾数据回收。

为了使垃圾回收达到更好的效果,根据对象的生命周期不一样,使用不同的垃圾回收的算法。
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。新生区中使用 Scavenge 算法,老生区中使用标记-清除算法和标记-整理算法。

加分回答

Scavenge 算法:

  1. 标记:对对象区域中的垃圾进行标记
  2. 清除垃圾数据和整理碎片化内存:副垃圾回收器会把这些存活的对象复制到空闲区域中,并且有序的排列起来,复制后空闲区域就没有内存碎片了
  3. 角色翻转:完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去

标记-清除算法:

  1. 标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
  2. 清除:将垃圾数据进行清除。
  3. 产生内存碎片:对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。

标记-整理算法

  1. 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。
  2. 整理:让所有存活的对象都向内存的一端移动
  3. 清除:清理掉端边界以外的内存

V8 是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。 为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法

说一说 vue 的 keep-alive ?

得分点

缓存组件、条件缓存、路由配合条件缓存、不重新加载、activated、deactivated

标准回答

<keep-alive>作用:缓存组件,提升性能,避免重复加载一些不需要经常变动且内容较多的组件。 <keep-alive>的使用方法:使用<keep-alive>标签对需要缓存的组件进行包裹,默认情况下被<keep-alive>标签包裹的组件都会进行缓存,区分被包裹的组件是否缓存有两种方法,第一种是给 keepalive 添加属性,组件名称指的是具体组件添加的 name,不是路由里面的 name。include 包含的组件(可以为字符串,数组,以及正则表达式,只有匹配的组件会被缓存)。exclude 排除的组件(以为字符串,数组,以及正则表达式,任何匹配的组件都不会被缓存)。第二种也是最常用的一种是,和路由配合使用:在路由中添加 meta 属性。 使用 keepalive 导致组件不重新加载,也就不会重新执行生命周期的函数,如果要解决这个问题,就需要两个属性进入时触发:activated 退出时触发:deactivated

加分回答

<keep-alive>适用的场景:首页展示固定数据的组件,比如 banner 九宫格

CSRF 攻击是什么?

得分点

CSRF 时跨站请求伪造、盗用用户身份发起请求

标准回答 CSRF 跨站点请求伪造(Cross Site Request Forgery)和 XSS 攻击一样,有巨大的危害性,就是攻击者盗用了用户的身份,以用户的身份发送恶意请求,但是对服务器来说这个请求是合理的,这样就完成了攻击者的目标。

CSRF 攻击的过程原理是:

  • 用户打开浏览器,访问目标网站 A,输入用户名和密码请求登录
  • 用户信息在通过认证后,网站 A 产生一个 cookie 信息返回给浏览器,这个时候用户以可正常发送请求到网站 A - 用户在没有退出网站 A 之前在同一个浏览器打开了另一个新网站 B。
  • 新网站 B 收到用户请求之后返回一些攻击代码,并发出一个请求要求访问返回 cookie 的网站 A - 浏览器收到这些攻击性代码之后根据新网站 B 的请求在用户不知道的情况下以用户的权限操作了 cookie 并向网站 A 服务器发起了合法的请求。

预防 CSRF 攻击主要有以下策略:

  • 使用验证码,在表单中添加一个随机的数字或者字母验证码,强制要求用户和应用进行直接的交互。
  • HTTP 中 Referer 字段,检查是不是从正确的域名访问过来,它记录了 HTTP 请求的来源地址。
  • 使用 token 验证,在 HTTP 请求头中添加 token 字段,并且在服务器端建立一个拦截器验证这个 token,如果 token 不对,就拒绝这个请求。

加分回答

验证 HTTP Referer 字段的好处就是实施起来特别简单,普通的网站开发不需要特别担心 CSRF 漏洞,只需要在最后面设置一个拦截器来验证 referer 的值就可以了,不需要改变已有的代码逻辑,非常便捷。但是这个方法也不是万无一失的,虽然 referer 是浏览器提供的,但是不同的浏览器可能在 referer 的实现上或多或少有自身的漏洞,所以使用 referer 的安全保证是通过浏览器实现的。使用 token 验证的方法要比 referer 更安全一些,需要把 token 放在一个 HTTP 自定义的请求头部中,解决了使用 get 或者 post 传参的不便性。

XSS 攻击是什么?

得分点

XSS 是跨站脚本攻击、向目标网站插入恶意代码、大量用户访问网站时运行恶意脚本获取信息

标准回答

XSS 是跨站脚本攻击(Cross Site Scripting),不写为 CSS 是为了避免和层叠样式表(Cascading Style Sheets)的缩写混淆,所以将跨站脚本攻击写为 XSS。
攻击者可以通过向 Web 页面里面插入 script 代码,当用户浏览这个页面时,就会运行被插入的 script 代码,达到攻击者的目的。
XSS 的危害一般是泄露用户的登录信息 cookie,攻击者可以通过 cookie 绕过登录步骤直接进入站点。
XSS 的分类分为反射型和存储型。

  • 反射型就是临时通过 url 访问网站,网站服务端将恶意代码从 url 中取出,拼接在 HTML 中返回给浏览器,用户就会执行恶意代码。
  • 存储型就是将恶意代码以留言的形式保存在服务器数据库,任何访问网站的人都会受到攻击。

预防 XSS 攻击的方案基本是对数据进行严格的输出编码,比如 HTML 元素的编码,JavaScript 编码,css 编码,url 编码等等。

加分回答

XSS 的危害:

  • 获取 cookie:网站中的登录一般都是用 cookie 作为某个用户的身份证明,这是服务器端返回的一串字符。如果 cookie 被攻击者拿到,那么就可以绕过密码登录。当空间、论坛如果可以被插入 script 代码,那么进入空间或者论坛的人的账号就可以轻易被攻击者获取。
  • 恶意跳转:直接在页面中插入 window.location.href 进行跳转。

XSS 的分类:

  • 反射型 XSS(非持久型 XSS):通过 URL 参数直接注入
  • 存储型 XSS(持久型 XSS):存储到数据库后读取时注入

XSS 的预防:

  • 浏览器的防御和“X-XSS-Protection”有关,默认值为 1,即默认打开 XSS 防御,可以防御反射型的 XSS,不过作用有限,只能防御注入到 HTML 的节点内容或属性的 XSS,例如 URL 参数中包含 script 标签。不建议只依赖此防御手段。
  • 防御 HTML 节点内容,通过转义<为<以及>为>来实现防御 HTML 节点内容。
  • 预防 HTML 属性,通过转义”->&quto 来实现防御,一般不转义空格,但是这要求属性必须带引号。
  • 预防 JavaScript 代码,通过将数据进行 JSON 序列化。
  • 防御富文本是比较复杂的工程,因为富文本可以包含 HTML 和 script,这些难以预测与防御,建议是通过白名单的方式来过滤允许的 HTML 标签和标签的属性来进行防御,大概的实现方式是:
  • 将 HTML 代码段转成树级结构的数据
  • 遍历树的每一个节点,过滤节点的类型和属性,或进行特殊处理
  • 处理完成后,将树级结构转化成 HTML 代码
  • 开启浏览器 XSS 防御:Http Only cookie,禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。

说一说 js 继承的方法和优缺点?

得分点

原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class

标准回答

原型链继承:

让一个构造函数的原型是另一个类型的实例,那么这个构造函数 new 出来的实例就具有该实例的属性,原型链继承的。

优点:

写法方便简洁,容易理解。

缺点:

在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享。同时在创建子类型的实例时,不能向超类型的构造函数中传递参数。

借用构造函数继承:

在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。

优点:

解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。

缺点:

借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

组合继承:

将原型链和借用构造函数的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。

优点:

解决了原型链继承和借用构造函数继承造成的影响。

缺点

无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

原型式继承:

在一个函数 A 内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上,函数 A 是对传入的对象执行了一次浅复制。ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数 A 方法效果相同。

优点

不需要单独创建构造函数。

缺点

属性中包含的引用值始终会在相关对象间共享

寄生式继承:

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

优点:

写法简单,不需要单独创建构造函数。

缺点:通过寄生式继承给对象添加函数会导致函数难以重用。

寄生组合式继承:

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

优点:

高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;

缺点:

代码复杂

加分回答

ES6 Class 实现继承。原理:原理 ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。

说一说 defer 和 async 区别?

得分点

加载 JS 文档和渲染文档可以同时进行、JS 代码立即执行、JS 代码不立即执行、渲染引擎和 JS 引擎互斥

标准回答

浏览器会立即加载 JS 文件并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行
加上 async 属性,加载 JS 文档和渲染文档可以同时进行(异步),当 JS 加载完成,JS 代码立即执行,会阻塞 HTML 渲染。
加上 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),当 HTML 渲染完成,才会执行 JS 代码。

加分回答

渲染阻塞的原因: 由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉

说一下浏览器如何渲染页面的?

得分点

dom 树、stylesheet、布局树、分层、光栅化、合成

标准回答

  • 浏览器拿到 HTML,先将 HTML 转换成 dom 树,
  • 再将 CSS 样式转换成 stylesheet,
  • 根据 dom 树和 stylesheet 创建布局树,
  • 对布局树进行分层,为每个图层生成绘制列表,
  • 再将图层分成图块,
  • 紧接着光栅化将图块转换成位图,最后合成绘制生成页面。

加分回答

分层的目的:避免整个页面渲染,把页面分成多个图层,尤其是动画的时候,把动画独立出一个图层,渲染时只渲染该图层就 ok,transform,z-index 等,浏览器会自动优化生成图层 光栅化目的:页面如果很长但是可视区很小,避免渲染非可视区的样式造成资源浪费,所以将每个图层又划分成多个小个子,当前只渲染可视区附近区域

说一说 computed 和 watch 的区别?

得分点

computed 值有缓存、触发条件是依赖值发生更改、 watch 无缓存支持异步、监听数据变化

标准回答

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是观察的作用,支持异步,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

加分回答

computed 应用场景:需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
watch 应用场景:需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

说一说 Vue 中 $nextTick 作用与原理?

得分点

异步渲染、获取 DOM、Promise

标准回答

Vue 在更新 DOM 时是异步执行的,在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。所以修改完数据,立即在方法中获取 DOM,获取的仍然是未修改的 DOM。 $nextTick的作用是:该方法中的代码会在当前渲染完成后执行,就解决了异步渲染获取不到更新后DOM的问题了。 $nextTick的原理:$nextTick 本质是返回一个 Promise

加分回答

应用场景:在钩子函数 created()里面想要获取操作 Dom,把操作 DOM 的方法放在$nextTick 中

说一说 new 会发生什么?

得分点

创建空对象、为对象添加属性、把新对象当作 this 的上下文、箭头函数不能作为构造函数

标准回答

new 关键字会进行如下的操作:

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤 1 新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

加分回答

new关键字后面的构造函数不能是箭头函数。

说一下 token 能放在 cookie 中吗?

得分点

能、不设置 cookie 有效期、重新登录重写 cookie 覆盖原来的 cookie

标准回答

能。
token 一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串) token可以存放在Cookie中,token 是否过期,应该由后端来判断,不该前端来判断,所以token存储在cookie中只要不设置cookie的过期时间就 ok 了,如果 token 失效,就让后端在接口中返回固定的状态表示token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。

加分回答

token 认证流程

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端签发一个 token ,并把它发送给客户端
  4. 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
  6. 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据

说一下浏览器输入 URL 发生了什么?

得分点

DNS 解析、TCP 握手、HTTP 缓存、重定向、服务器状态码、渲染引擎和 JS 引擎互斥、渲染过程、浏览器进程、网络进程、渲染进程

标准回答

  • 输入地址,浏览器查找域名的 IP 地址。
    浏览器向 该 IP 地址的 web 服务器发送一个 HTTP 请求,在发送请求之前浏览器和服务器建立 TCP 的三次握手,判断是否是 HTTP 缓存,如果是强制缓存且在有效期内,不再向服务器发请求,如果是 HTTP 协商缓存向后端发送请求且和后端服务器对比,在有效期内,服务器返回 304,直接从浏览器获取数据,如果不在有效期内服务器返回 200,返回新数据。 请求发送出去服务器返回重定向,浏览器再按照重定向的地址重新发送请求。 如果请求的参数有问题,服务器端返回 404,如果服务器端挂了返回 500。 如果有数据一切正常,当浏览器拿到服务器的数据之后,开始渲染页面同时获取 HTML 页面中图片、音频、视频、CSS、JS,在这期间获取到 JS 文件之后,会直接执行 JS 代码,阻塞浏览器渲染,因为渲染引擎和 JS 引擎互斥,不能同时工作,所以通常把 Script 标签放在 body 标签的底部。 渲染过程就是先将 HTML 转换成 dom 树,再将 CSS 样式转换成 stylesheet,根据 dom 树和 stylesheet 创建布局树,对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,紧接着光栅化将图块转换成位图,最后合成绘制生成页面。

说一说组件通信的方式?

得分点

父子通信、自定义属性、props、$emit、EventBus、$on、VueX

标准回答

Vue 组件的通信方式分为两大类,一类是父子组件通信,另一类是任何关系类型组件通信(父子、兄弟、非兄弟)

  • 父给子传递数据,通过给子组件添加自定义属性,
  • EventBus: 使用方法是创建一个新的 Vue 实例,需要通信的组件都引入该 Vue 实例,传递数据的组件使用 event.$emit('名称',参数)发送数据,接收数据的组件使用 event.$on('名称',方法)接收数据。 =
  • VueX: 集中管理项目公共数据,Vuex 的状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。

加分回答

EventBus 的优缺点,
缺点:vue 是单页应用,如果你在某一个页面刷新了之后,与之相关的 EventBus 会被移除,这样就导致业务走不下去。同时如果页面中有反复操作的业务,EventBus 在监听的时候就会触发很多次,需要好好处理 EventBus 在项目中的关系。在 vue 页面销毁时,同时移除 EventBus 事件监听。
优点:解决了多层组件之间繁琐的事件传播,使用原理十分简单,代码量少。适合业简单,组件传递数据较少的项目,大型项目业务复杂的还是尽量使用 VueX

说一说 v-if 和 v-show 区别?

得分点

v-show true/false 都渲染 、 v-if true 渲染 false 不渲染

标准回答

作用: 都是控制元素隐藏和显示的指令
区别: v-show: 控制的元素无论是 true 还是 false,都被渲染出来了,通过 display:none 控制元素隐藏 v-if: 控制的元素是 true,进行渲染,如果是 false 不渲染,根本在 dom 树结构中不显示

加分回答

应用: v-show: 适合使用在切换频繁显示/隐藏的元素上 v-if: 适合使用在切换不频繁,且元素内容很多,渲染一次性能消耗很大的元素上

React 生命周期的各个阶段是什么?

得分点:

constructor、componentDidMount、componentDidUpdate、componentWillUnmount、getDerivedStateFromProps、shouldComponentUpdate、getSnapshotBeforeUpdate、componentWillUpdate。

标准回答

React 的生命周期中常用的有:

  • constructor,负责数据初始化。
  • render,将 jsx 转换成真实的 dom 节点。componentDidMount,组件第一次渲染完成时触发。-
  • componentDidUpdate,组件更新完成时触发。
  • componentWillUnmount,组件销毁和卸载时触发。
    不常用的有:
  • getDerivedStateFromProps,更新 state 和处理回调。
  • shouldComponentUpdate,用于性能优化。
  • getSnapshotBeforeUpdate,替代了 componentWillUpdate。

加分回答

React 的生命周期中有常用的和不常用的。
常用的有:

  • constructor(): 完成了数据的初始化。注意:只要使用了 constructor()就必须写 super(),否则 this 指向会出错。
  • render(): render()函数会将 jsx 生成的 dom 插入到目标节点中。在每次组件更新时,react 通过 diff 算法比较更新前和更新之后的 dom 节点,找到最小的有差异的 dom 位置并更新,花费最小的开销。
  • componentDidMount(): 组件第一次渲染完成,此时 dom 节点已经生成,在这里调用接口请求,返回数据后使用 setState()更新数据后重新渲染。
  • componentDidUpdate(prevProps,prevState): 组件更新完成。每次 react 重新渲染之后都会进入这个生命周期,可以拿到更新之前的 props 和 state。
  • componentWillUnmount(): 在这个生命周期完成组件的数据销毁和卸载,移除所有的定时器和监听。

不常用的有:

  • getDerivedStateFromProps(nextProps,prevState): 代替老版的 componentWillReceiveProps()。官方将更新 state 与触发回调重新分配到了 componentWillReceiveProps()中,让组件整体的更新逻辑更加清晰,并且在当前生命周期中,禁止使用 this.props,强制让开发者们通过比较 nextProps 和 PrevState 去保证数据的正确行为。
  • shouldComponentUpdate(): return true 可以渲染,return false 不重新渲染。为什么会出现这个 SCU 生命周期?主要用于性能优化。也是唯一可以控制组件渲染的生命周期,在 setState 之后 state 发生改变组件会重新渲染,在当前生命周期内 return false 会阻止组件的更新。因为 react 中父组件重新渲染导致子组件也重新渲染,这时在子组件的当前生命周期内做判断是否真的需要重新渲染。
  • getSnapshotBeforeUpdate(prevProps,prevState): 代替 componentWillUpdate(),核心区别在于 getSnapshotBeforeUpdate()中读取到的 dom 元素状态是可以保证和 componentDidUpdate()中的一致。

React 组件间传值的方法有哪些?

得分点

props、context、发布/订阅。

标准回答

React 中组件之间的传值方法有很多,按照不同的组件间关系可以把组件传值的方法分为父子组件传值,跨级组件传值和非嵌套关系组件传值。

  • 父子组件常用的传值方法是当父组件给子组件传值时通过 props,子组件向父组件传值通过回调函数来传值。跨级组件常用的传值方法是 props 一层一层的传,或者使用 context 对象,将父组件设置为生产者而子组件都设置对应的 contextType 即可。
  • 非嵌套关系组件传值的方法是使用共同的父级组件进行 props 传值,或者通过 context 传值,推荐使用发布/订阅的自定义事件传值。

加分回答

React 中组件间传值方法有很多,按照不同的组件间关系可以把组件间传值的方法分为:

  • 父子组件传值:父子组件传值是最常见的应用场景也是非常简单的一种通信方式,父组件通过向子组件传递 props,子组件得到 props 之后做处理就行。而子组件向父组件传值需要通过回调函数触发,具体操作是父组件将一个函数作为 props 的属性传递给子组件,子组件通过 this.props.xxx()调用父组件的函数,参数根据需要自己进行搭配即可。
  • 跨级组件传值:跨级的组件之间传值无非就是重复多个父子组件传值的过程。一般跨级的传值方式有两种,分别是:
    • 一层一层的传递 props:写法很复杂,耦合程度也很高,如果两个组件之间隔了很多层,那么也会影响中间组件的性能,开销大。不过这种方法也是可以的,如果组件之间的层级不是非常多,可以考虑使用这个方法。
    • context 对象:context 相当于一个全局的变量,是一个大的容器,可以把需要传递的数据放在这个容器中,不论嵌套多深都可以轻易的使用。具体操作是创建一个 context 对象,需要输入默认值。在父组件中使用生产者标签将需要传值的所有子组件包裹起来。子组件通过指定 contextType 获取到这个 context 对象,直接调用 this.context 即可获得值。
  • 非嵌套关系组件传值:就是没有任何包含关系的组件之间的传值,包括了兄弟组件。对于肺嵌套关系组件传值一般用两种方法:
    • 通过相同的父组件传值:子组件通过回调函数的形式将数据传给父组件,父组件直接通过属性将数据传递给子组件
    • context:利用共同的父组件 context 对象进行传值 - 通过发布/订阅进行传递:也可以说是自定义事件。重点是在子组件的 componentDidMount 生命周期通过声明一个自定义事件,然后在 componentWillUnmount 生命周期组件销毁时移除自定义事件。

说一说盒模型?

得分点

标准盒模型、怪异盒模型、box-sizing:border-box、盒模型大小

标准回答

CSS 盒模型定义了盒的每个部分包含 margin, border, padding, content 。根据盒子大小的计算方式不同盒模型分成了两种,标准盒模型和怪异盒模型。

标准模型,给盒设置 widthheight,实际设置的是 content box。paddingborder 再加上设置的宽高一起决定整个盒子的大小。
怪异盒模型,给盒设置 widthheight,包含了paddingborder ,设置的 widthheight就是盒子实际的大小 默认情况下,盒模型都是标准盒模型 设置标准盒模型:box-sizing:content-box 设置怪异盒模型:box-sizing:border-box

说一说伪数组和数组的区别?

得分点

类型是 object、不能使用数组方法、可以获取长度、可以使用 for in 遍历

标准回答

伪数组它的类型不是 Array,而是 Object,而数组类型是 Array。可以使用的 length 属性查看长度,也可以使用[index]获取某个元素,但是不能使用数组的其他方法,也不能改变长度,遍历使用 for in 方法。
伪数组的常见场景:

  • 函数的参数 arguments
  • 原生 js 获取 DOM:document.querySelector(‘div’) 等
  • jquery 获取 DOM:$(“div”)等

加分回答

伪数组转换成真数组方法

  • Array.prototype.slice.call(伪数组)
  • [].slice.call(伪数组)
  • Array.from(伪数组) 转换后的数组长度由 length 属性决定。索引不连续时转换结果是连续的,会自动补位。

说一说如何实现可过期的 localstorage 数据?

得分点

惰性删除、定时删除

标准回答

localStorage 只能用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。所以要实现可过期的 localStorage 缓存的中重点就是:如何清理过期的缓存。
目前有两种方法,一种是惰性删除,另一种是定时删除。

  • 惰性删除是指某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。实现方法是,存储的数据类型是个对象,该对象有两个 key,一个是要存储的 value 值,另一个是当前时间。获取数据的时候,拿到存储的时间和当前时间做对比,如果超过过期时间就清除 Cookie。
  • 定时删除是指,每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对 CPU 的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对 localStorage 空间的浪费。实现过程,获取所有设置过期时间的 key 判断是否过期,过期就存储到数组中,遍历数组,每隔 1S(固定时间)删除 5 个(固定个数),直到把数组中的 key 从 localstorage 中全部删除。

加分回答

LocalStorage 清空应用场景:token 存储在 LocalStorage 中,要清空

说一说 axios 的拦截器原理及应用?

得分点

请求(request)拦截器、响应(response)拦截器、Promise 控制执行顺序、每个请求带上相应的参数、返回的状态进行判断(token 是否过期)

标准回答

axios 的拦截器的应用场景: 请求拦截器用于在接口请求之前做的处理,比如为每个请求带上相应的参数(token,时间戳等)。 返回拦截器用于在接口返回之后做的处理,比如对返回的状态进行判断(token 是否过期)。 xios 为开发者提供了这样一个 API:拦截器。拦截器分为 请求(request)拦截器和 响应(response)拦截器。 拦截器原理:创建一个 chn 数组,数组中保存了拦截器相应方法以及 dispatchRequest(dispatchRequest 这个函数调用才会真正的开始下发请求),把请求拦截器的方法放到 chn 数组中 dispatchRequest 的前面,把响应拦截器的方法放到 chn 数组中 dispatchRequest 的后面,把请求拦截器和相应拦截器 forEach 将它们分 unshift,push 到 chn 数组中,为了保证它们的执行顺序,需要使用 promise,以出队列的方式对 chn 数组中的方法挨个执行。

加分回答

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。从浏览器中创建 XMLHttpRequests,从 node.js 创建 http 请求,支持 Promise API,可拦截请求和响应,可转换请求数据和响应数据,可取消请求,可自动转换 JSON 数据,客户端支持防御 XSRF

说一说创建 ajax 过程?

得分点

new XMLHttpRequest()、设置请求参数 open()、发送请求 request.send()、响应 request.onreadystatechange

标准回答

创建 ajax 过程:

  1. 创建 XHR 对象:new XMLHttpRequest()
  2. 设置请求参数:request.open(Method, 服务器接口地址);
  3. 发送请求: request.send(),如果是 get 请求不需要参数,post 请求需要参数 request.send(data)
  4. 监听请求成功后的状态变化:根据状态码进行相应的处理。
1
2
3
4
5
XHR.onreadystatechange = function () {
if (XHR.readyState == 4 && XHR.status == 200) {
console.log(XHR.responseText); // 主动释放,JS本身也会回收的 XHR = null;
}
};

加分回答

POST 请求需要设置请求头

readyState 值说明

  • 0:初始化,XHR 对象已经创建,还未执行 open
  • 1:载入,已经调用 open 方法,但是还没发送请求
  • 2:载入完成,请求已经发送完成
  • 3:交互,可以接收到部分数据
  • 4:数据全部返回

status 值说明

  • 200:成功
  • 404:没有发现文件、查询或 URl
  • 500:服务器产生内部错误

说一下 fetch 请求方式?

得分点

Fetch 函数就是原生 js、没有使用 XMLHttpRequest 对象、头部信息、请求信息、响应信息等均分布到不同的对象

标准回答

fetch 是一种 HTTP 数据请求的方式,是 XMLHttpRequest 的一种替代方案。
Fetch 函数就是原生 js,没有使用 XMLHttpRequest 对象。fetch()方法返回一个 Promise 解析 Response 来自 Request 显示状态(成功与否)的方法。

加分回答

XMLHttpRequest 的问题

-所有的功能全部集中在一个对象上, 容易书写出混乱而且不容易维护的代码 -采用传统的事件驱动模式, 无法适配新的 Promise

API Fetch API 的特点

-精细的功能分割: 头部信息, 请求信息, 响应信息等均分布到不同的对象, 更利于处理各种复杂的数据交互场景 -使用 Promise API, 更利于异步代码的书写 -同源请求也可以自定义不带 cookie,某些服务不需要 cookie 场景下能少些流量

说一下有什么方法可以保持前后端实时通信?

得分点 轮询、长轮询、 iframe 流、WebSocket、SSE

标准回答

保持前后端实时通信的方法有以下几种:

  1. 轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。其缺点也很明显:连接数会很多,一个接受,一个发送。而且每次发送请求都会有 Http 的 Header,会很耗流量,也会消耗 CPU 的利用率。优点就是实现简单,无需做过多的更改。缺点是轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担
  2. 长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,如果没有新消息,就一直等待。有新消息,才会返回给客户端。在某种程度上减小了网络带宽和 CPU 利用率等问题。由于 http 数据包的头部数据量往往很大(通常有 400 多个字节),但是真正被服务器需要的数据却很少(有时只有 10 个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费。优点是做了优化,有较好的时效性。缺点是保持连接会消耗资源; 服务器没有返回有效数据,程序超时。
  3. iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长连接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 javascript),来实时更新页面。优点是消息能够实时到达;浏览器兼容好。缺点是服务器维护一个长连接会增加开销;IE、chrome、Firefox 会显示加载没有完成,图标会不停旋转。
  4. WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发和客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。缺点是浏览器支持程度不一致,不支持断开重连。
  5. SSE(Server-Sent Event)是建立在浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。SSE 是单向通道,只能服务器向浏览器发送,因为 streaming 本质上就是下载。 优点是 SSE 使用 HTTP 协议,现有的服务器软件都支持。SSE 属于轻量级,使用简单;SSE 默认支持断线重连;

加分回答

  • 轮询适用于:小型应用,实时性不高
  • 长轮询适用于:一些早期的对及时性有一些要求的应用:web IM 聊天
  • iframe 适用于:客服通信等
  • WebSocket 适用于:微信、网络互动游戏等
  • SSE 适用于:金融股票数据、看板等

说一下重绘、重排区别如何避免?

得分点

改变元素尺寸、重新计算布局树、重排必定重绘、重绘避开了重建布局树和分层、GPU 加速、脱离文档流、样式集中改变

标准回答

重排 :当 DOM 的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
重绘:当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,所以重绘跳过了创建布局树和分层的阶段。

重排需要重新计算布局树,重绘不需要,重排必定发生重绘,但是涉及到重绘不一定要重排。涉及到重排对性能的消耗更多一些。

触发重排的方法:

  • 页面初始渲染,这是开销最大的一次重排
  • 添加/删除可见的 DOM 元素
  • 改变元素位置
  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等
  • 改变元素内容,比如文字数量,图片大小等
  • 改变元素字体大小
  • 改变浏览器窗口尺寸,比如 resize 事件发生时
  • 激活 CSS 伪类(例如::hover
  • 设置 style 属性的值,因为通过设置 style 属性改变结点样式的话,每一次设置都会触发一次 reflow
  • 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight 等

避免重排的方式

  • 样式集中改变
  • 使用 absolute 或 fixed 脱离文档流
  • 使用 GPU 加速:transform

加分回答

GPU 的过程是以下这几步 :

  1. 获取 DOM 并将其分割成多个层(renderLayer)
  2. 将每个层栅格化,并独立的绘制进位图中
  3. 将这些位图作为纹理上传至 GPU
  4. 复合多个层来生成最终的屏幕图像(最后的 layer) 开启了 GPU 加速的元素被独立出来,不会再影响其他 dom 的布局,因为它改变之后,只是相当于被贴上了页面。

说一说 Vue 列表为什么加 key?

得分点

性能优化、diff 算法节点比对、key 不能是 index

标准回答

为了性能优化 因为 vue 是虚拟 DOM,更新 DOM 时用 diff 算法对节点进行一一比对,比如有很多 li 元素,要在某个位置插入一个 li 元素,但没有给 li 上加 key,那么在进行运算的时候,就会将所有 li 元素重新渲染一遍,但是如果有 key,那么它就会按照 key 一一比对 li 元素,只需要创建新的 li 元素,插入即可,不需要对其他元素进行修改和重新渲染。

加分回答

key 也不能是 li 元素的 index,因为假设我们给数组前插入一个新元素,它的下标是 0,那么和原来的第一个元素重复了,整个数组的 key 都发生了改变,这样就跟没有 key 的情况一样了

说一说 vue-router 实现懒加载的方法?

得分点

import、require

标准回答

ue-router 实现懒加载的方法有两种:

  • ES6 的 impot 方式:
1
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
  • VUE 中的异步组件进行懒加载方式:
1
component: (resolve) => require(["../views/About"], resolve);

加分回答

vue-router 实现懒加载的作用:性能优化,不用到该路由,不加载该组件。

ReactRouter 基本用法是什么?

得分点

路由的模式有两种、hash 模式、history 模式、路由的动态传参、重定向、高阶路由组件。

标准回答

react 的路由保证了界面和 URL 的同步,拥有简单的 API 和强大的功能。
react 中的路由模式有两种,分别是:hash 路由和 history 路由。

  • 首先用析构的方法引入需要用到的路由方式,需要注意的是路由所有的配置都必须被包裹在 hash 路由或者 history 路由里面。
  • 然后在路由标签内先再配置 Route 标签,它的参数有:path,路由匹配的路径。component,路由匹配成功之后渲染的组件。
  • react 中路由的跳转使用 Link 标签,它的参数 to 指路由匹配的路径,也需要引入。NavLink 标签和 Link 的区别就是渲染成 a 标签之后会自带一个 class 属性,对应的是 NavLink 标签的 active 属性。
  • react 路由中有高阶路由组件 withRouter,它和普通路由一样需要引入,主要作用是增加了路由跳转的方式,可以调用 history 方法进行函数中路由的跳转。
  • react 中路由的动态传值是一个重点,{/:属性名}和{/属性名/值}搭配的方式进行传值,在需要接收参数的组件通过 this.props.match.params 来进行接收。react 中路由的 query 传值是通过问号的方法将参数拼接在 url 之后,在需要接收参数的组件通过 url.parse(this.props.location.search).query 获取参数。
  • 路由的重定向需要用组件 Redirect 来完成,参数 to 是目标组件。
  • 路由的懒加载需要从 react 中引入 Suspense 和 lazy,引入组件时通过 lazy(() => import())来引入,使用 Suspense 标签将 Route 包裹起来即可。

加分回答

react 中路由模式分为 hash 路由和 history 路由。
hash 路由的原理是 window.onhashchange 监听,url 前面会有一个#号,这个就是锚点,每个路由前面都会有#锚点,特点是#之后的数据改变不会发起请求,因此改变 hash 不会重新加载页面。
history 路由的原理是 window.history API,在 HTML5 中新增了 pushState 和 replaceState 方法,这两个方法是在浏览器的历史记录栈上做文章,提供了对历史记录做修改的功能,虽然更改了 url 但是不会向服务器发起请求。
history 模式虽然去掉了 hash 模式的#锚点,但是它怕刷新,因为刷新时是真实的请求。而 hash 模式下,修改的是#锚点之后的信息,浏览器不会将#锚点之后的数据发送到服务器,所以没有问题。

setState 是同步还是异步的?

得分点

是同步也是异步、合成事件、生命周期函数、原生事件、定时器

标准回答

setState 在合成事件和生命周期函数中是异步的,在原生事件和定时器中都是同步的。

加分回答

setState 本身不分同步或者异步,而是取决于是否处于 batch update 中。
组件中的所有函数在执行时临时设置一个变量 isBatchingUpdates = true,当遇到 setState 时,如果 isBatchingUpdates 是 true,那么 setState 就是异步的,
如果是 false,那么 setState 就是同步的。
那么什么时候 isBatchingUpdates 会被设置成 false 呢?

  • 当函数运行结束时 isBatchingUpdates = false
  • 当函数遇到 setTimeout、setInterval 时 isBatchingUpdates = false
  • 当 dom 添加自定义事件时 isBatchingUpdates = false

react 事件绑定原理

得分点

非原生事件、SyntheticBaseEvent

标准回答

  • React 中 event 事件不是原生事件,而是对原生 event 进行了封装的新类 SyntheticBaseEvent,模拟出 DOM 事件的所有功能,通过 event.nativeEvent 可以获取到原生事件。
  • React17 版本开始所有事件都绑定在 root 根组件上,之前都是绑定在 document 上。
  • React 中 event 和 DOM 事件不一样,和 Vue 也不一样。

加分回答

React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在 root 处监听所有支持的事件,当事件发生并冒泡至 root 处时,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。另外冒泡到 root 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticBaseEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。

React 中 hooks 的优缺点是什么?

得分点

可读性强、组件层级变得更浅、不再需要考虑 class 组件中 this 指向、部分生命周期不支持

标准回答

优点:

  • 代码的可读性强,在使用 hooks 之前比如发布/订阅自定义事件被挂载在 componentDidMount 生命周期中,然后需要在 componentWillUnmount 生命周期中将它清楚,这样就不便于开发者维护和迭代。在使用 hooks 之后,通过 useEffect 可以将 componentDidMount 生命周期、componentDidUpdate 生命周期和 componentWillUnmount 生命周期聚合起来,方便代码的维护。
  • 组件层级变得更浅了,在使用 hooks 之前通常使用高阶组件 HOC 的方法来复用多个组件公共的状态,增强组建的功能,这样肯定是加大了组件渲染的开销,损失了性能。但是在 hooks 中可以通过自定义组件 useXxx()的方法将多个组件之间的共享逻辑放在自定义 hook 中,就可以轻松的进行数据互通。
  • 不再需要考虑 class 组件中 this 指向的问题,hook 在函数组件中不需要通过 this.state 或者 this.fn 来获取数据或者方法。

缺点:

hooks 的 useEffect 只包括了 componentDidMount、componentDidUpdate 还有 componentWillUnmount 这三个生命周期,对于 getSnapshotBeforeUpdate 和 componentDidCatch 等其他的生命周期没有支持。

加分回答

使用 useEffect 时候里面不能写太多依赖项,将各个不同的功能划分为多个 useEffect 模块,将各项功能拆开写,这是遵循了软件设计的“单一职责模式”。如果遇到状态不同步的情况,使用手动传递参数的形式。如果业务复杂,就使用 Component 代替 hooks,hooks 的出现并不是取代了 class 组件,而是在函数组件的基础上可以实现一部分的类似 class 组件功能。

说一说前端性能优化手段?

得分点

图片压缩和文件压缩、雪碧图/精灵图、节流防抖、HTTP 缓存、本地缓存、Vue 的 keep-alive 缓存、ssr 服务器端渲染、懒加载、对 dom 查询进行缓存、将 dom 操作合并

标准回答

前端性能优化分为两类,一类是文件加载更快,另一类是文件渲染更快。

  • 加载更快的方法: 让传输的数据包更小(压缩文件/图片):图片压缩和文件压缩 减少网络请求的次数:雪碧图/精灵图、节流防抖 减少渲染的次数:缓存(HTTP 缓存、本地缓存、Vue 的 keep-alive 缓存等)
  • 渲染更快的方法: 提前渲染:ssr 服务器端渲染 避免渲染阻塞:CSS 放在 HTML 的 head 中 JS 放在 HTML 的 body 底部 避免无用渲染:懒加载 减少渲染次数:对 dom 查询进行缓存、将 dom 操作合并、使用减少重排的标签

加分回答

雪碧图的应用场景一般是项目中不常更换的一些固定图标组合在一起,比如 logo、搜索图标、切换图标等。 电商项目中最常用到的懒加载,一般在查看商品展示的时候通常下拉加载更多,因为商品数据太多,一次性请求过来数据太大且渲染的时间太长。

说一说服务端渲染?

得分点

服务器端生成 HTML 直接返回给浏览器、减少网络传输、首屏渲染快、对搜索引擎友好

标准回答

SSR 是 Server Side Render 简称;页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的 html 就可以了。
和它对应的是,CSR 是 Client Side Render 简称;客户端在请求时,服务端不做任何处理,直接将前端资源打包后生成的 html 返回给客户端,此时的 html 中无任何网页内容,需要客户端去加载执行 js 代码才能渲染生成页面内容,同时完成事件绑定,然后客户端再去通过 ajax 请求后端 api 获取数据更新视图。

服务端渲染的优势:减少网络传输,响应快,用户体验好,首屏渲染快,对搜索引擎友好,搜索引擎爬虫可以看到完整的程序源码,有利于 SEO。

在 Vue 项目中实现服务端渲染方法:Vue 在客户端渲染中也是采用一定方法将虚拟 DOM 渲染为真实 DOM 的,那么服务端的渲染流程也是通过虚拟 DOM 的编译来完成的,编译虚拟 DOM 的方法是 renderToString。在 Vue 中,vue-server-renderer 提供一个名为 createBundleRenderer 的 API,这个 API 用于创建一个 render,并且自带 renderToString 方法。

加分回答

使用服务器端渲染 (SSR) 时还需要有一些权衡之处:

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
  • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

事件扩展符用过吗(…),什么场景下?

得分点

等价于 apply 的方式、将数组展开为构造函数的参数、数组字符串连接、浅拷贝

标准回答

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时, 将对象表达式按 key-value 的方式展开。
常见的场景:等价于 apply 的方式、将数组展开为构造函数的参数、字面量数组或字符串连接不需要使用 concat 等方法了、构造字面量对象时,进行浅克隆或者属性拷贝

加分回答

只能用于可迭代对象 在数组或函数参数中使用展开语法时, 该语法只能用于可迭代对象。

说一说 vue 钩子函数?

得分点 beforeCreate、created、beforeMounted、mounted beforeUpdate、updated 、 beforeDestroy、destroyed

标准回答

钩子函数用来描述一个组件从引入到退出的全过程中的某个过程,整个过程称为生命周期。 钩子函数按照组件生命周期的过程分为,挂载阶段=>更新阶段=>销毁阶段。
每个阶段对应的钩子函数

  • 挂载阶段:beforeCreate、created、beforeMounted、mounted
  • 更新阶段:beforeUpdate、updated
  • 销毁阶段:beforeDestroy、destroyed
    每个阶段特点与适合做什么
  • created:实例创建完成,可访问 data、computed、watch、methods 上的方法和数据,未挂载到 DOM,不能访问到 el 属性,el 属性,ref 属性内容为空数组常用于简单的 ajax 请求,页面的初始化
  • beforeMount:在挂载开始之前被调用,beforeMount 之前,会找到对应的 template,并编译成 render 函数
  • mounted:实例挂载到 DOM 上,此时可以通过 DOM API 获取到 DOM 节点,$ref 属性可以访问常用于获取 VNode 信息和操作,ajax 请求
  • beforeupdate:响应式数据更新时调用,发生在虚拟 DOM 打补丁之前,适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器
  • updated:虚拟 DOM 重新渲染和打补丁之后调用,组件 DOM 已经更新,可执行依赖于 DOM 的操作避免在这个钩子函数中操作数据,可能陷入死循环
  • beforeDestroy:实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例,常用于销毁定时器、解绑全局事件、销毁插件对象等操作

加分回答

父子组件钩子函数在三个阶段的代码执行顺序

  • 挂载:父亲 created> 子 created > 子 mounted> 父亲 mounted>
  • 更新:父亲 beforeUpdate > 子 beforeUpdated > 子 updated > 父亲 updated
  • 销毁:父亲 beforeDestroy> 子 beforeDestroy > 子 destroyed> 父 destroyed