我的前端面试提纲

3,220 阅读18分钟

阅读前提示

这个面试提示纯粹方便个人回忆一些碎片知识,会有点杂乱,也不一定正确,可以作为提纲看看,漏了哪些知识要复习的。

http:

post/get差别

get没有body,get无副作用,能被缓存,post有副作用

状态码

  • 201/post
  • 204可以被缓存,常见option请求
  • 301永久重定向,302临时重定向(同见307,308针对post),浏览器会缓存301的重定向结果。
  • 304 协商缓存,见缓存章节
  • 400 bad request表示http语法错误
  • 401 身份未验证
  • 403 禁止,身份已验证也可能因为权限不足而禁止
  • 405 方法被禁止,其中get、head不允许返回405
  • 502 bad gateway通常网关层接收上游服务器的错误响应(nginx)
  • 503 服务不可访问(常见服务启动期间)

缓存机制

  • 强缓存:Expires cache control:no stored(强制不缓存)no cached(强制协商缓存)max-age指定过期时间 public/private(共有私有)
  • 协商缓存:e-tag、vary、If-Modified-Since、If-Match等if开头的头部

cors

浏览器定义的跨域资源共享机制(浏览器独有,非http规范,服务器实际能收到请求,浏览器拦截响应),本规范要求跨域请求浏览器应当先发起option请求

  • 使用Access-Control-*的一系列头部控制资源获取
  • Access-Control-Allow-Credentials 标注是否能发送cookie

cookie

cookie用途一般是存储个性化设置/会话管理,cookie可以指定过期时间(在set-cookie头部配置),否则将在浏览器关闭后清除(部分浏览器会缓存)。

cookie可以通过设置secure字段,只允许在https请求中传输,可以通过设置http-only禁止脚本访问cookie(为了防止可能发生的xss)

cookie可以通过domain和path设置作用域,domain默认是本域名(不包含子域名)

cookie安全相关:

  • xss相关,被注入脚本,窃取cookie(包含个人敏感信息),需要设置http-only
  • csrf相关,钓鱼网站伪造一个发向服务器恶意请求会自动携带cookie

http2相关

http2自带多路复用,自http1.1后可以使用keep-alive头部保持tcp链接不关闭,但是http请求依然要排队发送,如果前面有个长请求将导致阻塞(浏览器在http1下最多开4~6个tcp链接,意思同时并发最高为6)

  • 多路复用,使用帧的形式实现,每个请求拆分成多帧,带上id,最后server端组装
  • server push,用的比较少,服务端推送,主要应用内联资源的延迟推送
  • 头部压缩/复用,实际就是服务端和客服端保存一些字典

dns相关

dns其实也是一个应用层协议和http一样,通过tcp、udp协议都能实现dns,dns本质就是将域名翻译成ip,但dns的一些特性成为实现cdn的核心。

每次域名访问都伴随着dns请求,同一个域名可以映射到多个ip,同时也可能存在多个dns服务器保存该域名的ip。

每个域名会分层:www.baidu.com.root(.root一般省略)从右到左分别是跟域名、顶级域名、次级域名、主机名,dns会先寻找跟域名的服务器地址,跟域名的地址会返回顶级域名的地址,依次类推,每次查询会有个ttl属性代表缓存时间。

每个域名还能配置cname,相当于一个别名,cname具体的值为另外一个域名,从而达到无感知域名跳转的效果,最后返回的ip将会是cname配置的域名的地址。

cdn和dns的关系:cdn就是依靠cname配置,将域名解析映射到实际的cdn专属dns服务器上,通过负载均衡/分布式部署等工程技术返回一个合理的ip地址。

tcp、udp、tls、ssl相关

tcp三次握手过程

  • [syn] seq x;[syn ack] ack: x + 1 seq:y;[ack] ack: y + 1 seq: x + 1;
  • seq = last ack; ack(总包大小) = last seq + last length(信号syn和fin占一个seq);

四次挥手过程

  • [client fin] seq x ack y;
  • [server ack] seq y ack x + 1(收到fin信号,不再接受新的包)
  • [server ack fin] seq y ack x + 1(处理完成缓存的所有包后发送)
  • [ack] seq x + 1 ack y + 1(发送收到,并关闭链接)

