浅谈一下前端那些高频面试题(二)

1,022 阅读18分钟

3.jpeg

前言

大家好我是小胡不糊涂,这已经是第二次发布文章了,续上上次的面试题在追加一些,这些题都是我自己整理和查阅资料总结出来的,目的就是想巩固知识和锻炼自己的写作总结能力,希望能一直坚持下去,如有说的不太对的地方还望多多包涵指点。想看上一篇文章在这# 浅总结一下前端常见面试题(一),前半部分比较简单基础,想看稍难一点的可以直接跳到最后,话不多说,咱们直接撸题😆😆!

CSS3、HTML5部分

光知道js和vue的知识还有有点不够的,基础也要打牢,浅复习一下css和html的部分吧,内容不多,但很常见,相信你们很快就可以看完🤣🤣

1.HTML 文件中的 DOCTYPE 是什么作用?

  • HTML超文本标记语言: 是一个标记语言, 就有对应的语法标准 DOCTYPE 即 Document Type,网页文件的文档类型标准。
  • 主要作用是告诉浏览器的解析器要使用哪种 HTML规范 或 XHTML规范来解析页面。 DOCTYPE 需要放置在 HTML 文件的标签之前

2.XML和JSON

  • 都是后台和前端进行交互时传递数据的一种格式 ,XML对数据的描述比较详细,体积比较大,而JSON的数据体积更小 ,传递速度比较快 相对而言更加轻量,而且与JavaScript交互比较方便,更容易解析处理

3.meta 标签有哪些常用用法?

meta在W3C的定义中是指元数据 也就是关于数据的一些信息 它不会显示在页面上 但是浏览器是可以读取到的

  • 设置网页关键词 (SEO)
  • 设置网页视口(viewport)控制视⼝的⼤⼩、缩放和⽐例等 (移动端开发)
  • 设置 http 响应头:Content-Type 网页内容类型 (字符集)

4.HTML5的新增内容

  1. 只有一种文件类型声明:<!DOCTYPE html>
  2. 增加了一些新的标签元素(aside、nav、video、section、footer,、header等)
  3. input 支持了几个新的类型值 :date, email, url 等等
  4. 新增了一些标签属性 :charset(⽤于 meta 标签);async,defer(⽤于 script 标签)
  5. 新增的全域属性 :contenteditable, draggable... hidden...
  6. 新增API:本地存储, 地理定位, Canvas绘图, 拖拽API, 即时通信 WebSocket...

5.link和@import到底有什么区别?

  1. link属于html标签,而@import是css提供的。
  2. link引入css时,在页面载入时同步进行加载,@import需要等页面网页完全载入以后才加载
  3. link是XHTML标签,没有兼容性问题,@import在CSS2.1提出,低版本游览器不支持
  4. link支持使用JavaScript控制DOM改变样式,而@import不支持

6.img标签 alt和title的区别

  • alt 和 title 是两个常用的属性,alt定义元素的替换文本,title定义元素的提示文本

7.BFC的理解

BFC 的全称是 Block Formatting Context,块级格式化上下文。这是一个用于在盒模型下布局块级盒子的独立渲染区域, 将处于BFC区域内和区域外的元素进行互相隔离,互不影响。

  • 何时会形成 BFC: 满足下列条件之一就可触发BFC:

    • HTML根元素
    • position 值为 absolute 或 fixed
    • float 值不为 none
    • overflow 值不为 visible
    • display 值为 inline-block 、 table-cell 或 table-caption
  • BFC 的应用场景

    • 防止两个相邻块级元素的上下 margin 发生重叠 (上下margin合并问题)
    • 清除浮动
    • 实现自适应布局, 防止元素被浮动元素覆盖(左边固定, 右边自适应)

