1
又到了每年的面试季,一些换工作的朋友最近也正在加紧复习中,在这里呢作者整理了十道前端面试中的精选问题和答案,希望对想要换工作的朋友有所帮助,同时如果在阅读的过程中发现文章的问题,也请在评论区告知我。

原文链接

React和Vue的区别?

  1. 数据流: React是单项数据流(props从父组件到子组件单向,数据到视图单向),Vue则是双向绑定(props在父子组件可以双向绑定,数据和视图双向绑定)
  2. 数据监听:React是在setState后比较数据引用的方式监听,Vue通过Es5的数据劫持方法defineProperty监听数据变化。
  3. 模板渲染:React是在js中通过原生的语法如if, map等方法动态渲染模板,Vue是通过指令来动态渲染模板。

为什么Vue数据频繁变化但dom只会更新一次?

考虑以下场景:

export default {
    data () {
        return {
            number: 0
        };
    },
    mounted () {
    var i = 0;
        for(var i = 0; i < 1000; i++) {
             this.number++;
        }
    }
}
当我们在一个同步任务中执行1000次number++操作,按照set -> 订阅器(dep) -> 订阅者(watcher) -> update的过程,dom应该会频繁更新,按理说这会很消耗性能,但实际上这个过程中dom只会更新一次。

这是因为vue的dom更新是一个异步操作,在数据更新后会首先被set钩子监听到,但是不会马上执行dom更新,而是在下一轮循环中执行更新。
具体实现是vue中实现了一个queue队列用于存放本次事件循环中的所有watcher更新,并且同一个watcher的更新只会被推入队列一次,并在本轮事件循环的微任务执行结束后执行此更新(UI Render阶段),这就是dom只会更新一次的原因。

js中的异步任务会进入任务队列在下一轮的事件循环中执行,更多事件循环的内容请参考Javascript运行机制之Event Loop

如何理解React Fiber?

在React 16前,组件的更新是递归组件树的方式实现的,这个递归更新的过程是同步的,如果组件树过于庞大,实际更新过程会造成一些性能上的问题。

在React 16中发布了React Fiber,React Fiber是一种能将递归更新组件树的任务分片(time-slicing)执行的算法。它将组件树的更新拆分为多个子任务,在子任务的执行过程中,允许暂存当前进度然后执行其他优先级较高的任务,如在更新过程中用户产生了交互,那么会优先去处理用户交互,然后在回归更新任务继续执行。更多相关React Fiber

你要如何设计一个报警系统?

从以下角度出发设计

1、项目错误信息采集

  • 组件生命周期运行错误
  • 事件中的错误

2、代码埋点

  • 在window.onerror、componentDidCatch、try catch等收集错误
  • 在具体事件中埋点收集

3、上报时机

  • 实时上传埋点数据,适用于对错误收集的实时性有一定要求项目
  • 定时上传埋点数据。
考虑到服务器压力因素,合理使用以上两种上报方案,(答到服务器压力点)

4、上报数据

  • 错误详细信息
  • 用户的环境数据采集(方便测试还原)

5、错误信息存储

  • 以时间命名的.log文件收集错误的详细信息
  • 在数据库中存储错误的统计信息用于错误的可视化展示
服务器上的日志存储方案有兴趣可以自己了解,要是再讲一讲GFSHDFS等分布式文件系统应该有加分。

6、错误的统计和报警

  • 以图表的方式分类并按时间展示错误信息。
  • 设定一个峰值为报警值并邮件通知管理员
关于峰值的设定需要考虑到应用使用的高峰段和低谷段,并且此峰值需要在各个场景下不断调优。

require和import的区别?

  • require是运行时调用,import是编译时调用(静态加载)
  • require是CommonJs规范,import是Es6的标准
  • require的一个模块就是一个文件,一个文件只能一个输出。import的一个文件可以有多个输出,并且在导入时可以选择部分导入。
import在编译时加载的特点使得其效率更高,也让静态分析和优化成为了可能。

webpack的tree shaking优化的基础就是import的静态导入。

require加载过程?

在node中使用requireexports时我们发现不管是在模块中还是在全局对象上,都不存在这两个方法,那么这两个方法是从何而来呢?

其实require方法本身是定义在Module中的,node在编译阶段将js文件包装在函数将其包装成模块:

(function (exports, require, module, __filename, __dirname) {
    file_content...
})

