Loading...
墨滴

前端一川

2021/04/20  阅读:29  主题:蔷薇紫

【前端面经】热乎的小米二面面经

写在前面

春招已经接近尾声,想必诸多学子也已收获满意的offer。而笔者在与一面阔别大概半个月之久,又收到小米的二面。相对于其他面试,小米更侧重的是你用最简练的语言能够最详细地表述你的想法,最后惯例得两道手撕代码题。

1 自我介绍、项目介绍

2 常规基础题

2.1 vuex是什么?怎么使⽤?哪种功能场景使⽤它?

  • 只⽤来读取的状态集中放在store中;改变状态的⽅式是提交mutations,这是个同步的事物;异步逻辑应该封装在action中。
  • 在main.js引⼊store,注⼊。新建了⼀个⽬录store,…export 。
  • 场景有:单⻚应⽤中,组件之间的状态、⾳乐播放、登录状态、加⼊购物⻋
  • state:Vuex 使⽤单⼀状态树,即每个应⽤将仅仅包含⼀个 store 实例,但单⼀状态树和模块化并不冲突。存放的数据状态,不可以直接修改⾥⾯的数据。
  • mutations:mutations 定义的⽅法动态修改 Vuex 的 store 中的状态或数据
  • getters:类似 vue 的计算属性,主要⽤来过滤⼀些数据。
  • action:actions 可以理解为通过将 mutations ⾥⾯处⾥数据的⽅法变成可异步的处理数据的⽅法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发action。

modules:项⽬特别复杂的时候,可以让每⼀个模块拥有⾃⼰的 state、mutation、action、getters,使得结构⾮常清晰,⽅便管理

2.2 关于响应式数据绑定,双向绑定机制:Object.defineProperty()

vue实现数据双向绑定主要是:采⽤数据劫持结合发布者-订阅者模式的⽅式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应监听回调。当把⼀个普通Javascript对象传给Vue实例来作为它的data选项时,Vue将遍历它的属性,⽤Object.defineProperty()将它们转为getter/setter。⽤户看不到getter/setter,但是在内部它们让Vue追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定将MVVM作为数据绑定的⼊⼝,整合ObserverCompileWatcher三者,通过Observer来监听⾃⼰的model的数据变化,通过Compile来解析编译模板指令(vue中是⽤来解析{{}}),最终利⽤watcher搭起observerCompile之间的通信桥梁,达到数据变化—>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

数据劫持:Vue内部使⽤了Object.defineProperty()来实现双向绑定,通过这个函数可以监听到set和get的事件。

