Javascript基础
# 继承
# 1、原型链继承
# Array.includes Array.indexOf()
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。
|
|
原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。
|
|
# 2、借用构造函数继承
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
|
|
核心代码是SuperType.call(this)
,创建子类实例时调用SuperType
构造函数,于是SubType
的每个实例都会将 SuperType 中的属性复制一份。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
# 3、组合继承
组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
|
|
缺点:
- 第一次调用
SuperType()
:给SubType.prototype
写入两个属性 name,color。 - 第二次调用
SuperType()
:给instance1
写入两个属性 name,color。
实例对象instance1
上的两个属性就屏蔽了其原型对象 SubType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
# 4、原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
|
|
object()对传入其中的对象执行了一次浅复制
,将构造函数 F 的原型直接指向传入的对象。
|
|
缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
另外,ES5 中存在Object.create()
的方法,能够代替上面的 object 方法。
# 5、寄生式继承
核心:在原型式继承的基础上,增强对象,返回构造函数
|
|
函数的主要作用是为构造函数新增属性和方法,以增强函数
|
|
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
# 6、寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
|
|
这个例子的高效率体现在它只调用了一次SuperType
构造函数,并且因此避免了在SubType.prototype
上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
# 7、混入方式继承多个对象
|
|
Object.assign
会把 OtherSuperClass
原型上的函数拷贝到 MyClass
原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
# 8、ES6 类继承 extends
extends
关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor
表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError
错误,如果没有显式指定构造方法,则会添加默认的 constructor
方法,使用例子如下。
|
|
extends
继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
|
|
# 总结
1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个 ReferenceError。
|
|
2、ES5 继承和 ES6 继承的区别
- ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.call(this)).
- ES6 的继承有所不同,实质上是先创建父类的实例对象 this,然后再用子类的构造函数修改 this。因为子类没有自己的 this 对象,所以必须先调用父类的 super()方法,否则新建实例报错。
# Proxy
使用Proxy
,你可以将一只猫伪装成一只老虎。下面大约有 6 个例子,我希望它们能让你相信,Proxy 提供了强大的 Javascript 元编程。
尽管它不像其他 ES6 功能用的普遍,但Proxy
有许多用途,包括运算符重载,对象模拟,简洁而灵活的 API 创建,对象变化事件,甚至 Vue 3 背后的内部响应系统提供动力。
Proxy
用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。
var proxy = new Proxy(target, handler);
Proxy
对象的所有用法,都是上面的这种形式。不同的只是handle
参数的写法。其中new Proxy
用来生成Proxy
实例,target
是表示所要拦截的对象,handle
是用来定制拦截行为的对象。
下面是 Proxy 最简单的例子是,这是一个有陷阱的代理,一个get
陷阱,总是返回42
。
|
|
结果是一个对象将为任何属性访问操作都返回“42”。这包括target.x
,target['x']
,Reflect.get(target, 'x')
等。
但是,Proxy 陷阱当然不限于属性的读取。它只是十几个不同陷阱中的一个:
- handler.get
- handler.set
- handler.has
- handler.apply
- handler.construct
- handler.ownKeys
- handler.deleteProperty
- handler.defineProperty
- handler.isExtensible
- handler.preventExtensions
- handler.getPrototypeOf
- handler.setPrototypeOf
- handler.getOwnPropertyDescriptor
# 用途
# 默认值/“零值”
在 Go 语言中,有零值的概念,零值是特定于类型的隐式默认结构值。其思想是提供类型安全的默认基元值,或者用 gopher 的话说,给结构一个有用的零值。
虽然不同的创建模式支持类似的功能,但 Javascript 无法用隐式初始值包装对象。Javascript 中未设置属性的默认值是undefined
。但 Proxy 可以改变这种情况。
|
|
函数withZeroValue
用来包装目标对象。如果设置了属性,则返回属性值。否则,它返回一个默认的**“零值”**。
从技术上讲,这种方法也不是隐含的,但如果我们扩展withZeroValue
,以 Boolean (false
), Number (0
), String (""
), Object ({}
),Array ([]
)等对应的零值,则可能是隐含的。
|
|
此功能可能有用的一个地方是坐标系。绘图库可以基于数据的形状自动支持 2D 和 3D 渲染。不是创建两个单独的模型,而是始终将z
默认为 0
而不是undefined
,这可能是有意义的。
# Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.
|
|
# 构造函数语法
new Promise( function(resolve, reject) {…} /* executor */ );
executor
executor 是带有
resolve
和reject
两个参数的函数 。Promise 构造函数执行时立即调用executor
函数,resolve
和reject
两个函数作为参数传递给executor
(executor 函数在 Promise 构造函数返回所建 promise 实例对象前被调用)。resolve
和reject
函数被调用时,分别将 promise 的状态改为fulfilled(完成)或 rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用 resolve 函数来将 promise 状态改成fulfilled,要么调用reject
函数将 promise 的状态改为 rejected。如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。
# 介绍
Promise
对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。
一个 Promise
有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
pending 状态的 Promise 对象可能会变为 fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then
方法绑定的处理方法(handlers )就会被调用(then 方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当 Promise 状态为fulfilled时,调用 then 的 onfulfilled 方法,当 Promise 状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。
因为 Promise.prototype.then
和 Promise.prototype.catch
方法返回 promise 对象, 所以它们可以被链式调用。
不要和惰性求值混淆: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如 Scheme. Javascript 中的 promise 代表一种已经发生的状态, 而且可以通过回调方法链在一起。 如果你想要的是表达式的延时计算,考虑无参数的"
箭头方法": f = () =>
表达式
创建惰性求值的表达式*,*使用 f()
求值。
注意: 如果一个 promise 对象处在 fulfilled 或 rejected 状态而不是 pending 状态,那么它也可以被称为settled状态。你可能也会听到一个术语resolved ,它表示 promise 对象处于 settled 状态。关于 promise 的术语, Domenic Denicola 的 States and fates 有更多详情可供参考。
# 属性
Promise.length
length 属性,其值总是为 1 (构造器参数的数目).
表示 Promise
构造器的原型.
# 方法
这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。这个新的 promise 对象在触发成功状态以后,会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值,顺序跟 iterable 的顺序保持一致;如果这个新的 promise 对象触发了失败状态,它会把 iterable 里第一个触发失败的 promise 对象的错误信息作为它的失败错误信息。Promise.all 方法常被用于处理多个 promise 对象的状态集合。(可以参考 jQuery.when 方法—译者注)
当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。
返回一个状态为失败的 Promise 对象,并将给定的失败信息传递给对应的处理方法
返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象),返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。通常而言,如果你不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。
# 原型
# 属性
Promise.prototype.constructor
返回被创建的实例函数. 默认为
Promise
函数.
# 方法
Promise.prototype.catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的 promise。当这个回调函数被调用,新 promise 将以它的返回值来 resolve,否则如果当前 promise 进入 fulfilled 状态,则以当前 promise 的完成结果作为新 promise 的完成结果.
Promise.prototype.then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来 resolve.
Promise.prototype.finally(onFinally)
添加一个事件处理回调于当前 promise 对象,并且在原 promise 对象解析完毕后,返回一个新的 promise 对象。回调会在当前 promise 运行完毕后被调用,无论当前 promise 的状态是完成(fulfilled)还是失败(rejected)
# 自己实现剖析
https://mp.weixin.qq.com/s/3xfLpQ2h0v8yt2W7opLwGw
# 20 行案例
https://mp.weixin.qq.com/s/oHBv7r6x7tVOwm-LsnIbgA
|
|
# 实现过程
首先来实现 Promise 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
function Promise(excutor) { var self = this; self.onResolvedCallback = []; // Promise resolve时的回调函数集 // 传递给Promise处理函数的resolve // 这里直接往实例上挂个data // 然后把onResolvedCallback数组里的函数依次执行一遍就可以 function resolve(value) { // 注意promise的then函数需要异步执行 setTimeout(() => { self.data = value; self.onResolvedCallback.forEach((callback) => callback(value)); }); } // 执行用户传入的函数 excutor(resolve.bind(self)); }
好,写到这里先回过头来看案例
«««< HEAD
# Event Loop
Event Loop是一个程序结构,用于等待和分派消息和事件
,我个人的理解是 JS 中的 Event Loop 是浏览器或 Node 的一种协调 JavaScript 单线程运行时不会阻塞的一种机制。
# JS 的单线程
很多人都知道的是,JavaScript 是一门动态的解释型的语言,具有跨平台性。在被问到 JavaScript 为什么是一门单线程的语言,有的人可能会这么回答:“语言特性决定了 JavaScript 是一个单线程语言,JavaScript 天生是一个单线程语言”,这只不过是一层糖衣罢了。
JavaScript 从诞生起就是单线程,原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
准确的来说,我认为 JavaScript 的单线程是指 JavaScript 引擎是单线程的,JavaScript 的引擎并不是独立运行的,跨平台意味着 JavaScript 依赖其运行的宿主环境 — 浏览器(大部分情况下是浏览器)。
浏览器需要渲染 DOM,JavaScript 可以修改 DOM 结构,JavaScript 执行时,浏览器 DOM 渲染停止。如果 JavaScript 引擎线程不是单线程的,那么可以同时执行多段 JavaScript,如果这多段 JavaScript 都操作 DOM,那么就会出现 DOM 冲突。
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
举个例子来说,在同一时刻执行两个 script 对同一个 DOM 元素进行操作,一个修改 DOM,一个删除 DOM,那这样话浏览器就会懵逼了,它就不知道到底该听谁的,会有资源竞争,这也是 JavaScript 单线程的原因之一。
# 浏览器
# 浏览器的多线程
之前说过,JavaScript 运行的宿主环境浏览器是多线程的。
以 Chrome 来说,我们可以通过 Chrome 的任务管理器来看看。
当你打开一个 Tab 页面的时候,就创建了一个进程。如果从一个页面打开了另一个页面,打开的页面和当前的页面属于同一站点的话,那么这个页面会复用父页面的渲染进程。
# 浏览器主线程常驻线程
GUI 渲染线程
- 绘制页面,解析 HTML、CSS,构建 DOM 树,布局和绘制等
- 页面重绘和回流
- 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
JS 引擎线程
- 负责 JS 脚本代码的执行
- 负责准执行准备好待执行的事件,即定时器计数结束,或异步请求成功并正确返回的事件
- 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
事件触发线程
- 负责将准备好的事件交给 JS 引擎线程执行
- 多个事件加入任务队列的时候需要排队等待(JS 的单线程)
定时器触发线程
- 负责执行异步的定时器类的事件,如 setTimeout、setInterval
- 定时器到时间之后把注册的回调加到任务队列的队尾
HTTP 请求线程
- 负责执行异步请求
- 主线程执行代码遇到异步请求的时候会把函数交给该线程处理,当监听到状态变更事件,如果有回调函数,该线程会把回调函数加入到任务队列的队尾等待执行
这里没看懂没关系,后面我会再说。
# 浏览器端的 Event Loop
上图是一张 JS 的运行机制图,Js 运行时大致会分为几个部分:
- Call Stack:调用栈(执行栈),所有同步任务在主线程上执行,形成一个执行栈,因为 JS 单线程的原因,所以调用栈中每次只能执行一个任务,当遇到的同步任务执行完之后,由任务队列提供任务给调用栈执行。
- Task Queue:任务队列,存放着异步任务,当异步任务可以执行的时候,任务队列会通知主线程,然后该任务会进入主线程执行。任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中。
说到这里,Event Loop 也可以理解为:不断地从任务队列中取出任务执行的一个过程。
# 同步任务和异步任务
上文已经说过了 JavaScript 是一门单线程的语言,一次只能执行一个任务,如果所有的任务都是同步任务,那么程序可能因为等待会出现假死状态,这对于一个用户体验很强的语言来说是非常不友好的。
比如说向服务端请求资源,你不可能一直不停的循环判断有没有拿到数据,就好像你点了个外卖,点完之后就开始一直打电话问外卖有没有送到,外卖小哥都会抄着锅铲来打你(狗头)。因此,在 JavaScript 中任务有了同步任务和异步任务,异步任务通过注册回调函数,等到数据来了就通知主程序。
- 同步任务:必须等到结果来了之后才能做其他的事情,举例来说就是你烧水的时候一直等在水壶旁边等水烧开,期间不做其他的任何事情。
- 异步任务:不需要等到结果来了才能继续往下走,等结果期间可以做其他的事情,结果来了会收到通知。举例来说就是你烧水的时候可以去做自己想做的事情,听到水烧开的声音之后再去处理。
从概念就可以看出来,异步任务从一定程度上来看比同步任务更高效一些,核心是提高了用户体验。
# Event Loop
Event Loop 很好的调度了任务的运行,宏任务和微任务也知道了,现在我们就来看看它的调度运行机制。
JavaScript 的代码执行时,主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中先执行,异步任务会在拿到结果的时候将注册的回调函数放入任务队列,当执行栈中的没有任务在执行的时候,引擎会从任务队列中读取任务压入执行栈(Call Stack)中处理执行。
# 宏任务和微任务
现在就有一个问题了,任务队列是一个消息队列,先进先出,那就是说,后来的事件都是被加在队尾等到前面的事件执行完了才会被执行。如果在执行的过程中突然有重要的数据需要获取,或是说有事件突然需要处理一下,按照队列的先进先出顺序这些是无法得到及时处理的。这个时候就催生了宏任务和微任务,微任务使得一些异步任务得到及时的处理。
曾经看到的一个例子很好,宏任务和微任务形象的来说就是:你去营业厅办一个业务会有一个排队号码,当叫到你的号码的时候你去窗口办充值业务(宏任务执行),在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不可能让你重新排队。
所以上文说过的异步任务又分为宏任务和微任务,JS 运行时任务队列会分为宏任务队列和微任务队列,分别对应宏任务和微任务。
先介绍一下(浏览器环境的)宏任务和微任务大致有哪些:
宏任务:
- script(整体的代码)
- setTimeout
- setInterval
- I/O 操作
- UI 渲染 (对这个笔者持保留意见)
微任务:
- Promise.then
- MutationObserver
# 事件运行顺序
- 执行同步任务,同步任务不需要做特殊处理,直接执行(下面的步骤中遇到同步任务都是一样处理) — 第一轮从 script 开始
- 从宏任务队列中取出队头任务执行
- 如果产生了宏任务,将宏任务放入宏任务队列,下次轮循的时候执行
- 如果产生了微任务,将微任务放入微任务队列
- 执行完当前宏任务之后,取出微任务队列中的所有任务依次执行
- 如果微任务执行过程中产生了新的微任务,则继续执行微任务,直到微任务的队列为空
- 轮循,循环以上 2 - 6
总的来说就是:同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 同步任务/宏任务 -> 执行产生的所有微任务(包括微任务产生的微任务) -> 循环……
注意:微任务队列
# 举个栗子
光说不练假把式,现在就来看一个例子:
举个栗子
放图的原因是为了让大家在看解析之前可以先自己按照运行顺序走一遍,写好答案之后再来看解析。
解析:
(用绿色的表示同步任务和宏任务,红色表示微任务)
|
|
- 首先遇到 console.log(),输出
script start
- 遇到 setTimeout 产生宏任务,注册到宏任务队列[setTimeout],下一轮 Event Loop 的时候在执行
- 然后遇到 new Promise 构造声明(同步),log 输出
promise1
,然后 resolve - resolve 匹配到 promise1 的第一个 then,把这个 then 注册到微任务队列[then11]中,继续当前整体脚本的执行
- 遇到最后的一个 log,输出
script end
,当前执行栈清空 - 从微任务队列中取出队头任务’then11’ 进行执行,其中有一个 log,输出
then11
- 往下遇到 new Promise 构造声明(同步),log 输出
promise2
,然后 resolve - resolve 匹配到 promise2 的第一个 then,把这个 then 注册到微任务队列[then2-1],当前 then11 可执行部分结束,然后产生了 promise1 的第二个 then,把这个 then 注册到微任务队列[then2-1, then12]
- 拿出微任务队头任务’then2-1’ 执行,log 输出
then2-1
,触发 promise2 的第二个 then,注册到微任务队列[then12, then2-2] - 拿出微任务队头任务’then12’,log 输出
then12
- 拿出微任务队头任务’then2-2’,log 输出
then2-2
- 微任务队列执行完毕,别忘了宏任务队列中的 setTimeout,log 输出
setTimeout
经过以上一番缜(xia)密(gao)分析,希望没有绕晕你,最后的输出结果就是:script start -> promise1 -> script end -> then11 -> promise2 -> then2-1 -> then12 -> then2-2 -> setTimeout
# 宏任务?微任务?
不知道大家看了宏任务和微任务之后会不会有一个疑惑,宏任务和微任务都是异步任务,微任务之前说过了是为了及时解决一些必要事件而产生的。
为什么要有微任务?
为什么要有微任务的原因前面已经说了,这里就不再赘述,简单说一下就是为了及时处理一些任务,不然等到最后再执行的时候拿到的数据可能已经是被污染的数据达不到预期目标了。是什么宏任务?什么是微任务?
相信大家在学习 Event Loop 查找资料的时候,肯定各种资料里面都会讲到宏任务和微任务,但是不知道你有没有灵魂拷问过你自己:什么是宏任务?什么是微任务?怎么区分宏任务和微任务?
不能只是默许接受这个概念,在这里,我根据我的个人理解进行一番说(hu)明(che)宏任务和微任务的真面目
其实在 Chrome 的源码中并没有什么宏任务和微任务的代码或是说明,在 JS 大会[3]上提到过微任务这个名词,但是也没有说到底什么是微任务。宏任务
文章最开始的时候说过,在 chrome 里,每个页面都对应一个进程。而该进程又有多个线程,比如 JS 线程、渲染线程、IO 线程、网络线程、定时器线程等等,这些线程之间的通信是通过向对象的任务队列中添加一个任务(postTask)来实现的。宏任务的本质可以认为是多线程事件循环或消息循环,也就是线程间通信的一个消息队列。就拿 setTimeout 举例来说,当遇到它的时候,浏览器就会对 Event Loop 说:嘿,我有一个任务交给你,Event Loop 就会说:好的,我会把它加到我的 todoList 中,之后我会执行它,它是需要调用 API 的。
宏任务的真面目是浏览器派发,与 JS 引擎无关的,参与了 Event Loop 调度的任务
微任务
微任务是在运行宏任务/同步任务的时候产生的,是属于当前任务的,所以它不需要浏览器的支持,内置在 JS 当中,直接在 JS 的引擎中就被执行掉了。
# 特殊的点
- async 隐式返回 Promise 作为结果
- 执行完 await 之后直接跳出 async 函数,让出执行的所有权
- 当前任务的其他代码执行完之后再次获得执行权进行执行
- 立即 resolve 的 Promise 对象,是在本轮"事件循环"的结束时执行,而不是在下一轮"事件循环"的开始时
# 再举个栗子
|
|
按照之前的分析方法去分析之后就会得出一个结果:script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
可以看出 async1 函数获取执行权是作为微任务的队尾,但是,在 Chrome73(金丝雀) 版本之后,async 的执行优化了,它会在 promise1 和 promise2 的输出之前执行。笔者大概了解了一下应该是用 PromiseResolve 对 await 进行了优化,减少了 Promise 的再次创建,有兴趣的小伙伴可以看看 Chrome 的源码。
# Node 中的 Event Loop
Node 中也有宏任务和微任务,与浏览器中的事件循环类似。Node 与浏览器事件循环不同,其中有多个宏任务队列,而浏览器是只有一个宏任务队列。
Node 的架构底层是有 libuv,它是 Node 自身的动力来源之一,通过它可以去调用一些底层操作,Node 中的 Event Loop 功能就是在 libuv 中封装实现的。
# 宏任务和微任务
Node 中的宏任务和微任务在浏览器端的 JS 相比增加了一些,这里只列出浏览器端没有的:
- 宏任务
- setImmediate
- 微任务
- process.nextTick
# 事件循环机制的六个阶段
Node 的事件循环分成了六个阶段,每个阶段对应一个宏任务队列,相当于是宏任务进行了一个分类。
- timers(计时器)
执行 setTimeout 以及 setInterval 的回调 - I/O callbacks
处理网络、流、TCP 的错误回调 - idel, prepare — 闲置阶段
node 内部使用 - poll(轮循)
执行 poll 中的 I/O 队列,检查定时器是否到时间 - check(检查)
存放 setImmediate 回调 - close callbacks
关闭回调,例如 sockect.on(‘close’)
# 轮循顺序
执行的轮循顺序 — 每个阶段都要等对应的宏任务队列执行完毕才会进入到下一个阶段的宏任务队列
- timers
- I/O callbacks
- poll
- setImmediate
- close events
每两个阶段之间执行微任务队列
# Event Loop 过程
- 执行全局的 script 同步代码
- 执行微任务队列,先执行所有 Next Tick 队列中的所有任务,再执行其他的微任务队列中的所有任务
- 开始执行宏任务,共六个阶段,从第一个阶段开始执行自己宏任务队列中的所有任务(浏览器是从宏任务队列中取第一个执行!!)
- 每个阶段的宏任务执行完毕之后,开始执行微任务
- TimersQueue -> 步骤 2 -> I/O Queue -> 步骤 2 -> Check Queue -> 步骤 2 -> Close Callback Queue -> 步骤 2 -> TimersQueue …
这里要注意的是,nextTick 事件是一个单独的队列,它的优先级会高于微任务,所以在当前宏任务/同步任务执行完成之后,会先执行 nextTick 队列中的所有任务,再去执行微任务队列中的所有任务。
# setTimeout 和 setImmediate
在这里要单独说一下 setTimeout 和 setImmediate,setTimeout 定时器很熟悉,那就说说 setImmediate
setImmediate() 方法用于把一些需要长时间运行的操作放在一个回调函数里,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数。从定义来看就是为了防止一些耗时长的操作阻塞后面的操作,这也是为什么 check 阶段运行顺序排的比较后。
# 举个栗子
我们来看这样的一个例子:
|
|
这里涉及 timers 阶段和 check 阶段,按照上面的运行顺序来说,timers 阶段是在第一个执行的,会早于 check 阶段。运行这段程序可以看到如下的结果:
可是再多运行几次,你就会看到如下的结果:
setImmediate 的输出跑到 setTimeout 前面去了,这时候就是:小朋友你是否有很多的问号 ❓
# 分析
我们来分析一下原因,timers 阶段确实是在 check 阶段之前,但是在 timers 阶段时候,这里的 setTimeout 真的到了执行的时间吗?
这里就要先看看 setTiemout(fn, 0)
,这个语句的意思不是指不延迟的执行,而是指在可以执行 setTimeout 的时候就立即执行它的回调,也就是处理完当前事件的时候立即执行回调。
在 Node 中 setTimeout 第二个时间参数的最小值是 1ms,小于 1ms 会被初始化为 1(浏览器中最小值是 4ms),所以在这里 setTimeout(fn, 0) === setTimeout(fn, 1)
setTimeout 的回调函数在 timers 阶段执行,setImmediate 的回调函数在 check 阶段执行,Event Loop 的开始会先检查 timers 阶段,但是在代码开始运行之前到 timers 阶段(代码的启动、运行)会消耗一定的时间,所以会出现两种情况:
- timers 前的准备时间超过 1ms,满足 loop -> timers >= 1,setTimeout 的时钟周期到了,则执行 timers 阶段(setTimeout)的回调函数
- timers 前的准备时间小于 1ms,还没到 setTimeout 预设的时间,则先执行 check 阶段(setImmediate)的回调函数,下一次 Event Loop 再进入 timers 阶段执行 timer 阶段(setTimeout)的回调函数
最开始就说了,一个优秀的程序员要让自己的代码按照自己想要的顺序运行,下面我们就来控制一下 setTimeout 和 setImediate 的运行。
- 让 setTimeout 先执行
上面代码运行顺序不同无非就是因为 Node 准备时间的不确定性,我们可以直接手动延长准备时间 👇
|
|
让 setImmediate 先执行
setImmediate 是在 check 阶段执行,相对于 setTimeout 来说是在 timers 阶段之后,只需要想办法把程序的运行环境控制在 timers 阶段之后就可以了。让程序至少从 I/O callbacks 阶段开始 — 可以套一层文件读写把把程序控制在 I/O callbacks 阶段的运行环境中 👇
|
|
# Node 11.x 的变化
timers 阶段的执行有所变化
|
|
- node 10 及之前的版本:
要考虑上一个定时器执行完成时,下一个定时器是否到时间加入了任务队列中,如果未到时间,先执行其他的代码。
比如:
timer1 执行完之后 timer2 到了任务队列中,顺序为timer1 -> timer2 -> promise resolve
timer2 执行完之后 timer2 还没到任务队列中,顺序为timer1 -> promise resolve -> timer2
- node 11 及其之后的版本:
timeout1 -> timeout2 -> promise resolve
一旦执行某个阶段里的一个宏任务之后就立刻执行微任务队列,这和浏览器端运行是一致的。
# 小结
Node 和端浏览器端有什么不同
- 浏览器端的 Event Loop 和 Node.js 中的 Event Loop 是不同的,实现机制也不一样
- Node.js 可以理解成有 4 个宏任务队列和 2 个微任务队列,但是执行宏任务时有 6 个阶段
- Node.js 中限制性全局 script 代码,执行完同步代码后,先从微任务队列 Next Tick Queue 中取出所有任务放入调用栈执行,再从其他微任务队列中取出所有任务放入调用栈中执行,然后开始宏任务的 6 个阶段,每个阶段都将其宏任务队列中的所有任务都取出来执行(浏览器是只取第一个执行),每个宏任务阶段执行完毕之后开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环
- 宏任务包括 ….
- 微任务包括 ….
看到这里,你应该对浏览器端和 Node 端的 Event Loop 有了一定的了解,那就留一个题目。
不直接放代码是想让大家先自己思考然后在敲代码运行一遍~
# void
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/void
# 语法
|
|
# 描述
这个运算符能向期望一个表达式的值是
undefined
的地方插入会产生副作用的表达式。
void 运算符通常只用于获取 undefined
的原始值,一般使用void(0)
(等同于void 0
)。在上述情况中,也可以使用全局变量
undefined
来代替(假定其仍是默认值)。
# 立即调用的函数表达式
在使用
立即执行的函数表达式时,可以利用 void
运算符让 JavaScript 引擎把一个function
关键字识别成函数表达式而不是函数声明(语句)。
|
|
# JavaScript URIs
当用户点击一个以 javascript:
URI 时,它会执行 URI 中的代码,然后用返回的值替换页面内容,除非返回的值是
undefined
。void
运算符可用于返回
undefined
。例如:
|
|
注意,虽然这么做是可行的,但利用 javascript:
伪协议来执行 JavaScript 代码是不推荐的,推荐的做法是为链接元素绑定事件。
# 在箭头函数中避免泄漏
箭头函数标准中,允许在函数体不使用括号来直接返回值。 如果右侧调用了一个原本没有返回值的函数,其返回值改变后,则会导致非预期的副作用。 安全起见,当函数返回值是一个不会被使用到的时候,应该使用 void
运算符,来确保返回
undefined
(如下方示例),这样,当 API 改变时,并不会影响箭头函数的行为。
|
|
确保了当 doSomething
的返回值从
undefined
变为 true
的时候,不会改变函数的行为
# undefined 与 null 的区别
大多数计算机语言,有且仅有一个表示"无"的值,比如,C 语言的 NULL,Java 语言的 null,Python 语言的 None,Ruby 语言的 nil。
有点奇怪的是,JavaScript 语言居然有两个表示"无"的值:undefined 和 null。这是为什么?
在 JavaScript 中,将一个变量赋值为 undefined 或 null,老实说,几乎没区别。
1 2 3
var a = undefined; var a = null;
上面代码中,a 变量分别被赋值为 undefined 和 null,这两种写法几乎等价。
undefined 和 null 在 if 语句中,都会被自动转为 false,相等运算符甚至直接报告两者相等。
1 2 3 4 5 6 7 8
if (!undefined) console.log("undefined is false"); // undefined is false if (!null) console.log("null is false"); // null is false undefined == null; // true
上面代码说明,两者的行为是何等相似!
既然 undefined 和 null 的含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加 JavaScript 的复杂度,令初学者困扰吗?Google 公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有 null,没有 undefined!
# 历史原因
原来,这与 JavaScript 的历史有关。1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 作为表示"无"的值。
根据 C 语言的传统,null 被设计成可以自动转为 0。
1 2 3 4 5
Number(null); // 0 5 + null; // 5
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够,有两个原因。
首先,null 像在 Java 里一样,被当成一个对象。但是,JavaScript 的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich 觉得表示"无"的值最好不是对象。
其次,JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。
因此,Brendan Eich 又设计了一个 undefined。
# 最初设计
JavaScript 的最初版本是这样区分的:null 是一个表示"无"的对象,转为数值时为 0;undefined 是一个表示"无"的原始值,转为数值时为 NaN。
1 2 3 4 5
Number(undefined); // NaN 5 + undefined; // NaN
# 目前的用法
但是,上面这样的区分,在实践中很快就被证明不可行。目前,null 和 undefined 基本是同义的,只有一些细微的差别。
**null 表示"没有对象",即该处不应该有值。**典型用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
1 2
Object.getPrototypeOf(Object.prototype); // null
**undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义。**典型用法是:
(1)变量被声明了,但没有赋值时,就等于 undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
(3)对象没有赋值的属性,该属性的值为 undefined。
(4)函数没有返回值时,默认返回 undefined。
1 2 3 4 5 6 7 8 9 10 11 12 13
var i; i; // undefined function f(x) { console.log(x); } f(); // undefined var o = new Object(); o.p; // undefined var x = f(); x; // undefined
# ES6
# 什么是 ES6
ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。
# 为什么使用 ES6 ?
每一次标准的诞生都意味着语言的完善,功能的加强。JavaScript 语言本身也有一些令人不满意的地方。
- 变量提升特性增加了程序运行时的不可预测性
- 语法过于松散,实现相同的功能,不同的人可能会写出不同的代码
# ES6 新增语法
# let(★★★)
ES6 中新增了用于声明变量的关键字
# let 声明的变量只在所处于的块级有效
|
|
**注意:**使用 let 关键字声明的变量才具有块级作用域,使用 var 声明的变量不具备块级作用域特性。
# 不存在变量提升
|
|
# 暂时性死区
利用 let 声明的变量会绑定在这个块级作用域,不会受外界的影响
|
|
# 经典面试题
|
|
**经典面试题图解:**此题的关键点在于变量 i 是全局的,函数执行时输出的都是全局作用域下的 i 值。
|
|
**经典面试题图解:**此题的关键点在于每次循环都会产生一个块级作用域,每个块级作用域中的变量都是不同的,函数执行时输出的是自己上一级(循环产生的块级作用域)作用域下的 i 值.
# 小结
- let 关键字就是用来声明变量的
- 使用 let 关键字声明的变量具有块级作用域
- 在一个大括号中 使用 let 关键字声明的变量才具有块级作用域 var 关键字是不具备这个特点的
- 防止循环变量变成全局变量
- 使用 let 关键字声明的变量没有变量提升
- 使用 let 关键字声明的变量具有暂时性死区特性
# const(★★★)
声明常量,常量就是值(内存地址)不能变化的量
# 具有块级作用域
|
|
# 声明常量时必须赋值
|
|
# 常量赋值后,值不能修改
|
|
# 小结
- const 声明的变量是一个常量
- 既然是常量不能重新进行赋值,如果是基本数据类型,不能更改值,如果是复杂数据类型,不能更改地址值
- 声明 const 时候必须要给定值
# let、const、var 的区别
- 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象
- 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升
- 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值
# 解构赋值(★★★)
ES6 中允许从数组中提取值,按照对应位置,对变量赋值,对象也可以实现解构
# 数组解构
|
|
# 对象解构
|
|
# 小结
- 解构赋值就是把数据结构分解,然后给变量进行赋值
- 如果结构不成功,变量跟数值个数不匹配的时候,变量的值为 undefined
- 数组解构用中括号包裹,多个变量用逗号隔开,对象解构用花括号包裹,多个变量用逗号隔开
- 利用解构赋值能够让我们方便的去取对象中的属性跟方法
# 箭头函数(★★★)
ES6 中新增的定义函数的方式。
|
|
函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号
|
|
如果形参只有一个,可以省略小括号
|
|
箭头函数不绑定 this 关键字,箭头函数中的 this,指向的是函数定义位置的上下文 this
|
|
# 小结
- 箭头函数中不绑定 this,箭头函数中的 this 指向是它所定义的位置,可以简单理解成,定义箭头函数中的作用域的 this 指向谁,它就指向谁
- 箭头函数的优点在于解决了 this 执行环境所造成的一些问题。比如:解决了匿名函数 this 指向的问题(匿名函数的执行环境具有全局性),包括 setTimeout 和 setInterval 中使用 this 所造成的问题
# 面试题
|
|
# 剩余参数(★★)
剩余参数语法允许我们将一个不定数量的参数表示为一个数组,不定参数定义方式,这种方式很方便的去声明不知道参数情况下的一个函数
|
|
# 剩余参数和解构配合使用
|
|
# ES6 的内置对象扩展
# Array 的扩展方法(★★)
# 扩展运算符(展开语法)
扩展运算符可以将数组或者对象转为用逗号分隔的参数序列
|
|
扩展运算符可以应用于合并数组
|
|
将类数组或可遍历对象转换为真正的数组
|
|
# 构造函数方法:Array.from()
将伪数组或可遍历对象转换为真正的数组
|
|
方法还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组
|
|
注意:如果是对象,那么属性需要写对应的索引
# 实例方法:find()
用于找出第一个符合条件的数组成员,如果没有找到返回 undefined
|
|
# 实例方法:findIndex()
用于找出第一个符合条件的数组成员的位置,如果没有找到返回-1
|
|
# 实例方法:includes()
判断某个数组是否包含给定的值,返回布尔值。
|
|
# String 的扩展方法
# 模板字符串(★★★)
ES6 新增的创建字符串的方式,使用反引号定义
|
|
模板字符串中可以解析变量
|
|
模板字符串中可以换行
|
|
在模板字符串中可以调用函数
|
|
# 实例方法:startsWith() 和 endsWith()
- startsWith():表示参数字符串是否在原字符串的头部,返回布尔值
- endsWith():表示参数字符串是否在原字符串的尾部,返回布尔值
|
|
# 实例方法:repeat()
repeat 方法表示将原字符串重复 n 次,返回一个新字符串
|
|
# Set 数据结构(★★)
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set 本身是一个构造函数,用来生成 Set 数据结构
|
|
Set 函数可以接受一个数组作为参数,用来初始化。
|
|
# 实例方法
- add(value):添加某个值,返回 Set 结构本身
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功
- has(value):返回一个布尔值,表示该值是否为 Set 的成员
- clear():清除所有成员,没有返回值
|
|
# 遍历
Set 结构的实例与数组一样,也拥有 forEach 方法,用于对每个成员执行某种操作,没有返回值。
|
|