「2022」JavaScript最新高频面试题指南(下)
前言
大家好,我是CoderBin。本次总结了关于JavaScript的上百道高频面试考点,并且会持续更新,感谢大家的留言点赞收藏 💗
如果文中有不对、疑惑或者错字的地方,欢迎在评论区留言指正🌻
本文是《「2022」JavaScript最新高频面试题指南(上)》的后续部分。
基础篇
81. instanceof 能否判断基本数据类型?
能,但是需要自定义instanceof
行为,例如:
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('CoderBin' instanceof PrimitiveString) // true
这里将原有的instanceof方法重定义,换成了typeof,因此能够判断基本数据类型。
82. typeof 能否正确判断类型?
-
对于原始类型来说,除了判断
null
类型结果是'object'
,其他的可以调用typeof
方法显示正确的类型。 -
但对于引用数据类型,除了函数之外,都会显示
'object'
。 -
因此采用
typeof
判断对象数据类型是不合适的,采用instanceof
会更好,instanceof
的原理是基于原型链的查询,只要处于原型链中,判断永远为true
83. 什么是BigInt?
BigInt
是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对 大整数 执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
84. 0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成 0.30000000000000004。
85. 什么是防抖节流函数,并且说说它们的实现思路
点击前往学习:# 深入浅出防抖与节流函数
86. 说说JavaScript中常见的几种内存泄漏的情况
- 以外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
没有使用声明关键字的变量会被当成全局变量
- 另一种意外的全局变量可能由
this
创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
上述使用严格模式,可以避免意外的全局变量
- 定时器也常会造成内存泄露
let someResource = getData()
setInterval(function() {
let node = document.getElementById('Node')
if (node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource)
}
}, 1000)
如果id
为Node的元素从DOM
中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource
的引用,定时器外面的someResource
也不会被释放
包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放
function bindEvent() {
var obj = document.createElement('XXX')
var unused = function() {
console.log(obj, '闭包内引用obj obj不会被释放')
}
obj = null // 解决方法
}
- 没有清理对
DOM
元素的引用同样造成内存泄露
const refA = document.getElementById('refA')
document.body.removeChild(refA) // dom删除了
console.log(refA, 'refA') // 但是还存在引用能console出整个div 没有被回收
refA = null
console.log(refA, 'refA') // 解除引用
包括使用事件监听addEventListener
监听的时候,在不监听的情况下使用removeEventListener
取消对事件监听
87. 说说你对BOM的理解,以及常见的BOM对象有哪些?
BOM
(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率
常见的BOM对象有:
-
window
是BOM的核心对象,它表示浏览器的一个实例。在浏览器中,window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。因此所有在全局作用域中声明的变量、函数都会变成window
对象的属性和方法 -
location
对象用于获取或设置窗体的 URL,并且可以用于解析 URL。 -
navigator
对象主要用来获取浏览器的属性,区分浏览器类型。 -
screen
对象保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度 -
history
对象主要用来操作浏览器URL
的历史记录,可以通过参数向前,向后,或者向指定URL
跳转
88. 正则表达式是什么,有哪些应用场景?
正则表达式是一种用来匹配字符串的强有力的方法
它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的
在 JavaScript
中,正则表达式也是对象,构建正则表达式有两种方式:
- 字面量创建,其由包含在斜杠之间的模式组成
const re = /\d+/g;
- 调用
RegExp
对象的构造函数
const re = new RegExp("\\d+","g");
const rul = "\\d+"
const re1 = new RegExp(rul,"g");
使用构建函数创建,第一个参数可以是一个变量,遇到特殊字符需要使用\
进行转义
使用场景: 验证手机号码,邮箱,用户名等等需要一定规则的字符就可以使用正则表达式去校验
89. new操作符是什么,具体干了什么?
在JavaScript
中,new
操作符用于创建一个给定构造函数的实例对象
new
关键字主要做了以下的工作:
- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
简单实现:
// 实现new操作符
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = mynew(Person, "CoderBin", 18)
console.log(p) // Person {name: "CoderBin", age: 18}
p.say() // CoderBin
90. 谈谈你对this对象的理解
函数的 this
关键字在 JavaScript
中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)
this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象
点击前往学习:# 一篇文章带你搞懂 this 的四个绑定规则 ✍
91. 什么是作用域链?
首先先来了解什么是作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性
我们一般将作用域分成:
-
全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
-
函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
-
块级作用域:ES6引入了
let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量
什么是作用域链:当在Javascript
中使用一个变量的时候,首先Javascript
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
92. 谈谈JavaScript中的类型转换机制
点击前往学习:# 面试官:你说说JavaScript中类型的转换机制
93. ES6中新增的Set、Map两种数据结构怎么理解?
点击前往学习:# 面试官:说说你对Set、Map的理解
94. 说说对Websocket的了解
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
优点:说到优点,这里的对比参照物是HTTP协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。
- 支持双向通信,实时性更强。
- 更好的二进制支持。
- 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
- 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
95. forEach有中断效果吗?如何中断forEach循环?
在forEach
中用return
不会返回,函数会继续执行。
中断方法
-
使用
try
监视代码块,在需要中断的地方抛出异常。 -
官方推荐方法(替换方法):用
every
和some
替代forEach
函数。every
在碰到return false
的时候,终止循环。some
在碰到return true
的时候,终止循环。
96. call、bind、apply三者的区别,如何实现?
call
、apply
、bind
作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this
指向
-
call
方法的第一个参数是this
的指向,后面传入的是一个参数列表。改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次 -
bind
方法和call很相似,第一参数是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数 -
apply
接受两个参数,第一个参数是this
的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
三者区别总结:
- 三者都可以改变函数的
this
对象指向 - 三者第一个参数都是
this
要指向的对象,如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
如何实现,请点击前往:
97. JavaScript中如何实现继承?
JavaScript实现继承有六种方法:原型链继承、盗用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承
点击前往学习:# 面试官:你说说 js 中实现继承有哪几种方法?
98. JavaScript中的原型,原型链分别是什么?
原型:
JavaScript` 常被描述为一种基于原型的语言——每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非实例对象本身
原型链:
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
99. 如何判断页面是通过PC端还是移动端访问?
-
使用
navigator.userAgent
,只要里面包含mobi
、android
、iphone
等关键字,就可以认定是移动设备。这种方法的优点是简单方便,缺点是不可靠,因为用户可以修改这个字符串,让手机浏览器伪装成桌面浏览器。 -
使用
window.screen.width
,如果屏幕宽度小于500像素,就认为是手机。这个方法的缺点在于,如果手机横屏使用,就识别不了。 -
使用
window.orientation
,侦测屏幕方向,手机屏幕可以随时改变方向(横屏或竖屏),桌面设备做不到。window.orientation
属性用于获取屏幕的当前方向,只有移动设备才有这个属性,桌面设备会返回undefined
。(注意:iPhone 的 Safari 浏览器不支持该属性。) -
使用
ontouchstart
事件,手机浏览器的 DOM 元素可以通过ontouchstart
属性,为touch
事件指定监听函数。桌面设备没有这个属性。 -
使用第三方的工具包,推荐
react-device-detect
,它支持多种粒度的设备侦测。import {isMobile} from 'react-device-detect'; if (isMobile) { // 当前设备是移动设备 }
100. 如何让Promise.all在抛出异常后依然有效?
- 在
promise.all
队列中,使用map每一个过滤每一个promise任务,其中任意一个报错后,return一个返回值,确保promise能正常执行走到.then
中。
const p1 = new Promise((resolve, reject) => {
resolve('p1');
});
const p2 = new Promise((resolve, reject) => {
resolve('p2');
});
const p3 = new Promise((resolve, reject) => {
reject('p3');
});
Promise.all([p1, p2, p3].map(p => p.catch(e => `出错后返回的值:${e}` )))
.then(values => {
console.log(values);
}).catch(err => {
console.log(err);
})
- 使用
Promise.allSettled
替代Promise.all()
。
Promise.allSettled()
方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。
101. Promist.catch后面的.then还会执行吗?
答案:.then会继续执行
虽然Promise是开发过程中使用非常频繁的一个技术点,但是它的一些细节可能很多人都没有去关注过。我们都知道.then
, .catch
, .finally
都可以链式调用,其本质上是因为返回了一个新的Promise实例。
catch的语法形式如下:
p.catch(onRejected);
.catch
只会处理rejected
的情况,并且也会返回一个新的Promise
实例。
.catch(onRejected)
与then(undefined, onRejected)
在表现上是一致的。
事实上,catch(onRejected)从内部调用了then(undefined, onRejected)。
- 如果
.catch(onRejected)
的onRejected
回调中返回了一个状态为rejected
的Promise
实例,那么.catch
返回的Promise
实例的状态也将变成rejected
。 - 如果
.catch(onRejected)
的onRejected
回调中抛出了异常,那么.catch
返回的Promise
实例的状态也将变成rejected
。 - 其他情况下,
.catch
返回的Promise
实例的状态将是fulfilled
。
102. es5中的类和es6中的class有什么区别?
在es5中主要是通过构造函数方式和原型方式来定义一个类,在es6中我们可以通过class来定义类。它们的区别有:
-
es6的class类必须new调用,不能直接执行。
-
class类不存在变量提升
-
class类无法遍历它实例原型链上的属性和方法
-
es6为new命令引入了一个
new.target
属性,它会返回new命令作用于的那个构造函数。如果不是通过new调用或Reflect.construct()
调用的,new.target
会返回undefined -
class类有static静态方法
103. 简单说说前端路由?
概念:前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。
使用场景:在单页面应用,大部分页面结构不变,只改变部分内容的使用
优缺点:
- 优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户
- 缺点:单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置
实现方式:前端路由一共有两种实现方式,一种是通过 hash
的方式,一种是通过使用 pushState
的方式。
104. 什么是点击穿透,如何解决?
在发生触摸动作约300ms之后,移动端会模拟产生click动作,它底下的具有点击特性的元素也会被触发,这种现象称为点击穿透。
常见场景
- 情景一:遮罩层点击穿透问题,点击遮罩层(mask)上的关闭按钮,遮罩层消失后发现触发了按钮下面元素的click事件。
- 情景二:跨页面点击穿透问题:如果按钮下面恰好是一个有href属性的a标签,那么页面就会发生跳转。
- 情景三:另一种跨页面点击穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了。
- 情景四:新页面中对应位置元素恰好是a标签,然后就发生连续跳转了。这种情况概率很低
发生的条件
- 上层元素监听了触摸事件,触摸之后该层元素消失
- 下层元素具有点击特性(监听了click事件或默认的特性(a标签、input、button标签))
解决点击穿透的方法
- 方法一:书写规范问题,不要混用touch和click。既然touch之后300ms会触发click,只用touch或者只用click就自然不会存在问题了。
- 方法二:吃掉(或者说是消费掉)touch之后的click,依旧用tap,只是在可能发生点击穿透的情形做额外的处理,拿个东西来挡住、或者tap后延迟350毫秒再隐藏mask、pointer-events、在下面元素的事件处理器里做检测(配合全局flag)等。
105. 简单说说setTimeout的运行机制
setTimeout
和 setInterval
的运行机制,其实就是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。
这意味着,setTimeout指定的代码,必须等到本次执行的所有同步代码都执行完,才会执行。
106. Promise.all和Promise.allSettled有什么区别?
最大的区别:Promise.allSettled
永远不会被reject。
使用Promise.all
时,一旦有一个promise出现了异常,被reject了,尽管能用catch捕获其中的异常,但你会发现其他执行成功的Promise的消息都丢失了。
而Promise.allSettled
不会有这种问题,我们只需专注在then语句里,当有promise被异常打断时,我们依然能妥善处理那些已经成功了的promise,不必全部重来。
107. 简单说说浏览器的垃圾回收机制有哪些?
JS会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 “垃圾回收”。
一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏"。
由于是自动回收,所以就存在一个 “内存是否需要被回收的” 的问题,但是这个问题的判定在程序中意味着无法通过某个算法去准确完整的解决,后面探讨的回收机制只能有限的去解决一般的问题。
回收算法:垃圾回收对是否需要回收的问题主要依赖于对变量的判定是否可访问,由此衍生出两种主要的回收算法:
- 标记清理
- 引用计数
标记清理:标记清理是js最常用的回收策略,2012年后所有浏览器都使用了这种策略,此后的对回收策略的改进也是基于这个策略的改进。其策略是:
- 变量进入上下文,也可理解为作用域,会加上标记,证明其存在于该上下文;
- 将所有在上下文中的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃有用;
- 在此之后再被加上标记的变量标记为准备删除的变量,因为上下文中的变量已经无法访问它们;
- 执行内存清理,销毁带标记的所有非活跃值并回收之前被占用的内存;
局限:
- 由于是从根对象(全局对象)开始查找,对于那些无法从根对象查询到的对象都将被清除
- 回收后会形成内存碎片,影响后面申请大的连续内存空间
引用计数:引用计数策略相对而言不常用,因为弊端较多。其思路是对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体的规则有:
- 声明一个变量,赋予它一个引用值时,计数+1;
- 同一个值被赋予另外一个变量时,引用+1;
- 保存对该值引用的变量被其他值覆盖,引用-1;
- 引用为0,回收内存;
局限:
最重要的问题就是,循环引用的问题
function foo() {
let a = new Object()
let b = new Object()
a.c = b
b.c = a //互相引用
}
根据之前提到的规则,两个都互相引用了,引用计数不为0,所以两个变量都无法回收。如果频繁的调用改函数,则会造成很严重的内存泄漏。
108. 箭头函数中的this指向哪里?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
可以⽤Babel理解⼀下箭头函数:
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj)
}
}
}
转换后:
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this
return function() {
console.log(_this === obj)
}
}
}
109. Object.assign和扩展运算符是深拷贝还是浅拷贝,两者区别是什么?
拓展运算符
let obj = {
inObj: { a: 1, b: 2 }
}
let newObj = { ...obj }
newObj.inObj.a = 2
console.log(obj) // {inObj: {a: 2, b: 2}}
使用扩展运算符创建出新的对象,但是执行newObj.inObj.a = 2
后,原对象的里面的值也被改变了
Object.assign()
let obj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, obj)
newObj.inObj.a = 2
console.log(obj) // {inObj: {a: 2, b: 2}}
情况和扩展运算符一样,原对象里面的值也被改变了。
所以,两者都是浅拷贝。
扩展操作符(…
)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。
Object.assign()
方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
110. 浏览器一帧都会干些什么?
我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:
- 接受输入事件
- 执行事件回调
- 开始一帧
- 执行 RAF (RequestAnimationFrame)
- 页面布局,样式计算
- 绘制渲染
- 执行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事而且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。
111. 说说Object.defineProperty与Proxy的区别?
在 Vue2.x 的版本中,双向绑定是基于 Object.defineProperty
方式实现的。而 Vue3.x 版本中,使用了 ES6 中的 Proxy
代理的方式实现。
- 使用
Object.defineProperty
会产生三个主要的问题:
- 不能监听数组的变化
- 必须遍历对象的每个属性。可以通过
Object.keys()
来实现 - 必须深层遍历嵌套的对象。通过递归深层遍历嵌套对象,然后通过
Object.keys()
来实现对每个属性的劫持
- 关于Proxy
- Proxy 针对的整个对象,
Object.defineProperty
针对单个属性,这就解决了需要对对象进行深度递归(支持嵌套的复杂对象劫持)实现对每个属性劫持的问题 - Proxy 解决了
Object.defineProperty
无法劫持数组的问题 - 比
Object.defineProperty
有更多的拦截方法,对比一些新的浏览器,可能会对 Proxy 针正对性的优化,有助于性能提升
112. base64编码图片,为什么会让数据量变大?
Base64编码的思想是是采用64个基本的ASCII码字符对数据进行重新编码。它将需要编码的数据拆分成字节数组。以3个字节为一组。按顺序排列24位数据,再把这24位数据分成4组,即每组6位。再在每组的的最高位前补两个0凑足一个字节。这样就把一个3字节为一组的数据重新编码成了4个字节。当所要编码的数据的字节数不是3的整倍数,也就是说在分组时最后一组不够3个字节。这时在最后一组填充1到2个0字节。并在最后编码完成后在结尾添加1到2个"="。
( 注BASE64字符表:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/)
从以上编码规则可以得知,通过Base64编码,原来的3个字节编码后将成为4个字节,即字节增加了33.3%,数据量相应变大。所以20M的数据通过Base64编码后大小大概为20M*133.3%=26.67M。
113. 虚拟DOM一定更快吗?
虚拟DOM/domDiff
我们常说的虚拟DOM是通过JS对象模拟出来的DOM节点,domDiff是通过特定算法计算出来一次操作所带来的DOM变化。react和vue中都使用了虚拟DOM,我们借着react聊聊虚拟DOM。
react中涉及到虚拟DOM的代码主要分为以下三部分,其中核心是第二步的domDiff算法:
- 把render中的JSX(或者createElement这个API)转化成虚拟DOM
- 状态或属性改变后重新计算虚拟DOM并生成一个补丁对象(domDiff)
- 通过这个补丁对象更新视图中的DOM节点
虚拟DOM不一定更快
干前端的都知道DOM操作是性能杀手,因为操作DOM会引起页面的回流或者重绘。相比起来,通过多一些预先计算来减少DOM的操作要划算的多。
但是,“使用虚拟DOM会更快”这句话并不一定适用于所有场景。例如:一个页面就有一个按钮,点击一下,数字加一,那肯定是直接操作DOM更快。使用虚拟DOM无非白白增加了计算量和代码量。即使是复杂情况,浏览器也会对我们的DOM操作进行优化,大部分浏览器会根据我们操作的时间和次数进行批量处理,所以直接操作DOM也未必很慢。
那么为什么现在的框架都使用虚拟DOM呢?因为使用虚拟DOM可以提高代码的性能下限,并极大的优化大量操作DOM时产生的性能损耗。 同时这些框架也保证了,即使在少数虚拟DOM不太给力的场景下,性能也在我们接受的范围内。
而且,我们之所以喜欢react、vue等使用了虚拟DOM框架,不光是因为他们快,还有很多其他更重要的原因。例如react对函数式编程的友好,vue优秀的开发体验等,目前社区也有好多比较这两个框架并打口水战的,我觉着还是在两个都懂的情况下多探究一下原理更有意义一些。
114. html文档渲染过程,css文件和js文件的下载,是否会阻塞渲染?
CSS阻塞
-
css 文件的下载和解析不会影响 DOM 的解析,但是会阻塞 DOM 的渲染。因为 CSSOM Tree 要和 DOM Tree 合成 Render Tree 才能绘制页面。
-
css 文件没下载并解析完成之前,后续的 js 脚本不能执行。
-
css 文件的下载不会阻塞前面的 js 脚本执行。(所以在需要提前执行不操作 dom 元素的 js 时,不妨把 js 放到 css 文件之前。)
js阻塞
js 文件的下载和解析会阻塞 GUI 渲染进程,也就是会阻塞 DOM 和 CSS 的解析和渲染。
- js 文件没下载并解析完成之前,后续的 HTML 和 CSS 无法解析
- js 文件的下载不会阻塞前面 HTML 和 CSS 的解析(当js放在body底部时)
115. JavaScript对象中,可枚举性(enumerable)是什么?
可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for...in
循环之中(除非属性名是一个Symbol)。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性。
for..in
循环Object.keys
方法JSON.stringify
方法
var o = { a: 1, b: 2 }
o.c = 3
// 添加 d 属性,值为 4,将 enumerable 为设为 false
Object.defineProperty(o, 'd', {
value: 4,
enumerable: false
})
console.log(o.d);
// 下面的方法取不到 o.d 的值
for (var key in o) console.log(o[key])
// 1
// 2
// 3
console.log(Object.keys(o)) // ["a", "b", "c"]
console.log(JSON.stringify(o)); // => "{a:1,b:2,c:3}"
上面代码中,d 属性的enumerable
为false
,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但还是可以直接获取它的值。
至于for...in
循环和Object.keys
方法的区别,在于前者包括对象继承自原型对象的属性,而后者只包括对象本身的属性。如果需要获取对象自身的所有属性,不管enumerable
的值,可以使用Object.getOwnPropertyNames
方法。
可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性。对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty
等定义的属性,该标识值默认为 false。
116. Object.create和new有什么区别?
js中创建对象的方式一般有两种 Object.create
和 new
const Base = function() {}
const o1 = Object.create(Base)
const o2 = new Base()
在讲述两者区别之前,我们需要知道:
- 构造函数Foo的原型属性
Foo.prototype
指向了原型对象。 - 原型对象保存着实例共享的方法,有一个指针
constructor
指回构造函数。 - js中只有函数有
prototype
属性,所有的对象只有 proto 隐式属性。
那这样到底有什么不一样呢?
- 先来看看
Object.create
的实现方式
Object.create = function(o) {
var F = function() {}
F.prototype = o
return new F()
}
可以看出来。Object.create
是内部定义一个对象,并且让F.prototype
对象 赋值为引进的对象/函数 o,并return
出一个新的对象。
- 再看看
const o2 = new Base()
的时候,new做了什么。
var o1 = new Object();
o1.[[Prototype]] = Base.prototype;
Base.call(o1);
new做法是新建一个obj对象o1,并且让o1的__proto__
指向了Base.prototype
对象。并且使用 call 进行强转作用环境。从而实现了实例的创建。
区别:看似是一样的。我们对原来的代码进行改进一下。
var Base = function () {
this.a = 2
}
var o1 = new Base();
var o2 = Object.create(Base);
console.log(o1.a); // 2
console.log(o2.a); // undefined
可以看到Object.create 失去了原来对象的属性的访问。再进行下改造:
var Base = function() {
this.a = 2
}
Base.prototype.a = 3
var o1 = new Base()
var o2 = Object.create(Base)
console.log(o1.a) // 2
console.log(o2.a) // undefined
总结
比较 | new | Object.create |
---|---|---|
构造函数 | 保留原构造函数属性 | 丢失原构造函数属性 |
原型链 | 原构造函数prototype属性 | 原构造函数/(对象)本身 |
作用对象 | function | function和object |
117. 为什么部分请求中,参数需要使用encodeURIComponent进行转码?
一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。
这是因为网络标准RFC 1738做了硬性规定:
“…Only alphanumerics [0-9a-zA-Z], the special characters “$-_.+!*’(),” [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”
这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致"URL编码"成为了一个混乱的领域。有没有办法,能够保证客户端只用一种编码方法向服务器发出请求?
就是使用Javascript先对URL编码,然后再向服务器提交,不要给浏览器插手的机会。因为Javascript的输出总是一致的,所以就保证了服务器得到的数据是格式统一的。
118. 箭头函数和普通函数有什么区别?
-
语法更加简洁、清晰
-
箭头函数不会创建自己的this(重点!)
箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
-
箭头函数继承而来的this指向永远不变(重点!)
-
.call()/.apply()/.bind()
无法改变箭头函数中this的指向 -
箭头函数不能作为构造函数使用
-
箭头函数没有自己的
arguments
。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。 -
箭头函数没有原型
prototype
-
箭头函数不能用作
Generator
函数,不能使用yeild
关键字
119. WebSocket中的心跳机制是为了解决什么问题?
- 为了定时发送消息,使连接不超时自动断线,避免后端设了超时时间自动断线。所以需要定时发送消息给后端,让后端服务器知道连接还在通消息不能断。
- 为了检测在正常连接的状态下,后端是否正常。如果我们发了一个定时检测给后端,后端按照约定要下发一个检测消息给前端,这样才是正常的。如果后端没有正常下发,就要根据设定的超时进行重连。
120. async/await 和 Promise 有什么关系?
Promise
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
async/await
es2017的新语法,async/await
就是generator + promise的语法糖
async/await
和 Promise 的关系非常的巧妙,await必须在async内使用,并装饰一个Promise对象,async返回的也是一个Promise对象。
async/await
中的return/throw
会代理自己返回的Promise的resolve/reject
,而一个Promise的resolve/reject
会使得await得到返回值或抛出异常。
-
如果方法内无await节点
- return 一个字面量则会得到一个
{PromiseStatus: resolved}
的Promise。 - throw 一个Error则会得到一个
{PromiseStatus: rejected}
的Promise。
- return 一个字面量则会得到一个
-
如果方法内有await节点
async
会返回一个{PromiseStatus: pending}
的Promise(发生切换,异步等待Promise的执行结果)。Promise
的resolve
会使得await
的代码节点获得相应的返回结果,并继续向下执行。Promise
的reject
会使得await
的代码节点自动抛出相应的异常,终止向下继续执行。
121. Promise中,resolve后面的语句是否还会执行?
会被执行。如果不需要执行,需要在 resolve 语句前加上 return。
122. CSR和SSR分别是什么?
CSR: 对于html的加载,以React为例,我们习惯的做法是加载js文件中的React代码,去生成页面渲染,同时,js也完成页面交互事件的绑定,这样的一个过程就是CSR(客户端渲染)。
SSR: 但如果这个js文件比较大的话,加载起来就会比较慢,到达页面渲染的时间就会比较长,导致首屏白屏。这时候,SSR(服务端渲染)就出来了:由服务端直接生成html内容返回给浏览器渲染首屏内容。
但是服务端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入js文件来辅助实现,我们把页面的展示内容和交互写在一起,让代码执行两次,这种方式就叫同构。
CSR和SSR的区别在于,最终的html代码是从客户端添加的还是从服务端。
123. 什么是内存泄漏,什么原因导致的?
内存泄露的解释:程序中己动态分配的堆内存由于某种原因未释放或无法释放。
- 根据JS的垃圾回收机制,当内存中引用的次数为0的时候内存才会被回收
- 全局执行上下文中的对象被标记为不再使用才会被释放
内存泄漏的几种场景:
- 全局变量过多。通常是变量未被定义或者胡乱引用了全局变量
- 闭包。 未手动解决必包遗留的内存引用。
- 事件监听未被移除
- 缓存。建议所有缓存都设置好过期时间。
124. web常见的攻击方式有哪些?以及如何进行防御?
常见的攻击方式有:XSS、CSRF、SQL注入
查看详情,请点击前往:# 知道了web的攻击方式,还不快防起来?
125. 什么是微前端?
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
各个前端应用还可以独立运行、独立开发、独立部署。
微前端不是单纯的前端框架或者工具,而是一套架构体系,
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
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
使用场景:任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
注意:如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因在于每个对象的创建,方法都会被重新赋值
每文一句:蜂采百花酿甜蜜,人读群书明真理。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
- 点赞
- 收藏
- 关注作者
评论(0)