「2022」JavaScript最新高频面试题指南(下)
前言
大家好,我是CoderBin。本次总结了关于JavaScript的上百道高频面试考点,并且会持续更新,感谢大家的留言点赞收藏 💗
如果文中有不对、疑惑或者错字的地方,欢迎在评论区留言指正🌻
本文是「2022」JavaScript最新高频面试题指南(上)的后续部分。
高频面试总结
基础篇
126. 微前端可解决什么痛点?
任何新技术的产生都是为了解决现有场景和需求下的技术痛点,微前端也不例外:
- 拆分和细化
当下前端领域,单页面应用(SPA)是非常流行的项目形态之一,而随着时间的推移以及应用功能的丰富,单页应用变得不再单一而是越来越庞大也越来越难以维护,往往是改一处而动全身,由此带来的发版成本也越来越高。微前端的意义就是将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
- 整合历史系统
在不少的业务中,或多或少会存在一些历史项目,这些项目大多以采用老框架类似(Backbone.js,Angular.js 1)的B端管理系统为主,介于日常运营,这些系统需要结合到新框架中来使用还不能抛弃,对此我们也没有理由浪费时间和精力重写旧的逻辑。而微前端可以将这些系统进行整合,在基本不修改来逻辑的同时来同时兼容新老两套系统并行运行。
127. 简单说说你对函数式编程的理解,以及有何优缺点?
函数式编程是一种"编程范式"(programming paradigm),一种编写程序的方法论
主要的编程范式有三种:命令式编程,声明式编程和函数式编程
相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程
优点:
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
- 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
- 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
- 隐性好处。减少代码量,提高维护性
缺点:
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
- 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作
128. 说说你对事件循环的理解
点击前往:# 面试官:说说你对事件循环的理解
129. JavaScript中执行上下文和执行栈是什么?
执行上下文:简单的来说,执行上下文是一种对Javascript
代码执行环境的抽象概念,也就是说只要有Javascript
代码运行,那么它就一定是运行在执行上下文中
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是
window
对象,this
指向这个全局对象 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- Eval 函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
紫色框住的部分为全局上下文,蓝色和橘色框起来的是不同的函数上下文。只有全局上下文(的变量)能被其他任何上下文访问
可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问
执行栈:执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文
当Javascript
引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中
每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中
引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文
130. 简单说说你对闭包的理解,以及使用场景?
闭包的理解:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
在 JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
使用场景:任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
注意:如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因在于每个对象的创建,方法都会被重新赋值
131. 简单说说你对ES6中的Module的理解,有哪些应用场景?
理解:模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体) 。
有两个基本的特征:外部特征和内部特征
- 外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能
- 内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)
为什么需要模块化:代码抽象、代码封装、代码复用、依赖管理
如果没有模块化,我们代码会怎样?
- 变量和方法不容易维护,容易污染全局作用域
- 加载资源的方式通过script标签从上到下。
- 依赖的环境主观逻辑偏重,代码较多就会比较复杂。
- 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃
因此,需要一种将JavaScript
程序模块化的机制,如
- CommonJs (典型代表:node.js早期)
- AMD (典型代表:require.js)
- CMD (典型代表:sea.js)
应用场景:如今,ES6
模块化已经深入我们日常项目开发中,像vue
、react
项目搭建项目,组件化开发处处可见,其也是依赖模块化实现
132. 你是怎么理解Promise的?
Promise
是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
(1) Promise 的实例有三个状态:
Pending
(进行中)Resolved
(已完成)Rejected
(已拒绝)
当把一件事情交给 promise 时,它的状态就是 Pending,任务完成了 状态就变成了 Resolved、没有完成失败了就变成了 Rejected。
(2) Promise 的实例有两个过程:
pending
-> fulfilled : Resolved(已完成)pending
-> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
(3) Promise 特点:
-
对象的状态不受外界影响。
promise
对象代表一个异步操作,有三种状态,pending
(进行中)、fulfilled
(已成功)、rejected
(已失败)。 -
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是
promise
这个名字的由来—“承诺”; 一旦状态改变就不会再变,任何时候都可以得到这个结果。 -
promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
,从pending
变为rejected
。这时就称为resolved
(已定型)。 -
如果改变已经发生了,你再对
promise
对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。
(4) Promise 的缺点:
无法取消 Promise
,一旦新建它就会立即执行,无法中途取消。 如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。 当处于 pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始 还是即将完成)。
总结(面试的时候可以直接回答总结):
Promise
对象是异步编程的一种解决方案,最早由社区提出。Promise
是一个构造函数,接收一个函数作为参数,返回一个 Promise
实例。
一个 Promise
实例有三种状态,分别是 pending
、resolved
和 rejected
,分别代表了进行中、已成功和已失败。实例的状态只能由 pending
转变 resolved
或者 rejected
状态,并且状态一经改变, 就凝固了,无法再被改变了。
状态的改变是通过 resolve()
和 reject()
函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise
实例的状态,它的原型上定义了一个 then
方法,使用这个 then
方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的
133. 简单说说你对pnpm的理解
pnpm 的官方文档是这样说的:
Fast, disk space efficient package manager
pnpm 本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,但它作为杀手锏的两个优势在于:
- 包安装速度极快;
- 磁盘空间利用非常高效。
pnpm 与 npm/yarn 相似,也是一个包管理器,但与他们不同的是,作者设计了一套理论上更完善的依赖结构以及高效的文件复用,来解决 npm/yarn 未打算解决或还不够完善的问题。
134. 异步编程有哪些实现方式?
js 中的异步机制可以分为以下几种:
-
第一种最常见的是使用回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
-
第二种是
Promise
的方式,使用Promise
的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个then
的链式调用,可能会造成代码的语义不够明确。 -
第三种是使用
generator
的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部我们还可以将执行权转移回来。当我们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候我们再将执行权给转移回来。因此我们在generator
内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式我们需要考虑的问题是何时将函数的控制权转移回来,因此我们需要有一个自动执行generator
的机制,比如说co
模块等方式来实现generator
的自动执行。 -
第四种是使用
async
函数的形式,async
函数是generator
和promise
实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await
语句的时候,如果语句返回一个promise
对象,那么函数将会等待promise
对象的状态变为resolve
后再继续向下执行。因此我们可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
135. JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
-
defer
属性: 给 js 脚本添加defer
属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了defer
属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。 -
async
属性: 给 js 脚本添加async
属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async
属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。 -
动态创建
DOM
方式: 动态创建DOM
标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script
标签来引入 js 脚本。 -
使用
setTimeout
延迟方法: 设置一个定时器来延迟加载js脚本文件 -
让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
136. Promise中的值穿透是什么?
解释:.then
或者 .catch
的参数期望是函数,传入非函数则会发生值穿透。
当 then
中传入的不是函数,则这个 then
返回的 promise
的 data
,将会保存上一个的promise.data
。这就是发生值穿透的原因。而且每一个无效的 then
所返回的 promise
的状态都为 resolved
。
Promise.resolve(1)
.then(2) // 注意这里
.then(Promise.resolve(3))
.then(console.log)
上面代码的输出是 1
137. 什么是PWA?
PWA
的中文名叫做渐进式网页应用,早在2014年, W3C 公布过 Service Worker 的相关草案,但是其在生产环境被 Chrome 支持是在 2015 年。因此,如果我们把 PWA 的关键技术之一 Service Worker 的出现作为 PWA 的诞生时间,那就应该是 2015 年。
自 2015 年以来,PWA 相关的技术不断升级优化,在用户体验和用户留存两方面都提供了非常好的解决方案。PWA 可以将 Web 和 App 各自的优势融合在一起:渐进式、可响应、可离线、实现类似 App 的交互、即时更新、安全、可以被搜索引擎检索、可推送、可安装、可链接。
需要特别说明的是,PWA 不是特指某一项技术,而是应用了多项技术的 Web App。其核心技术包括 App Manifest、Service Worker、Web Push,等等。
138. 什么是虚拟DOM,为什么需要虚拟DOM?
(1) 什么是虚拟DOM?
虚拟 DOM (Virtual DOM
)这个概念相信大家都不陌生,从 React
到 Vue
,虚拟 DOM
为这两个框架都带来了跨平台的能力(React-Native
和 Weex
)
实际上它只是一层对真实DOM
的抽象,以JavaScript
对象 (VNode
节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在Javascript
对象中,虚拟DOM
表现为一个 Object
对象。并且最少包含标签名 (tag
)、属性 (attrs
) 和子元素对象 (children
) 三个属性,不同框架对这三个属性的名命可能会有差别
创建虚拟DOM
就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM
对象的节点与真实DOM
的属性一一照应
(2) 为什么需要虚拟DOM?
DOM
是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM
操作引起的。真实的DOM节点,哪怕一个最简单的div也包含着很多属性,可以打印出来直观感受一下:
由此可见,操作
DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验
举个例子:你用传统的原生api
或jQuery
去操作DOM
时,浏览器会从构建DOM
树开始从头到尾执行一遍流程
当你在一次操作时,需要更新10个DOM
节点,浏览器没这么智能,收到第一个更新DOM
请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程
而通过VNode
,同样更新10个DOM
节点,虚拟DOM
不会立即操作DOM
,而是将这10次更新的diff
内容保存到本地的一个js
对象中,最终将这个js
对象一次性attach
到DOM
树上,避免大量的无谓计算
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,也可以是小程序以及各种GUI。
关于虚拟DOM的简单实现可以前往:# 手写实现一个简单的Mini-Vue系统
139. 深拷贝和浅拷贝有什么区别?
首先我们需要知道,JavaScript
中存在两大数据类型:
-
基本类型:基本类型数据保存在栈内存中
-
引用类型:引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中(栈中存地址,指向堆中的数据)
1. 浅拷贝:指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
常见浅拷贝实现方式:
Object.assign
Array.prototype.slice()
,Array.prototype.concat()
- 使用拓展运算符实现的复制
2. 深拷贝:深拷贝开辟一个新的栈,两个对象的属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见深拷贝实现方式:
-
_.cloneDeep()
,利用 lodash 库的深拷贝函数 -
jQuery.extend()
,利用 jquery 库的深拷贝函数 -
JSON.stringify()
,原生JSON方法,但是这种方式存在弊端,会忽略undefined
、symbol
和函数
-
手写循环递归
区别:
-
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
-
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
140. 从输入url,到页面的画面展示的过程
-
首先,在浏览器地址栏中输入url
-
浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。
-
在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。
-
浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。
-
握手成功后,浏览器向服务器发送http请求,请求数据包。
-
服务器处理收到的请求,将数据返回至浏览器
-
浏览器收到HTTP响应
-
读取页面内容,浏览器渲染,解析html源码
-
生成Dom树、解析css样式、js交互,渲染显示页面
141. ES6模块与CommonJS模块的相同点和区别?
-
相同点:CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变
-
区别:
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载
- CommonJS是对模块的浅拷贝,ES6 Module是对模块的引入,即ES6 Module只存只读,不能改变其值,具体点就是指针指向不能变,类似const 。
- import的接口是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向。可以对commonJS重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。
142. 微前端中的应用隔离是什么?
应用隔离问题主要分为主应用和微应用,微应用和微应用之间的JavaScript执行环境隔离,CSS样式隔离。
CSS隔离
当主应用和微应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个微应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于微应用与微应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离
每当微应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。
为此,需要在加载和卸载每个微应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。
沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。通常在Node.js端可以采用vm模块,而对于浏览器,则需要结合with关键字和window.Proxy对象来实现浏览器端的沙箱。
场景篇
1. 一个滚动公告组件,如何在鼠标滑入时停止播放,在鼠标离开时继续等待滑入时的剩余等待时间后播放?
轮播图的定时滚动,一般是使用 setInterval 实现。
可以监听轮播图的 mouseover
和 mouseout
事件,如果 mouseover
被触发,就清除定时轮播,并记录下一次轮播的剩余等待时间xs
,如果 mouseout
被触发,就在 xs
的时间后立即进行切换,并且开启定时轮播。
当然其中的细节还比较多,比如 mouseover
的过程中手动切换了轮播图该怎么处理等等。
2. 遍历一个任意长度的list中的元素并依次创建异步任务,如何获取所有任务的执行结果?
看到这个题目,大家首先想到的是 Promise.all
或者 Promise.allSettled
。
Promise.all
Promise.all
需要传入一个数组,数组中的元素都是 Promise
对象。当这些对象都执行成功时,则 all 对应的 promise 也成功,且执行 then 中的成功回调。如果有一个失败了,则 all 对应的 promise
失败,且失败时只能获得第一个失败 Promise
的数据。
const p1 = new Promise((resolve, reject) => {
resolve('成功了')
})
const p2 = Promise.resolve('success')
const p3 = Promise.reject('失败')
Promise.all([p1, p2]).then((result) => {
console.log(result) //["成功了", "success"]
}).catch((error) => {
//未被调用
})
Promise.all([p1, p3, p2]).then((result) => {
//未被调用
}).catch((error) => {
console.log(error) //"失败"
});
Promise.allSettled
Promise.allSettled()
可用于并行执行独立的异步操作,并收集这些操作的结果。
Promise.allSettled()
方法返回一个在所有给定的 promise 都已经 fulfilled 或 rejected 后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。
Promise.allSettled([p1, p2, p3])
.then(values => {
console.log(values)
})
3. 给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,请问会执行几次事件,然后会先执行冒泡还是捕获?
addEventListener绑定几次就执行几次
先捕获,后冒泡
4. 怎么预防用户快速连续点击,造成数据多次提交?
为了防止重复提交,前端一般会在第一次提交的结果返回前,将提交按钮禁用。
实现的方法有很多种:
- css设置
pointer-events
为none
- 增加变量控制,当变量满足条件时才执行点击事件的后续代码
- 如果按钮使用 button 标签实现,可以使用
disabled
属性 - 加遮罩层,比如一个全屏的loading,避免触发按钮的点击事件
- ...
也可以进行节流防抖操作,具体实现请点击前往学习:深入浅出防抖与节流函数
5. 连续bind()多次,输出的值是什么?
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
两次都输出 3。
在Javascript中,多次 bind()
是无效的。
更深层次的原因, bind()
的实现,相当于使用函数在内部包了一个 call
/ apply
,第二次 bind()
相当于再包住第一次 bind()
,故第二次以后的 bind
是无法生效的。
6. jquery的链式调用是怎么实现的?
我们都知道 jQuery 可以链式调用,比如:
$("div").eq(0).css("width", "200px").show();
链式调用的核心就在于调用完的方法将自身实例返回。
简单实现:
// 定义一个对象
class listFunc {
// 初始化
constructor(val) {
this.arr = [...val];
return this;
}
// 打印这个数组
get() {
console.log(this.arr);
return this;
}
// 向数组尾部添加数据
push(val) {
console.log(this.arr);
this.arr.push(val);
return this;
}
// 删除尾部数据
pop() {
console.log(this.arr);
this.arr.pop();
return this;
}
}
const list = new listFunc([1, 2, 3]);
list.get().pop().push('ldq')
7. 移动端的点击事件有延迟,时间是多久,为什么会有?怎么解决这个延时?
移动端点击有 300ms 的延迟,原因是移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。
有三种办法来解决这个问题:
- 通过 meta 标签禁用网页的缩放。
- 通过 meta 标签将网页的 viewport 设置为 ideal viewport。
- 调用一些 js 库,比如 FastClick
click 延时问题还可能引起点击穿透的问题,就是如果我们在一个元素上注册了 touchStart 的监听事件,这个事件会将这个元素隐藏掉,我们发现当这个元素隐藏后,触发了这个元素下的一个元素的点击事件,这就是点击穿透。
8. 改造下面的代码,让它输出1,2,3,4,5
for (var i = 0; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
- 第一种方法,使用
let
声明迭代变量i
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
- 第二种方法,利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, 0)
})(i)
}
- 第三种方法,给定时器传入第三个参数, 作为
timer
函数的第一个函数参数
for(var i = 1; i <= 5; i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}
9. 如何中断Promise?
Promise 有个缺点就是一旦创建就无法取消,所以本质上 Promise 是无法被终止的,但我们在开发过程中可能会遇到下面两个需求:
- 中断调用链
就是在某个 then/catch 执行之后,不想让后续的链式调用继续执行了。
somePromise
.then(() => {})
.then(() => {
// 终止 Promise 链,让下面的 then、catch 和 finally 都不执行
})
.then(() => console.log('then'))
.catch(() => console.log('catch'))
.finally(() => console.log('finally'))
一种方法是在then中直接抛错, 这样就不会执行后面的then, 直接跳到catch方法打印err(但此方法并没有实际中断)。但如果链路中对错误进行了捕获,后面的then函数还是会继续执行。
Promise的then方法接收两个参数:
Promise.prototype.then(onFulfilled, onRejected)
若onFulfilled或onRejected是一个函数,当函数返回一个新Promise对象时,原Promise对象的状态将跟新对象保持一致,详见Promises/A+标准。
因此,当新对象保持“pending”状态时,原Promise链将会中止执行。
Promise.resolve()
.then(() => {
console.log('then 1')
return new Promise(() => {})
})
.then(() => {
console.log('then 2')
})
.then(() => {
console.log('then 3')
})
.catch(err => {
console.log(err)
})
- 中断Promise
注意这里是中断而不是终止,因为 Promise 无法终止,这个中断的意思是:在合适的时候,把 pending 状态的 promise 给 reject 掉。例如一个常见的应用场景就是希望给网络请求设置超时时间,一旦超时就就中断,我们这里用定时器模拟一个网络请求,随机 3 秒之内返回。
function timeoutWrapper(p, timeout = 2000) {
const wait = new Promise((resolve, reject) => {
setTimeout(() => {
reject('请求超时')
}, timeout)
})
return Promise.race([p, wait])
}
10. 如何实现一个扫描二维码登录PC网站的需求?
二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情:
- 告诉系统我是谁
- 向系统证明我是谁
扫描二维码登录的一般步骤
- 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描
- 手机端打开应用,扫描PC端的二维码,扫描后,会提示"已扫描,请在手机端点击确认"
- 用户在手机端点击确认,确认后PC端登录就成功了
具体流程:生成二维码 -> 扫描二维码 -> 状态确认
1. 生成二维码
- PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端
- 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定
- 然后把二维码ID返回给PC端
- PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)
- 为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息,或者直接使用
websocket
,等待在服务端完成登录后进行通知
2. 扫描二维码
- 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID
- 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端
- 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端
- 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描
3. 状态确认
- 手机端在接收到临时
token
后会弹出确认登录界面,用户点击确认时,手机端携带临时token
用来调用服务端的接口,告诉服务端,我已经确认 - 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的
token
- 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的
token
- 到这里,登录就成功了,后端PC端就可以用
token
去访问服务端的资源了
11. JS中的倒计时,怎么实现纠正偏差?
在前端实现中我们一般通过 setTimeout
和 setInterval
方法来实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的,setTimeout
和 setInterval
的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。
一般解决倒计时中的误差的有这样两种办法:
(1)第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。
(2)第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout
递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。
12. 如何使用js实现拖拽功能?
一个元素的拖拽过程,我们可以分为三个步骤:
- 第一步是鼠标按下目标元素
- 第二步是鼠标保持按下的状态移动鼠标
- 第三步是鼠标抬起,拖拽过程结束
这三步分别对应了三个事件,mousedown
事件,mousemove
事件和 mouseup
事件。
只有在鼠标按下的状态移动鼠标我们才会执行拖拽事件,因此我们需要在 mousedown
事件中设置一个状态来标识鼠标已经按下,然后在 mouseup
事件中再取消这个状态。
在 mousedown
事件中我们首先应该判断,目标元素是否为拖拽元素,如果是拖拽元素,我们就设置状态并且保存这个时候鼠标的位置。
然后在 mousemove
事件中,我们通过判断鼠标现在的位置和以前位置的相对移动,来确定拖拽元素在移动中的坐标。最后 mouseup
事件触发后,清除状态,结束拖拽事件。
13. 导致页面加载白屏时间长的原因有哪些?怎么进行优化?
白屏时间:即用户点击一个链接或打开浏览器输入URL地址后,从屏幕空白到显示第一个画面的时间。
白屏时间的重要性:一个链接或者是直接在浏览器中输入URL开始进行访问时,就开始等待页面的展示。页面渲染的时间越短,用户等待的时间就越短,用户感知到页面的速度就越快。这样可以极大的提升用户的体验,减少用户的跳出,提升页面的留存率。
原因
- CSS的加载放在head里会阻塞渲染,加载时间过长会出现页面长时间白屏。
- js的的加载和执行会阻塞页面解析和渲染,加载时间过长会出现页面长时间白屏。
优化
- DNS解析优:化针对DNS Lookup环节,我们可以针对性的进行DNS解析优化。
- DNS缓存优化
- DNS预加载策略
- 稳定可靠的DNS服务器
- 根据浏览器对页面的下载、解析、渲染过程,可以考虑一下的优化处理:
- 尽可能的精简HTML的代码和结构
- 尽可能的优化CSS文件和结构
- 一定要合理的放置JS代码,尽量不要使用内联的JS代码
- 将渲染首屏内容所需的关键CSS内联到HTML中,能使CSS更快速地下载。在HTML下载完成之后就能渲染了,页面渲染的时间提前,从而缩短首屏渲染时间;
- 延迟首屏不需要的图片加载,而优先加载首屏所需图片(offsetTop<clientHeight)
- 尽量减小css、js体积,css尽量放在head标签内,head内放少量的css用于展示入场加载特效且隐藏页面真实内容,待css加载后再展示真实内容;对于js可加defer属性或者放到body最尾部以便DOM尽快展现,方便js操作DOM
14. 下面代码执行后输出什么?
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
结论: 输出5个6。
因为setTimeout
为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout
中的回调才依次执行,但输出 i
的时候当前作用域没有,往上一级再找,发现了i
,此时循环已经结束,i
变成了6
。因此会全部输出6
。
关于事件循环可点击前往学习:# 面试官:说说你对事件循环的理解
15. 关于事件循环的输出题
笔试篇
1. 请对以下数组,根据born
的值降序排列
const singers = [
{ name: 'Steven Tyler', band: 'Aerosmith', born: 1948 },
{ name: 'Karen Carpenter', band: 'The Carpenters', born: 1950 },
{ name: 'Kurt Cobain', band: 'Nirvana', born: 1967 },
{ name: 'Stevie Nicks', band: 'Fleetwood Mac', born: 1948 },
];
Array.prototype.sort()
方法用原地算法对数组的元素进行排序,并返回数组。在很多排序场景下推荐使用。
解答:
const result = singers.sort((a, b) => {
return a.born < b.born ? 1 : -1
})
console.log(result)
2. 使用js生成1-10000的数组
实现的方法很多,除了使用循环(for,while,forEach等)外,最简单的是使用Array.from
// 方法一
Array.from(new Array(10001).keys()).slice(1)
// 方法二
Array.from({length:10000},(node,i)=> i+1)
3. 实现以下转换,合并为连续的数字
[1,2,3,4,6,7,9,13,15]=>['1->4','6->7','9','13','15']
本题是一道比较简单的数组处理题目,主要有两个处理步骤:
- 将超过一个的连续数字元素,合并成
x->y
,比如 [1,2,3,4] 转成['1->4']
- 将非连续的数字元素,转成字符串
具体的实现代码如下:
function shortenArray(arr) {
// 处理边界
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
// 记录结果
const result = []
// 记录连续数字的开始位置
let start = 0
// 记录连续数字的结束位置
let last = 0
function pushArr(arrStart, arrEnd) {
if (arrStart === arrEnd) {
result.push(arr[arrStart].toString())
} else {
result.push(`${arr[arrStart]}->${arr[arrEnd]}`)
}
}
// 一次循环获取结果
for (let i = 1; i < arr.length; i++) {
const temp = arr[i]
if (arr[last] + 1 === temp) {
last = i
} else {
pushArr(start, last)
start = i
last = i
}
}
// 处理剩余数据
pushArr(start, last)
return result
}
console.log(shortenArray([1, 2, 3, 4, 6, 7, 9, 13, 15]))
// ['1->4','6->7','9','13','15']
4. 使用Promise实现每隔1秒输出1,2,3
这道题比较简单的一种做法是可以用Promise配合着reduce不停的在promise后面叠加.then,请看下面的代码:
const arr = [1, 2, 3]
arr.reduce((p, x) => {
return p.then(() => {
return new Promise(r => {
setTimeout(() => r(console.log(x)), 1000)
})
})
}, Promise.resolve())
还可以更简单的写法:
const arr = [1, 2, 3]
arr.reduce((p, x) => p.then(() => new Promise(r => setTimeout(() => r(console.log(x)), 1000))), Promise.resolve())
5. 如何使用js计算一个html页面有多少中标签?
这道题看似简单,但是是一个很有价值的一道题目。它包含了很多重要的知识:
- 如何获取所有DOM节点
- 伪数组如何转为数组
- 去重
过程
- 获取所有的DOM节点。
document.querySelectorAll('*')
此时得到的是一个NodeList集合,我们需要将其转化为数组,然后对其筛选。
-
转化为数组
[...document.querySelectorAll('*')]
一个拓展运算符就轻松搞定。
-
获取数组每个元素的标签名
[...document.querySelectorAll('*')].map(ele => ele.tagName)
使用一个map方法,将我们需要的结果映射到一个新数组。
-
去重
new Set([...document.querySelectorAll('*')].map(ele=> ele.tagName)).size
我们使用ES6中的Set对象,把数组作为构造函数的参数,就实现了去重,再使用Set对象的size方法就可以得到有多少种HTML元素了。
每文一句:蜂采百花酿甜蜜,人读群书明真理。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
转载自:https://juejin.cn/post/7166051817560735757