七月杭州前端常见面试真题(1)

277 阅读14分钟

1. 深拷贝和浅拷贝

浅拷贝:对数据仅进行一层的拷贝,当遇到深层次拷贝时仅复制地址,这就造成了当我们去修改拷贝了后的数据时被拷贝的原数据也会发生改变,应为他们同时共享了一个地址
const obj = {
  a: 1,
  goods: { name: '鞋', price: 199 }
}
const b = { ...obj }
b.goods.name = '球鞋'

如上我们最终就会发现obj.goods.name也变成了‘球鞋’

Object.assign方法,Array.slice方法和扩展运算符都可以实现浅拷贝

深拷贝:深拷贝能解决深层次拷贝引用数据类型公用同一个地址的问题,常用的深拷贝方法有

  1. 利用JSON方法深拷贝:

    JSON.parse(JSON.stringify(要拷贝的对象)),先将对象转换为字符串,再将字符串转换为对象,这样对象中的引用数据类型地址就变得不同,实现了简单的深拷贝,但是使用此方法有弊端,将会导致对象内的部分数据丢失或异常,比如function和undefind将直接丢失;NaN、Infinity 和-Infinity 变成 null; RegExp、Error对象只得到空对象

  2. 利用递归深拷贝:

    定义一个方法,使用for in去遍历对象内的数据,当数据为简单数据类型时直接拷贝到新对象中,当为引用数据类型时再次调用该方法去遍历其中数据,同时注意数组和对象的区别,当判断为数组时,定义一个新数组,当为对象时定义一个新对象

function deepClone(target) {
// 判断拷贝的数据是对象还是数组 生成定义的数据
  var newObj = Array.isArray(target) ? [] : {}
  for (k in target) {
    //循环时如果为引用数据类型,就再次调用此方法,将引用数据类型中的值取出来
    if (typeof target[k] === Object) {
      deepClone(target[k])
    } else {
      newObj[k] = target[k]
    }
  }
  return newObj
}

但是递归深拷贝也有一定的缺陷,当遇到时间,正则,Symbol,函数时需要正常拷贝,否则会造成数据丢失和异常

// 日期格式
if (obj instanceof Date) {
  return new Date(obj)
}
// Symbol
if (obj instanceof Symbol) {
  return new Symbol(obj)
}
// 函数
if (obj instanceof Function) {
  return new Function(obj)
}
// 正则
if (obj instanceof RegExp) {
  return new RegExp(obj)
}

递归深拷贝还有最重要的一个问题是循环引用,当一个对象自己引用自己时,使用递归去进行深拷贝就会陷入死循环,解决方法就是将每次拷贝后的数据进行存储,每次拷贝前判断该数据是否已被拷贝,是就直接返回,否则就进行拷贝后存储记录

  1. 利用lodash深拷贝 直接使用lodash工具进行深拷贝,工作中最常使用的深拷贝方法

2. substr和substring的区别

两者都是字符串截取的方法
  • substr()方法返回一个字符串中从指定位置开始到指定字符数的字符

  • substring()方法返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集。注意:该区间为左闭右开区间,不包含最后一个索引

尽管substr方法没有严格被废弃, 但它被认作是遗留的函数。未来可能被替换,所以如果可以我们尽量避免使用,使用substring替代使用

3. slice和splice的区别

  • slice方法字符串和数组都能使用,用来截取字符串或数组,使用时slice()方法返回一个新的数组对象或字符串,数组使用时这一对象是一个由 begin和end决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组和字符串不会被改变。返回值是由一个被提取元素组成的新数组或字符串

  • splice是数组的方法,用法多样万金油,可进行数组增删改操作,以数组形式返回被修改的内容,原数组会被改变,语法为array.splice(修改开始的位置,需要删除的元素个数(可选),要添加进数组的元素(可选)),返回值是由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

4. 事件循环—宏任务和微任务

首先我们要理解异步的概念,js是单线程的,代码会一行一行往下执行,因此当遇到耗时任务如定时器等时,导致要等待这个耗时任务先完成才能去执行下面的代码,浪费了时间和性能,因此这些耗时任务会js会委托宿主环境也就是浏览区去帮忙执行,执行完毕后再通知js去执行这些任务的回调,而这些由宿主环境委托执行的任务就被称为异步任务