8.怎么实现右边固定,左边自适应

  1. flex:让两个盒子平级处于同一个父盒子内,给父盒子设置display:flex,将右边的盒子宽度写死,左边的盒子设置flex:1
  2. 定位+overflow:让两个盒子平级处于同一个父盒子内,给父盒子设置position:relative,右边的子盒子设置宽高,设置position为absolute或fixed,top和right都为0,将左边的盒子设置overflow:hidden即可
  3. float:将右边的盒子设置好宽高并设置float:right,使两个盒子在一行显示,然后给左边盒子设为margin-right,设置的值等于右边盒子的宽度即可

9.如何实现三角形

将盒子的宽高设置为0,再设置盒子的border属性,将四个方向的盒子都设置的比较宽,然后清除盒子的border-top,并将border的left和right都设置为透明(transparent)即可得到一个三角形

div {
        width: 0;
        height: 0;
        border: 50px solid #333;
        border-top: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
     }

10.高度为0.5像素的线条

  1. 绘制1px的线条然后使用transform:scale(0.5) 或zoom(0.5)进行缩放
  2. 绘制一个渐变色的背景,一半有颜色一半无颜色

11.flex:1的理解

  • flex是一个复合属性是 flex-grow,flex-shrink,flex-basis 简写
  • flex:1 -> flex-grow:1;flex-shrink:1;flex-basis:0%;
  • flex-grow:1; 当父盒子宽度比所有子盒子宽度宽的时候,剩余空间放大比例, flex-shrink 剩余空间缩小比例
  • flex-basis不加的话,width就是减掉自身设置的数值,加上flex-basis:0%;原来width相当于没有写(flex 和 width)

12.在 script 标签上使用 defer 和 async 的区别是什么?

明确: defer 和 async 的使用, 可以用于提升网页性能

  1. <script src="example.js"></script>

    没有defer或async属性,浏览器会立即加载并执行相应的脚本。 不等待后续加载的文档元素,读到就开始加载和执行,此举会阻塞后续文档的加载

  2. <script async src="example.js"></script>

    有了async属性,表示后续文档的加载和渲染与js脚本的加载和执行是并行进行的,即异步执行;

  3. <script defer src="example.js"></script>

    有了defer属性,加载后续文档的过程和js脚本的加载是并行进行的(异步),此时的js脚本仅加载不执行, js脚本的 执行需要等到文档所有元素解析完成之后,DOMContentLoaded事件触发执行之前。

总结:

  1. defer和async在网络加载过程是一致的,都是异步执行的;(放在页面顶部, 也不会阻塞页面的加载, 与页面加 载同时进行)
  2. 两者的区别, 脚本加载完成之后, async是立刻执行, defer会等一等 (等前面的defer脚本执行, 等dom的加载) 所以, js脚本加上 async 或 defer, 放在头部可以减少网页的下载加载时间, 如果不考虑兼容性, 可以用于优化页面加载的性能

13.通过 CSS 的哪些方式可以实现隐藏页面上的元素?

  • opacity: 0;通过将元素的透明度设置为0,实现看起来隐藏的效果;但是依然会占用空间并可以进 行交互
  • visibility: hidden ;与透明度为0的方案非常类似,会占据空间,但不可以进行交互
  • overflow: hidden;只会隐藏元素溢出的部分;占据空间且不可交互
  • display: none ;可以彻底隐藏元素并从文档流中消失,不占据空间也不能交互,且不影响布局
  • z-index: -9999;通过将元素的层级置于最底层,让其他元素覆盖住它,达到看起来隐藏的效果
  • transform: scale(0,0);通过将元素进行缩放,缩小为0;依然会占据空间,但不可交互
  • left: -9999px ;通过将元素定位到屏幕外面,达到看起来看不到的效果

js部分

这部分直接上难点,也是非常常问的题,这四题几乎可以串联一起讲,如果都彻底理解了,面试也就大部分能灵活说明白了。😼😼

1.事件循环——EventLoop

JavaScript 是一门单线程执行的编程语言。也就是说,同一时间只能做一件事情。

事件循环(EventLoop)是JavaScript的运行机制,主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

单线程执行任务队列的问题: 如果前一个任务非常耗时,则后续的任务就不得不一直等待,从而导致程序假死的问题。

  • 为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:

同步任务(synchronous)

1.又叫做非耗时任务,指的是在主线程上排队执行的那些任务

2.只有前一个任务执行完毕,才能执行后一个任务

异步任务(asynchronous)

1.又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行

2.当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数

  • 同步任务和异步任务的执行过程

① 同步任务由 JavaScript 主线程次序执行

② 异步任务委托给宿主环境执行

③ 已完成的异步任务对应的回调函数,会被 加入到任务队列中等待执行

④ JavaScript 主线程的执行栈被清空后,会 读取任务队列中的回调函数,次序执行

⑤ JavaScript 主线程不断重复上面的第 4 步

EventLoop 的基本概念

image-20220726203139582.png JavaScript 主线程从任务队列中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop(事件循环)。

2.什么是宏任务和微任务

JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:

① 宏任务(macrotask)

1.异步 Ajax 请求、

2.setTimeout、setInterval、

3.文件操作

4.其它宏任务

② 微任务(microtask)

1.Promise.then、.catch 和 .finally

2.process.nextTick

3.其它微任务

宏任务和微任务的执行顺序

  • 每一个宏任务执行完之后,都会检查是否存在待执行的微任务, 如果有,则执行完所有微任务之后,再继续执行下一个宏任务。

image-20220726204750606.png

做两个小案例巩固一下

用Promise写一个红绿灯

function red() {
        console.log('red')
      }
      function green() {
        console.log('green')
      }
      function yellow() {
        console.log('yellow')
      }
      const light = function (timer, cb) {
        return new Promise((resolve) => {
          setTimeout(() => {
            cb()
            resolve()
          }, timer)
        })
      }
      const step = function () { // 定义红绿灯
        Promise.resolve()
          .then(() => {
            return light(3000, red)
          })
          .then(() => {
            return light(2000, green)
          })
          .then(() => {
            return light(1000, yellow)
          })
          .then(() => {
            return step()
          })
      }
      step()

看代码说出执行顺序

async function testSometing() {
        console.log('执行testSometing')
        return 'testSometing'
      }
      async function testAsync() {
        console.log('执行testAsync')
        return Promise.resolve('hello async')
      }
      async function test() {
        console.log('test start...')
        const v1 = await testSometing()
        console.log(v1)
        const v2 = await testAsync()
        console.log(v2)
        console.log(v1, v2)
      }
      test()
      var promise = new Promise((resolve) => {
        console.log('promise start...')
        resolve('promise')
      })
      promise.then((val) => console.log(val))
      console.log('test end...')

输出结果

image-20220726205433372.png

3.你是怎么理解ES6中 Promise的?使用场景

Promise,译为承诺,是异步编程的一种解决方案(而不会陷入回调地狱),比传统的解决方案(回调函数)更加合理和更加强大

在以往我们如果处理多层异步操作。

异步函数 在底层使用了 promise,因此了解 promise 的工作方式是了解 asyncawait 的基础。

