Skip to content
On this page

1、图片异步上传

要实现图片上传,首先要拿到图片的数据,在前端页面,可以使用 input 标签设置属性 type = "file" 或者其他组件的方法 让用户选择需要上传的图片,然后可以得到一个图片数据对象。

使用 FileReader API中的 readerAsArrayBuffer 或者 readerAsDataURL 方法,可以将图片数据转换为二进制的或者 Data URL

为了减少上传时间和网络流量,可以在前端对图片数据进行压缩,可以使用 HTML5 的 Canvas API 或第三方库如 compressor js、image-compressor 等进行图片压缩

使用 post 请求把图片数据上传服务器,服务器收到图片数据之后,会做相应的处理,比如保存到数据库或者文件系统中,然后返回 上传结果以及图片的地址

前端这边收到上传的响应结果之后,就显示上传成功或者失败等提示信息,上传成功的话,就展示一个预览图片的界面

需要注意的是,图片上传过程中可能要考虑图片数据的安全性和稳定性。可以使用 HTTPS 协议进行加密传输,对上传的数据进行验证和过滤, 设置合适的上传超时时间等措施,来保证上传请求的安全和稳定

2. 常见的数据类型

javaScript 的数据类型可以分为两种,一种是 值类型,也叫基本类型;还有一种是 引用类型

基本类型有:字符串string、数字number、布尔值boolean、null、undefined 以及 es6 新增的 symbol,代表独一无二的值

而引用类型的数据有:对象Object、数组Array、函数Function、日期Date以及正则表达式RegExp等

3. 常量的特点

常量一般使用 const 来命名,常量的特点如下:

  1. 常量的值一旦定义了就不能改变,但是如果是对象,可以修改对象中的值

  2. 必须在定义的时候就赋值,并且不能重新声明,也不能被删除

  3. 常量的作用域与变量相同,可以是全局或者局部的

  4. 常量可以是任何数据类型,包括字符串、数字、布尔值、对象等

4、http协议

HTTP 也叫做超文本传输协议,是一个属于应用层的面向对象的协议,在1990年提出,http协议的主要特点有:

  1. 支持 客户端/服务器模式:客户端发送请求,服务器提供响应

  2. 简单快速:客户端向服务器发送请求时,只需填写 请求方法 和 请求的路径(url)就可以进行发送了, 常用的请求方法有 GET、POST、PUT、DELETE、HEAD等,通信速度很快

  3. 无状态:HTTP协议是无状态协议,每个请求都是独立的,(服务器不会记住之前的请求信息)

  4. 无连接:http协议也是一种无连接协议,处理完一个请求响应之后就会断开连接,这种方式可以节省传输时间

  5. 灵活:HTTP允许传输任意类型的数据对象,传输的类型可以用 Content-Type 来指定

HTTP 请求由三部分组成:请求行、请求头、请求正文

  1. 请求行:是以一个请求方法开头,以空格分开,后面跟着的是请求的 URL 和 http协议的版本(GET /index.html HTTP/1.1)

  2. 请求头:包含一系列键值对,如 Content-Type、User-Agent、Accept 等,它是可选的,可以根据需要来添加

  3. 请求正文:可以包含一些表单数据、JSON数据、也可以省略

然后是 HTTP 响应,也通常是由 响应行、响应头、响应正文 组成

  1. 响应行:包含 http 版本、状态码、状态描述,以空格分开(HTTP1.1 200 ok)

    关于状态码:

     以1开头表示请求接收到了、
     
     200 就表示请求成功了、
     
     3 零几开头的一般表示重定向了、 
    
     4 零开头的是客户端错误
    
     5 零开头的是服务器错误
    
  2. 响应头:也是包含一系列键值对,例如 Content-Type、Content-Length、Set-Cookie 等

  3. 响应正文:就是包含了我们需要的信息,例如 html 文件、JSON 数据、图片数据等等

追问 http 和 https 的区别:

5. 数组的常用方法

push:向数组的末尾添加元素,返回数组的新长度

pop:把数组的最后一个元素弹出去,返回这个被删掉的元素

unshift:向数组的开头添加元素,返回数组的新长度

shift:把数组开头第一个元素删除,返回这个元素

slice(start, end):返回原数组中指定位置的一个新的数组,(左闭右开,不包含end元素)

splice(index, num(0表示不删除,不指定就会往后一直删), ?item1, ?item2....):

表示可以添加或者删除元素,会改变原数组,第一个参数指定插入或删除的数组位置,第二个参数表示删除的个数

第三个以后的参数,是要插入到数组的新元素

sort: 这个方法可以对数组进行排序

然后es6新增了一些数组方法,比如:

forEach:遍历数组元素,执行参数的回调函数,返回 undefined

map:遍历数组,结束之后会返回一个新的数组

filter:用来筛选元素的,符合条件的元素会作为一个新的数组来返回

some:数组中只要有一个满足条件的,就返回 true,然后停止遍历

every:遍历数组所有的元素,只有都符合条件才返回true

追问:

  1. slice,splice 的区别?

  2. 如何合并两个数组?

  3. sort都有哪些参数?

  4. 数组去重有哪些方法?

  5. 如何把伪数组转换为数组?

6、JS 原生事件如何绑定?

  1. 行内绑定,在标签上写上 on + 事件名称,例如点击事件 onClick,值为执行的函数

  2. 动态绑定,用 js 获取到 DOM元素对象,然后使用 DOM元素.事件名称

  3. 事件监听,同样先获取到 DOM元素对象,然后通过 .addEventListener(name, callBack) 传入事件名称和回调函数

7、作用域和作用域链

作用域 就是程序中变量和函数的可访问的范围,在 JS 中,分为全局作用域和局部作用域

全局作用域:全局作用域的变量在程序中的任何地方都可以被访问到,window 对象的内置属性都拥有全局作用域

局部作用域:也叫函数作用域,在函数内部定义的变量,只能在函数内部中使用,当函数执行结束之后,这个函数作用域 会被销毁,其中的变量也会被回收

然后 ES6 中还新增了 块级作用域,比如用 let 声明的变量只能在块级作用域中使用,有“暂时性死区”的特性

作用域最大的用处就是隔离变量,不同作用域下的同名变量不会发生冲突

然后作用域链:

就是当程序访问一个变量的时候,首先是会在当前作用域中查找是否有定义了这个变量,如果没有找到的话,就会去 上一级的作用域中去查找,直到找到全局作用域为止,这个查找的过程像形成链条一样,这就是 作用域链

8、var、let、const的区别

var 是 JS 最早用来声明变量的方法,因此它会有一些历史遗留的问题,比如:

  1. 变量提升,会提升到当前作用域的顶层,并且值为 undefined

  2. 可以重复声明变量,后面的声明会覆盖前面的

  3. 覆盖全局对象属性,变量提升的时候,如果在全局作用域中会提升到全局对象window上,也就是可能会覆盖一些 全局变量和方法

然后在 ES6 新增了 let、const 两个关键字来声明变量,let 声明变量,const 声明常量,它们之间的不同点主要是:

  1. 块级作用域,let 、const 有块级作用域,var 没有

  2. 暂时性死区,let 和 const 声明的变量不能在声明之前使用,不然会报错;而 var 可以在声明之前使用

  3. 初始值,const 定义的常量必须要有初始值,而 let 、var 可以不用设置初始值

  4. 重新赋值,let 、var 声明的变量可以重新赋值,而 const 不能,但是如果是对象的话,可以修改对象的属性值

9、闭包

闭包:就是一个函数可以访问另一个函数作用域内的变量,一般就是一个函数嵌套在另一个函数中时,内部函数可以访问外部函数的变量 外部函数运行时返回这个内部函数,就形成了一个闭包,它保留了外部函数的作用域链并可以继续访问这些本应该被回收的变量。

