写点什么

社招前端二面面试题

  • 2022 年 9 月 13 日
    浙江
  • 本文字数:10751 字

    阅读完需:约 35 分钟

说一下 web worker

在 HTML 页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。


如何创建 web worker:


  1. 检测浏览器对于 web worker 的支持性

  2. 创建 web worker 文件(js,回传函数等)

  3. 创建 web worker 对象

代码输出结果

const async1 = async () => {  console.log('async1');  setTimeout(() => {    console.log('timer1')  }, 2000)  await new Promise(resolve => {    console.log('promise1')  })  console.log('async1 end')  return 'async1 success'} console.log('script start');async1().then(res => console.log(res));console.log('script end');Promise.resolve(1)  .then(2)  .then(Promise.resolve(3))  .catch(4)  .then(res => console.log(res))setTimeout(() => {  console.log('timer2')}, 1000)
复制代码


输出结果如下:


script startasync1promise1script end1timer2timer1
复制代码


代码的执行过程如下:


  1. 首先执行同步带吗,打印出 script start;

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

  3. 之后是执行 Promise,打印出 promise1,由于 Promise 没有返回值,所以后面的代码不会执行;

  4. 然后执行同步代码,打印出 script end;

  5. 继续执行下面的 Promise,.then 和.catch 期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将 resolve(1)的值传到最后一个 then,直接打印出 1;

  6. 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出 timer2,在四秒后打印出 timer1。

同步和异步的区别

  • 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。

  • 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。

三栏布局的实现

三栏布局一般指的是页面中一共有三栏,左右两栏宽度固定,中间自适应的布局,三栏布局的具体实现:


  • 利用绝对定位,左右两栏设置为绝对定位,中间设置对应方向大小的 margin 的值。


.outer {  position: relative;  height: 100px;}
.left { position: absolute; width: 100px; height: 100px; background: tomato;}
.right { position: absolute; top: 0; right: 0; width: 200px; height: 100px; background: gold;}
.center { margin-left: 100px; margin-right: 200px; height: 100px; background: lightgreen;}
复制代码


  • 利用 flex 布局,左右两栏设置固定大小,中间一栏设置为 flex:1。


.outer {  display: flex;  height: 100px;}
.left { width: 100px; background: tomato;}
.right { width: 100px; background: gold;}
.center { flex: 1; background: lightgreen;}
复制代码


  • 利用浮动,左右两栏设置固定大小,并设置对应方向的浮动。中间一栏设置左右两个方向的 margin 值,注意这种方式**,中间一栏必须放到最后:**


.outer {  height: 100px;}
.left { float: left; width: 100px; height: 100px; background: tomato;}
.right { float: right; width: 200px; height: 100px; background: gold;}
.center { height: 100px; margin-left: 100px; margin-right: 200px; background: lightgreen;}
复制代码


  • 圣杯布局,利用浮动和负边距来实现。父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。


.outer {  height: 100px;  padding-left: 100px;  padding-right: 200px;}
.left { position: relative; left: -100px;
float: left; margin-left: -100%;
width: 100px; height: 100px; background: tomato;}
.right { position: relative; left: 200px;
float: right; margin-left: -200px;
width: 200px; height: 100px; background: gold;}
.center { float: left;
width: 100%; height: 100px; background: lightgreen;}
复制代码


  • 双飞翼布局,双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。


.outer {  height: 100px;}
.left { float: left; margin-left: -100%;
width: 100px; height: 100px; background: tomato;}
.right { float: left; margin-left: -200px;
width: 200px; height: 100px; background: gold;}
.wrapper { float: left;
width: 100%; height: 100px; background: lightgreen;}
.center { margin-left: 100px; margin-right: 200px; height: 100px;}
复制代码

什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

手写题:数组扁平化

function flatten(arr) {  let result = [];
for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])); } else { result = result.concat(arr[i]); } }
return result;}
const a = [1, [2, [3, 4]]];console.log(flatten(a));
复制代码

Object.is()

描述Object.is 不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。


  1. NaN=== 中是不相等的,而在 Object.is 中是相等的

  2. +0-0=== 中是相等的,而在 Object.is 中是不相等的


实现:利用 ===


