写点什么

百度前端二面常见面试题合集

作者:bb_xiaxia1998
  • 2022 年 9 月 12 日
    浙江
  • 本文字数:14702 字

    阅读完需:约 48 分钟

代码输出结果

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 中。

什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:


const a = "abc";a.length; // 3a.toUpperCase(); // "ABC"
复制代码


在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。


JavaScript 也可以使用Object函数显式地将基本类型转换为包装类型:


var a = 'abc'Object(a) // String {"abc"}
复制代码


也可以使用valueOf方法将包装类型倒转成基本类型:


var a = 'abc'var b = Object(a)var c = b.valueOf() // 'abc'
复制代码


看看如下代码会打印出什么:


var a = new Boolean( false );if (!a) {    console.log( "Oops" ); // never runs}
复制代码


答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

DNS 同时使用 TCP 和 UDP 协议?

DNS 占用 53 号端口,同时使用 TCP 和 UDP 协议。 (1)在区域传输的时候使用 TCP 协议


  • 辅域名服务器会定时(一般 3 小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用 TCP 而不是 UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。

  • TCP 是一种可靠连接,保证了数据的准确性。


(2)在域名解析的时候使用 UDP 协议


  • 客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。不用经过三次握手,这样 DNS 服务器负载更低,响应更快。理论上说,客户端也可以指定向 DNS 服务器查询时用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅支持 UDP 查询包。

escape、encodeURI、encodeURIComponent 的区别

  • encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。

  • encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。

  • escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

代码输出问题

window.number = 2;var obj = { number: 3, db1: (function(){   console.log(this);   this.number *= 4;   return function(){     console.log(this);     this.number *= 5;   } })()}var db1 = obj.db1;db1();obj.db1();console.log(obj.number);     // 15console.log(window.number);  // 40
复制代码


这道题目看清起来有点乱,但是实际上是考察 this 指向的:


  1. 执行 db1()时,this 指向全局作用域,所以 window.number * 4 = 8,然后执行匿名函数, 所以 window.number * 5 = 40;

  2. 执行 obj.db1();时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15。

对 HTML 语义化的理解

语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。通俗来讲就是用正确的标签做正确的事情。


语义化的优点如下:


  • 对机器友好,带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于 SEO。除此之外,语义类还支持读屏软件,根据文章可以自动生成目录;

  • 对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发者能清晰的看出网页的结构,便于团队的开发与维护。


常见的语义化标签:


<header></header>  头部
<nav></nav> 导航栏
<section></section> 区块(有语义化的div)
<main></main> 主要区域
<article></article> 主要内容
<aside></aside> 侧边栏
<footer></footer> 底部
复制代码

什么是 margin 重叠问题?如何解决?

问题描述: 两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。需要注意的是,浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在垂直方向


计算原则: 折叠合并后外边距的计算原则如下:


  • 如果两者都是正数,那么就去最大者

  • 如果是一正一负,就会正值减去负值的绝对值

  • 两个都是负值时,用 0 减去两个中绝对值大的那个


解决办法: 对于折叠的情况,主要有两种:兄弟之间重叠父子之间重叠 (1)兄弟之间重叠


  • 底部元素变为行内盒子:display: inline-block

  • 底部元素设置浮动:float

  • 底部元素的 position 的值为absolute/fixed


(2)父子之间重叠


  • 父元素加入:overflow: hidden

  • 父元素添加透明边框:border:1px solid transparent

  • 子元素变为行内盒子:display: inline-block

  • 子元素加入浮动属性或定位

为什么 0.1+0.2 ! == 0.3,如何让其相等

在开发过程中遇到类似这样的问题:


let n1 = 0.1, n2 = 0.2console.log(n1 + n2)  // 0.30000000000000004
复制代码


这里得到的不是想要的结果,要想等于 0.3,就要把它进行转化:


(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
复制代码


toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?


计算机是通过二进制的方式存储数据的,所以计算机计算 0.1+0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制是0.0001100110011001100...(1100 循环),0.2 的二进制是:0.00110011001100...(1100 循环),这两个数的二进制都是无限循环的数。那 JavaScript 是如何处理无限循环的二进制小数呢?


一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留 52 位,再加上前面的 1,其实就是保留 53 位有效数字,剩余的需要舍去,遵从“0 舍 1 入”的原则。


根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004


下面看一下双精度数是如何保存的:


  • 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0 表示正数,占用 1 位

  • 第二部分(绿色):用来存储指数(exponent),占用 11 位

  • 第三部分(红色):用来存储小数(fraction),占用 52 位


对于 0.1,它的二进制为:


0.00011001100110011001100110011001100110011001100110011001 10011...
复制代码


转为科学计数法(科学计数法的结果就是浮点数):


1.1001100110011001100110011001100110011001100110011001*2^-4
复制代码


可以看出 0.1 的符号位为 0,指数位为-4,小数位为:


1001100110011001100110011001100110011001100110011001
复制代码


那么问题又来了,指数位是负数,该如何保存呢?


IEEE 标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于 JavaScript 的数字是双精度数,这里就以双精度数为例,它的指数部分为 11 位,能表示的范围就是 0~2047,IEEE 固定双精度数的偏移量为 1023


  • 当指数位不全是 0 也不全是 1 时(规格化的数值),IEEE 规定,阶码计算公式为 e-Bias。 此时 e 最小值是 1,则 1-1023= -1022,e 最大值是 2046,则 2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013

  • 当指数位全部是 0 的时候(非规格化的数值),IEEE 规定,阶码的计算公式为 1-Bias,即 1-1023= -1022。

  • 当指数位全部是 1 的时候(特殊值),IEEE 规定这个浮点数可用来表示 3 个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为 0 的时候表示 NaN;小数位为 0 时,当符号位 s=0 时表示正无穷,s=1 时候表示负无穷。


对于上面的 0.1 的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.


所以,0.1 表示为:


0 1111111011 1001100110011001100110011001100110011001100110011001
复制代码


说了这么多,是时候该最开始的问题了,如何实现 0.1+0.2=0.3 呢?


对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对 JavaScript 来说,这个值通常为 2-52,在 ES6 中,提供了Number.EPSILON属性,而它的值就是 2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为 0.1+0.2 ===0.3


function numberepsilon(arg1,arg2){                     return Math.abs(arg1 - arg2) < Number.EPSILON;        }        
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
复制代码

isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

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

//通过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)
复制代码

实现 call、apply 及 bind 函数

(1)call 函数的实现步骤:


  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  • 处理传入的参数,截取第一个参数后的所有参数。

  • 将函数作为上下文对象的一个属性。

  • 使用上下文对象来调用这个方法,并保存返回结果。

  • 删除刚才新增的属性。

  • 返回结果。


Function.prototype.myCall = function(context) {  // 判断调用对象  if (typeof this !== "function") {    console.error("type error");  }  // 获取参数  let args = [...arguments].slice(1),    result = null;  // 判断 context 是否传入,如果未传入则设置为 window  context = context || window;  // 将调用函数设为对象的方法  context.fn = this;  // 调用函数  result = context.fn(...args);  // 将属性删除  delete context.fn;  return result;};
复制代码


(2)apply 函数的实现步骤:


  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  • 将函数作为上下文对象的一个属性。

  • 判断参数值是否传入

  • 使用上下文对象来调用这个方法,并保存返回结果。

  • 删除刚才新增的属性

  • 返回结果


Function.prototype.myApply = function(context) {  // 判断调用对象是否为函数  if (typeof this !== "function") {    throw new TypeError("Error");  }  let result = null;  // 判断 context 是否存在,如果未传入则为 window  context = context || window;  // 将函数设为对象的方法  context.fn = this;  // 调用方法  if (arguments[1]) {    result = context.fn(...arguments[1]);  } else {    result = context.fn();  }  // 将属性删除  delete context.fn;  return result;};
复制代码


(3)bind 函数的实现步骤:


  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 保存当前函数的引用,获取其余传入参数值。

  • 创建一个函数返回

  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。


Function.prototype.myBind = function(context) {  // 判断调用对象是否为函数  if (typeof this !== "function") {    throw new TypeError("Error");  }  // 获取参数  var args = [...arguments].slice(1),    fn = this;  return function Fn() {    // 根据调用方式,传入不同绑定值    return fn.apply(      this instanceof Fn ? this : context,      args.concat(...arguments)    );  };};
复制代码

什么是中间人攻击?如何防范中间人攻击?

中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。


攻击过程如下:


  • 客户端发送请求到服务端,请求被中间⼈截获

  • 服务器向客户端发送公钥

  • 中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个伪造的公钥,发给客户端

  • 客户端收到伪造的公钥后,⽣成加密 hash 值发给服务器

  • 中间⼈获得加密 hash 值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密 hash 值,发给服务器

  • 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

Unicode、UTF-8、UTF-16、UTF-32 的区别?

(1)Unicode

在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。


  • 它是基于拉丁字母的一套电脑编码系统。

  • 它定义了一个用于代表常见字符的字典。

  • 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。

  • 它是专门为英语而设计的,有 128 个编码,对其他语言无能为力


ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说UnicodeASCII 的超集。


Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。


Unicode的实现方式(也就是编码方式)有很多种,常见的是 UTF-8UTF-16UTF-32 USC-2

(2)UTF-8

UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是 1—4 个字节不等,它可以完全兼容ASCII码的 128 个字符。


注意: UTF-8 是一种编码方式,Unicode是一个字符集合。


UTF-8的编码规则:


  • 对于单字节的符号,字节的第一位为 0,后面的 7 位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。

  • 对于 n 字节的符号,第一个字节的前 n 位都是 1,第 n+1 位设为 0,后面字节的前两位一律设为 10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。


来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :



那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:


  • 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式

  • Unicode编码转换为二进制数(去掉最高位的 0)

  • 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为 0


来看一个实际的例子:“” 字的Unicode编码是:0x9A6C,整数编号是39532 (1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx (2)39532 对应的二进制数为1001 1010 0110 1100 (3)将二进制数填入 X 中,结果是:11101001 10101001 10101100

(3)UTF-16

1. 平面的概念


在了解UTF-16之前,先看一下平面的概念: Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放 65536(216)个字符,这称为一个平面,目前总共有 17 个平面。


最前面的一个平面称为基本平面,它的码点从 0 — 216-1,写成 16 进制就是U+0000 — U+FFFF,那剩下的 16 个平面就是辅助平面,码点范围是 U+10000—U+10FFFF


2. UTF-16 概念:


UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为 16 位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要 1 个或者 2 个 16 位长的码元来表示,因此UTF-16也是用变长字节表示的。


3. UTF-16 编码规则:


  • 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。

  • 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。


4. 编码识别


那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?


UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。


辅助平面共有 220 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。


因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。


5. 举例说明


以 "𡠀" 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:


  • 首先计算超出部分的结果:0x21800 - 0x10000

  • 将上面的计算结果转为 20 位的二进制数,不足 20 位就在前面补 0,结果为:0001000110 0000000000

  • 将得到的两个 10 位二进制数分别对应到两个区间中

  • U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后 10 个二进制位,得到 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00

(4) UTF-32

UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。


比如“” 字的 Unicode 编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的 UTF-32 编码。

(5)总结

Unicode、UTF-8、UTF-16、UTF-32 有什么区别?


  • Unicode 是编码字符集(字符集),而UTF-8UTF-16UTF-32是字符集编码(编码规则);

  • UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;

  • UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;

  • 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;

介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise

1)Promise 基本特性


  • 1、Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

  • 2、Promise 对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调 resolve 和失败时的回调 reject;另外 resolve 的参数除了正常值以外, 还可能是一个 Promise 对象的实例;reject 的参数通常是一个 Error 对象的实例。

  • 3、then 方法返回一个新的 Promise 实例,并接收两个参数 onResolved(fulfilled 状态的回调);onRejected(rejected 状态的回调,该参数可选)

  • 4、catch 方法返回一个新的 Promise 实例

  • 5、finally 方法不管 Promise 状态如何都会执行,该方法的回调函数不接受任何参数

  • 6、Promise.all()方法将多个多个 Promise 实例,包装成一个新的 Promise 实例,该方法接受一个由 Promise 对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例),注意参数中只要有一个实例触发 catch 方法,都会触发 Promise.all()方法返回的新的实例的 catch 方法,如果参数中的某个实例本身调用了 catch 方法,将不会触发 Promise.all()方法返回的新实例的 catch 方法

  • 7、Promise.race()方法的参数与 Promise.all 方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给 Promise.race()方法,并将返回值作为 Promise.race()方法产生的 Promise 实例的返回值

  • 8、Promise.resolve()将现有对象转为 Promise 对象,如果该方法的参数为一个 Promise 对象,Promise.resolve()将不做任何处理;如果参数 thenable 对象(即具有 then 方法),Promise.resolve()将该对象转为 Promise 对象并立即执行 then 方法;如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 fulfilled,其参数将会作为 then 方法中 onResolved 回调函数的参数,如果 Promise.resolve 方法不带参数,会直接返回一个 fulfilled 状态的 Promise 对象。需要注意的是,立即 resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

  • 9、Promise.reject()同样返回一个新的 Promise 对象,状态为 rejected,无论传入任何参数都将作为 reject()的参数


2)Promise 优点


  • ①统一异步 API

  • Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。

  • ②Promise 与事件对比

  • 和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。

  • ③Promise 与回调对比

  • 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。

  • ④Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。


3)Promise 缺点


  • 1、无法取消 Promise,一旦新建它就会立即执行,无法中途取消。

  • 2、如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

  • 3、当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

  • 4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。


4)简单代码实现最简单的 Promise 实现有 7 个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve 方法, reject 方法, then 方法


class Promise{  constructor(executor) {    this.state = 'pending';    this.value = undefined;    this.reason = undefined;    let resolve = value => {      if (this.state === 'pending') {        this.state = 'fulfilled';        this.value = value;      }    };    let reject = reason => {      if (this.state === 'pending') {        this.state = 'rejected';        this.reason = reason;      }    };    try {      // 立即执行函数      executor(resolve, reject);    } catch (err) {      reject(err);    }  }  then(onFulfilled, onRejected) {    if (this.state === 'fulfilled') {      let x = onFulfilled(this.value);    };    if (this.state === 'rejected') {      let x = onRejected(this.reason);    };  }}
复制代码


5)面试够用版


function myPromise(constructor){ let self=this;  self.status="pending" //定义状态改变前的初始状态   self.value=undefined;//定义状态为resolved的时候的状态   self.reason=undefined;//定义状态为rejected的时候的状态   function resolve(value){    //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){      self.value=value;      self.status="resolved";     }  }  function reject(reason){     //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){        self.reason=reason;        self.status="rejected";       }  }  //捕获构造异常   try{      constructor(resolve,reject);  }catch(e){    reject(e);    } }myPromise.prototype.then=function(onFullfilled,onRejected){   let self=this;  switch(self.status){    case "resolved": onFullfilled(self.value); break;    case "rejected": onRejected(self.reason); break;    default:   }}
// 测试var p=new myPromise(function(resolve,reject){resolve(1)}); p.then(function(x){console.log(x)})//输出1
复制代码


6)大厂专供版


const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected";const resolvePromise = (promise, x, resolve, reject) => {  if (x === promise) {    // If promise and x refer to the same object, reject promise with a TypeError as the reason.    reject(new TypeError('循环引用'))  }  // if x is an object or function,  if (x !== null && typeof x === 'object' || typeof x === 'function') {    // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.    let called    try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.      let then = x.then // Let then be x.then      // If then is a function, call it with x as this      if (typeof then === 'function') {        // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)        // If/when rejectPromise is called with a reason r, reject promise with r.        then.call(x, y => {          if (called) return          called = true          resolvePromise(promise, y, resolve, reject)        }, r => {          if (called) return          called = true          reject(r)        })      } else {        // If then is not a function, fulfill promise with x.        resolve(x)      }    } catch (e) {      if (called) return      called = true      reject(e)    }  } else {    // If x is not an object or function, fulfill promise with x    resolve(x)  }}function Promise(excutor) {  let that = this; // 缓存当前promise实例例对象  that.status = PENDING; // 初始状态  that.value = undefined; // fulfilled状态时 返回的信息  that.reason = undefined; // rejected状态时 拒绝的原因   that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数  that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数  function resolve(value) { // value成功态时接收的终值    if(value instanceof Promise) {      return value.then(resolve, reject);    }    // 实践中要确保 onFulfilled 和 onRejected ⽅方法异步执⾏行行,且应该在 then ⽅方法被调⽤用的那⼀一轮事件循环之后的新执⾏行行栈中执⾏行行。    setTimeout(() => {      // 调⽤用resolve 回调对应onFulfilled函数      if (that.status === PENDING) {        // 只能由pending状态 => fulfilled状态 (避免调⽤用多次resolve reject)        that.status = FULFILLED;        that.value = value;        that.onFulfilledCallbacks.forEach(cb => cb(that.value));      }    });  }  function reject(reason) { // reason失败态时接收的拒因    setTimeout(() => {      // 调⽤用reject 回调对应onRejected函数      if (that.status === PENDING) {        // 只能由pending状态 => rejected状态 (避免调⽤用多次resolve reject)        that.status = REJECTED;        that.reason = reason;        that.onRejectedCallbacks.forEach(cb => cb(that.reason));      }    });  }
// 捕获在excutor执⾏行行器器中抛出的异常 // new Promise((resolve, reject) => { // throw new Error('error in excutor') // }) try { excutor(resolve, reject); } catch (e) { reject(e); }}Promise.prototype.then = function(onFulfilled, onRejected) { const that = this; let newPromise; // 处理理参数默认值 保证参数后续能够继续执⾏行行 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; }; if (that.status === FULFILLED) { // 成功态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try{ let x = onFulfilled(that.value); resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上⼀一个onFulfilled的返回值 } catch(e) { reject(e); // 捕获前⾯面onFulfilled中抛出的异常then(onFulfilled, onRejected); } }); }) } if (that.status === REJECTED) { // 失败态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(that.reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } if (that.status === PENDING) { // 等待态// 当异步调⽤用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中 return newPromise = new Promise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { let x = onRejected(reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); }};
复制代码

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:


[1, [2, [3]]].flat(2)  // [1, 2, 3]
复制代码


现在就是要实现 flat 这种效果。


ES5 实现:递归。


function flatten(arr) {    var result = [];    for (var i = 0, len = arr.length; i < len; i++) {        if (Array.isArray(arr[i])) {            result = result.concat(flatten(arr[i]))        } else {            result.push(arr[i])        }    }    return result;}
复制代码


ES6 实现:


function flatten(arr) {    while (arr.some(item => Array.isArray(item))) {        arr = [].concat(...arr);    }    return arr;}
复制代码

如何优化关键渲染路径?

为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:


(1)关键资源的数量。


(2)关键路径长度。


(3)关键字节的数量。


关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。


优化关键渲染路径的常规步骤如下:


(1)对关键路径进行分析和特性描述:资源数、字节数、长度。


(2)最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。


(3)优化关键字节数以缩短下载时间(往返次数)。


(4)优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度

为什么有时候⽤translate 来改变位置⽽不是定位?

translate 是 transform 属性的⼀个值。改变 transform 或 opacity 不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。⽽改变绝对定位会触发重新布局,进⽽触发重绘和复合。transform 使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此 translate()更⾼效,可以缩短平滑动画的绘制时间。 ⽽translate 改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。

对 WebSocket 的理解

WebSocket 是 HTML5 提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于 TCP 传输协议,并复用 HTTP 的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。


WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。


WebSocket 原理:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者 ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有 ID 在接收者 ID 序列中的客户端才会处理这个事件。​


WebSocket 特点的如下:


  • 支持双向通信,实时性更强

  • 可以发送文本,也可以发送二进制数据‘’

  • 建立在 TCP 协议之上,服务端的实现比较容易

  • 数据格式比较轻量,性能开销小,通信高效

  • 没有同源限制,客户端可以与任意服务器通信

  • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL

  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。


Websocket 的使用方法如下:


在客户端中:


// 在index.html中直接写WebSocket,设置服务端的端口号为 9999let ws = new WebSocket('ws://localhost:9999');// 在客户端与服务端建立连接后触发ws.onopen = function() {    console.log("Connection open.");     ws.send('hello');};// 在服务端给客户端发来消息的时候触发ws.onmessage = function(res) {    console.log(res);       // 打印的是MessageEvent对象    console.log(res.data);  // 打印的是收到的消息};// 在客户端与服务端建立关闭后触发ws.onclose = function(evt) {  console.log("Connection closed.");}; 
复制代码

setInterval 模拟 setTimeout

描述:使用setInterval模拟实现setTimeout的功能。


思路setTimeout的特性是在指定的时间内只执行一次,我们只要在setInterval内部执行 callback 之后,把定时器关掉即可。


实现


const mySetTimeout = (fn, time) => {    let timer = null;    timer = setInterval(() => {        // 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了        clearInterval(timer);        fn();    }, time);    // 返回用于关闭定时器的方法    return () => clearInterval(timer);}
// 测试const cancel = mySetTimeout(() => { console.log(1);}, 1000); // 一秒后打印 1
复制代码

原型

JavaScript中的对象都有一个特殊的 prototype 内置属性,其实就是对其他对象的引用几乎所有的对象在创建时 prototype 属性都会被赋予一个非空的值,我们可以把这个属性当作一个备用的仓库当试图引用对象的属性时会出发get操作,第一步时检查对象本身是否有这个属性,如果有就使用它,没有就去原型中查找。一层层向上直到Object.prototype顶层
基于原型扩展描述一下原型链,什么是原型链,原型的继承,ES5和ES6继承与不同点。
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
百度前端二面常见面试题合集_JavaScript_bb_xiaxia1998_InfoQ写作社区