事件循环(eventloop):

  1. 主线程判断任务类型,如果是同步任务则主线程自己执行,如果是异步任务则交给宿主环境代为执行
  2. 宿主环境执行完异步任务将回调放入任务队列,先执行完的先放入
  3. 主线程执行完同步任务后去取任务队列中的回调,先放入的先执行
  4. 执行完取出来的任务后再去任务队列中取下一个,重复循环
  • 宏任务:由宿主环境发起的异步任务: setTimeOut、setInterval、特殊的(代码块、script)

  • 微任务:由js本身发起的异步任务: promise.then后面的内容和promise.catch后面的内容

    执行顺序:

  1. 先执行宏任务
  2. 查看微任务队列里是否有微任务
  3. 有就把微任务全部执行完毕后,再去执行下一个宏任务
  4. 没有就直接去执行下一个宏任务

练习:

console.log(1)
setTimeout(function() {
  console.log(2)
}, 2000)
new Promise(function(resolve) {
  console.log(3)
  resolve()
}).then(function() {
  console.log(4)
})

setTimeout(function() {
  console.log(5)
  new Promise(function(resolve) {
    console.log(6)
    resolve()
  }).then(function() {
    console.log(7)
  })
}, 3000)

setTimeout(function() {
  console.log(8)
  new Promise(function(resolve) {
    console.log(9)
    resolve()
  }).then(function() {
    console.log(10)
  })
}, 1000)
//答案:1、3、4、8、9、10、2、5、6、7

5. watch和计算属性

watch:监听,当某些数据props或data等数据发生变化时,会去执行回调函数,起到了观察的作用,不一定有return返回某个值

computed:计算属性,主要用于值的计算,计算场景,一定要return返回某个值,当一个值受多个数据影响时使用该属性,计算属性有缓存,当调用getter函数计算出结果后会将其放入缓存,只有当影响这个计算属性的值发生变化时,getter函数才会再次被调用,去计算新的值,并且再次存入缓存。

  • watch更适用于当某个数据发生变化时要去执行一些复杂操作的场景
  • computed更适用于某个数据依赖多个数据并且要进行计算的场景

6. js数据类型和数据类型检测

数据类型
  • 基础数据类型:数字型:Number,字符型:String,布尔型:Boolean,对空:Null,未定义:Undefind,新增属性:Symbol
  • 引用数据类型/复杂数据类型:Object,Array,Function 还有两个特殊的RegExp和Date

检测数据类型

  • typeof: 只能检测基础数据类型除null以外的值,null会返回Object,检测引用数据类型除了Function以外均会返回Object
  • instanceof:A instanceof B 判断构造函数B的prototype原型对象是否在A实例的原型链上,返回布尔值,因此此方法只适用于检测复杂数据类型,且由于Object的prototype一定处在复杂数据类型的原型链上所以若B为Object始终返回True
  • toString:Object的原型对象上的toString方法,调用该方法的对象返回一个格式为[[object ***]]格式的字符串,其中xxx就是该对象的数据类型,对于Object类型直接调用该方法,当其他类型对象要调用该方法时应使用call/apply来调用才能返回正确的类型信息,该方法为万金油方法
Object.prototype.toString.call('') ;              // [object String]
Object.prototype.toString.call(1) ;               // [object Number]
Object.prototype.toString.call(true) ;            // [object Boolean]
Object.prototype.toString.call(Symbol());         // [object Symbol]
Object.prototype.toString.call(undefined) ;       // [object Undefined]
Object.prototype.toString.call(null) ;            // [object Null]
Object.prototype.toString.call(new Function()) ;  // [object Function]
Object.prototype.toString.call(new Date()) ;      // [object Date]
Object.prototype.toString.call([]) ;              // [object Array]
Object.prototype.toString.call(new RegExp()) ;    // [object RegExp]
Object.prototype.toString.call(new Error()) ;     // [object Error]
Object.prototype.toString.call(document) ;        // [object HTMLDocument]
Object.prototype.toString.call(window) ;          // [object global] window 是全局对象 global 的引用
  • constructor:可以检测出字面量创建出的实例对象是由哪个构造函数创建出来的,不好用
  • isArray:检测是否为数组的简单方法

