前端面试重要问题总结(前端100问小结)(三)

第 21 题:有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

题目讨论

  1. Object.prototype.toString.call()
    每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"

Object.prototype.toString.call() 常用于判断浏览器内置对象时。
更多实现可见 谈谈 Object.prototype.toString

2. instanceof

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。

使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。

[]  instanceof Array; // true

但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

[]  instanceof Object; // true
3. Array.isArray()

功能:用来判断对象是否为数组
instanceof 与 isArray
当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

Array.isArray() 与 Object.prototype.toString.call()

Array.isArray()是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

第 22 题:介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

  1. 浏览器渲染机制
    浏览器采用流式布局模型(Flow Based Layout)
    浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)。
    有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
    由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

  2. 重绘
    由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline, visibility, color、background-color等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。

  3. 回流
    回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

<body>
<div class="error">
    <h4>我的组件</h4>
    <p><strong>错误:</strong>错误的描述…</p>
    <h5>错误纠正</h5>
    <ol>
        <li>第一步</li>
        <li>第二步</li>
    </ol>
</div>
</body>

在上面的HTML片段中,对该段落(

标签)回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.error和body – 视浏览器而定)。此外,

    也会有简单的回流,因为其在DOM中在回流元素之后。大部分的回流将导致页面的重新渲染。

回流必定会发生重绘,重绘不一定会引发回流。

  1. 浏览器优化
    现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。

主要包括以下属性或方法:

offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
width、height
getComputedStyle()
getBoundingClientRect()
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。

  1. 减少重绘与回流
    CSS:
    使用 transform 替代 top
    使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局
    避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局。
    尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。
    避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
<div>
  <a> <span></span> </a>
</div>
<style>
  span {
    color: red;
  }
  div > a > span {
    color: red;
  }
</style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。

将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。

避免使用CSS表达式,可能会引发回流。

将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。
CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

JavaScript:
避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

第 23 题:介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景

题目讨论
观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知
前端面试重要问题总结(前端100问小结)(三)_第1张图片
观察者模式 vs 发布-订阅模式

第 24 题:聊聊 Redux 和 Vuex 的设计思想

题目讨论
转型是讨论二者的设计思想,我从共同点和区别方面大致说一下:

共同点
首先两个都是处理非线性状态的工具库,基本上实现的思想都是:状态保存状态---->调度(动作)
------>还原器(Vuex里的突变)---->生成newState; 整个状态为同步操作;