流量控制

滑动窗口机制,每次发送包都会有receiver window来限制下次对方传过来包的大小

拥塞控制

  • 慢启动(包大小有小到大传输,确认网络环境)
  • 快重传(收到序号错误的包,立刻发出重复确认,而不是等带自己要发包再捎带)

udp

udp相对于tcp的特点,能够1对1,1对多传输,每个udp server端只负责发报文,不管报文是否发送成功,没有拥塞控制,所以对报文大小要把握比较好,udp头部只有八字节(端口号,报文长度,报文校验),比tcp的20字节要小很多。

tls ssl握手过程

tls和ssl是实现https的套接层协议,既不属于应用层,也不属于传输层。tls握手过程的本质是,用非对称加密协商出对称加密的秘钥,并使用秘钥加密通信。

  • client 发送 client hello,并附带randow number1,并附带客服端支持的加密算法列表
  • server 发送 server hello,并附带randow number2,选定加密算法。
  • server 发送证书给 client
  • server key exchange(部分算法需要发送一些加密算法的参数/条件)
  • server 发送 server hello done
  • client 收到 server 这些信息后,会向证书签发机构验证证书的合法性,取出证书公钥,并生成random number3,用公钥加密random3生成pre master key
  • server 收到 premaster key后要私钥解密出random3,此时client和server都有3个随机数,并且协商了加密算法,之后server和client将使用这3个随机数生成密钥进行通信。
  • client 通知 server 后续将使用密钥通信
  • 后续client和server会分别发送一条使用密钥加密的信息,双方都能识别意味着链接建立
  • application data

前端安全

编码相关

  • url使用ascii字符进行编码,具体为 % 后加十六进制编码如: !编码后为%20,由于 ascii 并没有规定中文编码,所以url中的中文编码由浏览器自行实现,一般为%加上utf-8编码
  • HTML规定了部分占位符来代表特殊符号,如:> 编码后为 >
  • 浏览器文本资源的编码一般有请求过程中的content-type决定,常为utf-8
  • JavaScript的number底层为double类型,遵循 IEEE-754,64位表示,1位符号位,11位指数位,52位小数位,这种表示形式导致了浮点数的不精确。

xss相关

xss主要发生于用户的输入构成了有意义的指令,xss大多发生在decode期间,解析JavaScript脚本、CSS样式解析、HTML文本解析、JsonDecode、UriDecode。 一些解决方案:

  • csp(浏览器自带的内容安全策略)
  • cors (浏览器自带的跨域资源共享策略)
  • 用户输入过滤,xss-filter

简易的xss-filter实现:

// 加上要过滤的敏感字符
var replacer = {
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;'
}
var xssFilter = (htmlStr) => {
    return htmlStr.replace(/[<>&]/g, (matcher) => {
        return replacer[matcher]
    })
}

CSRF跨域请求伪造

通过伪造合法的请求,窃取用户信息、破坏系统等。一般来说通过钓鱼网站,让用户发起了一些请求,这些请求携带了用户的信息从而导致破坏效果。csrf-token可以有效解决这类问题,像是提供第三方服务的考虑oauth。cookie可以携带same-site标志,禁止跨域携带。

为什么使用TypeScript?

表达能力比较强,写代码本质上是给人看,而JavaScript的表达能力比较鸡肋,过于自由的结果就是写的代码表达的含义只能通过命名来区别,抽象能力比较低,typescript提供类型/接口这种将数据结构抽象的能力,要求开发者在写代码之前,设计出合理的结构,而不是写一步看一步,有效提高代码的可读性,和方便后期维护。

TypeScript带来的额外优势:代码提示(JS的文档注释也可以做到,但后期维护并不方便)、减少因为类型导致的代码错误。

非得JavaScript不可嘛?并非如此,JavaScript也能写出优秀的代码,只是对人有要求,TypeScript付出一点学习成本可以有效降低人的要求(优秀的人才是最昂贵的而不是类型)

前端持久化相关

  • sessionStorage同步访问,会话级别,5mb
  • localStorage同步访问,永久有效,5mb
  • indexDB异步访问,支持事务,大小没有限制

存满了,再写入数据都会报错。