闭包的作用是:

1、可以读取函数内部的变量,实现函数的封装和私有化

2、让变量的值始终保持在内存中

闭包需要注意的地方是:

1、闭包的变量都保存在内存中,会消耗很多内存,造成网页的性能问题,解决办法是,在退出函数之前, 将不使用的局部变量全部删除

10、原型和原型链

在 JavaScript 中是通过构造函数来新建一个对象的,每一个构造函数都有一个内置属性叫 prototype 指向一个对象,

这个对象就是这个构造函数的原型,原型的作用呢,就是可以包含由某个构造函数 new 出来的所有实例对象都可以共享的属性和方法。

原型链就是:

当使用构造函数新建一个对象的时候,这个对象就会生成一个属性叫 __proto__指向了创建它的构造函数的 prototype 也就是原型

当我们访问对象的某一个属性的时候,如果这个对象没有这个属性,那它就会去它的原型对象里面去找这个属性,而原型对象也是有自己的原型对象的,

如果没有找到的话,就会继续往上找,直到找到 Object.prototype 指向的原型,也就是 null,null 是最顶级的了,在这个寻找的过程中,就像

形成了链条一样,这就是原型链的概念。

11、面向对象

在 es5 的时候是没有 class 类的概念的,但是可以通过构造函数和原型的方式去实现 class 类的这个类似功能

面向对象有三大特征:

  1. 封装:把一种事物的属性和方法封装到对象中

  2. 继承:子对象可以继承父对象的属性和方法

  3. 多态:同一个方法,子对象可以继承也可以自定义来跟父对象的内容保持不同

面向对象编程的优势:

容易维护、容易扩展、代码的质量和效率相对比较高

在 js 中,创建对象的方式有:

  1. 通过字面量 {} 来创建

  2. 执行 Object 构造函数来创建

  3. 通过 new 构造函数的形式来创建

然后遍历对象,可以通过:for ... in xx 方式来遍历

12、设计模式

设计模式就是一种被大家反复使用并且验证过的代码设计经验,是一种通用解决方案;它可以帮助 我们更好地去解决软件开发中常见的问题,提高代码的可维护性、写出来的代码更容易被人理解。

我们前端中常用到的设计模式主要有:

1、单例模式:一个类只能有一个实例,并提供一个访问它的全局访问点

  • 应用场景:浏览器的window对象(任何时候访问都是同一个对象)、弹窗(弹窗管理器,弹出顺序、次数)、全局状态管理store -- Vuex

2、工厂模式:用固定的方式批量创建对象

  • 应用场景:vue3中的 createComponent(传入组件类型\属性,返回不同组件实例)

3、观察者模式:设置观察的方法,观察某个对象是否发生变化,是一种一对多的关系,当对象发生变化就会通知到所有的观测者,执行对应方法

  • 应用场景:事件绑定、promise 等

4、发布/订阅者模式:发布者发布事件,通过中间层接收并通知订阅者,订阅者收到通知,更新对应的属性 执行对应的方法

  • 应用场景:典型的就是 vue中的 v-model 数据双向绑定

13、继承

继承就是 父类的属性和方法可以被子类继承下来,子类可以调用父类的属性和方法,避免重复编写代码

在 JS 中,实现继承的方式有好几种:

  1. 可以通过原型和原型链的方式实现继承:就是把父类的实例赋值给子类的原型,子类就获得了父类的属性和方法,

这种方式的优点是:可以实现属性和方法的复用,

但是这种方式的缺点是:

  • 所有实例对象都共享原型对象中的属性和方法,如果某个实例对象修改了原型对象中的属性,其他实例对象也会受到影响。

  • 创建子类实例时,无法向父类构造函数传递参数

  1. 通过构造函数来继承:在子类中调用父类.call(),复制了一份父类的属性或者方法给子类,

这种方式的优点是:

  • 解决了子类实例共享父类引用属性的问题

  • 创建子类实例时,可以向父类构造函数传递参数

