「2022」JavaScript最新高频面试题指南(上)

26,657 阅读1小时+

前言

大家好,我是CoderBin,本次总结了关于JavaScript的上百道高频面试考点,并且会持续更新,感谢大家的留言点赞收藏 💗

如果文中有不对、疑惑或者错字的地方,欢迎在评论区留言指正🌻

高频面试总结

【1】「2022」关于Vue的一些高频面试题总结

【2】「2022」CSS最新高频面试题指南

【3】「2022」TypeScript高频面试题汇总

【4】「2022」JavaScript最新高频面试题指南(上)

【5】「2022」JavaScript最新高频面试题指南(下)

基础篇

1. 将数组的length设置为0,取第一个元素会返回什么?

设置 length = 0 会清空数组,所以会返回 undefined

2. e.target 和 e.currentTarget有什么区别?

  • e.target:一个触发事件的对象的引用, 当事件处理程序在事件的冒泡或捕获阶段被调用时。指向触发事件监听的对象。 MDN解释

  • e.currentTarget:它指的是当事件遍历DOM时,标识事件的当前目标。它总是引用事件处理程序附加到的元素,而不是e.target,它标识事件发生的元素。指向添加监听事件的对象。 MDN解释

3. 如何获取到一个实例对象的原型对象?

  • 从 构造函数 获得 原型对象:
构造函数.prototype
  • 从 对象实例 获得 父级原型对象
方法一: 对象实例.__proto__ 【 有兼容性问题,不建议使用】
方法二:Object.getPrototypeOf(对象实例)

4. Math.ceil和Math.floor有什么区别?

Math.ceil(): 向上取整,函数返回一个大于或等于给定数字的最小整数。

Math.floor(): 向下取整,函数返回一个小于或等于给定数字的最大整数。

5. 浏览器的同源策略是什么?

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

所谓同源是指:域名协议端口相同。

另外,同源策略又分为以下两种:

  • DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
  • XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。

6. document.write和innerHTML有什么区别?

document.write:是直接写入到页面的内容流,如果在写之前没有调用document.open, 浏览器会自动调用open。

  • 每次写完关闭之后重新调用该函数,会导致页面被重写。

innerHTML:则是DOM页面元素的一个属性,代表该元素的html内容。你可以精确到某一个具体的元素来进行更改。如果想修改document的内容,则需要修改document.documentElement.innerElement

  • innerHTML将内容写入某个DOM节点,不会导致页面全部重绘
  • innerHTML很多情况下都优于document.write,其原因在于其允许更精确的控制要刷新页面的哪一个部分。

7. 使用原生js给一个按钮绑定两个onclick事件

使用事件监听 绑定多个事件

//事件监听 绑定多个事件
var btn = document.getElementById("btn");

btn.addEventListener("click",hello1);
btn.addEventListener("click",hello2);

function hello1(){
 alert("hello 1");
}
function hello2(){
 alert("hello 2");
}

8. 什么是类数组对象?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象。

类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有以下4种:

//(1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);

//(2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);

//(3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);

//(4)通过 Array.from 方法来实现转换
Array.from(arrayLike);

9. typeof NaN 的结果是什么?

NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

10. JavaScript中的基本数据类型有哪些?

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

11. 如何把十进制的0.2转换成二进制?

进制转换是比较基础的,如果大家熟悉 js 的 API ,那么会首先想到这两个方法:

  • 十进制转二进制:num.toString(2)
  • 二进制转十进制:parseInt(num, 2)

所以答案就是 (2).toString(2)

12. map和filter有什么区别?

首先,map和filter函数的参数,是完全相同的

array.map(function(currentValue,index,arr), thisValue)

array.filter(function(currentValue,index,arr), thisValue)

  • currentValue:数组元素;
  • index:索引
  • arr:原数组;
  • thisValue:作为该执行回调时使用,传递给函数,用作 "this" 的值

但是在用途上,它们是有区别的:

  1. map方法返回的新数组是原数组的映射,何为映射?就是和原数组的长度相同,数值做相应处理。
  2. filter方法返回的值是过滤原数组后的新数组,和原数组长度不同,数值不变。

示例

let arr = ["1","2","3"];
let a = arr.map((item,index,a) =>{
    return item + 1
});
console.log(a);//["11", "21", "31"]
let b = arr.filter((item,index,a) =>{
    return item > 1
})
console.log(b);//["2", "3"]

另外,filter可过滤NaN、null、undefined、0

let arr = [NaN,null,undefined,"0",0,1,2,3];
let newArr = arr.filter(item => item);
console.log(newArr);//["0", 1, 2, 3]

13. argments如何转换成数组?

argments属于类数组对象,转换成数组的方法可以看第八题

14. ['1', '2', '3'].map(parseInt) 的返回值是什么?

首先整个题目考校的是两个函数,和一个字符串转数字的概念

  1. 数组的map函数,接受三个参数,当前值,当前索引,当前数组。
  2. parseInt接受两个参数,需要转换的字符串,基数(基数取值范围2~36)
    var new_array = arr.map(function callback(currentValue, index, array) {  
      // Return element for new_array  
     })  
     parseInt(string, radix)
    
  3. 根据上面的两个函数的解释,我们可以发现实际上,上面的['1','2','3'].map(parseInt) 其实就是等价于下面的代码。
    ['1','2','3'].map((item, index) => {  
      return parseInt(item, index)  
    })  
    //  parseInt('1', 0)  1  
    //  parseInt('2', 1)  NaN  
    //  parseInt('3', 2)  NaN
    
  4. 如果我们需要返回1,2,3需要怎么办?
    function parseIntFun(item) {  
      return parseInt(item, 10)  
    }  
    ['1','2','3'].map(parseIntFun)  
    //  parseInt('1', 10)  1  
    //  parseInt('2', 10)  2  
    //  parseInt('3', 10)  3
    

