写点什么

拿到大厂前端 offer 的前端开发是怎么回答面试题的

作者:loveX001
  • 2022-11-09
    浙江
  • 本文字数:8600 字

    阅读完需:约 28 分钟

代码输出问题

function A(){}function B(a){  this.a = a;}function C(a){  if(a){this.a = a;  }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;
console.log(new A().a);console.log(new B().a);console.log(new C(2).a);
复制代码


输出结果:1 undefined 2


解析:


  1. console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;

  2. console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;

  3. console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。

一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。


而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

代码输出结果

async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");}async function async2() {  console.log("async2");}async1();console.log('start')
复制代码


输出结果如下:


async1 startasync2startasync1 end
复制代码


代码的执行过程如下:


  1. 首先执行函数中的同步代码async1 start,之后遇到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1

  2. 跳出async1函数后,执行同步代码start

  3. 在一轮宏任务全部执行完之后,再来执行await后面的内容async1 end


这里可以理解为 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。

冒泡排序--时间复杂度 n^2

题目描述:实现一个冒泡排序


实现代码如下:


function bubbleSort(arr) {  // 缓存数组长度  const len = arr.length;  // 外层循环用于控制从头到尾的比较+交换到底有多少轮  for (let i = 0; i < len; i++) {    // 内层循环用于完成每一轮遍历过程中的重复比较+交换    for (let j = 0; j < len - 1; j++) {      // 若相邻元素前面的数比后面的大      if (arr[j] > arr[j + 1]) {        // 交换两者        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];      }    }  }  // 返回数组  return arr;}// console.log(bubbleSort([3, 6, 2, 4, 1]));
复制代码


参考:前端进阶面试题详细解答

内存泄露

  • 意外的全局变量: 无法被回收

  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放

  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)

  • 闭包

  • 第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

  • 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

  • dom 引用: dom 元素被删除时,内存中的引用未被正确清空

  • 控制台console.log打印的东西


可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。


内存泄露排查方法(opens new window)

计算属性和 watch 有什么区别?以及它们的运用场景?

// 区别  computed 计算属性:依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值。  watch 侦听器:更多的是观察的作用,无缓存性,类似与某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作//运用场景  当需要进行数值计算,并且依赖与其它数据时,应该使用computed,因为可以利用computed的缓存属性,避免每次获取值时都要重新计算。  当需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch选项允许执行异步操作(访问一个API),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
复制代码

浅拷贝

// 这里只考虑对象类型function shallowClone(obj) {    if(!isObject(obj)) return obj;    let newObj = Array.isArray(obj) ? [] : {};    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)    for(let key in obj) {        // obj.hasOwnProperty() 方法只考虑对象自身的属性        if(obj.hasOwnProperty(key)) {            newObj[key] = obj[key];        }    }    return newObj;}
复制代码

选择排序--时间复杂度 n^2

题目描述:实现一个选择排序


实现代码如下:


function selectSort(arr) {  // 缓存数组长度  const len = arr.length;  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引  let minIndex;  // i 是当前排序区间的起点  for (let i = 0; i < len - 1; i++) {    // 初始化 minIndex 为当前区间第一个元素    minIndex = i;    // i、j分别定义当前区间的上下界,i是左边界,j是右边界    for (let j = i; j < len; j++) {      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j      if (arr[j] < arr[minIndex]) {        minIndex = j;      }    }    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者    if (minIndex !== i) {      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];    }  }  return arr;}// console.log(quickSort([3, 6, 2, 4, 1]));
复制代码

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?

1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题


  1. Vue 使用了 Object.defineProperty 实现双向数据绑定

  2. 在初始化实例时对属性执行 getter/setter 转化

  3. 属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)


所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)


2)接下来我们看看框架本身是如何实现的呢?


Vue 源码位置:vue/src/core/instance/index.js