使用require加载模块的过程

  1. 根据require的参数计算绝对路径path
  2. 根据path查找是否有缓存var cache = Module._cache[path],如果有缓存直接return
  3. 判断是否是内置模块如http,如果是直接return
  4. 生成模块实例,并缓存
      var module = new Module(path, parent);
      Module._cache[path] = module;
  1. 加载模块module.load(path);

你知道哪些前端安全问题,如何避免?

1、XSS攻击

  • 反射型XSS

攻击步骤

  1. 攻击者构造出带有恶意代码的URL,诱导用户点击
  2. 服务端取出恶意代码并拼接在html中返回给浏览器
  3. 浏览器执行恶意代码,攻击者获取用户信息或冒充用户行为进行攻击

eg:

//恶意URL: http://xxx.com?key=<script>document.cookie</script>

//服务端拼接
<div>@{{params.key}}</div>

//最终浏览器执行了此恶意代码获取到用户cookie
<div>
    <script>document.cookie</script>
</div>
  • 存储型XSS

攻击步骤

  1. 攻击者在商品评论页提交了恶意代码到目标数据库
  2. 用于访问该商品,服务端将商品评论从数据库取出并拼接在HTML中返回给浏览器
  3. 浏览器执行恶意代码,攻击者获取用户信息或冒充用户行为进行攻击
存储型XSS会将恶意代码保存在数据库中,会影响到所有使用的用户,相比于反射型XSS造成后果更严重。
  • DOM型XSS
DOM型XSS针对的主要是前端JS代码的漏洞

攻击步骤

  1. 攻击者提供带有恶意代码的URL,或者已经存在于数据库的恶意代码
  2. 浏览器从URL中获取恶意代码,或者从后端接口中获取到恶意代码
  3. 前端Javascript直接执行了这些恶意代码,造成DOM型XSS攻击

eg:

//恶意URL: http://xxx.com?key=document.cookie

//前端取出key字段并执行
evel(location.key)

防范存储和反射型XSS

  1. 前端渲染HTML,数据从接口中获取
  2. 在服务端拼接HTML时转义

防范DOM型XSS

  1. 小心innerHTML,outerHTML、eval等方法
  2. 小心setTimeout等能将字符串直接执行的方法

2、CSRF攻击

CSRF攻击实际上是利用了浏览器在向A域名发起请求前,会cookie列表中查询是否存在A域名的cookie。若是存在,则会将cookie添加至请求头中的机制。

这个机制不在乎请求具体是从哪个域名发出,它只是关心目标路由。

攻击步骤

  1. 用户访问正规网站WebA,浏览器保存下WebA为此用户设置的cookie
  2. 攻击者诱导用户点击不安全的网站WebB,此时从恶意网站WebBWebA发送的请求中已经带上了用户的cookie

防范CSRF攻击

  1. 如果是Ajax跨域请求,在Access-Control-Allow-Origin中设置安全的域名,如WebA的域名。
  2. 如果是form表单请求,后端需要验证http的Referer字段,确保来源是安全的。
  3. 推荐使用token验证

3、自动化脚本攻击

羊毛党通常使用脚本攻击我们的线上活动,获得非法利润,他们通常使用刷API接口,自动刷单等方式获取利润。

通常来说,我们需要人机识别来防范脚本攻击,在web前端服务端之间,添加一层风控系统,用于鉴别终端是否是机器。
image.png

但前端依然可以为羊毛党增加一些收入难度,想要薅羊毛先得过前端这一关(纸老虎)。

  1. token校验,前端通过加密算法生成token,由风控系统校验token,攻击者必须破解js生成token的算法才能进行下一步。
  2. 代码压缩和混淆,这里根据实际情设置混淆级别,太高级别的混淆可能会影响JS本身的执行效率。高级混淆后的代码能防止攻击者断点调试
  3. 收集用户行为,记录用户进入页面中行为,加密后交给风控系统(风控系统通过大数据分析地理位置、ip地址、行为数据等进行人机识别分析)
tips:在前端的加密过程中,我们可以使用一些DOM、BOM,API,因为攻击者通过API攻击无法直接模拟出真实浏览器的环境,就算模拟也需要费一番功夫,加大攻击者破解算法难度。