区别
最大的区别在于处理异步的不同,vuex里面多了一步commit操作,在action之后commit(mutation)之前处理异步,而redux里面则是通过中间件处理
知乎上看到的文章:Vuex,Flux,Redux,Redux-saga,Dva,MobX(https://zhuanlan.zhihu.com/p/53599723)

第 25 题:说说浏览器和 Node 事件循环的区别

先上链接

浏览器

关于微任务和宏任务在浏览器的执行顺序是这样的:
执行一只task(宏任务)
执行完micro-task队列 (微任务)
如此循环往复下去

  • 浏览器的task(宏任务)执行顺序在 html#event-loops 里面有讲就不翻译了
    常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
    常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。
Node

Node的事件循环是libuv实现的,引用一张官网的图:
前端面试重要问题总结(前端100问小结)(三)_第2张图片

大体的task(宏任务)执行顺序是这样的:

timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用。
poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
check 检测:setImmediate() 回调函数在这里执行。
close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on(‘close’, …)。
微任务和宏任务在Node的执行顺序
Node 10以前:

执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容
Node 11以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。

第 26 题:介绍模块化发展历程

可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、

IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。

(function(){
  return {
	data:[]
  }
})()

AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。

define('./index.js',function(code){
	// code 就是index.js 返回的内容
})

CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。

define(function(require, exports, module) {  
  var indexCode = require('./index.js');
});

CommonJS: nodejs 中自带的模块化。

var fs = require('fs');

UMD:兼容AMD,CommonJS 模块化语法。

webpack(require.ensure):webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。

import a from 'a';

第 27 题:全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?。

题目讨论
在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

var a = 12;
function f(){};

console.log(window.a); // 12
console.log(window.f); // f(){}

但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

let aa = 1;
const bb = 2;

console.log(window.aa); // undefined
console.log(window.bb); // undefined

在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:
前端面试重要问题总结(前端100问小结)(三)_第3张图片
通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中

怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。

let aa = 1;
const bb = 2;

console.log(aa); // 1
console.log(bb); // 2

第 28 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?

题目讨论
cookie:登陆后后端生成一个sessionid放在cookie中返回给客户端,并且服务端一直记录着这个sessionid,客户端以后每次请求都会带上这个sessionid,服务端通过这个sessionid来验证身份之类的操作。所以别人拿到了cookie拿到了sessionid后,就可以完全替代你。

token:登陆后后端不返回一个token给客户端,客户端将这个token存储起来,然后每次客户端请求都需要开发者手动将token放在header中带过去,服务端每次只需要对这个token进行验证就能使用token中的信息来进行下一步操作了。

xss:用户通过各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本获取信息,发起请求,之类的操作。

csrf:跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。csrf并不能够拿到用户的任何信息,它只是欺骗用户浏览器,让其以用户的名义进行操作。

csrf例子:假如一家银行用以运行转账操作的URL地址如下: http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。

上面的两种攻击方式,如果被xss攻击了,不管是token还是cookie,都能被拿到,所以对于xss攻击来说,cookie和token没有什么区别。但是对于csrf来说就有区别了。

以上面的csrf攻击为例:

cookie:用户点击了链接,cookie未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作。
token:用户点击链接,由于浏览器不会自动带上token,所以即使发了请求,后端的token验证不会通过,所以不会进行扣款操作。
这是个人理解的为什么只劫持cookie不劫持token的原因。

第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的

题目讨论

vue2.x特性:

核心是利用ES5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。
Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

Object.defineProperty(
obj,//定义属性的对象
prop,//要定义或修改的属性的名称
描述符,//将被定义或修改属性的名称【核心】

观察的功能就是用来监测数据的变化。实现方式是给非VNODE的对象类型数据添加一个观察,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个观察对象实例。
观察者是一个类,它的作用是给对象属性添加getter和setter,用于依赖收集和派发更新

vue3.0特性

为什么要替换Object.defineProperty:

替换不是因为不好,是因为有更好的方法使用效率更高
Object.defineProperty的缺点:

  1. 在Vue中,Object.defineProperty无法监控到数组下标的变化,
    导致直接通过数组的下标给数组设置值,不能实时响应。
push()
pop()
shift()
unshift()
splice()
sort()
reverse()

目前只针对以上方法做了hack处理,所以恰数组属性是检测不到的,有局限性。

  1. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。

Vue里,是通过递归以及遍历data对象来实现对数据的监控的,

如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。
而要取代它的Proxy有以下两个优点:

  1. 可以劫持整个对象,并返回一个新对象
  2. 有13种劫持操作

2. 什么是Proxy

Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

基本用法:

let p = new Proxy(target, handler);

参数:
target: 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。
p是Proxy对象,当其他操作对p进行更改的时候,会执行handler对象的方法。Proxy有13种数据劫持的操作,常用的handler处理方法:

get: 读取值,
set: 获取值,
has: 判断对象是否拥有该属性,
construct: 构造函数

举个例子:

let obj = {};
 let handler = {
   get(target, property) {
    console.log(`${property} 被读取`);
    return property in target ? target[property] : 3;
   },
   set(target, property, value) {
    console.log(`${property} 被设置为 ${value}`);
    target[property] = value;
   }
 }

 let p = new Proxy(obj, handler);
 p.name = 'tom' //name 被设置为 tom
 p.age; //age 被读取 3

共性:

依赖收集getter(重点关注以下两点)

  • *const dep = new Dep() // 实例化一个Dep实例
  • *在get函数中通过dep.depend做依赖收集

Dep是一个类,它定义了一些属性和方法,它有一个静态属性目标,这是一个大部分唯一的Watcher【同时只能有一个相互的Watcher被计算】。Dep实际上就是对Watcher的一种管理,Dep脱离Watcher单独存在是没有意义的。Watcher和Dep就是典型的观察者设计模式。

Watcher是一个类,在它的构造函数中定义了一些和Dep相关的属性:

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()	

收集过程:当我们实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行他的this.get()方法,进入get函数把Dep.target赋值生成当前渲染的watcher并压栈(以便恢复用)。接着执行vm._render()方法,生成渲染VNode,并且在这个过程对vm上的数据访问,这个时候就触发数据对象的getter(在此期间执行Dep.target.addDep(this)方法,将watcher订阅到该数据持有的dep的subs中,为后续数据变化时通知到某些subs做准备)。然后递归归遍历添加所有子项的getter。

Watcher在结构函数中初始化两个Dep实例副本。newDeps代表新添加的Dep实例数组,deps代表上一次添加一个Dep实例
数组。除对dep的订阅,然后把newDepsIds和depIds交换,newDeps和deps交换,并把newDepIds和newDeps清空。

考虑到Vue是数据驱动的,因此每次数据变化都会重新渲染,那么vm._render()方法会再次执行,并再次触发数据

收集依赖的目的是为了当这些响应式数据发生变化,触发它们的setter的时候,能知道应该通知其他订阅者去做相应的逻辑处理【派发更新】

派发更新setter(重点关注以下两点)

  • *childOb = !shallow && observe(newVal) // 如果shallow为false的情况,会对新设置的值变成一个响应式对象
  • *dep.notify() // 通知所有订阅者

派发过程:当我们组件中对响应的数据进行修改,就会触发setter的逻辑,最后调用dep.notify()方法,它是Dep的一个实例方法。具体做法是遍历依赖收集中建立的subs,也就是Watcher的实例枚举[subs细分在依赖收集getter中被添加,期间通过一些逻辑处理判断保证相同数据不会被添加多次],然后调用每一个watcher的update方法。

update函数中有个queueWatcher(this)方法重新放置了一个概念,是vue在做派发更新时优化的一个点,它并不会每次数据更改都会触发watcher替换,或者把这些watcher先添加到一个副本中,然后在nextTick后执行watcher的run函数

类别排序保证:

组件的更新由父到子。父组件创建早于子组件,watcher的创建也是
用户自定义watcher要早于渲染watcher执行,因为用户自定义watcher是在渲染watcher前创建的
如果一个组件在父组件watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。
一体遍历:排序完成后,对一体进行遍历,拿到对应的观察者,执行watcher.run()。
run函数解析:先通过得到this.get()它的当前值,然后做判断,如果满足新旧值不等,新值是对象类型,深度模式任何一个条件,则执行watcher的注意,注意某些函数执行的时候会把把第一个参数和第二个参数初始值新值和旧值旧值,这就是当我们自己添加watcher时候可以在参数中取到新旧值的来源。对应渲染watcher而言,在执行this.get()方法求值的时候,会执行getter方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,然后重新执行patch的过程。

第 30 题:两个数组合并成一个数组

请把两个数组 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’] 和 [‘A’, ‘B’, ‘C’, ‘D’],合并为 [‘A1’, ‘A2’, ‘A’, ‘B1’, ‘B2’, ‘B’, ‘C1’, ‘C2’, ‘C’, ‘D1’, ‘D2’, ‘D’]。

const arr1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
const arr2 = ['A', 'B', 'C', 'D']
const ret = []
let tmp = arr2[0]
let j = 0
for (let i=0;i<arr1.length;i++) {
  if (tmp === arr1[i].charAt(0)){
    ret.push(arr1[i])
  }else {
    ret.push(tmp)
    ret.push(arr1[i])
    tmp=arr2[++j]
  }
   if(i===arr1.length-1){
      ret.push(tmp)
    }
}
console.log(ret)

你可能感兴趣的:(前端技术笔记,js)