JavaScript
什么是闭包?
在某个内层的函数,引用了函数外的变量或函数,当这个内层函数还被引用时,引用的函数外的变量或函数不会被销毁,从而形成闭包
闭包的作用可以实现私有属性、方法,模块化等功能,减低污染作用域;但是要注意内存泄露的问题,因为形成闭包的函数,没被毁坏时,里面引用的变量也不会被销毁
常见闭包场景
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) //>> 6 6 6 6 6
}, i * 1000);
}
循环执行异步代码。比如循环执行 setTimeout,打印 i 的场景。最终所有的输出都是循环次数 + 1,因为 setTimeout 是异步执行,等 for 循环执行完后,索引值 i 等于循环次数 + 1,然后才继续执行 setTimeout 回调函数,这样因为作用域的关系,setTimeout 里面打印的 i 值,会按照作用域链一级级往上寻找,最终在全局作用域找到并打印
什么是作用域?
作用域是一套规则,规定在何处如何查找变量
JavaScript 使用的是词法作用域,词法作用域是定义在词法阶段,就是代码将变量和块作用域写在哪里决定(词法作用域由函数被声明时所在的位置所决定)
当要查询一个变量的时候,会先在当前的作用域查找,找不到就会一级级往上找,直到全局作用域为止;如果该变量没定义,就会抛出错误;找到到没定义,就返回 undefined
什么是原型?什么是原型链?
- 查看 prototype.md
new 操作符的作用?
- 查看 prototype.md
基本类型
- Boolean
- Undefined
- Null
- Number
- String
- Symbol
引用类型
- Object
- Array
- Function
- RegExp
- Date
什么是安全的整数?
在(-2^53, 2^53)范围内,双精度数表示和整数是一对一的
js 小数精度问题
0.1 + 0.2 = 0.30000000000000004
js 的遵循 IEEE 754 标准,使用 64 位固定长度表示,也就是双精度浮点数,规则如下:
- 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
- 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
- 尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零
当 0.1 + 0.2 时,两个数字会先转成二进制再相加;而浮点数的二进制表示是无穷的,根据 IEEE 754 的标准,小数部分最多支持 53 位二进制,当两个二进制相加后,再转成十进制,就会产生非 0.3 的结果
解决方式:
数据展示:
javascriptfunction strip(num, precision = 12) { return +parseFloat(num.toPrecision(precision)); }
为什么要用精度 12 ?一般情况精度确定到 12,能处理大部分情况,可以视情况调整精度
计算:
把小数部分转成整数后再运算,最后再除以那个倍数
内存泄露
查看“基础总结”
说说 this 的理解
this
就是当前函数运行时所在的环境
说说对作用域的理解
作用域主要分为全局作用域、函数内的作用域、块作用域等
全局作用域是根据宿主环境,比如浏览器就是 window,nodejs 就是 global
而函数内的作用域和块作用域,根据执行时的上下文,去查找某个属性时,会在当前的作用域开始查找,如果找不到,就会从它的父级的作用域开始找,直到全局作用域,如果都没有,就会返回 undefined
JS EventLoop 原理
JS 是单线程,这里引用阮一峰老师文章的内容:
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
JS 里面有分同步和异步,同步任务直接在主线程执行,异步任务会加入到一个任务队列中,等主线程的任务完成了后,就会执行异步任务。JS 会不停循环这个过程。
- 将同步任务放到调用栈,将异步任务加到任务队列中,其中的异步任务会分为 task 和 mircotask,像 AJAX、setTimeout 这些是 task,而 Promise 则是 mircotask
- 首先执行同步任务
- 然后执行 mircotask queue 中的任务(promise)
- 然后执行其他 task(DOM 回调、AJAX、setTimout)
- 然后一直循环上面的顺序
css
rem 原理
rem 单位兼容 IE9 以上,基本上移动端的浏览器都支持。它是根据 HTML 的字体大小而变化。1rem 就等于多少字体大小。而移动端的 rem 布局原理,就是根据浏览器的宽度、一个基准值而改变 HTML 的字体大小,再添加阿里的 flexible 方案,从而形成动态缩放的效果。
设置一个基准值,flexible 这个库就会根据当前浏览器的宽度,重新计算 HTML 的字体大小。比如设置基准值是 75,设计稿的宽度是 750px 的
在 750 宽的浏览器,HTML 的字体大小就是 75px,1rem 就等于 75px 。而我写某个样式时,直接就可以根据设计稿的大小 / 基准值,就是所要的 rem 值。再配上自动转换工具,可以直接写上 px。
link 和 @import 区别
- link 可以多个同时加载;@import 需要等页面加载完成后再加载
- link 没有兼容问题;@import 低版本 ie 不支持(ie5)
- link 可以通过 js 动态插入,@import 不行
border 三角形原理
每个位置的边框,其实是有三角形形成的
什么是 BFC
处于 BFC 的子级,与外部的元素互不干扰,是一个完全独立的区域,例如 margin,如果不是 BFC 状态,margin 的效果会影响到外部元素;计算 BFC 的高度时,浮动子元素也会算上
有什么方法可以触发 BFC
- overflow 不是 visible
- position 不是 static 和 relative
- display 是 flex 和 inline-block
- float 不是 none
margin 边界叠加问题
当两个 block 元素,在垂直方向有同一个方向的 margin 重叠,会两者取最大的进行渲染;比如上方元素的 margin-bottm 是 30px,下方的元素的 margin-top 是 100px,那最终渲染的结果是两个元素的距离是 100px
解决方式:
- 改用 padding
- 单方面加 margin
如果某个元素是嵌套元素,可以使用 overflow: hidden 处理,将该元素变成 bfc 状态,不影响外部元素
重绘重排问题
重绘:重新绘制该元素的一些属性,例如颜色、背景、transfrom 等不会影响布局的属性
重排:重排是在重绘的基础上,从新排计算排列布局属性数值等,而且会一层层往上级计算,直到没有影响为止
优化方式:
- 实现动画的时候,尽量使用 transfrom、opacity,而不是 top right bottom left,display 这些属性去做位移、显示隐藏等操作;因为 transform 和 opacity 不会影响到外部的元素
- 尽量少一个个样式改,可以的情况下,去改变标签的 className
- 要插入节点的时候,可以使用 DocumentFragment 一次性出入所有的节点
NodeJs
为什么要用 NodeJs
NodeJs 是一个单线程、异步、非阻塞io 的语言,这些特性适合高并发访问的项目;而且 NodeJs 是基于 JavaScript,这更利于前端可以快速学习、过渡,比如实现中间层、同构等
怎么理解异步
我们先说下同步,比如有三行代码,第二行代码是要发起一个同步 http 请求的,需要大量的时间,所以第三行代码要等第二行代码执行完才能执行
而如果我们使用异步,这时候第二行代码发起一个异步的 http 请求,执行后,就会先先不理,接着执行第三行代码;第二行代码等 http 请求响应后再继续
框架
vue
为什么使用 react
react 上手也是很快,单项数据流,数据流自上而下,显示清晰,debug 方便。而且 react 生态很火热,在 github 上有各种各样的功能库,足够满足很多项目需要的功能
vuex 和 redux 的区别
- vuex 和 redux 都是基于一个状态树进行维护数据
- redux 的 store 是不可变的,只能通过 reducer 得到的 state 进行替换;而 vuex 的 store 通过 mutation 对某个变量进行修改,而不是整个替换
- vuex 的异步处理,有自带的 action;而 redux 可以选择不同的第三方库配合 middleware 进行处理
单一状态树的好处:能够直接地定位任一特定的状态片段,也能很容易地获取整块数据的快照
vuex
- 要修改 store 里面的数据,要显示调用 mutation 才可以,这是为了可以追踪每一次状态的变化
- action 类似于 mutation,它可以调用 mutation,但不能直接修改状态值,在 action 可以包含任意异步操作
- 为什么 mutation 必须同步函数?这是作者的一种约定,mutation 是同步函数,才能让开发工具捕捉到每次的状态变更。因为 mutation 触发后, devtool 不知道回调函数什么时候才被调用,所以无法追踪数据变化。
vuex 概念:vuex 是专门配合 vue 的一个状态管理库,由一个状态树维护数据,它有几个核心的概念: state 就是一个状态树,保存着全部的状态;getter 用于封装业务逻辑从而获取数据的方法;mutation 是唯一能修改 state 的同步方法;action 用于处理异步操作的方法,不能直接修改数据,但能调用 mutation 去修改;module 划分不同的模块。其实整个状态管理库并不复杂,用 state 保存全部数据,只能通过 mutation 去修改 state 的值,当 state 修改后,就会通知 view 层进行更新操作。
redux
dispatch ,触发 action 传到 store,而 action 是 store 数据的唯一来源。官方的约定是 action 是一个 Object 的数据结构, type 字段是声明要执行的动作类型,其余是要传入的数据字段。action 只是描述有什么事情发生
reducer 是响应 dispatch 传入的 action,进行处理后再发送到 store 的纯函数。reducer 是描述如何更新 state 值。reducer 接收两个参数:state 和 action。官方约定 reducer 是纯函数,不能有
- 修改传入参数
- 执行有副作用的操作(请求 api 和路由跳转)
- 调用非纯函数(Date.now()) ??
reducer 里面不要修改 state 值,而是通过生成新的属性,例如用 Object.assign
redux 概念:单向数据流,维护唯一的、不可变的 store,state 的值是不可直接修改的,要通过触发 action 后,state 再去响应对应的 reducer,最后替换 state 值 。
什么是不可变数据:
不可变数据就是你不能直接修改它的值,而是通过复制它的值,并且产生一个新对象的方式来得到一个新的数据,包含里要修改的部分
不可变数据的优势:
带来更高的性能,更简单的调试,不会改变的数据比可以随意更改的数据更容易调试。
redux 的异步处理
像请求 API ,大概分为三个状态,start,success,fail
最简单的处理
首先在 component 里面,开始请求 api,随即修改请求状态为:start;当收到响应后,如果是成功,修改状态为:success,否则修改状态为:fail
存在问题:
- 每次写请求的代码,都需要重复这些代码
- 没有做竟态处理,当某个请求还没收到响应时,其他请求收到了响应,导致数据渲染有误
异步 action
工具
webpack
构建流程
- 解析webpack配置参数。
- 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
- 从配置的
entry
入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。 - 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
- 而 plugin 可以扩展功能,比如打包优化、资源管理等
- 递归完后得到每个文件的最终结果,根据
entry
配置生成代码块chunk
。 - 根据
output
的配置,输出所有chunk
到文件系统。
工具链
在网上看文章的时候,看到这个名词,具体意思就是在整个开发流程中所使用到的工具。这里我大概列下:
前端
- 原型、设计稿
- Axure,蓝湖,sketch,photoshop
- 代码管理
- git,gitlab
- 创建项目模板
- 自研一个脚手架,通过执行 npx ,根据要求输入内容和选择,生成一个项目模板
- 开发
- 处理资源、包管理工具
- webpack
- 框架
- Vue,React
- 处理资源、包管理工具
- 项目发布
- Jenkins
后端
网络
GET 和 POST 请求的区别
- 通常 GET 请求传入的参数是放在 URL 上;而 POST 是放在 body; URL 有长度限制;大概 2000 个字符左右;而 body 无限制;
- GET 和 POST 要传参,其实放 URL 或 BODY 都是可以的,没什么区别;上面的限制,只是浏览器的限制;两者更多的是语义上和规范上的区别
- 两种请求的安全性其实都一样,因为这个都是 HTTP 协议的,而 HTTP 协议是明文的,所以一样不安全;而参数存放的位置,body 相对比 url 安全而已
- GET 请求会产生一个 TCP 数据包;POST 会产生两个
- GET 请求会把 http header 和 data 一起发送
- POST 请求会先发送 header;等服务器响应 100 continue ,再发送 data
跨域
同源策略是浏览器的一个限制,同源是值“协议+域名+端口”一致,否则非同源。
解决跨域问题
- JSONP
- CORS,后端在 HTTP 请求的时候添加跨域头字段
- postMessage,解决多窗口之间,嵌套的 iframe,页面与新打开的窗口的消息传递
- 后端代理,后端服务通过请求对应的地址,将响应返回给前端,由后端实现
- nginx 反向代理,类似后端代理,使用 nginx 转发请求
HTTPS 能做什么?有什么用
HTTPS 就是在 HTTP 基础上多了一层 SSL,SSL 协议拥有加密、CA 的功能,可以建立一个信息安全通道,确认内容的真实性
使用 HTTPS 是代替 HTTP,HTTP 有一些不足的地方
通信使用明文,内容会被窃取
HTTP 是基于 TCP/IP 的,通信内容有可能被窥视(为啥 TCP 传输内容会被窥视?)
不验证通信方的身份,通信会被遭遇伪装
- HTTP 协议的请求和响应,不会对通信方进行确认(通信的对方,是否你要指定的主机)
- 任何人都可以对某个主机发起请求
无法证明报文的完整性,内容会被篡改
- 无法证明信息的完整性,内容可能会在传输中,被攻击者篡改
而 HTTPS 解决上面的问题
- 建立安全的通信通道,内容无法被窃取、篡改,保证完整性
- 服务器和客户端某一方拥有 CA 的话,即可判断通信方的真实存在
HTTP2.0
HTTP2.0 能显著提高性能
- 多路复用(Multiplexing),能允许同时通过单一的 HTTP/2 连接发起多重的请求- 响应消息,在同一个连接上进行交互数据;而HTTP1.x 版本,会对同一域名有限制的发送请求。所以减少了服务端的链路压力,连接吞吐量更大
- 首部压缩,传说的体积更少
- 服务端推送
基础
什么是 base64
base64 是由52个英文字符 + 10个数字字符 + 两个特殊字符(+, /)组成的,最后可能会有 = 进行补位;base64 用来表示二进制数据
base64 编码后的数据会比原本的长,为原来的 4 / 3 ;
原理:
- 将一段文字转为 ASCII 编码值,再转为二进制,并在前面加 0 ,补到 8 位数
- 把所有值拼合起来
- 然后每6位就转为十进制的值,得到索引值,这索引值是 base64 表的索引表
- 因为每 6 位进行裁剪,所以最大值也就 63 而已
- 然后根据得到的索引值,最后得到 base64 的字符串
- 最后会有可能出现余数,用 = 进行补位
而浏览器的 base64,像图片这些多媒体,还会在前面加上前缀:
编程原则
单一职责原则 - Single Responsibility Principle
一个类,最好只负责一件事,只有一个引起它变化的原因
我的理解:对封装的函数、类、组件等,根据功能或需求,按一定的粒度进行切分,降低该模块的复杂度,方便调试、测试和迭代
在我们平时开发时,比较常见的像原子类、一些工具函数(防抖、去重函数)等,都是按照单一职责原则,它们只有单一功能。对于这些工具函数,我个人觉得可以按照最小粒度进行切分
而组件这些相对比较大的模块,就要看具体需求进行切分以 UI 组件为例,Button 组件功能比较单一,而且很多其他组件可能会用到,所以单独切分;而像下拉组件的选择项、表格组件的列,这些是否要再单独封装出来?我觉得还是要看需求,想 Element-ui,Ant-design 这些 UI 框架,它们面向的是大众,会有不同的需求,所以他们会做到切分的越细越好,让你更方便个性化。而我们在做业务需求时,需求要的功能不是很复杂,但很多地方要用到,这时候我更倾向粒度更大得封装,让使用者可以用更少的代码去使用组件
总结来说,单一职责原则要根据对应的需求,从而确定切分的粒度
接口隔离原则 - Interface Segregation Principle
开放封闭原则
不应该强行要求客户端依赖于它们不用的接口
类之间的依赖应该建立在最小的接口上面
在不修改源码的基础上,提供可扩展模块的方式
比如封装了一个请求模块,里面要根据业务码判断请求是否成功、正常。但这个模块是通用型的,每个前端项目都要求用这个请求模块进行请求,这时候就可能会遇到后端返回的成功状态业务码会不一样。如果符合开放封闭原则的话,应该会有一个配置或方法,动态修改该请求模块成功状态码,而不需要修改该模块的源码
里氏替换原则
分两种情况,如果父类是可实例化的,继承该父类的子类,不能重新定义父类的方法;如果要覆盖,则父类不能实例化,只能是抽象类
依赖倒置原则
平时项目开发遇到什么问题?怎么解决?
使用 keep-alive 组件,解决后退页面时,保存之前的状态
项目需求,在 A 页面点击某个按钮跳去 B 页面时,再回到 A 页面时,要保存之前的状态,而其他情况进入该页面要刷新的状态。使用 keep-alive 组件和 keep-alive 的 include 属性,判断哪些页面组件需要缓存。再加上 vuex 去动态的控制哪些页面在什么时候需要缓存,在 activated
和 deactivated
的钩子函数中,去判断是否要缓存页面。还有根据 route 的 meta 属性也可以。
原本的思路是,全部组件都用 keep-alive 的组件,然后需要销毁组件的时候,在 deactivated 的钩子函数中,使用 destory 方法。但页面组件使用了该方法后,后面的路由会混乱,比如后退时,去到的页面组件并不是实际要去到的。
在做管理后台的项目时,根据业务需求,对 element-ui 的组件做二次封装
因为管理后台用的是 第三方的 UI 组件,比如 element-ui,而需求使用定制不一样的样式,所以把用到的 element-ui 组件做二次封装,定制样式。比如 Dialog 样式,顶部的样式、底部的按钮组。还有 Table 组件,添加默认样式和默认配置,普通的栏直接用传入配置和数据即可,无需再写 html,特殊的栏可以用 slot-scope。还有其他的 Select 组件、Checkbox 组件、分页组件等等。
使用 easy-mock 模拟数据,可以不用等后端有接口才开始写逻辑,加快开发速度
我之前开发的项目很多都是前后端分离的,前端切完图之后,就要等后端有接口和文档,才能继续开发。然后我们就产品原型出了之后,后端就会尽快出接口文档,定好返回值的 key 值和类型。我们后端用的是 swagger 生成文档的,swagger 可以导出一份 json 文件,再放到 easy-mock (一个网站)上,就可以直接拿来用,会返回假的数据。
使用脚手架,为模板项目快速开发;开始一些小工具,提升开发效率
有很多个项目的时候,如何很好地维护前端组件
之前公司在初期陆续有新的管理系统要进行开发,到现在,大概有 10 余个管理系统。这些管理系统外层框架的样式、交互,登录页,错误页,都是基本一致的,但这部分的功能,之前都是先通过复制粘贴的形式去开发,这样后期如果有迭代更新会很麻烦。
所以我们这边开始开发一个前端的业务组件库,将公共的组件抽离出去,通过 npm 包进行版本管理和使用。每次有问题修复或者功能迭代,只需更新该组件库,然后各个系统进行相关的依赖升级即可
开发前端工具库、组件库,调试问题
我会负责开发一些前端的公共的工具库和组件库,使用 npm 包进行管理,这些库之间有可能是相互调用的,在开发测试的时候,比如 A 工具库会使用 B 工具库,当 B 工具库更新后,要进行打包、发布版本到 npm 上,然后 A 工具库要进行更新依赖的操作。一旦工具库多起来后,这样地操作会非常麻烦
那时候我就在找一些工具,去管理这些库,用了一个叫 lernajs 的工具,它会对这些库在本地进行一个软链接,当 B 工具库修改代码后,无需发版,A 工具库那边会自动引用本地的代码,大大减少操作
其他
自我介绍
大家好,我从事前端工作两年半有多,主要使用 Vue 进行开发,负责的项目很多都是移动端 H5 和管理后台系统。也有负责前端组的公共组件开发,给到各个不同的管理后台系统进行使用。其中有一些小型项目的后端,也有用过 NodeJs 开发的经验
问公司的问题
- 贵公司的主要核心、盈利业务是什么?
- 开发团队、部门有多少人,各是什么类型
- 前端部门主要用的技术栈是什么?
- 一个项目的开发,前端是怎么分配任务?怎么协调合作的?
- 公司福利等