react相关

前沿Concurrent 模式、Suspense

解决渲染不能中断的问题,防抖、节流目标是把UI渲染放到合适的时间点,减少不必要的渲染;Concurrent 模型是能渲染就渲染,但高优先级的任务可以中断当前渲染,可以让react同时持有多个状态,类型layer层。

Concurrent 模式下react拥有多个平行世界(官方说法...平行世界应该代表一颗虚拟dom树),每个平行世界都做着自己的任务,fiber孜孜不倦的调度这些任务,但是这些任务来自不同世界,只有主世界的任务会展现在dom上,其余分世界的任务需要满足开发者设定的条件才能合并入主世界。

fiber解读

fiber是react从16.3开始启用的协调器,替换掉了之前的栈协调器,特点是时间分片,不阻塞渲染的情况下完成协调。

特点:

  • forceupdate/render/setstate返回一个work,并加入更新队列
  • 每个work拥有自己的优先级expriation timer,work会按照优先级执行,但是状态依然会按照work入队时间累加(保证状态的时序)
  • 同一个work可能会分布在多个帧之间执行,除非work被指定了同步执行,超时和首次渲染都会导致work同步执行
  • 每个组件对比完成后会检查空闲时间,如果空闲时间不足,会暂存任务,将控制权返回给浏览器
  • 在新的一帧里面,优先级低的任务可能会被高优先级的任务替换,导致低优先级任务执行结果丢弃,也导致了部分生命周期可能会重复执行。
  • commit side effect是同步
  • fiber使用了保存了镜像树,让低优先级任务有机会复用高优先级任务的成果

Vue相关

$nextTick实现流程

优先使用Promise,然后MutationObserver,之后setImmediate,最后setTimeout;前两者为microtask,后两者为macrotask。

nextTick使用了变量pending,防止流程的重入(即多次调用nextTick的场景)

vue初始化相关

vue的整体架构设计基于mixin和委托,利用这种模式划分处理模块。

  • Vue脚本加载期间,挂载静态全局方法,并通过混入的方式挂载原型方法,这期间干了这些事。
    • 将原型上的$data,$prop属性的访问请求委托到相应的实例对象上。
    • 还有其余Vue的原型方法,基本都是这个时期挂上去的($set、$watch、$on)。
  • Vue实例化流程
    • 合并配置,配置可能来自Vue.extend添加的配置,new Vue传入的配置,父实例传给子组件的配置等等。
    • 初始化生命周期相关的属性,维护父子跟组件关系
    • 维护原生事件和组件间的关系
    • 初始化渲染函数,创建了虚拟dom的构建函数,将实例属性$attr、$listeners设置为响应式$attr包含父作用域传进来的原生属性,$listeners则是除了native外的事件。
    • 触发beforeCreate钩子
    • 按顺序初始化injection、props、data、provider(响应式)
    • 触发create钩子
    • 执行mount
  • Vue响应式原理
    • 响应式核心方法defineProperty,响应式的属性触发getter时候,会导致初始化时创建的dep将当前活跃的watcher添加到dep中。
    • 触发setter时,会通知dep中的watcher
    • watcher初始化的时候,除了标为lazy外的watcher,会调用自己的get方法,将当前活跃的watcher标志位自身,并访问对应的响应式属性(render也是个watcher)。
  • Vue挂载流程
    • $mount的方法按照几个不同的包实现方法不一样,只描述web-runtime版本
    • 执行beforeMount钩子
    • 将渲染函数构建成一个watcher,该watcher的get方法
    // get方法大概长这样,render返回vnode
    () => updates(render())
    
    • 内部使用vm.$createElement构建虚拟dom,就是下面那个h,最后返回vnode树
    render(h) { return h('h1', '我是标题')}
    
    • _update方法负责将vnode dispatch到真实dom上,无论初始化渲染还是更新都是这个方法,后续属于snabbdom。
  • 更新流程
    • _update方法实际最终调用__patch__方法。
     vm._vnode = vnode
     // Vue.prototype.__patch__ is injected in entry points
     // based on the rendering backend used.
     if (!prevVnode) {
       // initial render
       vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
     } else {
       // updates
       vm.$el = vm.__patch__(prevVnode, vnode)
     }
    
    • 如果是首次挂载,直接把vnode传递给createElm生成dom,否则会有个对比的过程
    • diff的过程大概就是比较类型、比较key、递归调用进行比较子组件

