写点什么

社招前端二面常见面试题

作者:coder2028
  • 2022 年 9 月 09 日
    浙江
  • 本文字数:9445 字

    阅读完需:约 31 分钟

实现节流函数和防抖函数

函数防抖的实现:


function debounce(fn, wait) {  var timer = null;
return function() { var context = this, args = [...arguments];
// 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; }
// 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); };}
复制代码


函数节流的实现:


// 时间戳版function throttle(fn, delay) {  var preTime = Date.now();
return function() { var context = this, args = [...arguments], nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } };}
// 定时器版function throttle (fun, wait){ let timeout = null return function(){ let context = this let args = [...arguments] if(!timeout){ timeout = setTimeout(() => { fun.apply(context, args) timeout = null }, wait) } }}
复制代码


----问题知识点分割线----

JavaScript 脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。


一般有以下几种方式:


  • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。

  • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。

  • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。

  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载 js 脚本文件

  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。


----问题知识点分割线----

说一下 HTTP 3.0

HTTP/3 基于 UDP 协议实现了类似于 TCP 的多路复用数据流、传输可靠性等功能,这套功能被称为 QUIC 协议。


  1. 流量控制、传输可靠性功能:QUIC 在 UDP 的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些 TCP 中的特性。

  2. 集成 TLS 加密功能:目前 QUIC 使用 TLS1.3,减少了握手所花费的 RTT 数。

  3. 多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了 TCP 的队头阻塞问题。

  4. 快速握手:由于基于 UDP,可以实现使用 0 ~ 1 个 RTT 来建立连接。


----问题知识点分割线----

说一下怎么把类数组转换为数组?

//通过call调用数组的slice方法来实现转换Array.prototype.slice.call(arrayLike)
//通过call调用数组的splice方法来实现转换Array.prototype.splice.call(arrayLike,0)
//通过apply调用数组的concat方法来实现转换Array.prototype.concat.apply([],arrayLike)
//通过Array.from方法来实现转换Array.from(arrayLike)
复制代码


----问题知识点分割线----

为什么需要浏览器缓存?

对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。


所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。


使用浏览器缓存,有以下优点:


  • 减少了服务器的负担,提高了网站的性能

  • 加快了客户端网页的加载速度

  • 减少了多余网络数据传输


----问题知识点分割线----

0.1 + 0.2 === 0.3 嘛?为什么?

JavaScript 使用 Number 类型来表示数字(整数或浮点数),遵循 IEEE 754 标准,通过 64 位来表示一个数字(1 + 11 + 52)


  • 1 符号位,0 表示正数,1 表示负数 s

  • 11 指数位(e)

  • 52 尾数,小数部分(即有效数字)


最大安全数字:Number.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1,转换成整数就是 16 位,所以 0.1 === 0.1,是因为通过 toPrecision(16) 去有效位之后,两者是相等的。


在两数相加时,会先转换成二进制,0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,然后进行对阶运算,JS 引擎对二进制进行截断,所以造成精度丢失。


所以总结:精度丢失可能出现在进制转换和对阶运算中


----问题知识点分割线----

扩展运算符的作用及使用场景

(1)对象扩展运算符


对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。


let bar = { a: 1, b: 2 };let baz = { ...bar }; // { a: 1, b: 2 }
复制代码


上述方法实际上等价于:


let bar = { a: 1, b: 2 };let baz = Object.assign({}, bar); // { a: 1, b: 2 }
复制代码


Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。


同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。


let bar = {a: 1, b: 2};let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}
复制代码


利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。


需要注意:扩展运算符对对象实例的拷贝属于浅拷贝


(2)数组扩展运算符


数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。


console.log(...[1, 2, 3])// 1 2 3console.log(...[1, [2, 3, 4], 5])// 1 [2, 3, 4] 5
复制代码


下面是数组的扩展运算符的应用:


  • 将数组转换为参数序列


function add(x, y) {  return x + y;}const numbers = [1, 2];add(...numbers) // 3
复制代码


  • 复制数组


const arr1 = [1, 2];const arr2 = [...arr1];
复制代码


要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。


  • 合并数组


如果想在数组内合并数组,可以这样:


const arr1 = ['two', 'three'];const arr2 = ['one', ...arr1, 'four', 'five'];// ["one", "two", "three", "four", "five"]
复制代码


  • 扩展运算符与解构赋值结合起来,用于生成数组


const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest  // [2, 3, 4, 5]
复制代码


需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。


const [...rest, last] = [1, 2, 3, 4, 5];         // 报错const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 报错
复制代码


  • 将字符串转为真正的数组


[...'hello']    // [ "h", "e", "l", "l", "o" ]
复制代码


  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组


比较常见的应用是可以将某些数据结构转为数组:


// arguments对象function foo() {  const args = [...arguments];}
复制代码


用于替换es5中的Array.prototype.slice.call(arguments)写法。


  • 使用Math函数获取数组中特定的值


const numbers = [9, 4, 7, 1];Math.min(...numbers); // 1Math.max(...numbers); // 9
复制代码


----问题知识点分割线----

说一说你用过的 css 布局

gird布局,layout布局,flex布局,双飞翼,圣杯布局等
复制代码


----问题知识点分割线----

事件循环机制 (Event Loop)

事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。


先执行 Script 脚本,然后清空微任务队列,然后开始下一轮事件循环,继续先执行宏任务,再清空微任务队列,如此往复。


  • 宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering

  • 微任务:process.nextTick()/Promise


上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。


优先级


  • setTimeout = setInterval 一个队列

  • setTimeout > setImmediate

  • process.nextTick > Promise


for (const macroTask of macroTaskQueue) {    handleMacroTask();      for (const microTask of microTaskQueue) {          handleMicroTask(microTask);    }}
复制代码


----问题知识点分割线----

实现一个三角形

CSS 绘制三角形主要用到的是 border 属性,也就是边框。


平时在给盒子设置边框时,往往都设置很窄,就可能误以为边框是由矩形组成的。实际上,border 属性是右三角形组成的,下面看一个例子:


div {    width: 0;    height: 0;    border: 100px solid;    border-color: orange blue red green;}
复制代码


将元素的长宽都设置为 0


(1)三角 1


div {    width: 0;    height: 0;    border-top: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}
复制代码


(2)三角 2


div {    width: 0;    height: 0;    border-bottom: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}
复制代码


(3)三角 3


div {    width: 0;    height: 0;    border-left: 50px solid red;    border-top: 50px solid transparent;    border-bottom: 50px solid transparent;}
复制代码


(4)三角 4


div {    width: 0;    height: 0;    border-right: 50px solid red;    border-top: 50px solid transparent;    border-bottom: 50px solid transparent;}
复制代码


(5)三角 5


div {    width: 0;    height: 0;    border-top: 100px solid red;    border-right: 100px solid transparent;}
复制代码


还有很多,就不一一实现了,总体的原则就是通过上下左右边框来控制三角形的方向,用边框的宽度比来控制三角形的角度。


----问题知识点分割线----

哪些操作会造成内存泄漏?

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

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

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

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


----问题知识点分割线----

说一下常见的 HTTP 状态码?说一下状态码是 302 和 304 是什么意思?你在项目中出现过么?你是怎么解决的?

    <!-- 状态码:由3位数字组成,第一个数字定义了响应的类别 -->    <!-- 1xx:指示消息,表示请求已接收,继续处理 -->    <!-- 2xx:成功,表示请求已被成功接收,处理 -->    <!-- 200 OK:客户端请求成功         204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。         206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容 -->    <!-- 3xx 重定向 -->    <!-- 301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。         302 Found:临时重定向,表示请求的资源临时搬到了其他位置         303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问         307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET         304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有 -->    <!-- 4xx:客户端错误 -->    <!-- 400 Bad Request:客户端请求有语法错误,服务器无法理解。         401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。         403 Forbidden:服务器收到请求,但是拒绝提供服务         404 Not Found:请求资源不存在。比如,输入了错误的url         415 Unsupported media type:不支持的媒体类型 -->    <!-- 5xx:服务器端错误,服务器未能实现合法的请求。 -->    <!-- 500 Internal Server Error:服务器发生不可预期的错误。         503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常, -->
复制代码


----问题知识点分割线----

实现一个对象的 flatten 方法

题目描述:


const obj = { a: {        b: 1,        c: 2,        d: {e: 5}    }, b: [1, 3, {a: 2, b: 3}], c: 3}
flatten(obj) 结果返回如下// {// 'a.b': 1,// 'a.c': 2,// 'a.d.e': 5,// 'b[0]': 1,// 'b[1]': 3,// 'b[2].a': 2,// 'b[2].b': 3// c: 3// }
复制代码


实现代码如下:


function isObject(val) {  return typeof val === "object" && val !== null;}
function flatten(obj) { if (!isObject(obj)) { return; } let res = {}; const dfs = (cur, prefix) => { if (isObject(cur)) { if (Array.isArray(cur)) { cur.forEach((item, index) => { dfs(item, `${prefix}[${index}]`); }); } else { for (let k in cur) { dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`); } } } else { res[prefix] = cur; } }; dfs(obj, "");
return res;}flatten();
复制代码


----问题知识点分割线----

setTimeout(fn, 0)多久才执行,Event Loop

setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定


----问题知识点分割线----

Promise 是什么?

Promise 是异步编程的一种解决方案:从语法上讲,promise 是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。promise 有三种状态: pending(等待态),fulfiled(成功态),rejected(失败态) ;状态一旦改变,就不会再变。创造 promise 实例后,它会立即执行。


const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";
function MyPromise(fn) { // 保存初始化状态 var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值 this.value = null;
// 用于保存 resolve 的回调函数 this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数 this.rejectedCallbacks = [];
// 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); }
// 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); }}
MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; };
onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; };
// 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); }
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) { onResolved(this.value); }
if (this.state === REJECTED) { onRejected(this.value); }};
复制代码


----问题知识点分割线----

实现一个 add 方法

题目描述:实现一个 add 方法 使计算结果能够满足如下预期:add(1)(2)(3)()=6add(1,2,3)(4)()=10


其实就是考函数柯里化


实现代码如下:


function add(...args) {  let allArgs = [...args];  function fn(...newArgs) {    allArgs = [...allArgs, ...newArgs];    return fn;  }  fn.toString = function () {    if (!allArgs.length) {      return;    }    return allArgs.reduce((sum, cur) => sum + cur);  };  return fn;}
复制代码


----问题知识点分割线----

HTTP 1.1 和 HTTP 2.0 的区别

  • 二进制协议:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。

  • 多路复用: HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。

  • 数据流: HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。

  • 头信息压缩: HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。

  • 服务器推送: HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。


【1】队头堵塞:


队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。


----问题知识点分割线----

代码输出结果

var friendName = 'World';(function() {  if (typeof friendName === 'undefined') {    var friendName = 'Jack';    console.log('Goodbye ' + friendName);  } else {    console.log('Hello ' + friendName);  }})();
复制代码


输出结果:Goodbye Jack


我们知道,在 JavaScript 中, Function 和 var 都会被提升(变量提升),所以上面的代码就相当于:


var name = 'World!';(function () {    var name;    if (typeof name === 'undefined') {        name = 'Jack';        console.log('Goodbye ' + name);    } else {        console.log('Hello ' + name);    }})();
复制代码


这样,答案就一目了然了。


----问题知识点分割线----

水平垂直居中的实现

  • 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。


.parent {    position: relative;} .child {    position: absolute;    left: 50%;    top: 50%;    transform: translate(-50%,-50%);}
复制代码


  • 利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:


.parent {    position: relative;}
.child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;}
复制代码


  • 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况


.parent {    position: relative;}
.child { position: absolute; top: 50%; left: 50%; margin-top: -50px; /* 自身 height 的一半 */ margin-left: -50px; /* 自身 width 的一半 */}
复制代码


  • 使用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:


.parent {    display: flex;    justify-content:center;    align-items:center;}
复制代码


----问题知识点分割线----

JS 数据类型

基本类型:Number、Boolean、String、null、undefined、symbol(ES6 新增的),BigInt(ES2020)引用类型:Object,对象子类型(Array,Function)

用户头像

coder2028

关注

还未添加个人签名 2022.09.08 加入

还未添加个人简介

评论

发布
暂无评论
社招前端二面常见面试题_JavaScript_coder2028_InfoQ写作社区