本篇主讲JavaScript的面试题,Vue和React请看《Vue面试题》《React面试题》
基础知识点:
原型与原型链:
原型:被用于复制现有实例来生成新实例的函数
构造函数:用new来调用,就是为了创建一个自定义类
实例:是类在实例化之后一个一个具体的对象
原型链:实例与原型的链条,原型是prototype,链是__proto__
每个函数有一个原型对象,函数在创建时有一个默认属性 prototype,这个属性指向函数的原型对象
对函数进行 new 调用时,生成一个对象。对象内部链接 [[prototype]] 关联到函数的原型对象。
利用 Object.create(obj) 可以生成一个新对象。该对象 [[prototype]] 关联到传入的 obj 对象。
原型函数 Object 的原型对象即 Object.prototype 是所有普通原型链的终点。
原型链的基本概念:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。当我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。
JS继承
1、ES6 使用extends关键字对Class类继承
原理ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
优点:语法简单易懂,操作更方便。
缺点:不是所有浏览器都支持
2、原型链继承
使用new关键字,例如:Child.prototype = new Parent();
优点:写法方便简洁,容易理解,父类新增原型方法/原型属性,子类都能访问到。
缺点:对象实例共享所有继承的属性和方法。创建子类实例时,无法向父类构造函数传参。
3、构造函数
在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。
优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
4、原型+构造函数(常用)
原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性
优点: 解决了原型链继承和借用构造函数继承造成的影响。
缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
5、原型式继承 6、寄生式继承
7、寄生组合式继承(最优解)
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
优点:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
缺点:代码复杂
事件队列(宏任务微任务)
可以分为微任务(micro task)队列和宏任务(macro task)队列。
微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个。
宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。
微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。
常见宏任务:
setTimeout()
setInterval()
setImmediate(): 通常情况会比setTimeout(fn, 0)
先执行点击和键盘等事件
常见微任务:
promise.then()、promise.catch()
new MutaionObserver()
process.nextTick() :异步执行callback函数,比 "setTimeout(fn, 0) " 要高效很多。
事件循环
JS分为同步任务和异步任务。
同步任务都在主线程上执行,形成一个执行栈。
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。
防抖和节流
防抖和节流都是为了阻止操作高频触发,从而浪费性能。防抖执行最后一次,节流执行第一次。
防抖:是触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。适用于可以多次触发但触发只生效最后一次的场景。
节流:是高频事件触发,但在n秒内只会执行一次,如果n秒内触发多次函数,只有一次生效,节流会稀释函数的执行频率。
防抖 (Debouncing) 的含义是指在一定时间内,多次触发同一个事件,只执行最后一次操作。
应用场景如:
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次。
节流 (Throttling) 的含义是指在一定时间内,多次触发同一个事件,只执行第一次操作。
应用场景如:
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多
闭包
闭包就是函数内嵌套函数,使函数外可以读取到函数内部的变量。
function funA() { let a = 'A'; return function() { return a } }
闭包存在意义:
可以延长变量的生命周期,可以创建私有的环境
闭包好处:
1.可以让一个变量长期在内存中不被释放
2.避免全局变量的污染,和全局变量不同,闭包中的变量无法被外部使用
3.私有成员的存在,无法被外部调用,只能直接内部调用
坏处:消耗内存、使用不当会造成内存溢出问题。(手动创建后,可赋值null释放内存)
闭包可以完成的功能:封装函数然后延迟执行、模块化、防抖、节流......
function make_pow(n) { return function (x) { return Math.pow(x, n); } } var pow2 = make_pow(2); var pow3 = make_pow(3); console.log(pow2(5)); // 25 console.log(pow3(7)); // 343 // pow2和pow3调用方法时,不会被互相干扰
// 使用闭包来模拟私有方法,定义公共函数,并令其可以访问私有函数和变量,实现模块化 var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
Promise
Promise
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
async/await特点
async :“异步”的简写,async function 用于申明一个 function 是异步的;
await:可以认为是async wait的简写, 用于等待一个异步方法执行完成;
async
函数的返回值是 Promise 对象。
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
process、process.nextTick()和setImmediate()
process对象是一个Global全局对象,你可以在任何地方使用它,而无需require。
process.nextTick() 表示在事件循环(EventLoop)的下一次循环中调用 callback 回调函数。这不是 setTimeout(fn, 0) 函数的一个简单别名,因为它的效率高多了。该函数能在任何 I/O 事前之前调用回调函数。如果想要在对象创建之后而 I/O 操作发生之前执行某些操作,那么这个函数就十分重要了。
process.nextTick() 和setImmediate() 设置的回调函数都是在下一次 Tick 时被调用。
区别在于:
- 所属的观察者被执行的优先级不一样,process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者,idle 的优先级 > check
- process.nextTick() 所设置的所有回调函数都会放置在数组中,在下一次 Tick 时所有的都立即被执行,该操作较为轻量,时间精度高。setImmediate() 设置的回调函数是放置在一个链表中,每次 Tick 只执行链表中的一个回调。这是为了保证每次 Tick 都能快速地被执行。
Javascript的内存(垃圾)回收机制?
- 垃圾回收器会每隔一段时间找出那些不再使用的内存,然后为其释放内存
- 一般使用标记清除方法(mark and sweep), 当变量进入环境标记为进入环境,离开环境标记为离开环境
- 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
- 还有引用计数方法(reference counting), 在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个 变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。
- 在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的, 也就是说只要涉及BOM及DOM就会出现循环引用问题。
面试题:
解构赋值
let a = 1; let b = 2; 如果在不声明第三个变量的前提下,使a=2, b=1?
答案:选中👉[a, b] = [b, a]
js的变量提升
在js中,变量和函数的声明会被提升到最顶部执行
函数提升高于变量的提升
函数内部如果用var声明了相同名称的外部变量,函数将不再向上寻找
匿名函数不会提升
箭头函数和普通函数的区别?
- 箭头函数比普通函数更加简洁
- 箭头函数没有自己的this,this指向定义时所在作用域的this,且不会改变。
- 不可以当作构造函数,不可以使用new命令,否则会报错
- 不可以使用arguments,该对象在函数体内不存在,如果要用可以使用Rest参数代替
- 不可以使用yield命令,因此箭头函数不能用作Generator函数
this指向
this总是指向函数的直接调用者。
如果有new关键字,this指向new出来的对象
在事件中,this指向触发这个事件的对象
Promise 面试题 以下代码的执行结果是?
const promise = new Promise((resolve, reject) => { console.log(1) resolve() console.log(2) }) promise.then(() => { console.log(3) }) console.log(4)
答案:选中👉1,2,4,3
解释:以上考察的是关于promise的原理,promise的构造函数是同步执行的,当new Promise的一瞬间,1,2 就立刻被执行,而 .then方法是异步执行的,当执行完1和2之后,会执行输出4,最后执行输出3
常用数组方法:
forEach 循环遍历数组,不改变原数组
map 循环遍历数组、返回一个新的数组
push/pop 在数组的末尾添加/删除元素 改变原数组
unshift/ shift 在数组的头部添加/删除元素,改变原数组
concat:数组的拼接,不影响原数组,浅拷贝
join: 把数组转化为字符串
some: 有一项返回为true,则整体为true
every: 有一项返回为true,则整体为false
filter: 数组过滤
slice(start, end): 数组截取,包括开头,不包括截取,返回一个新的数组
splice(start, number, value): 删除数组元素,改变原数组
indexof/lastindexof: 查找数组项,返回对应的下标
sort:数组排序 改变原数组
reverse: 数组反转,改变原数组
数组去重的方法?
const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));// 2 3 5 4 const s2 = new Set([1, 2, 3, 4, 5, '5', 5, 5]); s2.size // 6 [...s2]// [1, 2, 3, 4, 5, '5'] // Set区分对象类型,所以5和"5"是两个不同的值
Array.filter() + indexOf() 判断:
let arr = [1,2,3,2,3]; let newArr = arr.filter(function(item,index){ return arr.indexOf(item) === index; // 因为indexOf 只能查找到第一个 });
Array.includes()
let arr1 = [1,2,3,2,3]; let arr2 = []; for(var i = 0; i < arr1.length; i++) { if(!arr2.includes( arr1[i] )) { arr2.push( arr1[i] ); } }
数组扁平化使用场景?如何实现数组扁平化?
快速获取/过滤嵌套数组中的某个值
arr.flat(Infinity)、reduce、es6 扩展运算符、JSON.stringify()+正则、arr.toString().split(',')、arr.join(',').split(',')、
// es6 Array.flat() let arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.flat(Infinity); } // reduce递归 function flatten1(arr) { return arr.reduce((result, item)=> { return result.concat(Array.isArray(item) ? flatten(item) : item); }, []); } // es6 扩展运算符 function flatten2(arr) { while (arr.some(i => Array.isArray(i))) { arr = [].concat(...arr); } return arr; } // JSON.stringify() function flatten(arr) { let str = JSON.stringify(arr); str = str.replace(/(\[|\])/g, ''); // 拼接最外层,变成JSON能解析的格式 str = '[' + str + ']'; return JSON.parse(str); }
数组求和
const arrSum = (arr) => {
const temp = arr.reduce((pre, now) => {
return pre+now
},0)
return temp
}
求数组的最大值/最小值?
Math.max();
Math.min();
为什么要使用组件?
方便维护、方便复用、提高开发效率
在组件的设计中,需要考虑什么?
可扩展性强、功能单一、便于复用,适用程度高
说出几个Webpack常用 loader 和 plugin:
loader处理语法、plugin帮助资源文件打包优化
babel-loader、vue-loader、 file-loader、url-loader、eslint-loader、cache-loader、css-loader......
uglify-webpack-plugin(压缩js)、optimize-css-assets-webpack-plugin(压缩css)、html-webpack-plugin、webpack-bundle-analyzer(打包文件体积可视化)......
webpack性能优化:
- resolve参数合理配置
- 合理使用plugin:DllPlugin(缓存模块)、happypack (多进程打包)、ParallelUglifyPlugin(多进程压缩)
- 压缩文件
- 缓存应用程序中更新的频率低的部分(如第三方库)
- 追踪代码大小,使用像 webpack-dashboard 和 webpack-bundle-analyzer 这样的工具来了解你的应用程序有多大
- 优化第三方依赖( webpack-libs-optimizations )
- 作用域提升(Scope Hoisting)
前端性能优化:
大致分为两部分:1、资源优化,通过打包配置优化资源,合理使用异步加载模块。2、代码优化,减少代码冗余,使用性能更好的API,减少重绘回流。
- 减少请求数量(合并文件、接口,使用SVG图片、字体图标来代替图片)
- 减小资源大小(文件压缩、GZIP)
- 优化网络连接(CDN加速、使用DNS预解析)
- 优化资源加载(模块按需加载、懒加载与预加载)
- 减少重绘回流
- 性能更好的API
- webpack优化
详解看这里《前端性能优化的七大手段》
虚拟dom为什么会快?
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。
如何在多个接口请求成功后,再执行后续操作?
通过Promise.all()
。Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
vue、react有什么区别?路由有什么区别?react路由不同之处在哪?
- 模板写法,Vue的模板写法接近标准HTML,只是多了一些属性;React的模板使用JSX书写,写法是原生JS。
- 监听数据变化的实现原理不同,vue 双向数据流,数据可变;React 单向响应数据流,强调数据的不可变。
- 在 Vue 中我们组合不同功能的方式是通过 mixin,而在React中我们通过 HoC (高阶组件)。
- 组件通信:父组件都可以通过 props 向子组件传递数据或者回调;子组件向父组件传值,Vue有事件和回调两种,一般更倾向于使用事件,但是在 React 中都是使用回调函数的。
网络相关知识:
https和http的区别:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
TCP/IP传输协议:
TCP/IP传输协议,即传输控制/网络协议,也叫作网络通讯协议。它是在网络的使用中的最基本的通信协议。TCP/IP传输协议对互联网中各部分进行通信的标准和方法进行了规定。并且,TCP/IP传输协议是保证网络数据信息及时、完整传输的两个重要的协议。TCP/IP传输协议是严格来说是一个四层的体系结构,应用层、传输层、网络层和数据链路层都包含其中。
详解看这里《TCP/IP协议详解》
TCP与UDP的区别:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道简单总结为下面表格:
TCP UDP 是否面向连接 面向连接 无连接 是否提高可靠性 可靠传输 不提供可靠性 是否流量控制 流量控制 不提供流量控制 传输速度 慢 快 协议开销 20字节 8字节
什么是RESTful?
RESTful是一种架构风格、设计风格,基于RESTful的web系统更有层次、简便、轻量级以及通过HTTP直接传输,RESTful web服务成为替代SOAP服务的一个更有前途的替代方案。
详解看这里《RESTful 架构详解》
管理面试篇
DevOps、研发效能平台、云原生是什么?
DevOps: 通过自动化的工具协作和沟通来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。从字面上来理解,DevOps 只是Dev(开发人员)+Ops(运维人员),实际上,它现在是一组过程、方法与系统的统称,包括产品规划、开发编码、构建、QA测试、发布、部署和维护。
一套完整的DevOps应该包括:
项目管理(PM)
:运营可以上去提问题,可以看到各个问题的完整的工作流,待解决未解决等;
代码管理
:gitlab。jenkins或者K8S都可以集成gitlab,进行代码管理,上线,回滚等;
持续集成CI(Continuous Integration)
:开发人员提交了新代码之后,立刻进行构建、(单元)测试。根据测试结果,我们可以确定新代码和原有代码能否正确地集成在一起。
持续交付CD(Continuous Delivery)
:完成单元测试后,可以把代码部署到连接数据库的 Staging 环境中更多的测试。如果代码没有问题,可以继续手动部署到生产环境中。
镜像仓库
:VMware Harbor,私服nexus。
容器
:Docker。
编排
:K8S。
服务治理
:Consul。
脚本语言
:Python。
日志管理
:Cat+Sentry,还有种常用的是ELK。
系统监控
:Prometheus。
负载均衡
:Nginx。
网关
:Kong,zuul。
链路追踪
:Zipkin。
产品和UI图
:蓝湖。
公司内部文档
:Confluence。
报警
:推送到工作群。
研发效能平台:
研发效能的目标。更高效、更高质量、更可靠、可持续地交付更优的业务价值。
- 更高效:更高的效率代表更快、更及时地交付,这样就能更早地进入市场,然后更早地学习、更早地调整,更早地降低风险,更早地锁定进展和价值。这是敏捷和精益思想的核心;
- 更高质量:我们研发的产品是有质量红线、有底线要求的。快速交付给客户有质量问题的功能除了会引发投诉以外没有任何价值。质量是内建的,不是事后检验出来的;
- 更可靠:我们要的是敏捷,而不是脆弱(agile rather than fragile),安全和合规方面要有保障。就像开车一样,只有车子更可靠、刹车更好,你才敢开得更快;
- 可持续:短期的取巧和”快糙猛”、小作坊式开发,只会给未来带来更多的技术债务和持久的效率低下,软件研发不是一锤子买卖,我们应该用”长线思维”来思考问题;
- 更优的业务价值:我们经常说”以终为始”,你提供给客户或业务的东西应该是有价值的,这是关于你为什么要做所有这些事情的根本出发点。
在这个概念的引导下,我们引出持续开发,持续集成,持续测试,持续交付和持续运维的理念,它们是研发效能落地的必要实践。与此同时,我们还需要从流动速度,长期质量,客户价值以及数据驱动四个维度来对研发效能进行有效的度量。
研发效能提升对于我们个人而言:强调功劳而不是苦劳、更聪明地工作、个人能力成长
“研发效能的黄金三角”由三个部分组成,分别是研发效能实践、研发效能平台和研发效能度量,它们形成一个彼此增强、迭代优化的增强回路,有效利用好这个模型可以促进企业研发效能持续增强、不断提升,最终助力企业和业务的成功。
云原生
云原生包含DevOps、持续交付、微服务、容器这四大特征。
关键技术包括:微服务、容器、服务网格、不可变基础设施、声明式API、DevOps。