但是它同样也是有缺点的:

  • 无法实现复用,每一个子类的实例都有一个新的属性和方法,如果实例对象创建多了,内存消耗过大
  1. 第三种方式是组合继承,结合了原型链和构造函数的优点,也就是:
  • 不存在这个引用属性共享问题

  • 可以传递参数

  • 方法可以复用

但是还是有缺点:子类原型上有一份多余的父类实例的属性

最后,还有其他的一些继承方式比如 原型式继承、寄生式继承等,但是用得就比较少了

14、DOM 操作

DOM (Document Object Model) 也叫文档对象模型

js 要操作 DOM, 浏览器提供了很多api接口

首先 HTML 的每个标签元素,属性,文本都可以看作是一个 DOM 的节点,这些节点构成了一颗 DOM 树

常用的获取 DOM 节点的方法是:document.getElementById、document.getElementByName(ClassName)(TagName) 等

还有 document.querySelector 获取一个元素节点,document.querySelectorAll 获取符合条件的所有元素节点

获取或者设置元素节点的属性值可以通过:getAttribute、setAttribute

还可以创建一个新的元素节点:document.createElement(),然后通过 appendChild,insertBefore 把这个 元素节点插入到 DOM 中,也可以通过 removeChid 删除一个子元素

还有其他一些常用方法:

parentNode 获取父节点 children 返回所有的子元素,childrenNodes 返回所有的子节点,包括文本、属性节点 和 HTML 元素

firstChild \ lastChild 获取第一个或者最后一个子元素,previousSibling \ nextSibling 获取上一个或者下一个兄弟节点

15、数组遍历方法

可以使用 for 循环或者 forEach 循环

还有数组方法 map 循环,它会返回一个数组,数组中的值为循环过程中 return 回来的

filter 数组筛选循环,把条件为真的元素作为一个新的数组来返回

some 方法:对数组中的每一项运行指定的回调函数,回调函数返回布尔值,如果找到符合条件的元素,就会停止遍历 否则就会全部遍历一遍然后返回true

every 方法:也是对数组中的每一项元素运行指定回调函数,跟some刚好相反,只有全部元素都符合条件才会返回true, 只要有一个不符合,就会停止遍历然后返回false

reduce 方法:接收一个函数作为累加器,每次循环都会接收上一次循环的函数计算运行的返回值,可以定义一个初始值作为 第一次运行时的“上一次的结果返回值”,不指定就为数组的第一个元素,reduce方法通常可以计算一个数组的累加值

以上就是我经常用到的一些数组遍历方法

16、在浏览器地址栏输入 URL 到 页面加载完成 这个过程发生了什么?

首先浏览器会对输入的 url 进行 DNS 解析,先判断本地有没有该域名的 IP 地址的缓存,有就返回这个 IP 地址,没有就 向 DNS 服务器发起请求,最后会拿到这个域名的 IP 地址向这个 IP 地址的服务器发起请求

然后会建立一个 TCP 连接,这时候要进行 “三次握手” 的过程:

1、首先客户端向服务器发送一个 SYN 请求报文和一个随机序列,表示希望建立连接

2、服务器收到 SYN 报文之后,返回一个 SYN+ACK 报文表示确认连接

3、客户端收到 SYN+ACK 报文会,再把 ACK 的值加1 之后 发送给服务器,表示确认建立连接

这时候就建立连接了,浏览器会发送 HTTP 请求,服务器收到页面请求后,会返回一个 HTML 文件作为响应数据, 浏览器收到响应后,就开始对 HTML 文件进行解析,开始这个页面的渲染过程

在页面渲染过程中,浏览器首先会根据 HTML 文件构建 DOM 树,根据解析到的 CSS 文件构建 CSSOM 树,如果遇到 script js脚本标签, 如果没有 defer 或者 async 属性,浏览器就会先解析 js 文件,会阻塞页面的渲染,所以 js 文件一般是异步加载或者放到后面的