综上所述,返回值是 [1,NaN,NaN]

15. 说说你对new.target的理解

new.target属性允许你检测函数或构造方法是否是通过new运算符被调用的。

在通过new运算符被初始化的函数或构造方法中,new.target返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是undefined。

我们可以使用它来检测,一个函数是否是作为构造函数通过new被调用的。

function Foo() {
  if (!new.target) {
    throw "Foo() must be called with new";
  }
  console.log("Foo instantiated with new");
}

Foo(); // throws "Foo() must be called with new"
new Foo(); // logs "Foo instantiated with new"

16. arguments这种类数组,如何遍历?

  1. 使用普通的for循环
for(var i = 0, len = arrayLike.length; i < len; i++) {
  ……
}
  1. 转换为真正的数组,再进行遍历(具体看第八题)

17. new Fn() 和 new Fn 有区别吗?

用 new 创建构造函数的实例时,通常情况下 new 的构造函数后面需要带括号(譬如:new fn())。具体的区别要看在什么场景下

有些情况下new的构造函数后带括号和不带括号的情况一致,譬如:

function Fn(){
  this.num = 1;
}
console.log(new Fn()); //Fn {num:1}
console.log(new Fn);   //Fn {num:1}

但有些情况下new的构造函数后带括号和不带括号的情况并不一致,譬如:

function Fn(){
  this.num = 1;
}
console.log(new Fn().num);  // 1
console.log(new Fn.num);    // 报错

结果分析:

  • 从报错信息来看,new Fn.num执行顺序是这样的:先执行Fn.num,此时返回结果为undefined;后执行new,因new后面必须跟构造函数,所以new undefined会报错。

  • new Fn().num相当于(new Fn()).num,所以结果返回1。

从结果来看,new Fn.num代码相当于new (Fn.num)new Fn().num相当于(new Fn()).num。由此看来 new 的构造函数后跟括号优先级会提升。

18. Object与Map有什么区别?

  • Object

在ECMAScript中,Object是一个特殊的对象。它本身是一个顶级对象,同时还是一个构造函数,可以通过它(如:new Object())来创建一个对象。我们可以认为JavaScript中所有的对象都是Object的一个实例,对象可以用字面量的方法const obj = {}声明。

  • Map

Map是Object的一个子类,可以有序保存任意类型的数据,使用键值对去存储,其中键可以存储任意类型,通过const m = new Map()即可得到一个map实例。

  • 使用场景
  1. 如果只需要简单的存储key-value的数据,并且key不需要存储复杂类型的,直接用对象
  2. 如果该对象必须通过JSON转换的,则只能用对象,目前暂不支持Map
  3. map的阅读性更好,所有操作都是通过api形式去调用,更有编程体验

20. cookie的有效时间设置为0会怎么样?

Cookie过期时间设置为0,表示跟随系统默认,其销毁与Session销毁时间相同,即都在浏览器关闭后的特定时间删除。如果我们写程序的时候不设置Cookie的有效时间,那么,Cookie的有效时间等效于会话时间。

21. const声明了数组,还能push元素吗,为什么?

可以

数组是引用类型,const声明的引用类型变量,不可以变的是变量引用始终指向某个对象,不能指向其他对象,但是所指向的某个对象本身是可以变的

22. 如何检测数组和对象?

  • 方法1:通过 ES6 中的 Array.isArray 来识别
console.log(Array.isArray([])) // true 
console.log(Array.isArray({})) // false
  • 方法2:通过 instanceof 来识别
console.log([] instanceof Array) // true 
console.log({} instanceof Array) // false
  • 方法3:通过调用 constructor 来识别
console.log([].constructor) // [Function: Array] 
console.log({}.constructor) // [Function: Object]
  • 方法4:通过 Object.prototype.toString.call 方法来识别
console.log(Object.prototype.toString.call([])) // [object Array] 
console.log(Object.prototype.toString.call({})) // [object Object]

23. 请简述 == 的机制

  • undefined == null,结果是true。且它俩与所有其他值比较的结果都是false
  • String == Boolean,需要两个操作数同时转为Number。
  • String/Boolean == Number,需要String/Boolean转为Number。
  • Object == Primitive,需要Object转为Primitive(具体通过valueOftoString方法)。

24. js中的undefined和ReferenceError:xxx is not defined 有什么区别?

  • undefined:当一个变量声明后,没有被赋值,那么它就是undefined类型。
  • ReferenceError:当尝试引用一个未定义的变量/函数时,就会抛出ReferenceError。

25. Math.ceil()、Math.round()、Math.floor() 三者的区别是什么?

  • Math.ceil() 向上取整
  • Math.round() 四舍五入
  • Math.floor() 向下取整

26. 解释下面代码的意图

Array.prototype.slice.apply(arguments)

arguments 为类数组对象,并不是真正的数组。

slice可以实现数组的浅拷贝。

由于 arguments不是真正的数组,所以没有slice方法,通过apply可以调用数组对象的slice方法,从而将arguments 类数组转换为数组。

27. 直接在script标签写export为什么会报错?

现代浏览器可以支持用 script 标签引入模块或者脚本,如果要引入模块,必须给 script 标签添加 type=“module”。如果引入脚本,则不需要 type。

28. mouseover和mouseenter有什么区别?

当鼠标移动到元素上时就会触发 mouseenter 事件,类似 mouseover,它们两者之间的差别是 mouseenter 不会冒泡。

由于 mouseenter 不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其 mouseover 和 mouseout 事件,但是却不会触发 mouseenter 和 mouseleave 事件。

29. offsetWidth/offsetHeight,clientWidth/clientHeight与scrollWidth/scrollHeight的区别?