HTTPS如何实现安全加密传输?

  1. 客户端发起请求,链接到服务器443端口
  2. 服务端的证书(自己制作或向三方机构申请),自己制作的证书需要客户端验证通过(用户点一下)。证书中包含了两个密钥,一个公钥一个私钥。
  3. 服务端将公钥返回到客户端,公钥中包含了证书颁发机构,证书过期时间等信息。
  4. 客户端收到公钥后,通过SSl/TSL层首先对公钥信息进行验证,如颁发机构过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。否则就生成一个随机值,然后使用公钥对此随机值进行加密,此加密信息只能通过服务端的私钥才能解密获取生成的随机值。
  5. 服务端获取到加密信息后使用私钥解密获得随机值,以后服务端和客户端的通讯都会使用此随机值进行加密,而这个时候,只有服务端和客户端才知道这个随机值(私钥),服务端将要返回给客户端的数据通过随机值加密后返回。
  6. 客户端用之前生成的随机值解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策。

HTTP/2如果实现首部压缩?

HTTP/2通过维护静态字典和动态字典的方式来压缩首部

  • 静态字典中包含了常见的头部名称或者头部名称和值的组合,如method:GET
  • 动态字典中包含了每个请求特有的键值对,如自定义的头信息,针对每个TCP connection,都需要维护一份动态字典。
  1. 对于静态字典中匹配的头部名称或头部名称和值的组合,可以使用一个字符表示,如建立连接时:

    method:GET 可以使用 1表示 (完全匹配)
    cookeie: xxx 可以使用 2:xxx表示 (头部匹配)

  2. 同时将cookeie: xxx加入动态字典中,这样后续的整个cookie键值对都可以使用一个字符表示:

    cookeie: xxx 可以使用 3表示 (加入到动态字典)

  3. 对于静态字典和动态字典中都不存在的内容,使用了哈夫曼(霍夫曼)编码来压缩体积。

更多相关内容请查看详情HTTP/2新特性

如何优化递归?

在Js代码执行时,会产生一个调用栈,执行某个函数时会将其压入栈,当它 return 后就会出栈。

而从某个函数调用另外一个函数时,就会为被调用的函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前栈,而调用函数的栈帧称为调用栈。

function A(){
    return 1;
}
function B(){
    A();
}
function C(){
    B();
}

C();
Js执行栈中除了当前执行函数的栈帧,还保存着调用其函数的栈帧,在A释放前,执行栈中保存了A、B、C的栈帧,过长的调用栈帧在Js中会导致一个栈溢出的错误。

栈溢出的错误常常出现在递归中。

当递归层次较深影响到代码运行效率,甚至出错后我们应该如何优化呢?

function fibonacci (n) {
    if ( n <= 1 ) {
    return 1
    };

    return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(100) //卡死

1、尾递归优化

仔细观察上述调用过程C -> B -> A,行程此调用栈的主要原因是在A执行完成后会将执行权返回B,此时B才能释放,B释放完成后继续讲执行权返回C,最后C释放。

尾调用

尾调用(Tail Call)是函数式编程的一个重要概念,是指某个函数的最后一步是调用另一个函数。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,所以在C调用B后就会释放。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

将fibonacci函数使用尾递归优化

// 尾递归的优化往往是通过修改函数参数完成的
function fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return fibonacci2 (n - 1, ac2, ac1 + ac2);
}
面试官:尾调用是ES6的新功能吧,而且只有严格模式才能生效,因为在非严格模式下,可以通过function.caller追踪到调用栈,还有其他方法吗?

2、循环代替递归

使用蹦床函数将递归转为循环执行

function trampoline(fn) {
  while (fn && fn instanceof Function) {
    fn = fn();
  }
  return fn;
}

function fibonacci3 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return fibonacci3.bind(null, n - 1, ac2, ac1 + ac2);
}

trampoline(fibonacci3(100)) //573147844013817200000
面试官:嗯,这样确实可以避免栈溢出的错误问题,那你能尝试下不使用递归思想实现求斐波那契数列的和呢?

3、使用动态规划思想实现

function dp(n) {

    if(n <= 1){
    return 1
    }
    var a = 1;
    var b = 2;
    var temp = 0;

    for(let i = 2; i < n; i++){
    temp = a + b;
    a = b;
    b = temp;
    }

    return temp
}
最终我们对递归的优化就是放弃了使用递归😃

周维
8 声望0 粉丝