1.如何获取数组中最大的数
1.es6拓展运算符...
Math.max(...arr)
2.es5 apply(与方法1原理相同)
Math.max.apply(null,arr)
3.for循环
let max = arr[0];
for (let i = 0; i < arr.length - 1; i++) {
max = max < arr[i+1] ? arr[i+1] : max
}
4.数组sort()
arr.sort((num1, num2) => {
return num1 - num2 < 0
})
arr[0]
5.数组reduce
arr.reduce((num1, num2) => {
return num1 > num2 ? num1 : num2}
)
2.数组和链表的使用场景
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
3.了解哪些排序算法,说说冒泡排序和快排的区别
冒泡排序是一种简单的排序方法,它的基本思想是:通过相邻两个元素之间的比较和交换,使较大的元素逐渐从前面移向后面(升序),就像水底下的气泡一样逐渐向上冒泡,所以被称为“冒泡”排序。
快速排序(Quick Sort) 是对冒泡排序的一种改进方法,在冒泡排序中,进行元素的比较和交换是在相邻元素之间进行的,元素每次交换只能移动一个位置,所以比较次数和移动次数较多,效率相对较低。而在快速排序中,元素的比较和交换是从两端向中间进行的,较大的元素一轮就能够交换到后面的位置,而较小的元素一轮就能交换到前面的位置,元素每次移动的距离较远,所以比较次数和移动次数较少,速度较快,故称为“快速排序”。
4.背包问题
5.浏览器缓存
6.输入一串 url 到浏览器,会发生什么?
1.浏览器输入url,按下回车键
2.浏览器根据域名查找对应的IP地址
3.浏览器打开TCP连接(默认端口是80),向该ip发送一个http请求。如果浏览器存储了该域名下的cookie,那么cookie也会放在浏览器中
4.服务器给浏览器进行一个306的永久重定向响应。
5.浏览器根据重定向地址进行http请求
6.服务器分析http请求,生成http响应,将响应发给客户端
7.浏览器收到响应后,生成主页框架,同时向服务端继续发送请求,请求的内容是主页的资源,比如图片、视频等
8.对于静态页面内容,浏览器通常会进行缓存。对于动态页面,浏览器通常不会进行缓存。缓存的时间也是有效的
9.浏览器向服务器发送异步请求,因为有些页面显示完成之后客户端仍需与服务器保持联系
10.整个过程结束后,浏览器关闭tcp连接
7.vm.$set 原理
8.深拷贝如何解决循环引用
只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可
function deepCopy2(obj, parent=null) {
//创建一个新对象
let result = {};
let keys = Object.keys(obj),
key = null,
temp = null,
_parent = parent;
//该字段有父级则需要追溯该字段的父级
while(_parent) {
//如果该字段引用了它的父级,则为循环引用
if(_parent.originParent === obj) {
//循环引用返回同级的新对象
return _parent.currentParent;
}
_parent = _parent.parent
}
for(let i=0,len=keys.length;i<len;i++) {
key = keys[i]
temp = obj[key]
// 如果字段的值也是一个新对象
if(temp && typeof temp === 'object') {
result[key] = deepCopy(temp, {
//递归执行深拷贝,将同级的待拷贝对象与新对象传递给parent,方便追溯循环引用
originParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
const obj1 = {
x:1
}
obj1.z = obj1;
const obj2 = deepCopy2(obj1);
9.vue 和 react 的区别
10.讲讲前端路由
路由是根据不同的 url 地址展示不同的内容或页面
早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。
后来页面越来越复杂,服务器压力越来越大,随着 ajax(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也可以控制 url 自行管理,前端路由由此而生。
单页面应用的实现,就是因为前端路由。
前端路由的实现其实很简单,本质上就是检测 url 的变化,截获 url 地址,然后解析来匹配路由规则。
在 2014 年之前,大家是通过 hash 来实现路由,这种 #。后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发 hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。
在2014年后,因为HTML5标准发布。多了两个 API,pushState 和 replaceState,通过这两个 API 可以改变 url 地址且不会发送请求。同时还有 onpopstate 事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。
1.Pjax(PushState + Ajax)
原理:利用ajax请求替代了a标签的默认跳转,然后利用html5中的API修改了url
API: history.pushState 和 history.replaceState
两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。
例子:
window.history.pushState(null, null, "name/blue");
//url: https://www.baidu.com/name/blue
window.history.pushState(null, null, "/name/orange");
//url: https://www.baidu.com/name/orange
12345
2.Hjax(Hash + Ajax)
原理:url 中常会出现 #,一可以表示锚点(如回到顶部按钮的原理),二是路由里的锚点(hash)。
Web 服务并不会解析 hash,也就是说 # 后的内容 Web 服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理。
hashchange 事件(监听 hash 变化触发的事件),当用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。
11.webpack 工作流
12.webpack 是如何解决两次引入的
13.布局的几种方式
1、静态布局(static layout)
2、流式布局(Liquid Layout)
3、自适应布局(Adaptive Layout)
4、响应式布局(Responsive Layout)
5、弹性布局(rem/em布局)
14.rem vw 的区别
15.vue 如何收集依赖的
vue中利用Object.defineProperty定义getter/setter实现了数据的响应式,里面重要的一点就是依赖收集,它是计算属性和被依赖属性之间的桥梁,有了依赖收集(Dep),当被依赖对象A的getter函数执行时,所有依赖它的东东就会被收集到A的依赖库中,当A的setter执行时,依赖库中的东东就会被一个一个执行,通知依赖对象B。而这些被封装的依赖是在B的getter执行的时候注入到Dep的静态属性target中的
16.vue 双向绑定原理
通过对象里的defineProperty方法里的get,set方法实现的,代码如下:
var obj = {}
Object.defineProperty(obj,"username",{
get:function(){
console.log("get init")
},
set:function(val){
console.log("set init")
document.getElementById('uname').value = val
}
})
Object.defineProperty(obj,"uname",{
set:function(val){
document.getElementById('username').value = val
}
})
document.getElementById('username').addEventListener("keyup",function(event){
obj.username = event.target.value
},false)
document.getElementById('uname').addEventListener("keyup",function(event){
obj.uname = event.target.value
},false)
//Object.defineProperty有两个底层的方法分别是set和get,双向绑定就主要用到了set。
//如上面代码,我们用Object.defineProperty来定义obj的属性,分别定义了username,uname两个属性,当我们给这两个属性赋值的时候,set方法就会被隐秘的调用。
//set方法里有个参数val,就是它这个属性的值。我们可以在set方法里,把这个val赋值给分别需要绑定的input的value值。
//再通过绑定keyup事件,将输入的内容赋值给分别定义的两个属性,这样就实现了vue的双向绑定。
//给input注册keyup事件,然后输入值的时候,把input里的值给obj的username的属性,由于obj的username属性里有两个底层的api,get和set方法,set方法是当obj的username属性值被设的时候,就会执行set方法,而set方法里有val参数,可以获取username属性的值,所以直接将span的内容设置为username属性的值,然后再将input窗口的值也设置为username属性的值,这样直接修改obj.username的值,input里也会更新value,这样就完成了双向绑定
17.函数式编程 如何理解纯函数
纯函数:满足一个条件:输入值确定,输出值就确定的函数
一般而言,我们需要实现函数式编程,我们所封装的方法应该是抽象的,应该是和外部状态无关系的,也就需要是纯函数的,这样才能保证抽象的方法可复用而且输出结果只决定于输入参数。 其实,js里面本身就有封装好的高阶函数,在一定程度上体现了函数式编程的思想,比如数组的map,filter,reduce等方法
18.简单说说浏览器渲染页面的过程
1 遇见HTML标记,调用HTML解析器解析为对应的token并构建DOM树
2 遇见style/link标记调用解析器处理CSS标记并构建CSS样式树
3 遇见script标记调用javascript解析器处理script标记,绑定事件、修改DOM树/CSS树等
4 将DOM树与CSS树合并成一个渲染树
5 根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖图形库)
6 将各个节点绘制到屏幕上
19.重绘和重排
重绘:是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
重排:当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
20.如何减少重绘重排
1.分离读写操作
var curLeft=div.offsetLeft;
var curTop=div.offsetTop;
div.style.left=curLeft+1+’px’;
div.style.top=curTop+1+’px’;
2.样式集中改变,可以添加一个类,样式都在类中改变
3.可以使用absolute脱离文档流。
4.使用 display:none ,不使用 visibility,也不要改变 它的 z-index
5.能用css3实现的就用css3实现。
21.为什么需要虚拟DOM
Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
22.diff 算法
what: 是一种对新旧虚拟DOM树进行比较,得出两者差异,确定最小DOM更新操作的算法。
why: 减少渲染真实DOM的开销,提高性能。
When: 页面更新,重新渲染时用到。
where: Vue中当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁。React在下一次 state 或 props 更新时, render()方法会返回一棵不同的树。在构建fiber时,标记effectTag为Placement、Update、Deletion等。在commitWork构建真实DOM时,按照effectTag规则生成DOM。
Who: Vue(patch、patchVnode、updateChildren)。React(reconcileChildFibers、reconcileChildrenArray、updateHostComponent等)。 how: 深度优先、同层比较。tag不同,生成树形结构不同。使用key来标识稳定节点。
23.介绍下 es6 新增了哪些特性
1.const和let
2.模板字符串
3.一些新增的字符串方法,如includes,startsWith,endsWith,repeat,padEnd,padStart
4.函数可以指定默认参数
5.函数可以使用可变数量参数
6.箭头函数
7.解构
8.数组新增方法,如Array.of,Array.from(),[].fill(),[].keys(),[].values(),[].entries(),find,findIndex,includes
9.类
24.Vue.nextTick 的原理和使用场景
原理:首先nextTick会将外部传进的函数回调存在内部数组中,nextTick内部有一个用来遍历这个内部数组的函数nextTickHandler,而这个函数的执行是异步的,什么时候执行取决于这个函数是属于什么类型的异步任务:微任务or宏任务。
主线程执行完,就会去任务队列中取任务到主线程中执行,任务队列中包含了微任务和宏任务,首先会取微任务,微任务执行完就会取宏任务执行,依此循环。nextTickHandler设置成微任务或宏任务就能保证其总是在数据修改完或者dom更新完然后再执行。(js执行机制可以看promise时序问题&js执行机制)
使用场景:当改变数据,视图没有按预期渲染时;都应该考虑是否是因为本需要在dom执行完再执行,然而实际却在dom没有执行完就执行了代码,如果是就考虑使用将逻辑放到nextTick中,有的时候业务操作复杂,有些操作可能需要更晚一些执行,放在nextTick中仍然没有达到预期效果,这个时候可以考虑使用setTimeout,将逻辑放到宏任务中。
25.Vue组件中data为什么是函数
组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。
26.说说事件循环
27.手写函数防抖和函数节流
函数节流:不断触发一个函数后,执行第一次,只有大于设定的执行周期后才会执行第二次
函数防抖:不断触发一个函数,在规定时间内只让最后一次生效,前面都不生效
/*
节流函数:fn:要被节流的函数,delay:规定的时间
*/
function throttle(fn,delay){
// 记录上一次函数出发的时间
var lastTime = 0
return function(){
// 记录当前函数触发的时间
var nowTime = new Date().getTime()
// 当当前时间减去上一次执行时间大于这个指定间隔时间才让他触发这个函数
if(nowTime - lastTime > delay){
// 绑定this指向
fn.call(this)
//同步时间
lastTime = nowTime
}
}
}
/*
防抖函数:fn:要被防抖的函数,delay:规定的时间
*/
function debounce(fn,delay){
var timer = null
// 清除上一次延时器
return function(){
clearTimeout(timer)
// 重新设置一个新的延时器
timer = setTimeout(() => {
fn.call(this)
}, delay);
}
}
28.数组去重有哪几种方式
1.双重循环比较去重
2.创建新数组,用indexof判断塞入新数组
3.通过js对象特性来去重
4.es6的set方法
29.判断变量的几种方式,有哪些不同
1.typeof
这个方法很常见,一般用来判断基本数据类型,如:string,number,boolean,symbol,bigint(es10新增一种基本数据类型bigint),undefined等。
2.instanceof
一般用来判断引用数据类型,如Object,Function,Array,Date,RegExp等,主要是判断一个实例是否属于某种类型
3.Object.prototype.toString(这个是判断类型最准的方法)
Object.prototype.toString.call来获取类型,toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object,xxx],xxx是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,… 基本上所有对象的类型都可以通过这个方法获取到。
4.constructor
当一个函数F被定义时,JS引擎会为F添加prototype原型,然后再在prototype上添加一个constructor属性,并让其指向F的引用。
null和undefined是无效的对象,因此是不会有constructor存在的,这两种类型的数据可以通过第四种方法来判断。
JS对象的constructor是不稳定的,这个主要体现在自定义对象上,当开发者重写prototype后,原有的constructor会丢失,constructor会默认为Object
30.call bind apply new 实现原理
call函数
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。 该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
// function.call(thisArg, arg1, arg2, …)
Function.prototype.call = function (context,...args) {
let context = context ? context : window;
let args = args ? args : [];
let key = Symbol();
context[key] = this;
let res = context[key](...args);
delete context[key];
return res;
}
apply函数
apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
// func.apply(thisArg, [argsArray])
Function.prototype.apply = function (context,args) {
let context = context ? context : window;
let args = args ? args : [];
let key = Symbol();
context[key] = this;
let res = context[key](...args);
delete context[key];
return res;
}
bind函数
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
// function.bind(thisArg[, arg1[, arg2[, …]]])
Function.prototype.bind = function (context,...args) {
if(typeof this !== 'function'){
return new TypeError('must be function');
}
let _this=this;
return function F(...newArgs){
if(this instanceof F){
return new _this(...args,...newArgs);
}
return _this.apply(context,...args,...newArgs);
}
}
new功能
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
-
创建一个空的简单JavaScript对象(即{});
-
链接该对象(即设置该对象的构造函数)到另一个对象 ;
-
将步骤1新创建的对象作为this的上下文 ;
-
如果该函数没有返回对象,则返回this。
function create(fn,...args){//fn是要new的函数,args是函数的参数 // 创建一个空对象obj,然后将fn的原型链接到obj的原型上 let obj = Object.create(fn.prototype); // 绑定 this 实现继承,obj 可以访问到构造函数中的属性 let res = fn.apply(obj,args); // 优先返回构造函数返回的对象,object/array/function优先返回,如果是其他类型则返回obj return res instanceof Object ? res : obj; }