offsetWidth/offsetHeight 返回的是元素的布局宽度,它的值包含 content + padding + border 包含了滚动条。

offsetTop 返回的是当前元素相对于其 offsetParent 元素的顶部的距离。

offsetLeft 返回的是当前元素相对于其 offsetParent 元素的左部的距离。

clientTop 返回的是上边框的宽度。

clientLeft 返回的左边框的宽度。

clientWidth/clientHeight 返回的是元素的内部宽度,它的值只包含 content + padding,如果有滚动条,不包含滚动条。

scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸。

scrollTop 属性返回的是一个元素的内容垂直滚动的像素数。

scrollLeft 属性返回的是元素滚动条到元素左边的距离。

30. toPrecision、toFixed、Math.round 三者的区别

  • toPrecision 用于处理精度,精度是从左至右第一个不为 0 的数开始数起。
  • toFixed 是对小数点后指定位数取整,从小数点开始数起。
  • Math.round 是将一个数字四舍五入到一个整数。

31. 什么是Polyfill?

Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。

比如说 querySelectorAll 是很多现代浏览器都支持的原生 Web API,但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就可以成为一个 Polyfill。

32. setTimeout为什么不能保证能够及时运行?

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop。

setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

浏览器的JS引擎遇到setTimeout,拿走之后不会立即放入异步队列,同步任务执行之后,timer模块会到设置时间之后放到异步队列中。js引擎发现同步队列中没有要执行的东西了,即运行栈空了就从异步队列中读取,然后放到运行栈中执行。所以setTimeout可能会多了等待线程的时间。

这时setTimeout函数体就变成了运行栈中的执行任务,运行栈空了,再监听异步队列中有没有要执行的任务,如果有就继续执行,如此循环,就叫Event Loop。

33. JS中如何阻止事件冒泡和默认事件?

  1. 阻止事件的冒泡方法:event.stopPropagation()

不让事件向 document 上蔓延,但是默认事件仍然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开,

  1. 阻止默认事件的方法:event.preventDefault()

比如在a标签的绑定事件上调用此方法,连接则不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;

  1. return false

这个方法比较暴力,它会同时阻止事件冒泡也会阻止默认事件;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()event.preventDefault()

34. 谈谈你对事件冒泡和捕获的理解

事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

事件冒泡: 微软提出了名为事件冒泡(event bubbling)的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。

因此在事件冒泡的概念下在p元素上发生click事件的顺序应该是p -> div -> body -> html -> document

事件捕获: 网景提出另一种事件流名为事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

因此在事件捕获的概念下在p元素上发生click事件的顺序应该是document -> html -> body -> div -> p

35. 什么是事件代理?

事件代理(Event Delegation)也称之为事件委托。是JavaScript中绑定事件的常用技巧。

顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。

事件代理的原理是DOM元素的事件冒泡

一个事件触发后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 捕获阶段:从window对象传导到目标节点(上层传到底层)称为“捕获阶段”(capture phase),捕获阶段不会响应任何事件;
  • 目标阶段:在目标节点上触发,称为“目标阶段”
  • 冒泡阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层。

事件代理的优点:

  • 可以大量节省内存占用,减少事件注册。
  • 可以实现当新增子对象时无需再次对其绑定(动态绑定事件)

36. 浏览器为什么要有跨域限制?

因为存在浏览器同源策略,所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到,跨域限制主要的目的就是为了用户的上网安全。

如果浏览器没有同源策略,会存在什么样的安全问题呢。下面从 DOM 同源策略和 XMLHttpRequest 同源策略来举例说明:

如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:

做一个假网站,里面用 iframe 嵌套一个银行网站 xxxbank.com 把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。这时如果用户输入账号密码,我们的主网站可以跨域访问到 xxxbank.com  的 dom 节点,就可以拿到用户的账户密码了。

如果没有 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:

用户登录了自己的银行页面 xxxbank.com, 该网站向用户的 cookie 中添加用户标识。用户浏览了恶意页面 evil.com, 执行了页面中的恶意  AJAX 请求代码。evil.com  向 xxxbank.com 发起 AJAX HTTP 请求,请求会默认把 xxxbank.com  对应 cookie 也同时发送过去。银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。而且由于 Ajax 在后台执行,用户无法感知这一过程。

因此,有了浏览器同源策略,我们才能更安全的上网。

37. XML和JSON有什么区别?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它完全独立于语言。它基于JavaScript编程语言,易于理解和生成。

xml 是可扩展标记语言,它定义了一组规则,用于以人类可读和机器可读的格式编码文档。XML的设计目标侧重于Internet上的简单性,通用性和可用性。它是一种文本数据格式,通过Unicode为不同的人类语言提供强大的支持。尽管XML的设计侧重于文档,但该语言被广泛用于表示任意数据结构,例如Web服务中使用的那些数据结构。

以下是JSON和XML之间的一些区别:

1、JSON是一种轻量级的数据交换格式;XML是可扩展标记语言。

2、JSON是基于JavaScript语言;XML源自SGML。

3、JSON是一种表示对象的方式;XML是一种标记语言,使用标记结构来表示数据项。

4、JSON不提供对命名空间的任何支持;XML支持名称空间。

5、JSON支持数组;XML不支持数组。

6、XML的文件相对难以阅读和解释;JSON的文件非常易于阅读。

7、JSON不使用结束标记;XML有开始和结束标签。

8、JSON的安全性较低;XML比JSON更安全。

38. null和undefined有什么区别?

首先 Undefinednull 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

  1. undefined 代表的含义是未定义,一般变量声明了但还没有定义的时候会返回 undefined.

  2. null 代表的含义是空对象,主要用于赋值给一些可能会返回对象的变量,作为初始化。

  3. 当对null类型使用 typeof 进行判断时,会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

  4. 当对undefined类型使用 typeof 进行判断时,会返回'undefined'

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