var data = { name'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value
function observe(obj{
    // 判断类型
    if (!obj || typeof obj !== 'object') {
     return
    }
    //Object.keys(obj)将对象转为数组
    Object.keys(obj).forEach(key => {
     defineReactive(obj, key, obj[key])
    })
}
function defineReactive(obj, key, val{
    // 递归⼦属性
    observe(val)
    Object.defineProperty(obj, key, {
        enumerabletrue,
        configurabletrue,
        getfunction reactiveGetter({
            console.log('get value')
            return val
        },
        setfunction reactiveSetter(newVal{
            console.log('change value')
            val = newVal
        }
    })
}

Proxy 与 Object.defineProperty 对⽐

Object.defineProperty 虽然已经能够实现双向绑定了,但是他还是有缺陷的 .

  • 只能对属性进⾏数据劫持,所以需要深度遍历整个对象 对于数组不能监听到数据的变化
  • 虽然 Vue 中确实能检测到数组数据的变化,但是其实是使⽤了 hack 的办法,并且也是有缺陷的。

web网站中常见攻击手法和原理

  • 跨站脚本攻击(xss):恶意攻击者通过往web页面中插入恶意html代码,当用户浏览该页面时,嵌入web里面的html代码会被执行,从而达到恶意攻击用户的特殊目的。
  • sql注入:sql注入就是把sql命令插入到web表单进行提交,或输入域名,或页面请求的查询字符串,最终达到欺骗服务器执行恶意sql命令的目的。具体而言,就是利用现有应用程序,将恶意的sql命令注入到后台数据库引擎中进行执行。
  • cookie攻击:通过js很容易访问到当前网站的cookie,你可以打开任何网站,然后在浏览器地址栏输入javascript:alert(doucment.cookie),立刻可以看到当前站点的cookie,攻击者可以利用这个特性取得用户的关键信息。假设这个网站仅依赖cookie进行用户身份验证,那么攻击者就可以假冒你的身份来做一些事情。现在多数浏览器都支持在cookie上打上HttpOnly的标记,但凡有这个标记的cookie就无法通过js来获取,如果能够在关键cookie上打上标记,就可增强cookie的安全性。
  • HTTP Headers攻击:凡是用浏览器查看任何web网站,无论你的web网站采用何种技术和框架,都用到了http协议。http协议在Response headercontent之间,有一个空行,即两组CRLF(0x0D 0A)字符这个空行标志着headers的结束和content的开始。攻击者利用这一点,只要攻击者有办法将任意字符注入到headers中,这种攻击就可以发生。
  • 文件上传攻击:文件上传漏洞就是利用对用户上传的文件类型判断不完善,导致攻击者上传非法类型的文件,从而对网站进行攻击。比如可以上传一个网页木马,如果存放文件的目录刚好有执行脚本的权限,那么攻击者就可以得到一个webshell

Vue中diff原理

要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排。有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们

diff算法包括一下几个步骤:

  • JavaScript对象结构表示DOM树的结构;然后用这个树构建一个真正的DOM树,插到文档当中
  • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较(diff),记录两棵树差异
  • 把2所记录的差异应用到步骤1所构建的真正的DOM树上(patch),视图就更新了

diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法 逐个遍历newVdom的节点,找到它在oldVdom中的位置,如果找到了就移动对应的DOM元素,如果没找到说明是新增节点,则新建一个节点插入。遍历完成之后如果oldVdom中还有没处理过的节点,则说明这些节点在newVdom中被删除了,删除它们即可。

vue模板编译原理

模板转换成视图的过程整个过程:

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树

  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。 我们对上图几个概念加以解释:

  • 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。

  • VNode虚拟节点:它可以代表一个真实的dom节点。通过createElement方法能将VNode渲染成dom节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。

  • patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出,patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。VueVirtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

介绍下你了解Webpack多少知识

基本概念:

  • 入口(Entry):指示webpack应该使用哪个模块,来构建其内部依赖图的开始。
  • 加载器(Loader):webpack默认处理js和json文件,loader配置webpack去处理其他类型的文件,将其转为有效模块给应用程序使用,并添加到依赖图中。
  • 插件(Plugins):loader用于转换某些类型的模块,而插件用于执行范围更广的任务。比如:打包优化、资源管理、注入环境变量等。
  • 模式(Mode):设置当前配置文件在开发和生产环境下的优化行为,默认为生产环境。
  • 输出(Output):指示webpack应该在哪输出它创建的bundle,以及如何命名文件。入口文件可以有多个,但是出口文件只能有一个。

Loader和Plugin的区别:

  • Loadermodule.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)。
  • Pluginplugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

在我的个人理解中,plugin更像是对loader的补充,两者进行相辅相成,loader大多是固定的配置,而plugin能够处理更加灵活的设置。

核心作用:

  • 打包压缩:在进行开发时,项目文件是千姿百态的,此时可以使用Webpack将不同模块有序进行打包整合,根据业务进行进行划分模块,使得结构清晰可读。整个项目在开发过程中,代码和文件是比较庞大的,如果进行项目部署时,会占用很大的内存,因此可以进行压缩,将原先几十M降低成几M,甚至几百K。
  • 编译兼容:相信在实际开发中,由于历史的原因各种浏览器遗留下很多兼容性问题,一方面我们积极学习浏览器的新性能,另一方面又要兼顾旧浏览器的问题。通过Webpack进行按需加载器的机制,可以实现在配置bebel-loader时,对预定义的环境进行配置,将其对新旧浏览器进行兼容。与此同时,由于浏览器只能读取html、js等文件,因此可以通过webpack将非js文件模块转为可读js文件模块。
  • 能力拓展:通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

3 手撕代码题

3.1 千分位格式化数字

用js实现如下功能,将给定的数字转化成千分位的格式,如把12345678转化成12,345,678

这题目相对是比较简单了,能够用来解决的问题的方法也有很多,最简单的可以用正则化进行处理。

  • 正则化
let num = 12345678;
let str = num.toString();
let newStr = str.replace(/(\d)(?=(?:\d{3})+$)/g,"$1,");
  • 将字符串拆分拼接

思路:将数字转换为字符串(toString())再打散成数组(split),如果直接数字转换为数组,就是一整个放进去了,不能单独取到每一位。然后通过循环,逐个倒着把数组中的元素插入到新数组的开头(unshift),第三次或三的倍数次,插入逗号,最后把新数组拼接成一个字符串。

let num = 12345678;
function Thousands(num){
  //将数字转换为字符串后进行切分为数组
  let numArr = num.toString().split("");
  let arr = [];
  let count = 0;//用于计数
  for(let i = numArr.length-1;i>=0;i--){
    count++;
    //从numArr末尾取出数字后插入arr中,其实就是对齐进行倒序
    arr.unshift(numArr[i]);
    //当count每到三位数字,则进行追加逗号。i!=0即取到第1位的时候,前面不用加逗号。
    if(!(count%3)&&i!==0) arr.unshift(",");
  }
  //将数组拼接为字符串
  return arr.join("");
}
Thousands(num);

缺点:一位一位的加进去,性能差,且还要先转换成字符串再转换成数组。

  • 用charAt()获取子字符串,主要用到字符串拼接

思路:不先转为数组,直接获取字符串的每一个字符进行拼接。

let num = 12345678;
function Thousands(num){
  //将数字转换为字符串
  let str = num.toString();
  let res = "";//用于接收拼接后的新字符串
  let count = 0;//用于计数
  for(let i = str.length-1;i>=0;i--){
    count++;
    //从numArr末尾取出数字后插入arr中,其实就是对齐进行倒序
    res = str.charAt(i) + res;
    //当count每到三位数字,则进行追加逗号。i!=0即取到第1位的时候,前面不用加逗号。
    if(!(count%3)&&i!==0) res = ',' + res;
  }
  //将数组拼接为字符串
  return res;
}
Thousands(num);

缺点:依旧需要进行一一分割拼接。

  • 每截取三位进行拼接

思路:每次取末三位子字符串放到一个新的空字符串里并拼接上之前的末三位,原本数组不断截掉后三位直到长度小于三个,最后把剥完的原数组拼接上新的不断被填充的数组。

let num=123345678;
function Thousands(num){
    //将数字转换为字符串
    let str = num.toString();
    let res = "";//用于接收拼接后的新字符串
    while(str.length>3){
        res = "," + str.slice(-3) + res;
        str = str.slice(0,str.length-3)
    }
    if(str) return str + res;
};
Thousands(num);

3.2 比较两个对象的属性和值是否相同

题目描述:

obj1 = {name:"wenbo",age:12,score:[120,121,113]};
obj2 = {age:12,name:"wenbo",score:[120,121,113]};
  • 遍历进行比较

思路:对两个对象进行遍历取值进行比较

function fun(obj1,obj2){
  //判断obj1、obj2是否为Object类型
  let o1 = obj1 instanceof Object;
  let o2 = obj2 instanceof Object;
  //如果两者有不是对象类型的,既可以直接进行等值比较
  if(!o1 || !o2) return obj1 === obj2;
  //如果两个是对象类型,且两者的键值对个数不同
  if(Object.keys(obj1).length!==Object.keys(obj2).length) return false;
  //当以上情况均不是,则进行遍历比较
  for(let key in obj1){
    //需要判断两个对象的此key对应的值是否为对象类型
    let flag1 = obj1[key] instanceof Object;
    let flag2 = obj2[key] instanceof Object;
    if(flag1 && flag2){
      fun(obj1[key],obj2[key])
    }else if(obj1[key] !== obj2[key]){
      return false;
    }
  }
  return true;
}
let obj1 = {name:"wenbo",age:12,score:[120,121,113]};
let obj2 = {age:12,name:"wenbo",score:[120,121,113]};
fun(obj1,obj2);

亦或:

function fun(obj1,obj2){
  //判断obj1、obj2是否为Object类型
  let o1 = obj1 instanceof Object;
  let o2 = obj2 instanceof Object;
  //如果两者有不是对象类型的,既可以直接进行等值比较
  if(!o1 || !o2) return obj1 === obj2;
  //如果两个是对象类型,且两者的键值对个数不同
  if(Object.keys(obj1).length!==Object.keys(obj2).length) return false;
  //取对象obj1和obj2的属性名
  let obj1Props = Object.getOwnPropertyNames(obj1);
  //循环取出属性名,再判断属性值是否一致
  for (let i = 0; i < obj1Props.length; i++) {
    let propName = obj1Props[i];
    //需要判断两个对象的此key对应的值是否为对象类型
    let flag1 = obj1[propName] instanceof Object;
    let flag2 = obj2[propName] instanceof Object;
    if(flag1 && flag2){
      fun(obj1[propName],obj2[propName])
    }else if(obj1[propName] !== obj2[propName]){
      return false;
    }
  }
  return true;
}
let obj1 = {name:"wenbo",age:12,score:[120,121,113]};
let obj2 = {age:12,name:"wenbo",score:[120,121,113]};
console.log(fun(obj1,obj2));;
  • 需要考虑的问题

当对象遍历过程中,遇到对象的属性时Object类型,且指向的是该对象,那么需要考虑的是以上代码还能运行成功吗? 如:

let obj1 = {name:"wenbo",age:12,score:[120,121,113]};
obj1.temp = obj1;
let obj2 = {age:12,name:"wenbo",score:[120,121,113};
obj2.temp = obj1;

思路:新建一个数组,将obj1遍历过的键值存储在数组中,再下一次进行遍历时发现一样的值,直接跳过进行比较。

参考文章

  • 1.《当面试官问Webpack的时候他想知道什么》:https://mp.weixin.qq.com/s/RmcMWzkAiOrOOyxtPSZDUg
  • 2.《webpack的面试题总结》:https://juejin.cn/post/6844903877771264013
关注我们,变得更强。
关注我们,变得更强。

前端一川

2021/04/20  阅读:29  主题:蔷薇紫

作者介绍

前端一川