当 DOM 树和 CSSOM 树构建完成,它们两个就会合并生成一颗渲染树(render tree),它包含了每个可见元素的布局信息

然后浏览器会遍历这颗渲染树,调用 UI 接口将每个元素绘制到页面上,页面就显示出来了

最后是 TCP 四次挥手然后断开连接

  • 客户端(想结束了) -- FIN --> 服务器,
  • 服务器(开始准备了) -- ACK --> 客户端
  • 服务器(可以结束了) -- FIN --> 客户端,
  • 客户端(ok)-- ACK --> 服务器

17、JS 事件代理

事件代理,也叫事件委托,本来应该加在子元素身上的事件,却把事件放到父元素上

这里是利用到了事件冒泡的机制,事件冒泡就是指:当触发一个元素的事件,这个元素会把事件一层层往上冒泡。 它的父级元素会接收到这个事件,一直到往上传播到 window 对象为止

使用事件委托的优点是:

  • 效率高,比如 v-for 循环渲染一个列表页,不用为每一个子元素绑定事件,只需要绑定事件给父元素就可以了

  • 程序逻辑比较方便,新创建的 DOM 事件也可以冒泡这个事件,不用担心元素销毁了事件也会丢失的问题

  • 鼠标事件:click,dblclick点击和双击,mousedown,mouseup 等还有键盘事件:keydown、keyup、keypress 等等事件都可以适合用来进行事件委托

18、call、apply、bind

call、apply、bind 都是可以改变 this 指向的

call方法的第一个参数是要绑定的作用域的对象,第二第三之后的参数为函数运行的参数

apply方法也是一样,第一个参数是绑定this的对象,但是第二个参数是个数组,里面放着函数要运行的参数

bind方法的第一个参数是绑定的this对象,第二个第三个和之后的参数为函数的入参,但是bind跟call和apply 不同的地方是,call和apply都是立即执行的,bind 不会立即执行,而是会返回一个函数, 要立即执行的话得再多个一个()去运行

19、深拷贝 和 浅拷贝

深拷贝和浅拷贝都是针对 JavaScript 中的引用类型的数据来说的,比如:对象、数组、函数等等

浅拷贝就是说,在复制一个引用类型的数据比如对象的时候,它只复制了对象在内存中的引用地址,而不是复制整个对象本身。 也就是说,当我们对拷贝后的新对象进行修改时,原始对象也会跟着改变。

而深拷贝就是完完全全复制了一个一模一样的新对象,它在内存中是独立的一个空间,对这个新对象修改的时候,完全不会影响到原始对象

实现这个深拷贝常用方法有:

  1. 利用json的序列化方法可以实现深拷贝,使用 json.stringify 把一个对象序列化转为字符串,然后结合 json.parse 反序列化 这个也就是还原这个对象,就可以简单粗暴地实现了深拷贝,但是会有一些问题,拷贝的对象里如果有函数、undefined 和 symbol 类型的属性,在序列化的时候会消失

  2. 可以使用第三方插件来实现,网上有很多库都可以实现深拷贝,如 lodash 的 _.cloneDeep方法

  3. 可以自己配合项目要求手写一个递归函数实现,就是把要拷贝对象的属性和方法一个个找出来,复制到一个新的对象中,基本数据类型就直接复制, 而引用数据类型就会递归查找基本的类型数据进行复制,复制到对应的一个新的对象里面,整个过程递归便利结束之后返回的就是 深拷贝之后的新的一份数据

常用的浅拷贝方法是:

  1. Object.assign():该方法可以实现浅拷贝,也可以实现一维对象的深拷贝

  2. 展开运算符(...):将一个对象展开为多个参数,可以用来复制对象。

  3. 对于数组而言,slice() 、concat() 方法可以用来实现数组的浅拷贝

  4. 手动实现,浅拷贝就是基本类型复制一份,引用类型就复制一个引用地址就好了,可以自己写一个方法