7. 生命周期,父子组件生命周期

生命周期:vue实例由被创建到销毁的过程
  1. 创建空实例
  2. 初始化数据
  3. 编译模板
  4. 挂载DOM
  5. 渲染和更新数据后重渲染
  6. 销毁卸载

vue有四个生命周期阶段,创建阶段,挂载阶段,更新阶段,销毁阶段 每个阶段都有两个生命周期钩子函数:beforecreate,created,beforemount,mounted,beforeupdate,updated,beforedestroy,destroyed共八个生命周期钩子函数,在created中我们一般进行ajax请求获取数据,mounted中一般进行DOM操作,updated中进行数据的更新,beforedestroy中一般进行时间绑定、定时器等的销毁操作

另外如果组件使用了keep-alive进行了缓存切换操作,created钩子函数将只被执行一次,销毁阶段的钩子函数直接不执行,此时就要使用activated和deactivated两个钩子函数来解决

  • 父子组件vue生命周期钩子函数执行顺序
  1. 父beforecreate

  2. 父created

  3. 父beforemount

  4. 子beforecreate

  5. 子created

  6. 子beforemount

  7. 子mounted

  8. 父mounted

  9. 父beforeupdate

  10. 子beforeupdate

  11. 子updated

  12. 父updated

  13. 父beforedestroy

  14. 子beforedestroy

  15. 子destroyed

  16. 父destroyed

8. new的过程

  1. 在内存中新开辟一块空间存放空对象
  2. 让空对象的__proto__对象原型指向构造函数的prototype原型对象
  3. 修改this指向,指向新生成的空对象
  4. 执行构造函数中的代码,挂载属性和方法
  5. 返回实例对象

9. 原型链

每个实例对象都有__proto__属性指向生成该实例对象的构造函数的prototype实例对象,但是prototype也是个对象,是对象就有生成它的构造函数,因此该构造函数为Object,该构造函数也必有一个prototype实例对象,因此也能有一个__proto__属性指向Object的prototype原型对象,继续网上找则返回null 这样一层一层往上找的链就是原型链,当一个对象要调用某个方法或属性时会先查找自身实例对象上是否存在该属性或方法,没有则查找prototype上,还是没有则顺着原型链一层层往上查找,__proto__属性的意义就在此,给对象查找指明了一个方向或者说是个路线

10. 如何实现三角形

用css实现一个三角形,主要是利用 border 属性

三角形往哪个方向,那个方向无需设置border,而相反方向设置border颜色,相邻两边的border设为透明

11. 高度为0.5像素的线怎么实现