一个 Promise 必然处于以下几种状态之一:

  • 待定 (pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已成功 (fulfilled/resolved): 意味着操作成功完成,承诺实现了。
  • 已拒绝 (rejected): 意味着操作失败。

promise如何执行呢?

当 promise 被调用后,它会以处理中状态 (pending) 开始。 这意味着调用的函数会继续执行,而 promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。

被创建的 promise 最终会以被解决状态 (resolved)被拒绝状态 (rejected) 结束,并在完成时调用相应的回调函数(传给 thencatch)。

有两个注意点:

  • Promise只以第一次为准,第一次成功就永久resolved,第一次失败就永远状态为rejected
  • Promise中有throw的话,就相当于执行了reject

promise.all()

  • Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入,并且只返回一个Promise实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组。
  • Promise.all 等待所有都完成(或第一个失败)。
  • Promise.all 在任意一个传入的 promise 失败时返回失败。例如,如果你传入的 promise中,有四个 promise 在一定的时间之后调用成功函数,有一个立即调用失败函数,那么 Promise.all 将立即变为失败。

promise.race()

  • Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
  • race 函数返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。(类似于赛跑)

4.async/await是什么?

ES7 标准中新增的 async 函数,从目前的内部实现来说其实就是 Generator 函数的语法糖,使得异步操作变得更加方便

使用 关键字 async 来表示,在函数内部使用 await 来表示异步。

它基于 Promise,并与所有现存的基于 Promise 的 API 兼容。

async 关键字

  1. async 关键字用于声明⼀个异步函数,该函数的执行不会阻塞后面代码的执行
  2. async声明的函数的返回本质上是一个Promise,async函数内部会返回一个Promise对象,then方法回调函数的参数
  3. async 函数内部可以使⽤ await

await 关键字

  1. await 用于等待异步的功能执⾏完毕 var result = await someAsyncCall()
  2. await 放置在 Promise 调⽤之前,会强制 async 函数中其他代码等待,直到 Promise 完成并返回结果
  3. await 只能与 Promise ⼀起使⽤
  4. await 只能在 async 函数内部使⽤
  5. await后面跟的是Promise 执行后返回的成功或者失败的值,取到值后才会往下执行

总结:

  1. 同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的)
  2. 和同步代码更一致的错误处理方式( async/await 可以⽤成熟的 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
  3. 调试时的阅读性, 也相对更友好

浏览器JSONP原理

基本原理: 主要就是利用了 script 标签的src没有跨域限制来完成的。

执行过程

  1. 前端定义一个解析函数(如: jsonpCallback = function (res) {})
  2. 通过params的形式包装script标签的请求参数,并且声明执行函数(如cb=jsonpCallback)
  3. 后端获取到前端声明的执行函数(jsonpCallback),并以带上参数且调用执行函数的方式传递给前端
  4. 前端在script标签返回资源的时候就会去执行jsonpCallback并通过回调函数的方式拿到数据了

缺点:

  • 只能进行GET请求

优点:

  • 兼容性好,在一些古老的浏览器中都可以运行

Vue部分

这部分的知识点以后大半都会更新发布较难的点了,都牵涉到原理方向的问题,我会尽力说的明白易懂一些,如有说的不太对的地方还请多多包涵指点😺😺

1.Vue中的$nextTick有什么作用?

  • NextTick是什么?

官方对其的定义:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

什么意思呢?

我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

  • 为什么要有nexttick?

举个例子

{{num}}
for(let i=0; i<100000; i++){
    num = i
}

如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质也是一种优化策略

  • 使用场景

    如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()

    第一个参数为:回调函数(可以获取最近的DOM结构)

    第二个参数为:执行函数上下文

    • 组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上
    • $nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情

2.vue 中 route 和 router 的区别

  • route 是当前路由信息,可以获取到当前路由地址参数等等
  • router 是全局路由(VueRouter)实例对象,可以通过 router 进行路由的跳转后退等等

EventBus如何实现

  • 使用场景:兄弟组件传值
  • 创建一个中央事件总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 另一个兄弟组件通过$on监听自定义事件

Bus.js

// 创建一个中央时间总线类  
class Bus {  
  constructor() {  
    this.callbacks = {};   // 存放事件的名字  
  }  
  $on(name, fn) {  
    this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {  
    if (this.callbacks[name]) {  
      this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  
  
// main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  

Children1.vue

this.$bus.$emit('foo')  

Children2.vue

this.$bus.$on('foo', this.handle)  

3.Vue2数据响应式式的原理

  • 用语义化的说话来说这块的知识可能会更懂一点,按照以下这种思路和顺序能让你在面试中更加得心应手

1.所谓的数据响应式指的是数据变了,视图跟着变

2.总结成一句话:就是利用Object.defineProperty数据劫持观察者模式来实现的

3.我们vue内部把data中对象进行了转换,把所有的属性递归实现了劫持Object.defineProperty,每个数据创建一个Dep被观察者

4.对模板进行编译compiler提取里面所有的需要数据的地方变成watcher观察者,把watcher加入到对应的Dep的观察者列表中

5.当某个data中属性变化的时候,由于被劫持了,所以vue内部是知道的,调用对应的dep去通知观察者列表中所有的观察者

6.观察者就去更新视图

可能会追问:

答出关键得分点是重点

  • 为什么data中数据变了,vue怎么知道? -> 数据劫持,Object.defineProperty
  • 数据变了一个,为什么组件中一堆用到这个数据的地方全变了? -> 观察者模式,这是一对多的关系
  • 为什么数据必须在data中初始化,后添加的为什么不是响应式 -> 因为vue在初始化的时候会把data中属性全部变成Object.defineProperty写法,后添加没有转换,所以就不是响应式(错过了初始化转换的时机)
  • 为什么数组的索引不是响应式的 ?为什么要用this.set(或者用splice)来处理?>(vue官方基于性能的考虑,所以没有对数组进行转换,但是为我们提供了set (或者用splice)来处理? -> (vue官方`基于性能`的考虑,所以没有对数组进行转换,但是为我们提供了set实现响应变化)

虚拟DOM和diff算法

4.虚拟DOM

虚拟dom是对真实的dom映射,它就是一个普通的js对象。列如:真实的dom写法:<div></div> ,虚拟dom写法: <div id="abc">hello</div> {tagName:'div',attrs:{id:'abc'},children:['hello']}

为什么要有虚拟dom?

  • 真实的 DOM 对象身上有一堆的属性和方法,操作起来性能慢,而且直接操作真实的 DOM 每操作一次就会导致一次重绘和回流,所以不太好,频繁使用还会影响页面性能,反之频繁操作虚拟 dom,不存在性能问题,等数据全部更新完之后,只会更新真实 DOM 树需要改变的地方,而且渲染新的真实的 dom,只会有一次重绘和回流

  • 组件初始化的时候会在的时候创建一个虚拟 dom 树,当数据有更新的时候,会生成一棵新的虚拟 dom 树,接下来会对新旧 dom 树按照是广度优先、同层进行比较(时间复杂度是 O(n) —>线性复杂度)(数据结构知识,不太明白的同学可以直接略过)

diff算法

image-20220728202331222.png

当数据变化时,虚拟dom也要进行dom对比更新,使用diif算法使得对比更加高效,diff算法有三种情况

  • 情况一:元素(标签)变了,直接这块销毁重建

  • 情况二:元素没有变,属性或内容变了,修改属性或内容(就地复用

  • 情况三:当使用v-for遍历的时候,存在二种情况:(key的重要性

    • 没有key的时候或者key是索引index的时候,它会采用就地复用(就地更新)原则,在顺序上位置是同一个索引就会被认为是同一个元素,正常情况下效率确实会很高,但是当你采用unshift,sort,reverse这些方法破坏顺序的时候,就会出现不必要的元素也要去更新内容或属性
    • 有key的时候,key就是一个唯一的标识,就像人的身份证号一样的,有了这个之后,哪怕是顺序被打乱了,但是vue知道只要key一样就是同一个元素,就不会出现去更新旧的元素的情况

重点

  • 其实虚拟dom最大的作用并不是搭配diff算法高效更新的(数据变了的时候视图更新效率更高),这只是其中一个而已,最重要的是可以跨平台

    • 意思说的是一套代码,可以生成不同平台的最终代码

      1. 如果是html浏览器 渲染成真实的DOM
      2. 如果是android,渲染成android对应的UI代码
      3. 如果是ios,渲染成对应IOS代码
      4. 等等等......

写在最后

这篇总结题目就暂时这么多吧,最近有点忙可能还要过一阵子再次更新了,下一篇的题目就不在说小题目了,咱们直接看精髓题目讲解😆😆,想看上一篇的文章在这:# 浅总结一下前端常见面试题(一),下一篇终极文章也更了# 浅总结一下前端那些高频面试题(终极篇),发布文章仅仅是为了复习巩固和分享我的知识,如有不太对的地方还请多多包涵指点一番,关注小胡不迷路,咱们下篇文章见!😼😼