20、浏览器是如何渲染页面的?

浏览器拿到html文件后,会按照一定步骤的来渲染页面:

  1. 首先是根据html文件构建 DOM树 和 CSSOM树。构建 DOM树 期间,如果遇到 JS,阻塞 DOM树 及 CSSOM树 的构建,优先加载JS文件, 加载完毕,再继续构建 DOM树 及 CSSOM树。

  2. 构建渲染树(Render Tree):渲染树(Render Tree)由DOM树、CSSOM树合并而成,构建渲染树, 根据渲染树计算每个可见元素的布局,并输出到绘制流程,将像素渲染到屏幕上

  3. 页面的重绘(repaint)与重排(reflow,也有称回流):页面渲染完成后,若JS操作了DOM节点,根据JS对DOM操作动作的大小, 浏览器对页面进行重绘或是重排。

  • 重绘(repaint):渲染树节点发生改变,但不影响空间位置及大小,改变颜色 字体等属性

  • 重排(reflow)(回流):当节点发生改变,位置大小变化(如宽、高、内边距、外边距、或是float、position、display:none;等等), 节点位置变化,此时触发浏览器重排(reflow),需要重新生成渲染树

  • 减少重排:

    • 多次改变样式合并成一个,用一个class来操作

    • 使用DocumentFragment进行缓存操作,引发一次回流和重绘;

    • 需要多次重排的元素用绝对或是固定定位,例如动画元素、弹窗、返回顶部的那个图标

21、防抖节流

防抖节流主要是应对连续触发的事件触发频率太高的问题

防抖:给目标事件方法添加一个时间限定,setTimeout延迟执行时机

节流:在防抖的基础上,添加一个控制器,让函数执行一次后,在某个时间段内暂时失效, 过了这段时间后再重新激活效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在 指定的时间期限内不再工作,直至过了这段时间才重新生效

应用场景: 搜索框连续输入,弹出提示,可以用节流的方式设定时间间隔 滚动条连续滚动效果

resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)

22、外部js文件先加载还是onload先执行

这个执行的顺序其实与js文件放置的位置和加载方式有关:

浏览器在解析HTML文档的时候,会构建一个DOM数嘛,当浏览器遇到js文件也就是 <script>标签时,它会停止解析 HTML, 并开始加载 js 文件,如果这个 <script> 标签具有 async 或者 defer 属性,那么浏览器会把这个js文件挂起来进行异步加载, 等到HTML解析完了才会执行js文件

而 onload 事件会在所有的页面元素(包括图片、js脚本、样式表等)加载完成后自动触发,因此这个它们之间的执行顺序就有好几种情况:

  1. 如果 js文件在头部标签 <head>里面,那么浏览器会先执行 js 文件再执行 onload 事件

  2. 如果 js文件放到在body标签里面,那么它会按照body标签的先后顺序进行加载,js文件在前面就会先加载, 如果js文件放在 body的底部, 那么就是在body加载完之后,才加载js文件,onload 事件会先执行然后再到 js文件加载

由于浏览器在解析js文件的时候,它会阻塞页面的加载,因此我们一般是把js文件放到body底部的,确保页面的内容尽快刷新。

23、对同步异步的理解

同步和异步是 js 中的两种消息通信机制,因为JavaScript的单线程,因此同个时间只能处理同个任务,所有任务都需要排队,

前一个任务执行完,才能继续执行下一个任务,但是这样的话,如果是一些耗时的操作,就会阻塞后面代码的执行, 因此 js 把任务分为了同步任务和异步任务:

同步任务是在主线程里排队执行任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程, 比如 DOM元素 的渲染,其实就是一个同步任务

而异步任务就是先挂起等待中的任务,继续执行后面的代码,等到结果返回了,再回头执行挂起的这个任务,像图片的加载,音乐的加载, 或者发送 ajax 请求,其实就是一个异步任务,比如 调用者发送一个请求,这个请求就会挂起来,当这个请求完成后, 会通过状态、通知来通知调用者一个结果,或者通过回调函数处理这个调用结果