JavaScript基础相关

作用域相关

  • 全局声明const let class作用域挂在一个名为scope的作用域上,而不是window
  • "use strict",var声明的变量也不会挂在window上
  • var const let class函数声明都存在作用域提升,但let、const、class不允许初始化前使用(暂时性死区)。
  • 访问或者使用typeof检测在暂时性死区的变量会报ReferenceError
  • 严格模式下不允许在块作用域中声明函数
  • 块作用域中的函数,函数体提升到块作用域的顶部,但是变量本身会提升到全局作用域,块作用域中不允许使用var声明已经声明过得函数。
// SyntaxError
if(true) {
    var a = 0;
    function a() {}
}

// ok
typeof a // undefined
if(true) {
    typeof a // function
    function a() {}
    function a() {}
}

类型相关

a instanceof b检测b的原型是否在a的原型链上。

function A() {};
var a = new A();
a instanceof A; // true
a.constructor = A;

其他类型相关参考:[] == ![] !? 浅析JS的类型系统和隐式类型转换

操作符相关

  • 逗号操作符 - 对它的每个操作数求值(从左到右),并返回最后一个操作数的值(exp1, exp2)
  • in操作符会检索原型链上的属性
  • for..in会遍历所有可枚举的属性,包括原型链上的,但是原生方法一般不可枚举
  • for..of使用迭代器遍历
  • 操作符执行优先级
    • 20 - 圆括号(永远先执行圆括号内的运算)
    • 19 - 成员访问、需计算的成员访问、无参数的new调用、函数调用。
    • 17 - i++,i--
    • 16 - ! ~ +i -i ++i --i typeof void delete await
    • 15 - **
    • 14 - * / %
    • 13 - + -
    • 12 - >> << >>>
    • 11 - > < >= <= instanceof
    • 10 - == != === !==
    • 9 ~ 4 - & ^ | && || ? :
    • 3 - 赋值操作
    • 2 - yield
    • 1 - 展开运算符
    • 0 - 逗号
// 之前很火的面试题,因为成员访问运算高于赋值运算
var a = { b: { d: 2 } }
var c = a;
a.b = a = { f: 3 };
console.log(a, c)
// { f: 3 }   { b: { d: 2 } }

class相关

  • 类中的所有方法是不可枚举的,自定义的原型方法需要手动设置
  • 继承类会继承超类的静态方法和实例方法,继承类的this在调用super后创建。
// es5继承的实现
function Bar() {
    this.bar = 'bar'
}

Bar.prototype.getBar = function() {
    return this.bar
}

function Foo() {
    this.foo = 'foo'
    Bar.call(this)
}

Foo.prototype = Object.create(Bar.prototype);
Foo.prototype.constructor = Foo;
Foo.prototype.getFoo = function() {
    return this.foo;
}

箭头函数相关

  • 箭头函数没有prototype属性,使用new调用会报错typeerror
  • 箭头函数使用call、apply、bind会忽略传入的this
  • 箭头函数的this继承父级

文件相关

  • File对象除了由开发构建,还能来自于input[type="file"]和drop事件的event.dataTransfer.files。
  • File继承于Blob(本质应该是文件描述符)
  • 现代浏览器ajax都可以直接发送File/Blob(binary)
  • URL.createObjectURL可以为File/Blob/MediaSource等数据结构生成url链接(可以用于本地上传文件的预览,如PDF预览/图片预览),使用完后需要手动调用revokeObjectURL释放引用。
  • FileReader接口主要用于将File/Blob转化为array buffer/dataurl(base64)/text(根据对象的mine-type进行编码)
  • Blob.slice方法主要用于大文件的分片传输

nodejs相关

nodejs架构

  • Javascript
  • C/C++ Binding/Addons(C++插件)
  • libuv(异步调度)/v8(JS执行引擎)/其他系统底层工具(http等)