不同浏览器直接设置0.5px高的线展示的效果是不同的,其中Chrome把0.5px四舍五入变成了1px,而firefox/safari能够画出半个像素的边,并且Chrome会把小于0.5px的当成0,而Firefox会把不小于0.55px当成1px,Safari是把不小于0.75px当成1px
  • 最容易想到的方法是使用c3的缩放transform:scaleY(0.5)去实现1px缩小一半的效果,但是Chrome/Safari中的效果会变虚,并不是很完美,而Firefox中比较完美看起来是实的而且还很细,效果和直接设置0.5px一样。所以通过transform: scale会导致Chrome变虚了,而粗细几乎没有变化。但是如果加上transform-origin: 50% 100%效果就改观了,在各个浏览器中全部实现了清晰的0.5px高的细线效果

  • 除此以外还可以使用linear-gradient属性来设置渐变实现视觉上的0.5px,逻辑上可行 使用linear-gradient(0deg, #fff, #000):渐变的角度从下往上,从白色#fff渐变到黑色#000,而且是线性的,但是实际效果和直接使用缩放的效果一样不尽如意,是通过虚化使1px看起来像0.5px

  • 还有另外一种方法,使用boxshadow,设置box-shadow的第二个参数为0.5px,表示阴影垂直方向的偏移为0.5px(此方法不适用于safari浏览器因为该浏览器中不允许设置小于1px的boxshadow)怎么画一条0.5px的边(更新)

12. 重绘与回流

  • 重绘指的是元素的几何属性发生变化或者样式发生变化后进行样式的重新绘制,表现为元素的外观被改变

  • 回流指的是一部分或者整个DOM树需要进行重新分析,或者节点的尺寸发生变化,变现为重新生成布局,重新排列元素

重绘不一定会出现回流,回流必定导致重绘,一个页面必定要经历至少一次重绘和回流即初始化渲染, 我们应尽量避免页面出现频繁的回流和重绘,它们都会导致我们的页面用户体验变差,尤其要避免发生回流

何时会触发回流?
  1. 添加/删除DOM元素
  2. 修改元素大小尺寸,宽高边距等
  3. 修改元素位置
  4. 内容发生改变,如文本和图片变化导致宽高发生变化
  5. 页面初始化渲染
  6. 浏览器窗口尺寸发生改变
浏览器对重绘重排的优化

由于回流和重绘的触发实在太容易了,因此浏览器会优化这些操作,聪明的浏览器会维护一个队列,把所有这些会导致回流和重绘的操作都放入这个队列,在经过一段时间后或者其中数量到一定量时,浏览器会flush这个队列,进行一个批处理。这样多次的重绘回流就变成了一次。 但是一些特殊操作代码会导致浏览器提前flush队列导致优化失效(向浏览器请求样式信息offestTop/scrollTop等等)

我们应该如何优化页面渲染性能
  1. 对样式进行批量操作,最大化利用浏览器优化
  2. 尽量避免在循环遍历中去请求offsetTop等样式信息,这样会导致浏览器提前flush队列使优化失效
  3. 动画效果尽量使用transform去实现而不是改变top等值,因为transform只是视觉上效果的改变,不会影响到其它盒子,只会触发自身重绘
  4. 使用DocumentFragment文档碎片,它作为一个容器会将一些元素保存其中,当元素发生改变时,页面不会更新,只有当其中存储的元素全部更新完毕后,把DocumentFragment插入到页面上时才会触发更新,vue的底层渲染更新就运用了文档碎片
<ul id="box"></ul>

<script>
  let ul = document.getElementById("box")
  for (let i = 0; i < 20; i++) {
    let li = document.createElement("li")
    li.innerHTML = "index: " + i
    // 20次回流重绘
    ul.appendChild(li)
  }

  // let ul = document.getElementById("box")
  // let fragment = document.createDocumentFragment()
  // for (let i = 0; i < 20; i++) {
  //     let li = document.createElement("li")
  //     li.innerHTML = "index: " + i
  //     fragment.appendChild(li)
  // }
    // 1次回流重绘
  // ul.appendChild(fragment)
</script>

13. v-model底层原理

Vue中数据变了,视图层也会发生变化,当使用v-model命令时就可以实现双向绑定,即视图层改变数据也会发生改变,一般绑定在Input输入框上

v-model实现原理:v-model可以认为是使用v-bind和v-on组合使用实现双向绑定的语法糖 给一个input输入框绑定v-bind:value='变量'实现数据层向视图层的数据绑定,这样数据变化,input中的内容也变化,再给input绑定个input事件即v-on:input(@input)在输入时执行 变量=$event.target.value实现视图层变化数据层也发生变化,到此v-model双向绑定就完成了

    <input type="text" :value='msg' @input='msg=$event.target.value'/>

14. let const var区别

let和const是ES6出现的声明方式和var类似,let用于声明变量,const用于声明常量,let和const不存在变量提升,也不允许重复命名,有let和const声明的大括号会形成块级作用域,let和const存在暂时性死区,在使用了let和const的块级作用域中,该块级区域就绑定了命名的变量,不再受外界影响,凡是在声明之前就使用这些变量,就会报错。
var a = 10

if (true) {
  a = 20
  let a = 10
}
// 以上情况称为:暂时性死区TDZ(temporal dead zone)