39. 为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

40. “严格模式”是什么?

除了正常运行模式,ECMAscript 5添加了第二种运行模式:"严格模式"(strict mode)。顾名思义,这种模式使得Javascript在更严格的条件下运行。

设立"严格模式"的目的,主要有以下几个:

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript做好铺垫。

"严格模式"体现了Javascript更合理、更安全、更严谨的发展方向,包括IE 10在内的主流浏览器,都已经支持它,许多大项目已经开始全面拥抱它。

另一方面,同样的代码,在"严格模式"中,可能会有不一样的运行结果;一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。

41. Babel是什么?

Babel 是一个 JavaScript 编译器,同时也是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

42. npm是什么?

npm是Node.js的包管理工具,它的诞生也极大的促进了前端的发展,在现代前端开发中都离不开npm的身影。 常见的使用场景有以下几种:

  • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

42. '1'.toString() 为什么不会报错?

其实在这个语句运行的过程中做了这样几件事情:

let s = new Object('1'); 
s.toString(); 
s = null;
  • 第一步: 创建Object类实例。注意为什么不是String?由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类。
  • 第二步: 调用实例方法。
  • 第三步: 执行完方法立即销毁这个实例。

整个过程体现了 基本包装类型 的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。

43. WebSocket如何兼容低版本浏览器?

  • Adobe Flash Socket;
  • ActiveX HTMLFile (IE) ;
  • 基于 multipart 编码发送 XHR;
  • 基于长轮询的 XHR;

44. 什么是跨域?

跨域本质是浏览器基于同源策略的一种安全手段

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

45. JavaScript字符串常用的方法有哪些?

  1. concat():用于将一个或多个字符串拼接成一个新字符串

  2. slice()、substr()、substring():这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。

  3. trim()、trimLeft()、trimRight():删除前、后或前后所有空格符,再返回新的字符串

  4. repeat():接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

  5. padEnd():复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

  6. toLowerCase()、 toUpperCase():大小写转化

  7. charAt():返回给定索引位置的字符,由传给方法的整数参数指定

  8. indexOf():从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )

  9. startWith()、includes():从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值

  10. split:把字符串按照指定的分割符,拆分成数组中的每一项

  11. match:接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组

  12. replace:接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)

面试时能说几个是几个

46. ES6 中数组做了哪些新扩展?

点击前往:# ES6中数组做了哪些新扩展?

47. 如何判断当前脚本运行在浏览器还是Node中?

通过判断 Global 对象是否为 window,如果不为 window,当前脚本没有运行在浏览器中。

this === window ? 'browser' : 'node';

48. 说说你对以下几个生命周期事件的理解?

  1. DOMContentLoaded
  2. load
  3. beforeunload
  4. unload

点击前往:# 浅析HTML页面的生命周期

49. JavaScript中的变量提升是什么?

函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。

在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向,其中创建变量对象的过程中,首先会为 arguments 创建一个属性,值为 arguments,然后会扫描 function 函数声明,创建一个同名属性,值为函数的引用,接着会扫描 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。

50. null是对象吗?为什么?

null不是对象。

虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。

51. JavaScript本地存储的方式有哪些,有什么区别,以及有哪些应用场景?

javaScript本地缓存的方式主要有以下四种:

  1. localStorage
  2. sessionStorage
  3. cookie
  4. indexedDB

它们的区别和应用场景,点击前往:# 关于JavaScript的本地存储方案

52. typeof与instanceof的区别?

首先,typeofinstanceof都是判断数据类型的方法,区别如下:

  1. typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值

  2. instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型(除非自定义instanceof方法)

  3. typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串

52. 谈谈你知道的DOM常见的操作

点击前往:# 面试官:谈谈你知道的DOM常见的操作

53. ==和===的区别,分别在什么情况下使用?

  1. == 相等:先检查两个操作数数据类型,如果相同, 则进行===比较, 如果不同, 会进行一次类型转换, 转换成相同类型后再进行比较。比较规则如下:
  • 两个都为简单类型,字符串和布尔值都会转换成数值,再比较
  • 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
  • 两个都为引用类型,则比较它们是否指向同一个对象
  • nullundefined 相等
  • 存在 NaN 则返回 false
  1. === 全等:即数据类型与值都必须相等。如果数据类型不一致,则返回false
  • null 和 undefined 比较,会返回false

使用场景: 除了在比较对象属性为null或者undefined的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)

54. JavaScript中的数组有哪些常用的方法?

  1. 增加:push、unshift、splice、concat
  2. 删除:pop、shift、splice、slice
  3. 修改:splice
  4. 查找:indexOf、includes、find
  5. 排序:reverse、sort
  6. 转换:join
  7. 迭代:some、every、forEach、filter、map、reduce

具体用法请前往:# JavaScript数组常用方法大全

55. ES6中对函数做了哪些拓展?

  1. 允许为函数的参数设置默认值,并且可以结合解构赋值使用;
  2. 函数具有length属性,返回没有指定默认值的参数个数;
  3. 函数具有name属性,返回该函数的函数名;
  4. 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
  5. 新增“箭头函数”写法

拓展:箭头函数写法有以下注意点

  • 函数体内的this指向外层做作用域
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数

55. ES6中对象新增了哪些拓展?

  1. 当对象键名与对应值名相等的时候,可以进行简写 const obj = { name }
  2. 允许字面量定义对象时,将表达式放在括号内
