时间总是过得很快,虽然自知经验浅薄却也挡不住涌动的心,所以有一天,一个练习时长一年半的前端决定找工作
笔者为19年本科毕业,加上实习差不多有一年半经验,此次面试公司皆为深圳的中小公司。一些非常基础的内容在这里没有列出来,列出的部分会简单作答或附带参考链接
一·上市前夕的某证券公司
这家公司虽然规模不算特别大,却有6轮面试,最后他们的CTO问了我3个问题,我想那时候我知道了什么叫做三不知
1·js继承的几种方法
通过继承我们可以使子类具有父类的各种方法和属性,避免了代码的重复输出.
有以下几种继承方法
借用构造函数)(apply,call): 无法实现函数复用,失去原型链的关系()
组合继承:在使用构造函数继承之后,手动将实例的原型指向构建的构造函数,
将构建的构造函数的constructor指向自己
原型式继承:利用一个临时性的构造函数(空对象)作为中介,
将某个对象直接赋值给构造函数的原型实现继承(原型中引用类型值会被修改,无法传递参数)
寄生式继承:在原型式继承的基础上,通过封装继承过程的函数增强对象,返回对象(同上)
寄生组合式继承
2·js原型链里有什么,构造器和原型分别代表了什么
如果你没有听过构造器(自然,这不可能),那你应该知道原型(propotype)和隐式原型(__proto__),如果都没听过,下面就有一个试听的机会
首先如果你有半小时以上的时间,我建议你看看这个系列,因为他讲的太好了
那为什么我还要讲我的呢,因为我知道大家日摸万鱼,不一定有时间去看,首先上图
所有的引用类型都有一个_proto_(隐式原型)属性,每个函数除了有_proto_属性, 还有prototype(显式原型),属性值为一个普通的对象。 所有引用类型的_proto_指向它构造函数的prototype 当一个对象调用自身不存在的属性/方法时,会先去它的_proto_上查找,也就是它的构造函数的prototype;如果没有找到,就会去该构造函数的prototype的proto指向的上一级函数的prototype中查找,最后指向null。这样一层一层向上查找的关系会形成一个链式结构,称为原型链
那什么是构造器呢?
每个实例对象都从原型中继承了一个constructor(构造器)属性,该属性指向了用于构造此实例对象的构造函数
3·内存泄漏,优化方法
面试官说到内存泄露的时候,听到这个陌生又熟悉的概念,感觉离我非常遥远,是呀,cv仔怎么会在乎内存泄露呢,
就我认为,内存泄漏是一个需要理解的概念,是我们开发大型程序需要考虑的问题
我们需要理解垃圾回收机制的两种类型
* 引用计数算法:他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免(ie使用)
* 标记清除算法:清除无法从全局对象触及到的对象。要注意的就是明确切断需要回收的对象与根部的联系,及时清除引用非常重要
也要理解新生代和老年代的分类
优化方法
及时清除引用 比如removeAllChildren()这样
多使用局部变量
使用weakMap 弱引用
二·某家银行外包
一家给银行做外包的公司
1·讲一下登录流程
这里给个大概的思路
1. 前端输入账户和密码,如果登陆成功之后后台返回tokon
2·我们把token存起来,然后利用这个token去请求携带用户信息的接口
3·如果我们的路由表在本地,那么我们可以根据用户的角色配合router的mate标签动态添加角色路由
,如果路由表在后台,那没话说,直接渲染就行
4·跳转首页
2·几种定位方式的区别,只实现垂直定位
垂直水平居中也是个非常脸熟的问题
`一般来说,我们可以使用flex布局,或者使用绝对定位会有好几种解决方案`
那么仅仅需要垂直居中呢?在这里只举两个简单的方法
* 使用line-height,设置和height一样 缺点很明显,只对文本生效
* 元素有宽高,设置绝对定位,配合margin:auto;
3·使用字体图标的好处
我当时只想到可以减少http请求,其实其他优点也还有
轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,不需要下载一个个图像。这样可以减少HTTP的请求数量,而且和HTML5的离线存储配合,可以对性能做出优化。
灵活性:不调字体可以像页面中的文字一样,通过font-size属性来对其进行大小的设置,而且还可以添加各种文字效果,如color、hover、filter、text-shadow、transform等效果。灵活的简直不像话!
兼容性:图标字体支持现代浏览器,甚至是低版本的IE浏览器,所以可以放心的使用它。
相比于位图放大图片会出现失真、缩小又会浪费掉像素点,图标字体不会出现这种情况。
3·闭包和高阶函数
闭包感觉每次都能说,只是说了之后感觉似是而非的样子。
定义:闭包是指有权访问另一个函数作用域中的变量的函数 作用:1·一个是前面提到的可以读取函数内部的变量,可以使用这种方法来创建私有变量, 2·另一个就是让这些变量的值始终保持在内存中,不会被垃圾回收机制所清除
高阶函数,只是听起来高阶,其实我们用的map,filter,reduce都是高阶函数
定义:参数或是返回值为函数的函数称为高阶函数
3·描述css选择器找到节点的原理
其实面试官没打算问这个问题,他问的是重绘和回流,我说不要使用嵌套过多的选择器,这样会导致回流
比如div p span {} 比直接在span上加类名更消耗性能
那css选择器是怎样找到节点的呢?
CSS的工作方式:
当浏览器显示文档时,它必须将文档的内容与其样式信息结合。它分两个阶段处理文档:
浏览器将 HTML 和 CSS 转化成 DOM (文档对象模型)。DOM在计算机内存中表示文档。它把文档内容和其样式结合在一起。
浏览器显示 DOM 的内容。
然后,浏览器会『从右往左』解析CSS选择器,这样能有效避免回溯,加快匹配。
所以最后我还是认为简写是能够提高css匹配效率的,希望知道的能在评论区指正。
6·Var的作用域,函数提升
var的作用与也是个老生常谈的问题,总的来说有两种
* 变量在函数内声明:局部作用域,只能在函数内访问,函数执行完之后局部变量会自动销毁
* 变量在函数外定义,即为全局变量:全局变量有 全局作用域: 网页中所有脚本和函数均可使用
(如果变量在函数内没有声明(没有使用 var 关键字),该变量为全局变量)
函数提升其实和变量提升是类似的,需要注意的是,js中创建函数有两种方式:
函数声明式和函数字面量式。只有函数声明才存在函数提升
console.log(f1); // function f1() {}
console.log(f2); // undefined
function f1() {} // 函数提升,整个代码块提升到文件的最开始
var f2 = function() {}
7·从输入url到页面渲染发生了什么,优化方法
简要来说会有以下几个阶段
- DNS 解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
三·一家专门做网站的公司
这家公司虽然规模不大,面试的人来的却非常频繁,看到年龄和我相仿的面试官,我有点亲切
1·udp tcp
? tcp我还听过,udp又是什么,我们前端太难了
TCP:Transmission Control Protocol, 传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。说人话就是这个协议能保证传输信息的安全性,至少要经过三个步骤,也就是我们所说的三次握手。
那有聪明的小伙伴就有疑问了,你这搞这么多花样累不累,我要是不需要这么安全可以吗?
当然可以
UDP :User Datagram Protocol 用户数据报协议 相比于TCP的面向连接需要反复确认的繁琐步骤,UDP是一中性格特立独行并且主观性超强的非面向连接的协议,使用udp协议经常通信并不需要建立连接,它只是负责把数据尽可能快的发送出去,简单粗暴,并且不可靠,而在接收端,UDP把每个消息断放入队列中,接收端程序从队列中读取数据
udp也就是干这个的,虽然UDP不可靠,但是它的传输速度快,效率高,在一些对数据准确性要求不高的场景
UDP就变得很有用了,比如qq语音、qq视频。
2·自定义组件的渲染过程
这个还没有找到合适的解答,求帮忙。。
3·爬楼梯(斐波那契数列)
初级写法(使用递归的思想)
function MapSort(num){
if(num==1||num==2){
return 1
}
return MapSort(num-1) + MapSort(num-2)
}
但是这样的缺点很明显,在递归的过程中会有很多重复的计算,并调用大量的栈
4·前后端多次失败请求,前端只返回一个错误处理
通常情况下,我们会对前端的错误请求的报错做统一的错误处理,那如果我们有多个错误请求同时发出呢,这时候就要对弹窗逻辑进行相应的改写,有两个思路
1·如果发现有弹窗,接下来的就不显示
2·新弹窗取代旧的弹窗
5·网页内容更新之后怎么让用户页面也更新
这是由于我们每次更新之后,用户不一定刷新我们的网页,可能新旧内容会冲突。
我知道可以通过更改缓存策略来清除缓存 比如在请求头设置cache:false,或ifModified :true
同样的,我们也可以在网页后加随机数或时间戳,这样网页内容也会更新
6·按键级别的权限控制
按钮的权限控制有好几种实现方法
我之前的公司简单粗暴,在页面顶部封装了一个组件,每个页面对应的按钮是直接从后台渲染的
这样问题也比较明显,就是如果对页面,例如表格每条数据的操作就有些吃力,那时候会考虑下面的方法。
自定义指令
vue2中,我们代码复用和抽象的主要形式是组件,然而有些时候我们还是需要对普通dom进行底层操作,
自定义指令就是这时候被你认识的
/**权限指令js文件**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// el:指令所绑定的元素,可以用来直接操作 DOM
// binding:一个对象,很多必要的 property
//node:Vue 编译生成的虚拟节点
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}
在main.js引入之后,在页面是这样使用的
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
参考链接,vue官网
7·单页面应用优化
可以从这些方面考虑
-
路由懒加载
-
适当利用keep-alive缓存页面
-
使用v-show v-if区分使用场景
-
v-for 遍历避免同级使用 v-if(v-for的优先级更高)
-
长列表性能优化
纯粹的数据展示,不需要做响应化可以使用Object.freeze冻结数据
-
事件的销毁
addEventListener,setTimout()等方法不会随组件销毁,需要在beforeDestory页面销毁
-
图片懒加载
-
computed 和 watch 区分使用场景
computed
计算属性,类似于过滤器,对绑定到视图的数据进行处理,并监听变化进而执行对应的方法,有缓存
因为对应的computed作为计算属性定义值并返回对应的结果给这个变量,变量不可被重复定义和赋值
computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后
下一次获取computed的值时才会重新调用对应的getter来计算```
watch:
更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值
当数据变化时来执行回调进行后续操作
无缓存性,页面重新渲染时值不变化也会执行
小结:
当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
异:它们其实都是vue对监听器的实现,只不过computed主要用于对同步数据的处理
watch则主要用于观测某个值的变化去完成一段开销较大的复杂业务逻辑。
能用computed的时候优先用computed,避免了多个数据影响其中某个数据时多次调用watch
(computed不支持深度监听)
-
第三方插件按需引入
-
无状态的组件标记为函数式组件
-
子组件分割
-
变量本地化
-
SSR
四·一家做跨境电商的公司
这家公司给我的感觉很好,算是小而精致的那种,面试官都比较和善,问的问题也比较典型
1·大数据相加和数字精度问题
在这里有个很经典的问题,js中0.1+0.2等于多少?
新前端可能会嗤之以鼻,说这也是个问题?这确实是个问题。结果是这个
这?原因是在JS 中的 Number 是一个双精度浮点类型,双精度浮点类型在 64 位存储空间中的内容如下:
todo:
| 1位 | 11位 |52位 | |--------------:| -----------:|------:| | 符号位 | 指数位 | 小数位 | | 0表示正,1表示负 | -1022~+1023| 任意 |
... 然后转换成十进制:0.30000000000000004
尾数位最大是 52 位,因此Javascript中能精准表示的最高整数是 Math.pow(2, 53) - 1,十进制即 9007199254740991(但Javascript可以表示的最大数是: 1.7976931348623157e+308,虽然可以表示,但存在精度丢失的问题
Math.pow(2, 53) //输出9007199254740992 最高整数
Math.pow(2, 53) + 1 //输出9007199254740992 丢失精度
怎么解决呢,对于小数部分(浮点数)我们可以
四舍五入
(0.1 + 0.2).toFixed(1);//0.3
这似乎有点掩耳盗铃的感觉
换算成整型计算
(0.1*10 + 0.2*10)/10;//0.3
如果需要真正的精确,我们一般会借用第三方库
math.js 或者 bigNumber.js
2·双飞翼布局
双飞翼?听到这个名字我不禁有些想入非非,和面试官一顿攀谈交心之后
我悟了
说起双飞翼,就不得不说圣杯布局
如果利用flex布局,实现这种会简单很多,可以用不得
圣杯布局和双飞翼布局基本上是一致的,都是两边固定宽度,中间自适应的三栏布局
其中,中间栏放到文档流前面,保证先行渲染。
3·xss和csrf攻击
- XSS(css)Cross Site Script(跨站脚本攻击) 攻击者在目标网站植入恶意脚本(js / html),用户在浏览器上运行时可以获取用户敏感信息(cookie / session)、修改web页面以欺骗用户、与其他漏洞相结合形成蠕虫等 防御方法
* 对url进行转译 encodeURIComponent
* mate标签设置内容安全政策(CSP)
* 限定部分的输入类型
- CSRF(跨站请求伪造)(Cross-site request forgery) 你可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义进行恶意请求。它能做的事情有很多包括:以你的名义发送邮件、发信息、盗取账号、购买商品、虚拟货币转账等。总结起来就是:个人隐私暴露及财产安全问题。
* 阐述 CSRF 攻击思想:(核心2和3)
1、浏览并登录信任网站(举例:淘宝)
2、登录成功后在浏览器产生信息存储(举例:cookie)
3、用户在没有登出淘宝的情况下,访问危险网站
4、危险网站中存在恶意代码,代码为发送一个恶意请求(举例:购买商品/余额转账)
5、携带刚刚在浏览器产生的信息进行恶意请求
6、淘宝验证请求为合法请求(区分不出是否是该用户发送)
7、达到了恶意目标
防御措施(推荐添加token / HTTP头自定义属性)
涉及到数据修改操作严格使用 post 请求而不是 get 请求
HTTP 协议中使用 Referer 属性来确定请求来源进行过滤(禁止外域)
请求地址添加 token ,使黑客无法伪造用户请求
HTTP 头自定义属性验证(类似上一条)
显示验证方式:添加验证码、密码等
4·为什么vue顶部只有一个div
这个有两种情况
-
vue实例
Vue其实并不知道哪一个才是我们的入口,因为对于一个入口来讲,这个入口就是一个Vue类,Vue需要把这个入口里面的所有东西拿来渲染、处理,最后再重新插入到dom中。如果同时设置了多个入口,那么vue就不知道哪一个才是这个类.如果设置多个的话,也仅仅能渲染第一个
-
单文件组件
通常情况下,在vue组件里,我们所有的内容都写在template里,像这样
<template> <div class="box">//只能有一个顶部容器 这里是页面内容 </div> </template>
我们要先看一看template这个标签,这个标签是HTML5出来的新标签,它有三个特性:
-
隐藏性:该标签不会显示在页面的任何地方,即便里面有多少内容,它永远都是隐藏的状态;
-
任意性:该标签可以写在页面的任何地方,甚至是head、body、sciprt标签内;
-
无效性:该标签里的任何HTML内容都是无效的,不会起任何作用;
那么对于一个.vue来讲,这个template里面的内容就是会被vue处理为虚拟dom并渲染的内容,导致结果又回到了开始 :既然一个.vue单文件组件是一个vue实例,那么这个实例的入口在哪里? 道理和刚刚也是一样的,通过这个‘根节点’,来递归遍历整个vue‘树’下的所有节点,并处理为vdom,最后再渲染成真正的HTML,插入在正确的位置
hash和history的原理
这部分是vue-router里的知识点
传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系
-
hash 模式 vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件
-
history 模式 由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求
讲一下浏览器缓存
简要的说浏览器缓存分为两个部分,强制缓存和协商缓存,发生在我们cdn解析的过程中
new的过程发生了什么
其实new的想法非常朴素,new帮我们完成创建空对象,绑定目标对象的原型等操作,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来
function newF() {
let obj = {}; // 创建一个新的对象
// 取出第一个参数,该参数就是我们将会传入的构造函数,
比如在调用newF(P)的时候,Constructor就是P本身
// arguments本身是伪数组所以借用[],
会被shift去除第一个参数,剩余的就是构造器P的参数
let Constructor = [].shift.call(arguments);
// 将obj的原型指向构造函数,此时obj可以访问构造函数原型中的属性
obj.__proto__ = Constructor.prototype;
// 改变构造函数的this的指向,使其指向obj, 此时obj也可以访问构造函数中的属性了
let result = Constructor.apply(obj, arguments);
//通过apply改变this指向,调用函数,这样新对象obj就有了构造函数的属性
return typeof result === 'object' ? result : obj
// 确保 new 出来的是个对象 返回的值是什么就return什么
五·某做家电的网络部
这家公司问的都是一些比较发散的题目
1·Ts type和instance区别
暂时对ts不太清楚,以后补上。大佬也可以在评论区填坑
2·vuex的mutation原理,$nexttick原理
- mutation原理 这个问题问的我很忧伤,我说使用commit发送到mutation,然后修改state,但是差点意思
// 源码位置 https://github.com/vuejs/vuex/blob/665455f8daf8512e7adbf63c2842bc0b1e39efdb/src/store.js#L417
function registerMutation (store, type, handler, path = []) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(getNestedState(store.state, path), payload)
})
}
registerMutation 是对 store 的 mutation 的初始化,它接受 4 个参数,store为当前 Store 实例,type为 mutation 的 key,handler 为 mutation 执行的回调函数,path 为当前模块的路径。mutation 的作用就是同步修改当前模块的 state ,函数首先通过 type 拿到对应的 mutation 对象数组, 然后把一个 mutation 的包装函数 push 到这个数组中,这个函数接收一个参数 payload,这个就是我们在定义 mutation 的时候接收的额外参数。这个函数执行的时候会调用 mutation 的回调函数,并通过 getNestedState(store.state, path) 方法得到当前模块的 state,和 playload 一起作为回调函数的参数
-
nexttick原理
DOM更新之后执行的回调,确保操作的是更新之后的dom 异步操作分为宏任务和微任务,这里是把nexttick的代码放在任务队列最后一个宏任务 UI re nder(加入宏任务或者微任务,优先微任务promise)
3·了解过微前端吗?没有
将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品 就我的理解呢:是一种更彻底的模块化,我们可以更自由的升级功能,部署
4·ssr为什么更有效率
简单理解呢,服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器。这样就少去了我们客户端js对代码解析的过程
5·为什么使用vue
开放性题目,可以从以下几点说说。
- 声明式,响应式的数据绑定
- 组件化的开发
- Virtual DOM
六·高频部分
其实公司不止这几家,只是目前中小公司对于一至两年的要求相对比较宽容, 很多公司没有问到特别偏的东西,对于算法基本止步于递归和冒泡这些基础的内容 甚至于有一些知识点出现的频率很高,面试之前需要填坑,不然别人以为你过来捣乱的 一定要掌握的有这些,因为时间有限,这部分就只是提出来,有兴趣的可以自己记录下
js部分
es6新特性
peomise
闭包和他的作用
深浅拷贝
原型链
引用类型和基本类型
类型判断
宏任务微任务
事件机制
防抖节流
vue部分
vue通信方法
单页面优化方法
webpack是干嘛的
nextTick用法和原理
双向绑定原理
怎么传参
history和hash模式的区别
css部分
实现动画
重绘回流
css新特性
浏览器部分
浏览器缓存
localstorage,sessionstorage,cookie
六.悉悉索索
其实面试已经过去好几天了,新工作依然繁忙,这篇总结花的时间也超过了我的预计,前前后后看了50篇以上的文章,大部分没有贴出来,中间还有一些不是很清楚的地方,希望大家看了能够指正。
总结做完,深圳名义上的夏季也就结束了,虽然无知无觉,但总会留下点什么吧