nodejs事件模型与浏览器的event loop有比较大的差距

  • Timer(setTimeout setInterval)
  • pending Task(部分系统回调,例如tcp error)
  • idle,prepare(内部使用)
  • poll(大部分异步回调都在这个阶段执行,且这个阶段会阻塞,直到下一个timer或者存在setImmediate的回调)
  • check(setImmediate)
  • close callback(关闭的回调在这执行,tcp.on('close'))

每种任务结束都会执行,proccess.nextTick和其他微任务。

内存相关

  • 经常泄露内存的场景
    • 大量永不决议的promise
    • 框架context之外的引用情况
    • js引用dom没有释放的场景
    • 单例引用已经销毁的句柄(Component/Incomingmessage)
    • 还有闭包的引用问题
  • v8内存分配情况
    • Buffer不属于v8的内存分配,使用C++申请内存
    • v8堆内存分为新生代和老生代,新生代使用Scavenge算法分配,每次gc将from区域的内存复制到to区域,对于已经经历过该算法的内存或者to区域空闲率超过25%,会被移动到老生代
    • 老生代使用标记-清楚算法gc。

以下为个人备忘录部分

个人介绍

17年毕业,当年3月开始进行前端开发到现在应该三年左右,目前在一家做银行资产管理系统的公司任职前端开发工程师,主要负责交易和风控相关的业务,业务特点是复杂表单交互,业务之外还参与一些前端工程化的工作和前沿的分享(比如:组件库/脚手架的维护/),之前一家公司做一些小项目的前端负责人包含(PC/小程序/h5)。技术特点是:熟悉react/vue/mobx,读过部分核心源码,了解大多特性的实现原理,熟悉JavaScript大部分特性,大概有一年的typescript使用经验。nodejs比较熟悉koa和eggjs,对nodejs的一些核心模块也有一定理解。

项目中能讲的点:推动typescript、微服务化、dashboard模式

微服务化

业务背景

业务特点:业务专业性较强,各个业务虽然互相依赖,但是开发人员通常只理解自己那块业务。

启动产品化开发,组织架构变动,由之前十多人的组拆分成多个业务小组+架构组,方便人才的定向培养。

原有UI工程在一个项目内(还有一些不在同个项目的UI工程也需要合并进来),本来同一个组还能由组长统一协同,现在分散到各组,协同成本增加,各个业务线需求紧急度和发版频率也不一致,每个业务线也拥有业务相关的公用组件,导致发生把代码切分成多个项目单独维护的想法,线上通过代码聚合的组织方式。

具体实施

第一阶段先使用iframe的方式进行快速的切分,外部菜单通过菜单的业务属性展示不同的iframe,完成第一部的切分。

iframe的缺点:存在跨域的问题,跨业务模块的组件通信有很大的限制,对于业务模块的布局模式也有很大的限制,本身iframe的开销也很大。

动态加载编译好的js,返回react.element,动态挂载,内部使用memory route,存在的问题:和下面的dashboard模式有冲突,不能灵活组合,

自应用跟节点增加needToRenderAtPortal属性,保存对应的路径和挂载的元素。

dashboard模式

业务背景

资产管理平台这种类型的产品会涉及到多个使用角色:基金经理、操作员等等,每个角色对信息的关注不一样,但是每种人总是关注某几个类型的界面;于是乎需要频繁的切换界面;于是乎我们抽象出工作台这么一种模式,将页面打散,可供用户自由组装成自己的工作台。从技术上来讲工作台就是提供可供拖拽组合的界面,并能够记住用户的个性化设置。

具体实施

本质上没有什么技术上的难度,难度在于产品抽象上,比如工作台使用平铺还是层级关系,前者可以扩展几个组件之间的联动关系,后者方便用户操作。

组件注册成为可拖拽组合的组件,使用写死的URL的模式,因为使用装饰器注册对懒加载不友好。

最后选择了平铺的方式,平铺的方式如果拓展组件间的交互方式又是一个问题,不可能每个组件之间都有个性化的交互,所以这里有需要对组件本身进行类型的交互。 比如:searchBar可以提交一个search update事件,所有的可搜索的组件会响应这个事件,调用自身实现的相应方法进行更新。

成果

在对客户进行poc的过程中,客户对我们这种交互模式表示特别的关心

命令式的控制文档更新,精准原子化,语法可以使用类似sql