Object.is = function(x, y) {    if(x === y) {        // 当前情况下,只有一种情况是特殊的,即 +0 -0        // 如果 x !== 0,则返回true        // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断        return x !== 0 || 1 / x === 1 / y;    }    // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样    // x和y同时为NaN时,返回true    return x !== x && y !== y;}
复制代码

对 CSS 工程化的理解

CSS 工程化是为了解决以下问题:


  1. 宏观设计:CSS 代码如何组织、如何拆分、模块结构怎样设计?

  2. 编码优化:怎样写出更好的 CSS?

  3. 构建:如何处理我的 CSS,才能让它的打包结果最优?

  4. 可维护性:代码写完了,如何最小化它后续的变更成本?如何确保任何一个同事都能轻松接手?


以下三个方向都是时下比较流行的、普适性非常好的 CSS 工程化实践:


  • 预处理器:Less、 Sass 等;

  • 重要的工程化插件: PostCss;

  • Webpack loader 等 。


基于这三个方向,可以衍生出一些具有典型意义的子问题,这里我们逐个来看:


(1)预处理器:为什么要用预处理器?它的出现是为了解决什么问题?


预处理器,其实就是 CSS 世界的“轮子”。预处理器支持我们写一种类似 CSS、但实际并不是 CSS 的语言,然后把它编译成 CSS 代码: 那为什么写 CSS 代码写得好好的,偏偏要转去写“类 CSS”呢?这就和本来用 JS 也可以实现所有功能,但最后却写 React 的 jsx 或者 Vue 的模板语法一样——为了爽!要想知道有了预处理器有多爽,首先要知道的是传统 CSS 有多不爽。随着前端业务复杂度的提高,前端工程中对 CSS 提出了以下的诉求:


  1. 宏观设计上:我们希望能优化 CSS 文件的目录结构,对现有的 CSS 文件实现复用;

  2. 编码优化上:我们希望能写出结构清晰、简明易懂的 CSS,需要它具有一目了然的嵌套层级关系,而不是无差别的一铺到底写法;我们希望它具有变量特征、计算能力、循环能力等等更强的可编程性,这样我们可以少写一些无用的代码;

  3. 可维护性上:更强的可编程性意味着更优质的代码结构,实现复用意味着更简单的目录结构和更强的拓展能力,这两点如果能做到,自然会带来更强的可维护性。


这三点是传统 CSS 所做不到的,也正是预处理器所解决掉的问题。预处理器普遍会具备这样的特性:


  • 嵌套代码的能力,通过嵌套来反映不同 css 属性之间的层级关系 ;

  • 支持定义 css 变量;

  • 提供计算函数;

  • 允许对代码片段进行 extend 和 mixin;

  • 支持循环语句的使用;

  • 支持将 CSS 文件模块化,实现复用。


(2)PostCss:PostCss 是如何工作的?我们在什么场景下会使用 PostCss?


它和预处理器的不同就在于,预处理器处理的是 类 CSS,而 PostCss 处理的就是 CSS 本身。Babel 可以将高版本的 JS 代码转换为低版本的 JS 代码。PostCss 做的是类似的事情:它可以编译尚未被浏览器广泛支持的先进的 CSS 语法,还可以自动为一些需要额外兼容的语法增加前缀。更强的是,由于 PostCss 有着强大的插件机制,支持各种各样的扩展,极大地强化了 CSS 的能力。


PostCss 在业务中的使用场景非常多:


  • 提高 CSS 代码的可读性:PostCss 其实可以做类似预处理器能做的工作;

  • 当我们的 CSS 代码需要适配低版本浏览器时,PostCss 的 Autoprefixer 插件可以帮助我们自动增加浏览器前缀;

  • 允许我们编写面向未来的 CSS:PostCss 能够帮助我们编译 CSS next 代码;


(3)Webpack 能处理 CSS 吗?如何实现? Webpack 能处理 CSS 吗:


  • Webpack 在裸奔的状态下,是不能处理 CSS 的,Webpack 本身是一个面向 JavaScript 且只能处理 JavaScript 代码的模块化打包工具;

  • Webpack 在 loader 的辅助下,是可以处理 CSS 的。


如何用 Webpack 实现对 CSS 的处理:


  • Webpack 中操作 CSS 需要使用的两个关键的 loader:css-loader 和 style-loader

  • 注意,答出“用什么”有时候可能还不够,面试官会怀疑你是不是在背答案,所以你还需要了解每个 loader 都做了什么事情:

  • css-loader:导入 CSS 模块,对 CSS 代码进行编译处理;

  • style-loader:创建 style 标签,把 CSS 内容写入标签。


在实际使用中,css-loader 的执行顺序一定要安排在 style-loader 的前面。因为只有完成了编译过程,才可以对 css 代码进行插入;若提前插入了未编译的代码,那么 webpack 是无法理解这坨东西的,它会无情报错。

HTTP 1.0 和 HTTP 1.1 之间有哪些区别?

HTTP 1.0 和 HTTP 1.1 有以下区别


  • 连接方面,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。

  • 资源请求方面,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

  • 缓存方面,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。

  • http1.1 中新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。

  • http1.1 相对于 http1.0 还新增了很多请求方法,如 PUT、HEAD、OPTIONS 等。

当在浏览器中输入 Google.com 并且按下回车之后发生了什么?

(1)解析 URL: 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。


(2)缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。


(3)DNS 解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。


(4)获取 MAC 地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。


(5)TCP 三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK 报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个 ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。


(6)HTTPS 握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。


(7)返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。


(8)页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。


(9)TCP 四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。

代码输出结果

async function async1 () {  console.log('async1 start');  await new Promise(resolve => {    console.log('promise1')  })  console.log('async1 success');  return 'async1 end'}console.log('srcipt start')async1().then(res => console.log(res))console.log('srcipt end')
复制代码


输出结果如下:


script startasync1 startpromise1script end
复制代码


这里需要注意的是在async1await后面的 Promise 是没有返回值的,也就是它的状态始终是pending状态,所以在await之后的内容是不会执行的,包括async1后面的 .then

发布订阅模式

题目描述:实现一个发布订阅模式拥有 on emit once off 方法


实现代码如下:


class EventEmitter {  constructor() {    this.events = {};  }  // 实现订阅  on(type, callBack) {    if (!this.events[type]) {      this.events[type] = [callBack];    } else {      this.events[type].push(callBack);    }  }  // 删除订阅  off(type, callBack) {    if (!this.events[type]) return;    this.events[type] = this.events[type].filter((item) => {      return item !== callBack;    });  }  // 只执行一次订阅事件  once(type, callBack) {    function fn() {      callBack();      this.off(type, fn);    }    this.on(type, fn);  }  // 触发事件  emit(type, ...rest) {    this.events[type] &&      this.events[type].forEach((fn) => fn.apply(this, rest));  }}// 使用如下// const event = new EventEmitter();
// const handle = (...rest) => {// console.log(rest);// };
// event.on("click", handle);
// event.emit("click", 1, 2, 3, 4);
// event.off("click", handle);
// event.emit("click", 1, 2);
// event.once("dbClick", () => {// console.log(123456);// });// event.emit("dbClick");// event.emit("dbClick");
复制代码

JavaScript 有哪些数据类型,它们的区别?

JavaScript 共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。


其中 Symbol 和 BigInt 是 ES6 中新增的数据类型:


  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。

  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。


这些数据可以分为原始数据类型和引用数据类型:


  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)

  • 堆:引用数据类型(对象、数组和函数)


两种类型的区别在于存储位置的不同:


  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;

  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。


堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:


  • 在数据结构中,栈中数据的存取方式为先进后出。

  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。


在操作系统中,内存被分为栈区和堆区:


  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

对对象与数组的解构的理解

解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。 1)数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据的:


const [a, b, c] = [1, 2, 3]
复制代码


最终,a、b、c 分别被赋予了数组第 0、1、2 个索引位的值:


数组里的 0、1、2 索引位的元素值,精准地被映射到了左侧的第 0、1、2 个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:


const [a,,c] = [1,2,3]
复制代码


通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:


2)对象的解构 对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:


const stu = {  name: 'Bob',  age: 24}
复制代码


假如想要解构它的两个自有属性,可以这样:


const { name, age } = stu
复制代码


这样就得到了 name 和 age 两个和 stu 平级的变量:


注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:


const { age, name } = stu
复制代码

代码输出结果

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。

代码输出结果

function fn1(){  console.log('fn1')}var fn2
fn1()fn2()
fn2 = function() { console.log('fn2')}
fn2()
复制代码


输出结果:


fn1Uncaught TypeError: fn2 is not a functionfn2
复制代码


这里也是在考察变量提升,关键在于第一个 fn2(),这时 fn2 仍是一个 undefined 的变量,所以会报错 fn2 不是一个函数。

代码输出结果

var obj = {   say: function() {     var f1 = () =>  {       console.log("1111", this);     }     f1();   },   pro: {     getPro:() =>  {        console.log(this);     }   }}var o = obj.say;o();obj.say();obj.pro.getPro();
复制代码


输出结果:


1111 window对象1111 obj对象window对象
复制代码


解析:


  1. o(),o 是在全局执行的,而 f1 是箭头函数,它是没有绑定 this 的,它的 this 指向其父级的 this,其父级 say 方法的 this 指向的是全局作用域,所以会打印出 window;

  2. obj.say(),谁调用 say,say 的 this 就指向谁,所以此时 this 指向的是 obj 对象;

  3. obj.pro.getPro(),我们知道,箭头函数时不绑定 this 的,getPro 处于 pro 中,而对象不构成单独的作用域,所以箭头的函数的 this 就指向了全局作用域 window。

代码输出结果

var A = {n: 4399};var B =  function(){this.n = 9999};var C =  function(){var n = 8888};B.prototype = A;C.prototype = A;var b = new B();var c = new C();A.n++console.log(b.n);console.log(c.n);
复制代码


输出结果:9999 4400


解析:


  1. console.log(b.n),在查找 b.n 是首先查找 b 对象自身有没有 n 属性,如果没有会去原型(prototype)上查找,当执行 var b = new B()时,函数内部 this.n=9999(此时 this 指向 b) 返回 b 对象,b 对象有自身的 n 属性,所以返回 9999。

  2. console.log(c.n),同理,当执行 var c = new C()时,c 对象没有自身的 n 属性,向上查找,找到原型 (prototype)上的 n 属性,因为 A.n++(此时对象 A 中的 n 为 4400), 所以返回 4400。

页面有多张图片,HTTP 是怎样的加载表现?

  • HTTP 1下,浏览器对一个域名下最大 TCP 连接数为 6,所以会请求多次。可以用多域名部署解决。这样可以提高同时请求的数目,加快页面图片的获取速度。

  • HTTP 2下,可以一瞬间加载出来很多资源,因为,HTTP2 支持多路复用,可以在一个 TCP 连接中发送多个 HTTP 请求。

用户头像

还未添加个人签名 2022.09.05 加入

还未添加个人简介

评论

发布
暂无评论
社招前端二面面试题_JavaScript_夏天的味道123_InfoQ写作社区