let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
  1. 在解构赋值中,未被读取的可遍历的属性,可以分配到指定的对象上面。注意:拓展运算符形式的解构赋值必须是最后一个参数,否则会报错
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
  1. ES6 一共有 5 种方法可以遍历对象的属性。
    • for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)

    • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名

    • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名

    • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名

    • Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举

  2. 对象新增的方法:
    • Object.is():严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身

    • Object.assign():方法用于对象的合并,将源对象source(第二个参数)的所有可枚举属性,复制到目标对象target(第一个参数)

    • Object.getOwnPropertyDescriptors():返回指定对象所有自身属性(非继承属性)的描述对象

    • Object.setPrototypeOf():方法用来设置一个对象的原型对象

    • Object.getPrototypeOf(): 用于读取一个对象的原型对象

    • Object.keys(): 返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组

    • Object.values(): 返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组

    • Object.entries(): 返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组

    • Object.fromEntries(): 用于将一个键值对数组转为对象

对象的方法具体使用可点击前往 MDN-Object

56. var、let、const三者的区别?

1. var特点

  • 声明作用域(函数作用域)
  • 变量提升
  • 可重复声明同个变量
  • 全局声明的变量会成为全局对象的属性

2. let特点

  • 块级作用域
  • 不存在变量提升
  • 不允许重复声明
  • 防止变量泄露(for循环的迭代变量)

3. const特点

  • 声明变量时,同时初始化
  • 块级作用域
  • 暂时性死区
  • 不可修改
  • 不允许重复声明
  • 不能用来声明迭代变量

详细解析可点击前往:# 面试官:你说说var、let、const三者的区别

57. script标签放在header里和放在body底部有什么区别?

1、浏览器是从上到下解析HTML的。
2、放在head里的js代码,会在body解析之前被解析;放在body底部的js代码,会在整个页面加载完成之后解析。

  • head 部分中的脚本: 需调用才执行的脚本或事件触发执行的脚本放在HTML的head部分中。当你把脚本放在head部分中时,可以保证脚本在任何调用之前被加载,

  • body 部分中的脚本: 当页面被加载时立即执行的脚本放在HTML的body部分。放在body部分的脚本通常被用来生成页面的内容。

脚本会阻塞页面的渲染,所以推荐将其放在 body 底部,因为当解析到 script 标签时,通常页面的大部分内容都已经渲染完成,让用户马上能看到一个非空白页面。

58. 什么是同步和异步?

同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。

异步:异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理,这样就可以提高执行的效率了,即异步是我们发出的一个请求,该请求会在后台自动发出并获取数据,然后对数据进行处理,在此过程中,我们可以继续做其他操作,不管它怎么发出请求,不关心它怎么处理数据。

59. 遍历数组的方法都有哪些?

for循环方法:forfor...offor...in

内置方法:some、every、forEach、filter、map、reduce

具体用法请前往:# JavaScript数组常用方法大全

60. 简单说说对web worker的了解

Web Workers 是在HTML5中出现的,它可以使一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞。

它的作用就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给worker线程执行,这样主线程就会变得轻松,不会被阻塞或拖慢。这并不意味着JS语言本身支持了多线程能力,而是浏览器作为宿主环境提供了JS一个多线程运行的环境。

61. js实现的动画和CSS动画分别有什么优缺点?

1. JS 动画

优点

  • 控制能力很强, 可以在动画播放过程中对动画进行控制:开始、暂停、回放、终止、取消都是可以做到的。
  • 动画效果比css3动画丰富,有些动画效果,比如曲线运动,冲击闪烁,视差滚动效果,只有js动画才能完成
  • CSS3有兼容性问题,而JS大多时候没有兼容性问题

缺点

  • 代码的复杂度高于CSS动画
  • JavaScript在浏览器的主线程中运行,而主线程中还有其它需要运行的JavaScript脚本、样式计算、布局、绘制任务等,对其干扰导致线程可能出现阻塞,从而造成丢帧的情况

2. CSS动画

优点

  • 浏览器可以对动画进行优化
  • 代码相对简单,性能调优方向固定
  • 对于帧速表现不好的低版本浏览器,CSS3可以做到自然降级,而JS则需要撰写额外代码

缺点

  • 运行过程控制较弱,无法附加事件绑定回调函数
  • 代码冗长,想用CSS实现稍微复杂一点动画,最后CSS代码都会变得非常笨重

62. 前端实现动画有哪些方式?

前端常用的动画实现方式有以下种:

  1. css3的transition 属性
  2. css3的animation 属性
  3. 原生JS动画
  4. 使用canvas绘制动画
  5. SVG动画
  6. Jquery的animate函数
  7. 使用gif图片
  • 代码复杂度方面,简单动画:css代码实现会简单一些,js复杂一些。 复杂动画的话:css代码就会变得冗长,js实现起来更优。
  • 动画运行时,对动画的控制程度上, js 比较灵活,能控制动画暂停,取消,终止等,css动画不能添加事件,只能设置固定节点进行什么样的过渡动画。
  • 兼容方面, css 有浏览器兼容问题,js大多情况下是没有的。
  • 性能方面, css动画相对于优一些,css 动画通过GUI解析,js动画需要经过js引擎代码解析,然后再进行 GUI 解析渲染。

63. 说一说Cookie、SessionStorage、LocalStorage三者的区别?

首先,Cookie、SessionStorage、 LocalStorage都是浏览器的本地存储。它们之前的区别可以从以下几个方面来说:

  1. 写入方式:cookie是由服务器端写入的,而SessionStorage、 LocalStorage都是由前端写入的

  2. 生命周期:cookie的生命周期是由服务器端在写入的时候就设置好的,LocalStorage是写入就一直存在,除非手动清除,SessionStorage是页面关闭的时候就会自动清除。

  3. 存储大小:cookie的存储空间比较小大概4KB,SessionStorage、 LocalStorage存储空间比较大,大概5M。

  4. 数据共享:三者数据共享都遵循同源原则,SessionStorage还限制必须是同一个页面。

  5. 发送请求是否携带:在前端给后端发送请求的时候会自动携带Cookie中的数据,但是SessionStorage、 LocalStorage不会。

  6. 应用场景:Cookie一般用于存储登录验证信息SessionID或者token,LocalStorage常用于存储不易变动的数据,减轻服务器的压力,SessionStorage可以用来检测用户是否是刷新进入页面,如音乐播放器恢复播放进度条的功能。

