前言
众所周知,js可以被划分为三大块,分别是ECMAScript、DOM、BOM。
其中ES是js的核心部分,定义了js的语法、数据结构和运算等;
DOM是document Object Model(文档对象模型)它是js操作页面的接口,将页面结构表示为一个可操作的树结构;
BOM 提供了与浏览器窗口进行交互的接口,允许操作浏览器窗口的属性和功能,如控制浏览器历史记录、导航等。
ECMAScript
js上下文
js代码在执行前,js引擎要做一番准备工作,就是创建对应的执行上下文。执行上下文可分为三类:全局执行上下文、函数上下文与eval上下文,eval上下文一般不会使用
==全局上下文==
全局上下文只有一个,一般由browser创建,也就是window对象,可以通过this直接访问它。window是一个全局对象,在全局环境的任意地方都能访问到它,window上预定义了大量属性和方法,同时window是var声明的全局变量的载体,通过window可以访问到所有我们通过var声明的变量
==函数执行上下文==
函数执行上下文可以有无数个,每一个函数调用时都会创建一个函数上下文(注:同一个函数被多次调用,每次调用都会产生一个新的上下文)。代码执行期间创建的所有上下文都会存放到执行上下文栈中,js代码首次运行,会创建全局上下文并压入执行栈中,之后,每次有函数调用,都会生成一个新的函数执行上下文压入栈中。在js代码执行完成之前总有全局上下文位于栈底部
js执行上下文的创建可分为创建阶段和执行阶段,
- 创建阶段(在调用函数,未执行任何代码之前)
下面的三种是ES3旧的说法:
- 创建作用域链
- 创建变量,函数和参数
- 确定
this,在全局执行上下文中,this的值指向全局对象;在函数执行上下文中,如果函数属于一个对象,this的值会指向这个对象,否则this指向全局对象或undefined
新的说法:
- 绑定
this:在全局执行上下文中,this的值指向全局对象;在函数执行上下文中,this的值取决于它是如何被调用的。如果函数被一个引用对象调用,this的值会指向这个对象,否则this指向全局对象或undefined - 创建
词法环境:词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量(函数)的名称,变量是对实际对象(包括函数类型对象)或原始值的引用。 - 创建变量环境组件参考文档:理解js上下文
1
2
3
4
5ExecutionContext = {
ThisBinding = <this value> // this绑定
LexicalEnvironment = {...} // 词法环境
VariableEnvironment = {...} // 变量环境
}
js事件流
==冒泡事件流==
IE提出,事件从最具体的元素接收,逐级向上传播
==捕获事件流==
NetScape提出,事件由最大的容器元素接收,逐级传播到具体元素
addEventListener事件委托,事件流的三个阶段
捕获阶段==>目标阶段==>冒泡阶段,当事件处于目标阶段,触发两次,分别属于捕获和冒泡两个阶段
IE8以下只支持冒泡事件流,只有目标和冒泡阶段,且冒泡只到document
event对象常用属性
Target事件目标currentTarget绑定事件的元素,和this的指向相同preventDefault()取消事件的默认行为,比如点击跳转type被触发的事件的类型eventPhase调用事件处理程序的阶段:0表示这个时间没有事件正在被处理,1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段addEventListener 语法
mdn
接受四个(旧版三个)参数:typestring类型 事件类型listener必须是一个实现了EventListener接口的对象,或者是一个函数-
options有关listener的参数对象,旧版本浏览器没有这个参数,仍然将第三个参数认为是useCapture,options参数的安全检查 capture捕获阶段触发,这个参数主要用的场景有:需要考虑父元素和子元素事件触发的顺序;在捕获阶段阻止冒泡阶段的传播;多层级元素监听同一事件once只调用一次passivetrue表示listener永远不会调用preventDefault(),否则忽略并控制台警告,在触摸事件和滚动事件通常将这个设置为true,否则页面不能响应触摸/滚动signalAbortSignal,该AbortSignal的abort()方法被调用时,监听器会被移除。useCaptureboolean类型,是否在捕获阶段触发,默认false将事件监听器函数单独提出为一个函数,而不是传入匿名函数或箭头函数,以防止重复添加同样的监听函数1
2
3
4
5
6
7
8
9
10el.addEventListener(type, listener, useCapture);
// options.signal使用
const controller = new AbortController();
el.addEventListener('click', (event) => {
event.target.textContent = 'tt';
// 在特定条件下移除监听器
if(event.target.textContent === 'tt') {
controller.abort();
}
}, {signal: controller.signal})
==关于传递给addEventlistener的回调函数中this的值==
- 如果传入匿名函数,函数内this指向该元素的引用,等同于
event.currentTarget的值 - 如果传入箭头函数,函数内this等于声明此箭头函数的上下文的this
- 还有一种情况,如果是在html中为元素指定onclick属性,那么这个属性中的js语句实际上会被包裹在一个处理函数中,处理函数中的this指向目标元素。
==使用handleEvent特殊函数来捕获任意事件==
1 | const Something = function (element) { |
==往listener传入数据==
一种办法是使用this传值
1 | const obj = { |
还可以在listener内使用添加监听器时所在的作用域的变量,要注意的是如果在listener内修改了变量,在外部作用域并不能得到体现,因为当监听器事件执行,外部作用域所在的代码已经执行完毕。这里需要区分的一个细节是,如果引用的是一个对象,由于对象只要被引用就会一直留存与内存中,即使它的作用域已经结束。(基于这一点,可以在一个函数执行完毕后返回一个对象使其一直留存于内存中)
1 | const data = "something"; |
自定义事件
可以使用Event构造函数创建事件
1 | const event = new Event("build"); |
使用CustomEvent创建自定义事件,可在第二个参数的detail属性传递自定义数据
1 | const event = new CustomEvent("build", {detail: 'some'}); |
EventTarget.dispatchEvent()向指定事件目标派发一个event,同步地调用所有事件监听器,返回值是一个布尔值,当派发的event可被取消(即cancelable === true)时且至少有一个事件监听器调用了preventDefault方法,返回false,否则true
1 | const event = new Event('click', { |
==自定义事件的使用场景==
主要用于解耦不同模块之间的通信,解决一些无法之间通过标准事件机制处理的问题
迭代器和生成器
==理解迭代==
先讲几个与迭代有关的关键词:有序、可以按某种顺序遍历、明确的开始和结束
计数循环就是一种最简单的迭代,数据本身是有序的,每一项可以通过索引来获取,可以通过递增索引来遍历。但是通过索引访问这种方式并不够通用,不是所有的数据类型都可以通过索引访问。
先抛出几个名词
Iterable可迭代接口(可迭代协议),要求返回一个生成可迭代对象(迭代器)的工厂函数Iterator迭代器,是一次性的,用于迭代器关联的可迭代对象。有一个next方法,next方法返回一个可迭代对象IteratorResult可迭代对象,包括两个属性:{done: Boolean, value: any},done表示是否还可以再次调用next(),vaule是可迭代对象的下一个值
ES6支持了迭代器模式:有一些结构可被称为’可迭代对象‘,它们实现了Iterable接口,任意可迭代对象都可以被迭代器Iterator消费。在ES中,实现一个可迭代协议意味着:
- 需要暴露一个属性作为’默认迭代器‘,这个属性的key必须是
Symbol.iterator - ’默认迭代器‘必须引用一个迭代器工厂函数,调用这个工厂函数会返回一个新迭代器
大部分内置数据类型都实现了Iterable接口,包括String/Array/Map/Set/arguments对象/NodeList等dom集合类型,Number和Object类型没有Iterable接口。访问默认迭代器属性可以返回对应的迭代器工厂函数,例如:
1 | const str = 'abc'; |
js的很多原生语言结构中会自动调用这个工厂函数,包括:
for-of循环- 数组结构
- 扩展操作符
Array.from- 创建集合、映射
Promise.all()和Promise.race()yield*操作符
除了js一些数据类型自带的迭代器接口,还可以自定义一个迭代器,只需要实现一个Iterator接口,下面是一个例子
1 | class Counter { |
==关闭一个迭代器==
- 迭代器可以提前终止,在可迭代对象耗尽之前,通过
return方法终止一个迭代器,要求返回一个有效的IteratorResult对象。 return方法是可选的,因此不是所有迭代器都可关闭,比如数组类型,要判断一个迭代器是否可关闭,可以测试迭代器返回的return属性是不是一个函数对象。对于一个未关闭的迭代器,可以从上次离开的地方继续迭代- 给一个不可关闭的迭代器增加一个
return方法并不能将它变成可关闭的,虽然return方法还是会被调用
生成器
ES6新增的一种灵活结构,能够在一个函数块内暂停和回复代码执行。通过生成器可以实现自定义迭代器
==定义一个生成器==
生成器的形式是一个函数,在函数名之前加一个 * 符号表面这是一个生成器。要注意的是箭头函数不能用来定义生成器函数。*左右的空格不影响生成器函数的声明
1 | function *generator() {} |
==生成器的返回值==
返回一个生成器对象,是一个类似于迭代器的对象,实现了Iterator接口,具有next方法。调用next方法会返回一个类似可迭代对象的对象:{done: Boolean, value: any}
定义完生成器函数,调用生成器函数得到一个生成器对象时不会执行函数体内容,而只会在初次调用next方法之后执行。
生成器对象实现了Iterable接口(注意和上面的实现了Iterator接口区分开来),这意味着生成器对象有一个key为Symbol.iterator的属性方法,这个属性方法能够返回一个默认的迭代器,而这个默认的的迭代器指向生成器对象自身,形成一个自引用
1 | function *generator() {} |
上面讲到,生成器有暂停执行和开始执行的状态,在遇到yield之前,代码正常执行;遇到yield关键词后,代码会停止执行,函数作用域的状态会保留。yield关键词可以结合return关键词理解为在函数中间返回,return关键词则是在函数结尾返回。通过yield退出的生成器函数会处在 done: false 状态,而通过return退出的生成器函数会处在 done: true 状态。yield关键词只能在生成器函数内直接的函数块中使用,不可嵌套使用
1 | function *generator() { |
生成器函数生成的每个生成器对象直接互相独立区分作用域,各自调用next方法互不影响
==生成器的使用场景==
结合迭代器的Iterator接口,如果将生成器对象当做迭代器使用,会十分方便:生成器对象在调用next方法后会开始或重新开始执行代码,而迭代器执行迭代的过程正是在不断调用next方法的过程
1 | function *generator() { |
事件循环
==JS单线程==
JavaScript的一大特点是单线程,即在同一时间只能做一件事,JavaScript的主要用途是通过操作dom实现和用户的互动,这决定了JavaScript只能是单线程,否则会带来复杂的多线程问题。因此虽然web work允许JavaScript创建多线程,但子线程是无法操作dom的,操作dom的任务需要交给主线程来执行。
单线程的优点是:代码运行比较轻松,不必处理多线程中可能出现的一些复杂情况。但单线程可能会遇到阻塞,导致页面卡住。对此,js采用的方式是异步回调函数机制,
Js中的任务可分为同步任务和异步任务,同步任务在主线程上排队执行,形成一个执行栈(execution context stack),前一个任务执行完成才能执行下一个任务;异步任务则不进入主线程,而是放入任务队列(task queue),只有task queue通知主线程某个异步任务可以执行了,该异步任务才会进入主线程执行。一旦stack中的所有同步任务执行完毕,将从task queue中读取任务。
有哪些异步任务?
axios,
settimeout、setInterminal
promise
async/await
在js中异步编程的进化过程callback -> promise -> generator -> async、await
为什么要引入两种任务?
在任务队列task queue中,所有任务会按照先进先出的执行顺序来执行,我们无法准确地控制这些任务添加到队列中的位置,有可能存在需要先执行的高优先级的任务需要尽快执行,因此需要多种任务类型,比如一个setTimeout任务,如果没有微任务的概念,这个setTimeout任务会一直阻塞后面的其他异步任务。
不同的异步任务被分为宏任务和微任务,分别对应宏任务队列和微任务队列
浏览器环境下的event loop
宏任务由==浏览器(或nodejs)运行时发起==。
宏任务有:
- Script代码
- setTimeout()
- setInterval()
- postMessage()
- I/O
- UI交互
微任务由==js引擎本身发起==。【注意理解这两者的差别:运行时必作行政部门,js引擎比作核心研发部门。“浏览器或nodejs发起”指的是由运行时安排任务,js引擎只负责执行而不关心执行的时机;“js引擎本身发起”意味着任务的产生和执行时机完全掌控在js引擎手中】
微任务优先级高于宏任务。
微任务有:
- Promise/then/catch/finally
- mutationObserver
- process.nextTick
异步任务的结果会放到task queue中,具体又分为宏任务队列和微任务队列。当执行栈为空,主线程查看微任务队列中是否有任务存在,如果存在,主线程会依次执行微任务队列中的异步任务,直到微任务队列为空,再去宏任务队列中取出一个最前面的任务加入执行栈。因此,在同一个event loop中,微任务永远在宏任务之前执行。
宏任务和微任务的执行顺序
Event loop中的每一次循环称为一个tick,每一个tick的步骤如下:
- 执行一个宏任务(如果栈中没有则从任务队列获取)
- 执行过程中遇到微任务,将微任务添加到任务队列的微任务队列
- 宏任务执行完毕,这个时候执行栈空了,从微任务队列中获取任务并执行,直到微任务队列为空。
- 执行完所有微任务后,如果有必要,会进行页面渲染
- 下一个event loop。
==总结==:Js代码运行过程:读取js代码,将同步任务加入到执行栈,遇到异步任务,细分为宏任务和微任务,遇到宏任务加到宏任务队列,遇到微任务则加到微任务队列,当执行栈为空,首先从微任务队列中获取微任务并执行(执行过程中如果遇到新的微任务则继续添加到微任务队列),直到微任务队列为空,微任务队列清空后,从宏任务队列中拿一个最前面的任务执行(宏任务中同步任务在执行栈中执行,宏任务中遇到的微任务也添加到微任务队列),宏任务执行完成后,执行栈空,再次从微任务队列获取任务。完成一次event loop。
NodeJs环境下的event loop
Event loop几个阶段
Node环境中的event loop和browser大致相同,nodejs使用V8引擎作为js解析的引擎,在I/O处理方面则使用了自己设计的libuv。Node的event loop存在几个阶段,如下图所示:
Times定时器阶段:处理setTimeout和setInterval回调函数,进入这个节点后,主线程会检查一下当前时间是否满足定时器条件,满足就执行回调函数,否则进入下一阶段。
Poll轮询阶段:用于等待未返回的I/O事件,比如服务器响应,用户移动鼠标等,此阶段时间会较长,如果没有其他异步任务,会一直停留在此阶段。
Check检查阶段:执行setImmediate的回调函数。
Node环境下的宏任务和微任务
宏任务有:
- SetImmediate
- SetTimeout
- SetInterval
- Script代码
- I/O操作
微任务有: - Process.nextTick
- Promise.then
Process.nextTick()和setImmediate
Event loop进行一次完整的行程,称为一个tick。Process.nextTick是一个独立于event loop外的任务队列,在每一次event loop阶段完成后都会去检查nextTick队列,如果里面有任务,这部分任务将优先于微任务执行,所以nextTick队列里的任务是所有异步任务中最快执行的。将一个函数传给nextTick函数,则告诉引擎在下一个tick之前调用此函数,setTimeout(function, 0)会在下一个tick结束时执行此function,而nextTick中的函数会在下一个tick开始前执行。当要确保在下一个tick中代码已被执行,使用nextTick。
SetImmediate方法会立即执行,实际上它只会在poll阶段之后即check阶段才会执行。SetImmediate和setTimeout很相似。
JS定时器
SetInterval类似于setTimeout,它会在指定的时间间隔一直执行回调函数,只有使用cleanInterval才会停止,通常在setinterval内部调用cleanInterval,自行判断是否停止。
如果一个函数总是花费相同的时间,可以用setInterval,但是如果函数会有不同的执行时间,比如因为网络的原因,可以在回调函数完成后递归使用setTimeout:
1 | const myFunc = () => { |
ECMS2015/ES6语法
原始值包装类型
js提供了三种特殊的引用类型:Boolean、Number、String,这些类型具有引用类型一样的特点,也具有与各自原始类型对应的特殊行为。每当用到某个原始类型值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。
栗子:
1 | let s = "i am a text"; |
s是一个字符串类型变量,属于原始类型,本身没有substring方法。当执行到s.substring(2)时,后台会执行如下操作:
- 创建一个
String类型的实例; - 调用实例的
substring方法; - 销毁实例;
这些行为会让原始值具有对象的行为。
【注1】:当我们使用let s = new String('text')显式地创建一个变量时,s的类型是'object',
【注2】:注意区分 使用转型函数 和 使用new调用构造函数 两种情况是不一样的,例如:
1 | let n1 = Number(123); |
let和var
- 在使用
var声明变量时,变量会被自动添加到最近的上下文。在函数内,最近的上下文是函数局部上下文,在with语句中,最近的也是函数上下文。如果未使用声明就使用的变量,将自动被添加到全局上下文。在严格模式中,未经声明就使用的变量会报错。 let的作用域限制在当前代码块中,let声明的变量不会进行变量提升,let不允许在同一个作用域中重复声明同一变量,一个{}会成为一个块级作用域(const声明的变量同let),==var的作用域范围是函数作用域==,let声明的范围是块作用域。块作用域是函数作用域的子集。const声明常量,用const声明的基础类型变量不可修改,const声明的引用型变量可以修改内容,但不可改变指向的对象。用const声明的变量的同时必须初始化。- 使用
const或let声明的变量只有在执行到声明所在位置之后才能被访问,在这之前,变量处于‘临时死区’,任何访问尝试将报错ReferenceError,因此认为let声明的变量是非提升的。而var会有变量声明提升,即所有变量声明放到函数作用域的顶部【注:undefined是一种数据类型,变量已声明但未赋值会有undefined值,访问对象中不存在的属性也会返回undefined,ReferenceError是一种错误类型,变量未声明却被访问 或 访问超出作用域的变量会抛出此种错误】 - 在循环中,
let声明的变量i只在本轮循环中有效,每次循环的i都是一个新的变量。而且,循环变量所在的部分和循环体内部处于两个不同的作用域 let和const声明的变量挂载到
【more】函数声明提升
和var的变量声明提升类似,函数声明也会被提升到当前作用域的顶部,在函数声明之前可以调用该函数。不同的是,对于变量提升,只有变量名被提升,初始值不会被提升;而函数声明提升会提升整个函数的定义,包括函数体,即函数声明的初始化会立即完成
【注意】将匿名函数赋值给一个变量(函数表达式)属于变量声明,不是函数声明,因此只提升声明,不提升函数体
1 | for (let i = 0; i < 3; i++) { |
1 | let a=0; |
全局对象
浏览器中的全局对象是window,node环境中全局对象是global
- 全局环境下,
this返回全局对象;在node模块中,this返回当前模块;ES6模块中this返回undefined - 单独的一个函数中的
this指向全局对象,而作为一个对象的方法属性的函数中的this指向根据其被调用的场景不同而不同
变量解构赋值–模式匹配
等号两边的模式相同,变量将会被赋予对应的值,注意需要是数组
1 | let [a,b,c]=[1,2,3] |
解构不成功,变量的值是 undefined。解构可设置默认值,只有当数组内对应位置的成员严格等于(===)undefined,才会使用默认值。对于表达式,比如函数,是惰性求值的(即在用到之前都不会执行)
解构在内部私用ToObject()把源数据转换成对象,原始值会被当成对象,根据ToObject的定义,null和undefined不能被解构,否则会抛出错误
1 | let {length} = "foobar"; |
如果是给事先声明的变量赋值,解构表达式必须包含在一对括号中
1 | let name, age; |
多个属性的解构是一个输出无关的顺序化操作,当前一部分赋值成功但后面某个赋值失败,整个解构赋值会完成一部分,
箭头函数
- 箭头函数没有自己的
this,它的this是定义该函数时所在的作用域的this - 箭头函数没有
arguments对象,没有原型对象 - 箭头函数不可作为构造函数(因为它没有自己的this,并且没有prototype属性,这些都是构造函数必要的)箭头函数直接返回一个对象,使用()
1
2
3
4
5
6
7
8
9
10
11
12let obj={
f:()=>{console.log(this)}
}
obj.f(); // window,因为f所在的作用域是最外层的js,它没有被其他函数包裹。
let obj2={
n:'ll',
f2:function(){
let ff=()=>{console.log(this.n)};
return ff;
}
}
obj2.f2(); // obj2对象1
const result = (name, age) => ({name, age})
扩展运算符,可理解把后面的变量展开,参考文档:使用扩展运算符的10个方法
js数组
可以通过字面量表示法和Array构造函数来创建一个数组。其中构造函数还有两个用于创建数组的静态方法,Array.from()和Array.of()
==Array.from()==
第一个参数是一个类数组对象,即任何可迭代的对象,或者有length属性和可索引元素的结构。第二个参数可选,接受一个映射函数,可以直接增强新数组的值。第三个可选参数用于指定映射函数中this的值(在箭头函数中不适用)
1 | const a=[1,2,3,4] |
==Array.of()==:把一组参数转换为数组
1 | Array.of(1,2,3,4);// [1,2,3,4] |
in关键字【用法:for(let a in object)】当对象是数组时,变量指的是数组的索引;当对象是对象时,变量指的是对象的属性
需要判断一个变量是否包含在数组内时,使用数组的方法includes
3.
4. Const 声明的对象可以被改变内部属性的值,使用Object.freeze方法可以冻结属性,属性将不能被修改
5. Object.keys遍历对象中的key值
定型数组
定型数组(Typed Array)指的是一种包含特殊数组类型的数组,能够非常高效灵活地处理原始二进制数据
ArrayBuffer 是一个定型数组的构造函数,必须传入字节大小,创建后无法改变大小,但可用slice切割全部或部分
DataView是一种允许读写 ArrayBuffer 的视图
1 | const buf = new ArrayBuffer(2);// 在内存中分配8字节 |
定型数组与数组类似,数组的大部分方法也适用于定型数组,但是以下会改变数组大小的方法不适用:concat, pop, push, shift, unshift, splice
Promise
一些promise的理解:promise用来描述一个异步操作,用生命周期来描述这个异步操作的状态。
then()
promise对象的then方法接受两个回调函数记为(onResolveed和onRejected)作为参数,第一个是promise被完成时要调用的函数,第二个参数是promise被拒绝时要调用的函数,这两个参数都是可选的,
1 | let promise = readFile('example.txt'); |
then方法也会返回一个promise(下面称之为pp),pp的状态和结果取决于传给then方法的回调函数的返回值,该返回值会通过Promise.resolve() 包装生成一个新的promise(不论是onResolved还是onRejected回调,其返回值都用Promise.resolve包装,onResolved好理解,onRejected可以理解为这个错误得到了处理,不再抛出异常):
- 如果回调函数返回一个普通非
promise对象,pp的状态为“fulfilled”,结果为此返回值 - 如果回调函数内抛出了异常,
pp的状态为rejected,结果为这个异常 - 如果回调函数返回一个
promise,pp的状态和结果和这个promise一致 - 如果没有传这个回调函数,则把上一个promise resolve的结果原样用promise.resolve()包装后向后传递
catch()
promise有一个catch方法,等同于只传递拒绝函数给then方法
1 | asyncFun().then((res) => { |
- 上一个
promise状态为resolve所以最好是在1
2
3
4
5
6
7
8
9
10
11asyncFn.then((res) => {
return 'i am resolved'
}).catch((err) => {
console.log('something error');
return 'i am catch function return value'
})
/*
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: "i am resolved"
*/promise链尾部加上catch,如果出现异常,会被catch捕获和处理;如果正常被resolved,catch不会做什么,返回一个原模原样的promise。
finally()
在promise转换为resolved或者rejected后执行finally代码,在finally代码中无法的值promise的状态,finally也会返回一个新promsie实例,大多数情况下会将父promise原样传递返回,其他情况:如果返回一个pending状态的promise或finally代码中抛出异常or拒绝promise,则返回相应的promise
promise.all()
promise.all()方法接受单个可迭代对象作为参数,返回值是一个promise,作为参数的可迭代对象中每一个元素都是promise。
- 只有这些
promise全部resolved(与顺序无关),返回值promise才会resolved,此时返回值promise的状态为fulfilled,结果为参数promise数组对应的返回值数组,返回值数组的顺序和入参中promise顺序一致; - 如果任意
promise被拒绝,返回值promise将立即被拒绝(不管参数中其他promise状态如何),此时返回值promise的状态为rejected,结果为触发promise.all方法被rejected的那个参数promise的返回值。
例1:例2:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let p1 = new Promise((resolve, reject) => {
resolve(1111);
});
let p2 = new Promise((resolve, reject) => {
resolve(2222);
})
let pp = Promise.all([p1, p2]);
console.log(pp);
/*
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Array(2)
*/
pp.then((val) => {
console.log(Array.isArray(val)); // true
console.log(val); // [1111, 2222]
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let p1 = new Promise((resolve, reject) => {
reject(1111);
});
let p2 = new Promise((resolve, reject) => {
resolve(2222);
})
let pp = Promise.all([p1, p2]);
console.log(pp);
/*
[[Prototype]]: Promise
[[PromiseState]]: "rejected"
[[PromiseResult]]: 1111
*/
pp.catch((val) => {
console.log(Array.isArray(val)); // true
console.log(val); // 1111
})
promise.allSettled()
和promise.all()方法类似,是一个promise的并发方法,通常使用的场景是:
有多个互相不依赖彼此的异步任务 或者 要知道所有promise的结果时,会用promise.allSettled,相比之下,promise.all方法用在任务相互依赖,或者 在任何promise被拒绝时立即停止
promise.race()
promise.race()方法和promise.all()方法类似,接受一个元素为promise的可迭代对象作为参数,返回值是一个promise,不同的是,参数promise中只要有一个状态变成已决的(resolved或者rejected),返回值promise立刻改变状态,变成和这个胜出的promise一样的状态和一样的结果。(参照race的意思理解,最先完成的promise决定了返回值promise的状态和结果,其余promise将被忽略)
thenable对象
- 当一个对象拥有一个能接受
resolve和reject参数的then方法时,该对象就会被认为是一个非promise的thenable对象; promise.resolve()与promise.reject()都能接受非promise的thenable对象作为参数,传入非promise的thenable时,这两个方法会创建一个新的promise,这个promise会在then函数之后被调用。因此可以使用promise.resoleve或promise.reject来将thenable转换成一个已决/已拒的promise。- 这过程中,
promise.resolve会调用thenable的then方法,根据then方法内代码来更新thenable的promise状态以及promise结果。特别的,如果Promise.resolve接收一个Promise作为参数,它的行为将类似与一个空包装,原样返回参数promise的状态,这意味着==Promise.resolve是一个幂等方法==。下面是一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 例1
let thenable = {
then: function(resolve, reject) {
resolve(11)
}
};
let p1 = Promise.resolve(thenable)
console.log(p1); // Promise { <pending> / PromiseState: "fulfilled", PromiseResult: 11}
// 例2
let thenable = {
then: function(resolve, reject) {
reject(11)
}
};
let p1 = Promise.resolve(thenable)
console.log('jieguo ', p1); // Promise {<pending> / PromiseState: "rejected", PromiseResult: 11}
let p3 = Promise.resolve(1);
console.log(p3 === Promise.resolve(p3)); // true- 对
Promise.reject而言,它也会实例化一个状态为rejected的promise并抛出错误,但无论传入的参数是什么,返回值Promise状态始终是rejected,结果则是传入的这个参数,无论这个参数是什么。1
2
3
4
5console.log(Promise.reject(Promise.resolve(1)));
// Promise {<rejected>: Promise}
// PromiseState: "rejected"
// PromiseResult: Promise
- 对
1 | try { |
原因是try catch 同步代码无法捕获promise抛出的异步错误。上述例2中promise抛出的错误没有抛到同步代码的线程里,而是到了异步消息处理队列中
其余
一个promise的then方法和catch方法可以在任何时候被设置,当遇到传参给then或传参给catch的语句时,将根据promise的状态来决定是否立即将then/catch的内容push到任务队列中
以前的笔记
Promise在es2015中被引入,用来解决著名的回调地狱问题,但是它们带来了语法的复杂性。Promise被调用后,它会以处理中状态开始,promise最终会以被解决状态或者被拒绝状态结束,并在完成时,调用相应的回调函数(传递给then或者catch的)。使用new Promise可以构造一个Promise并初始化:

参考文档:promise
Promise可以被解决(在then调用)或者被拒绝(在catch调用),promise可以放回一个promise,形成promise链。执行resolve后,promise的状态会变成fulfilled;执行reject后,promise的状态会变成rejected;promise中的throw相当于执行了reject
1 | class Promise { |
Async和await
为什么引入Async和await
Async 和 await 建立在promise之上,减少了promise的样板,Async和await是原语,它们使代码看起来是同步的,实际上是异步的并且在后台无阻塞,
Async后面接一个会return promise的函数,并执行它,await只能放在Async函数里面,async函数执行完成后返回一个状态为fulfilled的promise,Async用于声明一个异步的function,这个函数会返回一个promise,await用于等待一个异步方法执行完成,await规定异步操作只能一个一个排队执行.
async修饰的函数会返回一个promise
这个promise的结果取决于:
- 如果函数的返回值不是一个
promise对象,那么async函数返回的promise的result是这个函数的返回值 - 如果函数的返回值是一个
promise对象,那么async函数返回的promise用于和这个promise对象一样的状态和结果
这个promise的状态决定于: - 函数内抛出异常,
promise的状态为rejected,否则promise的状态为fulfilled。1
2
3
4
5
6
7
8
9
10
11async function f1() {
return 'hello';
}
let re = f1();
console.log(re); // 是一个Promise(promiseState: "fulfilled", promiseResult: "hello")
async function f2() {
throw new Error('haha')
}
let re = f2();
console.log(re); // 是一个promise(promiseState: "rejected", promiseResult: Error('haha')
await一般用于修饰一个promise对象,await期待接受一个实现了thenable的对象,但不要求,不是对象也可以,不是对象的值会被直接当成期约值
- 当用await来修饰一个promise时,相当于调用这个promise的then方法,并获取的成功结果,这个成功结果会成为await的返回值;
- 如果await修饰的promise在执行时状态变为rejected,await将抛出异常,需要用try catch捕获。
- 如果await修饰的内容不是一个promise对象,会将其封装成一个promise
1 | async function f3() { |
【注】在forEach函数中,如果传入的回调函数用await修饰来等待异步操作完成,在实际执行中,forEach不会暂停等待异步,而是继续遍历,因为await只会阻塞async函数内的代码执行,将其加入微任务队列,然后继续下一次for循环。
1 | let arr=[1,2,3] |
但在for in和for of循环中,会等待await异步函数执行完成后再进行下一次循环
1 | let arr=[1,2,3] |
对于一个处于pending状态的promise p来说,resolve的执行,会将p.then()中的内容移入到微任务队列
对于一个处于fullfilling状态的promise p来说,会立即将p.then()中的内容移入微任务队列
注:一个event loop内只会执行一个任务,注意每次循环的边界,什么时候开始进入下一次循环(
await平行加速,在使用多个await的时候,如果按顺序写下来,那么代码会在每个await处停止。如果这些异步操作直接没有依赖,顺序不是固定的,那么可以一次性初始化好所有promise,再分别等待结果,如下:
1 | async function delay(id) { |
待补充…….
async和await的原理
迭代器和生成器
padStart, padEnd
1 | // padStart用于头部补全,padEnd用于尾部补全 |
JavaScript 严格模式
使用严格模式:
为整个JS文件设置严格模式,在所有语句前放置一个特殊语句
1 |
|
为函数设置严格模式,在函数体内所有语句前声明特殊语句
1 | function strict() { |
严格模式VS非严格模式
==过失错误转为异常
- 变量必须声明才能使用
- 静默失败的赋值操作将抛出异常,例如对NaN进行赋值、对不可写或只读属性赋值
- 禁止函数的参数名重名
==简化变量使用 - 禁用with语句,避免with引起的“块内任何名称d都可以映射到with传进来的对象属性,也可以映射到包围这个块的作用域内的变量”问题
- 不再为eval的上层范围(包围eval代码块的范围)引入新变量
- 禁止删除声明变量,即delete操作
==安全使用JavaScript - 禁止this指向全局上下文对象
什么情况下适合使用严格模式
小技巧
1 | ('0' + Math.floor(0.12)).slice(-2) // 来向前补充0 |
change事件
并不是每次当用户更改
<input>、<select>、<textarea>元素的值时,change事件在这些元素上触发。和input事件不同的是,并不是每次元素的value改变时都会触发change事件。基于表单元素的类型和用户对元素的操作的不同,
change事件触发的时机也不同:
- 当
<input type="checkbox">元素被选中或取消选中时(通过点击或使用键盘);- 当
<input type="radio">元素被选中时(但不是取消选中时);- 当用户显式提交改变时(例如:通过鼠标点击了
<select>中的一个下拉选项,通过<input type="date">元素选择了一个日期,通过<input type="file">元素上传了一个文件等)- 当标签的值被修改并且失去焦点后,但未提交时(例如:对
<textarea>、text、search、url、tel、password类型的 `` 元素进行编辑后)。
expression(表达式)和statements(语句)的区别
- 表达式是可以解析为一个值的有效代码单元
- 语句是执行一个操作的代码单元
任何需要表达式的地方都可以用语句来代替, 称为
语句表达式,但需要语句的地方不能用表达式类代替
<script>的标识
使用type="module"修饰的script标签,表示,这是一个ES6模块,内部的代码有其特殊的作用域,不与全局作用域混淆,并且可以使用import和export关键词。例如:
1 | <script type="module"> |