以上就是我对代码的同步异步的一个理解

24、事件循环机制 eventloop

浏览器在执行js代码的时候是从上往下,一行一行执行的,先执行同步代码,再执行异步代码,这里就用到了事件循环(Event Loop) 它负责协调异步操作和同步代码的执行。

JavaScript 是单线程的,这意味着它一次只能执行一个任务,事件循环使 JavaScript 能够在执行同步代码的同时, 处理异步操作(如定时器、用户交互和网络请求)的回调函数。

其中,这个执行步骤是:

1、首先,把同步代码按照顺序放入js主线程的执行栈中,依次执行,执行完毕就清空执行栈

2、如果遇到异步代码,会放到相应的任务队列中,异步任务分为了宏任务和微任务,宏任务就是 setTimeoutsetInterval等 它们的回调函数是放到一个宏任务队列中,而微任务就是promise等,放到了微任务队列中

3、当同步代码执行完成后,js 会通过 event loop 轮询机制,把微任务队列中的任务放到执行栈中执行,会清空微任务队列

4、接下来,事件循环检查宏任务队列,事件循环将执行第一个任务,然后返回到微任务队列,检查是否有新的微任务需要执行

5、事件循环在微任务队列和宏任务队列之间不断轮询,执行异步任务,当两个队列都为空时,事件循环将等待新的任务(如用户交互或网络请求回调)

以上就是我对事件循环的理解

25、宏任务和微任务

宏任务和微任务都属于异步任务,宏任务包括 setTimeout、setInterval、I/O操作等,微任务包括 promise 的 .then 、resolve、reject 等

首先先执行同步代码,遇到异步任务,如果是宏任务就放入宏任务队列,微任务放到微任务队列,当同步代码执行完毕,就会执行微任务队列,直到 微任务队列清空,然后从宏任务队列调用宏任务到主线程执行,就这样不断循环,直到所有任务执行完毕

26、setTimeout时间为0不会立即触发,以及误差的原因

js 是单线程执行的, 当设置 setTimeout 的时间为 0 时,就代表立即插入任务队列,但不是立即执行,仍然要等待前面代码执行完毕

setTimeout 执行时间有误差是因为 setTimeout 为异步任务,它的耗时=执行时间+队列中等待时间,等待时间会让 eventloop 产生误差 因为它要先执行完同步任务,才会去执行异步任务,因此,如果有一些耗时操作、或者网络延迟等其他一些因素影响,就会导致执行时间产生误差

27、【算法面试题】数组扁平化

数组扁平化,简单来说就是一个降维过程,多维数组经过扁平化操作变成一维数组

可以写一个递归遍历函数或者利用 reduce 去做这个数组扁平化方法

1、递归函数就是遍历嵌套的数组,将内层的数组逐一展开并将里面的元素添加到结果数组里面,最后返回这个结果数组

function flatten(arr) {
    let result = []
    for(let i = 0; i < arr.length; i++) {
        if(Array.isArray(arr[i])) {
            result.push(...flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result
}

2、使用 reduce 方法

function flatten(arr) {
  return arr.reduce((prev, next) => {
    // 判断next是否为数组,是就递归,否则连接进结果数组里面
    return prev.concat(Array.isArray(next) ? flatten(next) : next);
  }, []);
}

3、使用 es6 提供的 flat 方法,它的参数是选择数组进行扁平化的深度

const arr = [1, [2, [3, 4], 5], 6];
arr.flat(3); // 选择嵌套深度为3 ,得到 [1, 2, 3, 4, 5, 6]

28、【算法面试题】对象扁平化

嵌套层级很深的对象,经过扁平化变成深度为1的对象,可以使用递归遍历,代码如下:

function flattenObj(obj) {
    let result = {}

}

Released under the MIT License.