扩展:从安全性来说,因为每次http请求都会携带cookie信息,这样无形中浪费了带宽,所以cookie应该尽可能少的使用,另外cookie还需要指定作用域,不可以跨域调用,限制比较多。但是用来识别用户登录来说,cookie还是比storage更好用的。其他情况下,尽量使用storage。

storage在存储数据的大小上面秒杀了cookie,现在基本上很少使用cookie了。

localStorage和sessionStorage唯一的差别一个是永久保存在浏览器里面,一个是关闭网页就清除了信息。localStorage可以用来跨页面传递参数,sessionStorage用来保存一些临时的数据,防止用户刷新页面之后丢失了一些参数。

64. js中数组是如何在内存中存储的?

数组不是以一组连续的区域存储在内存中,而是一种哈希映射的形式。它可以通过多种数据结构来实现,其中一种是链表。

js分为基本类型和引用类型:

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问;
  • 引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

65. 如何检测浏览器版本?

检测浏览器版本一共有两种方式:

  1. 一种是检测 window.navigator.userAgent 的值,但这种方式很不可靠,因为 userAgent 可以被改写,并且早期的浏览器如 ie,会通过伪装自己的 userAgent 的值为 Mozilla 来躲过服务器的检测。

  2. 第二种方式是功能检测,根据每个浏览器独有的特性来进行判断,如 ie 下独有的 ActiveXObject

66. JavaScript中的错误有哪些类型?

  1. ErrorError是最基本的错误类型,其他的错误类型都继承自该类型。因此,所有错误的类型共享了一组相同的属性。 这个类型的错误很少见。一般使用开发人员自定义抛出的错误。

  2. EvalError:这个错误会在使用eval()函数发生异常时候抛出。

  3. RangeError:在数值超出相应范围时触发。

  4. ReferenceError:一般出现在变量找不到的情况时触发。

  5. SyntaxError:当Javascript语言解析代码时,Javascript引擎发现了不符合语法规范的tokens或token顺序时抛出SyntaxError。

  6. TypeError:这个错误在JavaScript中是经常遇到的,不管是初学者还是老手。在变量中保存着以外的类型时,或者在访问不存在的方法时。都会导致这种错误。但是归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。

  7. URIError:在使用encodeURI或者decodeURI因为URL格式不正确时,就会导致URIError错误。这种错误也很少见。

67. ajax、fetch、axios有什么区别?

(1) AJAX

Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

(2)Fetch

fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。

fetch的优点:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

fetch的缺点:

  • fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • fetch默认不会带cookie,需要添加配置项:fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios

Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

68. for...in 和 for...of 有什么区别?

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下:

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for…in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for…of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结:for...in循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

69. 如果new一个箭头函数会怎么样?

会直接报错。箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

1、创建一个空的简单JavaScript对象(即{});

2、为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;

3、将步骤1新创建的对象作为this的上下文 ;

4、如果该函数没有返回对象,则返回this。

所以,上面的第二、三步,箭头函数都是没有办法执行的。

70. 检测数据类型的方式有哪些?

1. typeof:其中数组、对象、null都会被判断为object,其他判断都正确。

console.log(typeof 1);              // number
console.log(typeof true);            // boolean
console.log(typeof 'CoderBin');       // string
console.log(typeof [1,2]);           // object    
console.log(typeof function(){});     // function
console.log(typeof {});             // object
console.log(typeof undefined);       // undefined
console.log(typeof null);           // object

2. instanceof:可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

console.log(1 instanceof Number);               // false
console.log(true instanceof Boolean);            // false 
console.log('CoderBin' instanceof String);        // false 
console.log([1,2,3] instanceof Array);           // true
console.log(function(){} instanceof Function);    // true
console.log({} instanceof Object);              // true

可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

3. constructor

console.log((1).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('CoderBin').constructor === String); // true
console.log(([1,2]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

4. Object.prototype.toString.call(): 使用 Object 对象的原型方法 toString 来判断数据类型:

var test = Object.prototype.toString;
console.log(test.call(1));
console.log(test.call(true));
console.log(test.call('CoderBin'));
console.log(test.call([1,2,3]));
console.log(test.call(function(){}));
console.log(test.call({}));
console.log(test.call(undefined));
console.log(test.call(null));

// 结果如下:
// [object Number]
// [object Boolean]
// [object String]
// [object Array]
// [object Function]
// [object Object]
// [object Undefined]
// [object Null]

71. Object.is() 与比较操作符"==="、"=="的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。

  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。

  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

72. isNaN 和 Number.isNaN 函数有什么区别?

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

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

总结: 和全局函数 isNaN() 相比,Number.isNaN() 不会自行将参数转换成数字,只有在参数是值为 NaN 的数字时,才会返回 true。

Number.isNaN() 方法确定传递的值是否为NaN,并且检查其类型是否为Number。它是原来的全局isNaN() 的更稳妥的版本。

73. 谈谈你对浏览器中进程和线程的理解

进程和线程(一个进程中可以有多个线程)

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)

  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

进程之间互相独立。线程是不能单独存在的,它是由进程来启动和管理的。

进程和线程的关系

  • 进程中任意一个线程执行出错,都会导致整个进程的崩溃
  • 线程之间共享进程中的数据
  • 当一个进程关闭之后,操作系统会回收进程所占用的内存
  • 进程之间的内容相互隔离
  • 一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,就需要使用用于进程间通信(IPC)的机制

浏览器是多进程的

一般每打开一个Tab页,就相当于创建了一个独立的浏览器进程。但有时会进行进程合并。

74. 什么是尾调用优化和尾递归?

点击前往:# 关于 "尾调用优化" 的那些事儿 👏

75. JS代码中的"use strict"是什么意思?

use strict 是一种ECMAscript5添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。

设立"严格模式"的目的,主要有以下几个:

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript 做好铺垫。

区别:

  • 禁止使用with语句。
  • 禁止this关键字指向全局对象。
  • 对象不能有重名的属性。

76. Service worker是什么?

Srvice workerPWA 的重要组成部分,W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API,主要用来做持久的离线缓存,也是Web Worker的升级版。

Service worker (简称 SW) 是一个注册在指定源和路径下的事件驱动 Worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

77. 如何判断一个对象是不是空对象?

  1. 使用 Object.keys() 将对象属性名组成数组,如果 length0 表示空对象
  2. 使用 JSON.stringify() 将对象转成字符串,如果为 {} 则表示空对象
// 方法 1 
Object.keys(obj).length === 0 

// 方法 2 
JSON.stringify(obj) === '{}'

78. JSBridge是什么?

JSBridge 是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置、摄像头)。