export function set (target: Array<any> | Object, key: any, val: any): any {  // target 为数组    if (Array.isArray(target) && isValidArrayIndex(key)) {    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // key 已经存在,直接修改属性值    if (key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = (target: any).__ob__  // target 本身就不是响应式数据, 直接赋值  if (!ob) {    target[key] = val    return val  }  // 对属性进行响应式处理  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}
复制代码


我们阅读以上源码可知,vm.$set 的实现原理是:


  1. 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  2. 如果目标是对象,会先判读属性是否存在、对象是否是响应式,

  3. 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理


defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法

节流

**节流(throttle)**:触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似 qq 飞车的复位按钮。


核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果 N 秒内触发则直接返回。


应用:节流常应用于鼠标不断点击触发、监听滚动事件。


实现:


// 版本一:标志实现function throttle(fn, wait){    let flag = true;  // 设置一个标志    return function(...args){        if(!flag) return;        flag = false;        setTimeout(() => {            fn.call(this, ...args);            flag = true;        }, wait);    }}
// 版本二:时间戳实现function throttle(fn, wait) { let pre = 0; return function(...args) { let now = new Date(); if(now - pre < wait) return; pre = now; fn.call(this, ...args); }}
复制代码

代码输出结果

const first = () => (new Promise((resolve, reject) => {    console.log(3);    let p = new Promise((resolve, reject) => {        console.log(7);        setTimeout(() => {            console.log(5);            resolve(6);            console.log(p)        }, 0)        resolve(1);    });    resolve(2);    p.then((arg) => {        console.log(arg);    });}));first().then((arg) => {    console.log(arg);});console.log(4);
复制代码


输出结果如下:


374125Promise{<resolved>: 1}
复制代码


代码的执行过程如下:


  1. 首先会进入 Promise,打印出 3,之后进入下面的 Promise,打印出 7;

  2. 遇到了定时器,将其加入宏任务队列;

  3. 执行 Promise  p 中的 resolve,状态变为 resolved,返回值为 1;

  4. 执行 Promise first 中的 resolve,状态变为 resolved,返回值为 2;

  5. 遇到 p.then,将其加入微任务队列,遇到 first().then,将其加入任务队列;

  6. 执行外面的代码,打印出 4;

  7. 这样第一轮宏任务就执行完了,开始执行微任务队列中的任务,先后打印出 1 和 2;

  8. 这样微任务就执行完了,开始执行下一轮宏任务,宏任务队列中有一个定时器,执行它,打印出 5,由于执行已经变为 resolved 状态,所以resolve(6)不会再执行;

  9. 最后console.log(p)打印出Promise{<resolved>: 1}

代码输出结果

console.log('1');
setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') })})process.nextTick(function() { console.log('6');})new Promise(function(resolve) { console.log('7'); resolve();}).then(function() { console.log('8')})
setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') })})
复制代码


输出结果如下:


176824359111012
复制代码


(1)第一轮事件循环流程分析如下:


  • 整体 script 作为第一个宏任务进入主线程,遇到console.log,输出 1。

  • 遇到setTimeout,其回调函数被分发到宏任务 Event Queue 中。暂且记为setTimeout1

  • 遇到process.nextTick(),其回调函数被分发到微任务 Event Queue 中。记为process1

  • 遇到Promisenew Promise直接执行,输出 7。then被分发到微任务 Event Queue 中。记为then1

  • 又遇到了setTimeout,其回调函数被分发到宏任务 Event Queue 中,记为setTimeout2



上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。发现了process1then1两个微任务:


  • 执行process1,输出 6。

  • 执行then1,输出 8。


第一轮事件循环正式结束,这一轮的结果是输出 1,7,6,8。


(2)第二轮时间循环从**setTimeout1**宏任务开始:


  • 首先输出 2。接下来遇到了process.nextTick(),同样将其分发到微任务 Event Queue 中,记为process2

  • new Promise立即执行输出 4,then也分发到微任务 Event Queue 中,记为then2



第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:


  • 输出 3。

  • 输出 5。


第二轮事件循环结束,第二轮输出 2,4,3,5。


(3)第三轮事件循环开始,此时只剩 setTimeout2 了,执行。


  • 直接输出 9。

  • process.nextTick()分发到微任务 Event Queue 中。记为process3

  • 直接执行new Promise,输出 11。

  • then分发到微任务 Event Queue 中,记为then3



第三轮事件循环宏任务执行结束,执行两个微任务process3then3


  • 输出 10。

  • 输出 12。


第三轮事件循环结束,第三轮输出 9,11,10,12。


整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。

代码输出结果

var length = 10;function fn() {    console.log(this.length);}
var obj = { length: 5, method: function(fn) { fn(); arguments[0](); }};
obj.method(fn, 1);
复制代码


输出结果: 10 2


解析:


  1. 第一次执行 fn(),this 指向 window 对象,输出 10。

  2. 第二次执行 arguments[0],相当于 arguments 调用方法,this 指向 arguments,而这里传了两个参数,故输出 arguments 长度为 2。

代码输出结果

const p1 = new Promise((resolve) => {  setTimeout(() => {    resolve('resolve3');    console.log('timer1')  }, 0)  resolve('resovle1');  resolve('resolve2');}).then(res => {  console.log(res)  // resolve1  setTimeout(() => {    console.log(p1)  }, 1000)}).finally(res => {  console.log('finally', res)})
复制代码


执行结果为如下:


resolve1finally  undefinedtimer1Promise{<resolved>: undefined}
复制代码


需要注意的是最后一个定时器打印出的 p1 其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个 Promise 的返回值,而这道题中.finally上一个 Promise 是.then(),但是这个.then()并没有返回值,所以 p1 打印出来的 Promise 的值会是undefined,如果在定时器的下面加上一个return 1,则值就会变成 1。

什么情况会阻塞渲染?

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。


当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

Vue 路由守卫有哪些,怎么设置,使用场景等

常用的两个路由守卫:router.beforeEach 和 router.afterEach
每个守卫方法接收三个参数:
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。
在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。判断是否登录,是否拿到对应的路由权限等等。

复制代码

instanceof

作用:判断对象的具体类型。可以区别 arrayobjectnullobject 等。


语法A instanceof B


如何判断的?: 如果 B 函数的显式原型对象在 A 对象的原型链上,返回true,否则返回false


注意:如果检测原始值,则始终返回 false


实现:


function myinstanceof(left, right) {    // 基本数据类型都返回 false,注意 typeof 函数 返回"function"    if((typeof left !== "object" && typeof left !== "function") || left === null) return false;    let leftPro = left.__proto__;  // 取左边的(隐式)原型 __proto__    // left.__proto__ 等价于 Object.getPrototypeOf(left)    while(true) {        // 判断是否到原型链顶端        if(leftPro === null) return false;        // 判断右边的显式原型 prototype 对象是否在左边的原型链上        if(leftPro === right.prototype) return true;        // 原型链查找        leftPro = leftPro.__proto__;    }}
复制代码

代码输出结果

Promise.resolve('1')  .then(res => {    console.log(res)  })  .finally(() => {    console.log('finally')  })Promise.resolve('2')  .finally(() => {    console.log('finally2')      return '我是finally2返回的值'  })  .then(res => {    console.log('finally2后面的then函数', res)  })
复制代码


输出结果如下:


1finally2finallyfinally2后面的then函数 2
复制代码


.finally()一般用的很少,只要记住以下几点就可以了:


  • .finally()方法不管 Promise 对象最后的状态如何都会执行

  • .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是无法知道 Promise 最终的状态是resolved还是rejected

  • 它最终返回的默认会是一个上一次的 Promise 对象值,不过如果抛出的是一个异常则返回异常的 Promise 对象。

  • finally 本质上是 then 方法的特例


.finally()的错误捕获:


Promise.resolve('1')  .finally(() => {    console.log('finally1')    throw new Error('我是finally中抛出的异常')  })  .then(res => {    console.log('finally后面的then函数', res)  })  .catch(err => {    console.log('捕获错误', err)  })
复制代码


输出结果为:


'finally1''捕获错误' Error: 我是finally中抛出的异常
复制代码

说一下 vue3.0 你了解多少?

 <!-- 响应式原理的改变 Vue3.x 使用Proxy取代 Vue2.x 版本的Object.defineProperty --> <!-- 组件选项声明方式Vue3.x 使用Composition API setup 是Vue3.x新增的一个选项,他    是组件内使用Composition API 的入口 --> <!-- 模板语法变化slot具名插槽语法 自定义指令 v-model 升级 --> <!-- 其它方面的更改Suspense支持Fragment(多个根节点) 和Protal (在dom其他部分渲染组建内容)组件     针对一些特殊的场景做了处理。基于treeshaking优化,提供了更多的内置功能。 -->
复制代码

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。


(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。


(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。


(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。


(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。


(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。


(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。


  • 在无损压缩的情况下,相同质量的 WebP 图片,文件大小要比 PNG 小 26%;

  • 在有损压缩的情况下,具有相同图片精度的 WebP 图片,文件大小要比 JPEG 小 25%~34%;

  • WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
拿到大厂前端offer的前端开发是怎么回答面试题的_JavaScript_loveX001_InfoQ写作社区