实际上,JSBridge 就像其名称中的Bridge的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是构建 Native 和非 Native 间消息通信的通道,而且这个通信的通道是双向的。

  • 双向通信的通道: JS 向 Native 发送消息: 调用相关功能、通知 Native 当前 JS 的相关状态等。
  • Native 向 JS 发送消息: 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

79. [] == ![] 结果是什么?

结果为 true

==(双等于) 中,左右两边都需要转换为数字然后进行比较。

  • []转换为数字为0

  • ![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为true, 因此![]false,进而再转换成数字,变为00 == 0, 结果为 true

80. Object.is 和 === 有什么区别?

Object.is()===(严格等于)的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。

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中常见的几种内存泄漏的情况

  1. 以外的全局变量
function foo(arg) {
  bar = "this is a hidden global variable";
}

没有使用声明关键字的变量会被当成全局变量

  1. 另一种意外的全局变量可能由 this 创建:
function foo() {
  this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

上述使用严格模式,可以避免意外的全局变量

  1. 定时器也常会造成内存泄露
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 // 解决方法
}
  1. 没有清理对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对象有:

  1. window是BOM的核心对象,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法

  2. location对象用于获取或设置窗体的 URL,并且可以用于解析 URL。

  3. navigator 对象主要用来获取浏览器的属性,区分浏览器类型。

  4. screen对象保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度

  5. history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转

88. 正则表达式是什么,有哪些应用场景?

正则表达式是一种用来匹配字符串的强有力的方法

它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的

在 JavaScript中,正则表达式也是对象,构建正则表达式有两种方式:

  1. 字面量创建,其由包含在斜杠之间的模式组成
const re = /\d+/g;
  1. 调用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引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

什么是作用域链:当在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监视代码块,在需要中断的地方抛出异常。

  • 官方推荐方法(替换方法):用everysome替代forEach函数。

    • every 在碰到return false的时候,终止循环。
    • some 在碰到return true的时候,终止循环。

96. call、bind、apply三者的区别,如何实现?

callapplybind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

  1. call方法的第一个参数是this的指向,后面传入的是一个参数列表。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

  2. bind方法和call很相似,第一参数是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

  3. apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

三者区别总结:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

如何实现,请点击前往:

# 手写 call 方法 超级详细 ⚡⚡⚡

# 手写 bind 方法,超级详细 ⚡⚡⚡

# 手写 apply 方法 超级详细 ⚡⚡⚡

97. JavaScript中如何实现继承?

JavaScript实现继承有六种方法:原型链继承、盗用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承

点击前往学习:# 面试官:你说说 js 中实现继承有哪几种方法?

98. JavaScript中的原型,原型链分别是什么?

原型: JavaScript` 常被描述为一种基于原型的语言——每个对象拥有一个原型对象

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

原型链: 原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

99. 如何判断页面是通过PC端还是移动端访问?

  1. 使用 navigator.userAgent,只要里面包含mobiandroidiphone等关键字,就可以认定是移动设备。这种方法的优点是简单方便,缺点是不可靠,因为用户可以修改这个字符串,让手机浏览器伪装成桌面浏览器。

  2. 使用 window.screen.width,如果屏幕宽度小于500像素,就认为是手机。这个方法的缺点在于,如果手机横屏使用,就识别不了。

  3. 使用 window.orientation,侦测屏幕方向,手机屏幕可以随时改变方向(横屏或竖屏),桌面设备做不到。window.orientation属性用于获取屏幕的当前方向,只有移动设备才有这个属性,桌面设备会返回undefined。(注意:iPhone 的 Safari 浏览器不支持该属性。)

  4. 使用 ontouchstart事件,手机浏览器的 DOM 元素可以通过ontouchstart属性,为touch事件指定监听函数。桌面设备没有这个属性。

  5. 使用第三方的工具包,推荐react-device-detect,它支持多种粒度的设备侦测。

    import {isMobile} from 'react-device-detect'; 
    if (isMobile) { 
      // 当前设备是移动设备 
    }
    

100. 如何让Promise.all在抛出异常后依然有效?

  1. 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);
})
  1. 使用 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回调中返回了一个状态为rejectedPromise实例,那么.catch返回的Promise实例的状态也将变成rejected
  • 如果.catch(onRejected)onRejected回调中抛出了异常,那么.catch返回的Promise实例的状态也将变成rejected
  • 其他情况下,.catch返回的Promise实例的状态将是fulfilled

102. es5中的类和es6中的class有什么区别?

在es5中主要是通过构造函数方式和原型方式来定义一个类,在es6中我们可以通过class来定义类。它们的区别有:

  1. es6的class类必须new调用,不能直接执行。

  2. class类不存在变量提升

  3. class类无法遍历它实例原型链上的属性和方法

  4. es6为new命令引入了一个new.target属性,它会返回new命令作用于的那个构造函数。如果不是通过new调用或Reflect.construct()调用的,new.target会返回undefined

  5. class类有static静态方法

103. 简单说说前端路由?

概念:前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。

使用场景:在单页面应用,大部分页面结构不变,只改变部分内容的使用

优缺点

  • 优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户
  • 缺点:单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置

实现方式:前端路由一共有两种实现方式,一种是通过 hash 的方式,一种是通过使用 pushState 的方式。

104. 什么是点击穿透,如何解决?

在发生触摸动作约300ms之后,移动端会模拟产生click动作,它底下的具有点击特性的元素也会被触发,这种现象称为点击穿透。

常见场景

  1. 情景一:遮罩层点击穿透问题,点击遮罩层(mask)上的关闭按钮,遮罩层消失后发现触发了按钮下面元素的click事件。
  2. 情景二:跨页面点击穿透问题:如果按钮下面恰好是一个有href属性的a标签,那么页面就会发生跳转。
  3. 情景三:另一种跨页面点击穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了。
  4. 情景四:新页面中对应位置元素恰好是a标签,然后就发生连续跳转了。这种情况概率很低

发生的条件

  • 上层元素监听了触摸事件,触摸之后该层元素消失
  • 下层元素具有点击特性(监听了click事件或默认的特性(a标签、input、button标签))

解决点击穿透的方法

  1. 方法一:书写规范问题,不要混用touch和click。既然touch之后300ms会触发click,只用touch或者只用click就自然不会存在问题了。
  2. 方法二:吃掉(或者说是消费掉)touch之后的click,依旧用tap,只是在可能发生点击穿透的情形做额外的处理,拿个东西来挡住、或者tap后延迟350毫秒再隐藏mask、pointer-events、在下面元素的事件处理器里做检测(配合全局flag)等。

105. 简单说说setTimeout的运行机制

setTimeoutsetInterval 的运行机制,其实就是将指定的代码移出本次执行,等到下一轮 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) 过程中浏览器又干了些什么呢?

image.png

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  • 接受输入事件
  • 执行事件回调
  • 开始一帧
  • 执行 RAF (RequestAnimationFrame)
  • 页面布局,样式计算
  • 绘制渲染
  • 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事而且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

111. 说说Object.defineProperty与Proxy的区别?

在 Vue2.x 的版本中,双向绑定是基于 Object.defineProperty 方式实现的。而 Vue3.x 版本中,使用了 ES6 中的 Proxy 代理的方式实现。

  1. 使用 Object.defineProperty 会产生三个主要的问题:
  • 不能监听数组的变化
  • 必须遍历对象的每个属性。可以通过 Object.keys() 来实现
  • 必须深层遍历嵌套的对象。通过递归深层遍历嵌套对象,然后通过 Object.keys() 来实现对每个属性的劫持
  1. 关于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 属性的enumerablefalse,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但还是可以直接获取它的值。

至于for...in循环和Object.keys方法的区别,在于前者包括对象继承自原型对象的属性,而后者只包括对象本身的属性。如果需要获取对象自身的所有属性,不管enumerable的值,可以使用Object.getOwnPropertyNames方法。

可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性。对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false。

116. Object.create和new有什么区别?

js中创建对象的方式一般有两种 Object.createnew

const Base = function() {}
const o1 = Object.create(Base)
const o2 = new Base()

在讲述两者区别之前,我们需要知道:

  • 构造函数Foo的原型属性Foo.prototype指向了原型对象。
  • 原型对象保存着实例共享的方法,有一个指针constructor指回构造函数。
  • js中只有函数有 prototype 属性,所有的对象只有 proto 隐式属性。

那这样到底有什么不一样呢?

  1. 先来看看 Object.create 的实现方式
Object.create = function(o) {
  var F = function() {}
  F.prototype = o
  return new F()
}

可以看出来。Object.create是内部定义一个对象,并且让F.prototype对象 赋值为引进的对象/函数 o,并return出一个新的对象。

  1. 再看看 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

总结

比较newObject.create
构造函数保留原构造函数属性丢失原构造函数属性
原型链原构造函数prototype属性原构造函数/(对象)本身
作用对象functionfunction和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. 箭头函数和普通函数有什么区别?

  1. 语法更加简洁、清晰

  2. 箭头函数不会创建自己的this(重点!)

    箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

  3. 箭头函数继承而来的this指向永远不变(重点!)

  4. .call()/.apply()/.bind()无法改变箭头函数中this的指向

  5. 箭头函数不能作为构造函数使用

  6. 箭头函数没有自己的 arguments。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。

  7. 箭头函数没有原型 prototype

  8. 箭头函数不能用作 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。
  • 如果方法内有await节点

    • async会返回一个{PromiseStatus: pending}的Promise(发生切换,异步等待Promise的执行结果)。
    • Promiseresolve会使得await的代码节点获得相应的返回结果,并继续向下执行。
    • Promisereject 会使得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 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

各个前端应用还可以独立运行、独立开发、独立部署。

微前端不是单纯的前端框架或者工具,而是一套架构体系,

由于篇幅原因,下半部分内容将 「2022」JavaScript最新高频面试题指南(下) 发布。持续更新中...


每文一句:星星使天空绚烂夺目;知识使人增长才干。

本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!

本文正在参加「金石计划 . 瓜分6万现金大奖」