banner
NEWS LETTER

I Know JS 概念篇

Scroll down

前言

众所周知,js可以被划分为三大块,分别是ECMAScript、DOM、BOM。
其中ES是js的核心部分,定义了js的语法、数据结构和运算等;
DOM是document Object Model(文档对象模型)它是js操作页面的接口,将页面结构表示为一个可操作的树结构;
BOM 提供了与浏览器窗口进行交互的接口,允许操作浏览器窗口的属性和功能,如控制浏览器历史记录、导航等。
how does js work.webp

ECMAScript

js上下文

js代码在执行前,js引擎要做一番准备工作,就是创建对应的执行上下文。执行上下文可分为三类:全局执行上下文、函数上下文与eval上下文,eval上下文一般不会使用

==全局上下文==
全局上下文只有一个,一般由browser创建,也就是window对象,可以通过this直接访问它。window是一个全局对象,在全局环境的任意地方都能访问到它,window上预定义了大量属性和方法,同时window是var声明的全局变量的载体,通过window可以访问到所有我们通过var声明的变量

==函数执行上下文==
函数执行上下文可以有无数个,每一个函数调用时都会创建一个函数上下文(注:同一个函数被多次调用,每次调用都会产生一个新的上下文)。代码执行期间创建的所有上下文都会存放到执行上下文栈中,js代码首次运行,会创建全局上下文并压入执行栈中,之后,每次有函数调用,都会生成一个新的函数执行上下文压入栈中。在js代码执行完成之前总有全局上下文位于栈底部

js执行上下文的创建可分为创建阶段和执行阶段,

  1. 创建阶段(在调用函数,未执行任何代码之前)
    下面的三种是ES3旧的说法:
  • 创建作用域链
  • 创建变量,函数和参数
  • 确定this,在全局执行上下文中,this的值指向全局对象;在函数执行上下文中,如果函数属于一个对象,this的值会指向这个对象,否则this指向全局对象或undefined

新的说法:

  • 绑定this:在全局执行上下文中,this的值指向全局对象;在函数执行上下文中,this的值取决于它是如何被调用的。如果函数被一个引用对象调用,this的值会指向这个对象,否则this指向全局对象或undefined
  • 创建词法环境词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量(函数)的名称,变量是对实际对象(包括函数类型对象)或原始值的引用。
  • 创建变量环境组件
    1
    2
    3
    4
    5
    ExecutionContext = {
    ThisBinding = <this value> // this绑定
    LexicalEnvironment = {...} // 词法环境
    VariableEnvironment = {...} // 变量环境
    }
    参考文档:理解js上下文

js事件流

==冒泡事件流==
IE提出,事件从最具体的元素接收,逐级向上传播

==捕获事件流==
NetScape提出,事件由最大的容器元素接收,逐级传播到具体元素

addEventListener事件委托,事件流的三个阶段

捕获阶段==>目标阶段==>冒泡阶段,当事件处于目标阶段,触发两次,分别属于捕获和冒泡两个阶段
img
IE8以下只支持冒泡事件流,只有目标和冒泡阶段,且冒泡只到document

event对象常用属性

  • Target 事件目标
  • currentTarget 绑定事件的元素,和this的指向相同
  • preventDefault() 取消事件的默认行为,比如点击跳转
  • type 被触发的事件的类型
  • eventPhase 调用事件处理程序的阶段:0表示这个时间没有事件正在被处理,1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段

    addEventListener 语法

    mdn
    接受四个(旧版三个)参数:
  • typestring类型 事件类型
  • listener 必须是一个实现了EventListener接口的对象,或者是一个函数
  • options 有关listener的参数对象,旧版本浏览器没有这个参数,仍然将第三个参数认为是useCaptureoptions参数的安全检查
  • capture 捕获阶段触发,这个参数主要用的场景有:需要考虑父元素和子元素事件触发的顺序在捕获阶段阻止冒泡阶段的传播多层级元素监听同一事件
  • once 只调用一次
  • passivetrue表示listener永远不会调用preventDefault(),否则忽略并控制台警告,在触摸事件和滚动事件通常将这个设置为true,否则页面不能响应触摸/滚动
  • signal AbortSignal,该 AbortSignal 的 abort() 方法被调用时,监听器会被移除。
  • useCapture boolean类型,是否在捕获阶段触发,默认false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    el.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的值==

  1. 如果传入匿名函数,函数内this指向该元素的引用,等同于event.currentTarget的值
  2. 如果传入箭头函数,函数内this等于声明此箭头函数的上下文的this
  3. 还有一种情况,如果是在html中为元素指定onclick属性,那么这个属性中的js语句实际上会被包裹在一个处理函数中,处理函数中的this指向目标元素。

==使用handleEvent特殊函数来捕获任意事件==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Something = function (element) {
// |this| is a newly created object
this.name = "Something Good";
this.handleEvent = function (event) {
console.log(this.name); // 'Something Good', as this is bound to newly created object
switch (event.type) {
case "click":
// some code here…
break;
case "dblclick":
// some code here…
break;
}
};

// Note that the listeners in this case are |this|, not this.handleEvent
// 因为当给addEventListener传递一个对象而不是函数时,会默认查找并调用改对象的handleEvent方法
// 这个写法的好处是能够保证handleEvent方法中的this始终执行当前对象
element.addEventListener("click", this, false);
element.addEventListener("dblclick", this, false);

// You can properly remove the listeners
element.removeEventListener("click", this, false);
element.removeEventListener("dblclick", this, false);
};
const s = new Something(document.body);

==往listener传入数据==
一种办法是使用this传值

1
2
3
4
const obj = {
data: "something"
}
el.addEventListener('click', function() {}.bind(obj));

还可以在listener内使用添加监听器时所在的作用域的变量,要注意的是如果在listener内修改了变量,在外部作用域并不能得到体现,因为当监听器事件执行,外部作用域所在的代码已经执行完毕。这里需要区分的一个细节是,如果引用的是一个对象,由于对象只要被引用就会一直留存与内存中,即使它的作用域已经结束。(基于这一点,可以在一个函数执行完毕后返回一个对象使其一直留存于内存中)

1
2
3
4
5
6
7
8
9
10
11
12
const data = "something";
el.addEventListener('click', function() {
console.log(data);
})
// 引用对象
const data = {
prop: 'something'
}
el.addEventListener('click', function() {
console.log(data.prop);
data.prop = 'other thing'
})

自定义事件

可以使用Event构造函数创建事件

1
2
3
4
const event = new Event("build");
el.addEventListener('build', (e) => {});

el.dispatchEvent(event); // 触发该事件

使用CustomEvent创建自定义事件,可在第二个参数的detail属性传递自定义数据

1
2
3
4
const event = new CustomEvent("build", {detail: 'some'});
el.addEventListener('buikd', (e) => {
console.log(e.detail); // some
})

EventTarget.dispatchEvent()向指定事件目标派发一个event,同步地调用所有事件监听器,返回值是一个布尔值,当派发的event可被取消(即cancelable === true)时且至少有一个事件监听器调用了preventDefault方法,返回false,否则true

1
2
3
4
5
const event = new Event('click', {
cancelable: true
});
const cancelled = !el.dispatchEvent(event);
if(cancelled) console.log('is canceled')

==自定义事件的使用场景==
主要用于解耦不同模块之间的通信,解决一些无法之间通过标准事件机制处理的问题


迭代器和生成器

==理解迭代==
先讲几个与迭代有关的关键词:有序、可以按某种顺序遍历、明确的开始和结束
计数循环就是一种最简单的迭代,数据本身是有序的,每一项可以通过索引来获取,可以通过递增索引来遍历。但是通过索引访问这种方式并不够通用,不是所有的数据类型都可以通过索引访问。

先抛出几个名词

  • 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集合类型,NumberObject类型没有Iterable接口。访问默认迭代器属性可以返回对应的迭代器工厂函数,例如:

1
2
const str = 'abc';
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }

js的很多原生语言结构中会自动调用这个工厂函数,包括:

  • for-of循环
  • 数组结构
  • 扩展操作符
  • Array.from
  • 创建集合、映射
  • Promise.all()Promise.race()
  • yield* 操作符

除了js一些数据类型自带的迭代器接口,还可以自定义一个迭代器,只需要实现一个Iterator接口,下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Counter {
constructor(limit) {
this.limit = limit;
this.count = 1;
}
// 这里用了一个闭包,用于在每次创建实例后都能独立计数
[Symbol.iterator]() {
let count = 1;
let limit = this.limit;
return {
next() {
if(count <= limit) {
return {value: count++, done: false}
} else {
return {value: undefined, done: true}
}
}
}
}
}

==关闭一个迭代器==

  • 迭代器可以提前终止,在可迭代对象耗尽之前,通过return方法终止一个迭代器,要求返回一个有效的IteratorResult对象。
  • return方法是可选的,因此不是所有迭代器都可关闭,比如数组类型,要判断一个迭代器是否可关闭,可以测试迭代器返回的return属性是不是一个函数对象。对于一个未关闭的迭代器,可以从上次离开的地方继续迭代
  • 给一个不可关闭的迭代器增加一个return方法并不能将它变成可关闭的,虽然return方法还是会被调用

生成器

ES6新增的一种灵活结构,能够在一个函数块内暂停和回复代码执行。通过生成器可以实现自定义迭代器

==定义一个生成器==
生成器的形式是一个函数,在函数名之前加一个 * 符号表面这是一个生成器。要注意的是箭头函数不能用来定义生成器函数。
*左右的空格不影响生成器函数的声明

1
2
3
4
5
6
7
8
function *generator() {}
const g1 = function* () {}
const g2 = {
*generator() {}
}
class G3 {
* generator() {}
}

==生成器的返回值==
返回一个生成器对象,是一个类似于迭代器的对象,实现了Iterator接口,具有next方法。调用next方法会返回一个类似可迭代对象的对象:{done: Boolean, value: any}

定义完生成器函数,调用生成器函数得到一个生成器对象时不会执行函数体内容,而只会在初次调用next方法之后执行。

生成器对象实现了Iterable接口(注意和上面的实现了Iterator接口区分开来),这意味着生成器对象有一个keySymbol.iterator的属性方法,这个属性方法能够返回一个默认的迭代器,而这个默认的的迭代器指向生成器对象自身,形成一个自引用

1
2
3
4
5
6
7
function *generator() {}
const g = generator();

console.log(g); //generator {<suspended>}
console.log(g[Symbol.iterator]); //ƒ [Symbol.iterator]() { [native code] }
console.log(g[Symbol.iterator]()); // generator {<suspended>}
console.log(g === g[Symbol.iterator]()); // true

上面讲到,生成器有暂停执行开始执行的状态,在遇到yield之前,代码正常执行;遇到yield关键词后,代码会停止执行,函数作用域的状态会保留。yield关键词可以结合return关键词理解为在函数中间返回,return关键词则是在函数结尾返回。通过yield退出的生成器函数会处在 done: false 状态,而通过return退出的生成器函数会处在 done: true 状态。
yield关键词只能在生成器函数内直接的函数块中使用,不可嵌套使用

1
2
3
4
5
6
7
8
9
function *generator() {
yield 'aaa';
yield 'bbb';
return 'ccc';
}
const g = generator();
console.log(g.next()); // {value: 'aaa', done: false}
console.log(g.next()); // {value: 'bbb', done: false}
console.log(g.next()); // {value: 'ccc', done: true}

生成器函数生成的每个生成器对象直接互相独立区分作用域,各自调用next方法互不影响

==生成器的使用场景==
结合迭代器的Iterator接口,如果将生成器对象当做迭代器使用,会十分方便:生成器对象在调用next方法后会开始或重新开始执行代码,而迭代器执行迭代的过程正是在不断调用next方法的过程

1
2
3
4
5
6
7
8
9
10
11
12
function *generator() {
yield '111';
yield '222';
return '333';
}
const g = generator();
for(let i of g) {
console.log(i);
}
// '111'
// '222'
// '333'不会输出,因为是return

事件循环

==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的步骤如下:

  1. 执行一个宏任务(如果栈中没有则从任务队列获取)
  2. 执行过程中遇到微任务,将微任务添加到任务队列的微任务队列
  3.  宏任务执行完毕,这个时候执行栈空了,从微任务队列中获取任务并执行,直到微任务队列为空。
  4.  执行完所有微任务后,如果有必要,会进行页面渲染
  5.  下一个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
2
3
4
5
const myFunc = () => {
// do something
setTimeout(myFunc, 1000);
}
setTimeout(myFunc, 1000)

ECMS2015/ES6语法

原始值包装类型

js提供了三种特殊的引用类型:Boolean、Number、String,这些类型具有引用类型一样的特点,也具有与各自原始类型对应的特殊行为。每当用到某个原始类型值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。

栗子:

1
2
let s = "i am a text";
console.log(s.substring(2))

s是一个字符串类型变量,属于原始类型,本身没有substring方法。当执行到s.substring(2)时,后台会执行如下操作:

  1. 创建一个String类型的实例;
  2. 调用实例的substring方法;
  3. 销毁实例;
    这些行为会让原始值具有对象的行为。

【注1】:当我们使用let s = new String('text')显式地创建一个变量时,s的类型是'object'
【注2】:注意区分 使用转型函数使用new调用构造函数 两种情况是不一样的,例如:

1
2
3
4
let n1 = Number(123);
console.log(typeof n1); // 'number'
let n2 = new Number(456);
console.log(typeof n2); // 'object'

let和var

  1. 在使用var声明变量时,变量会被自动添加到最近的上下文。在函数内,最近的上下文是函数局部上下文,在with语句中,最近的也是函数上下文。如果未使用声明就使用的变量,将自动被添加到全局上下文。在严格模式中,未经声明就使用的变量会报错。
  2. let的作用域限制在当前代码块中,let声明的变量不会进行变量提升,let不允许在同一个作用域中重复声明同一变量,一个{}会成为一个块级作用域(const声明的变量同let),==var的作用域范围是函数作用域==,let声明的范围是块作用域。块作用域是函数作用域的子集。
  3. const声明常量,用const声明的基础类型变量不可修改,const声明的引用型变量可以修改内容,但不可改变指向的对象。用const声明的变量的同时必须初始化。
  4. 使用constlet声明的变量只有在执行到声明所在位置之后才能被访问,在这之前,变量处于‘临时死区’,任何访问尝试将报错ReferenceError,因此认为let声明的变量是非提升的。而var会有变量声明提升,即所有变量声明放到函数作用域的顶部【注:undefined是一种数据类型,变量已声明但未赋值会有 undefined 值,访问对象中不存在的属性也会返回 undefinedReferenceError是一种错误类型,变量未声明却被访问 或 访问超出作用域的变量会抛出此种错误】
  5. 在循环中,let声明的变量i只在本轮循环中有效,每次循环的i都是一个新的变量。而且,循环变量所在的部分循环体内部处于两个不同的作用域
  6. letconst声明的变量挂载到

【more】函数声明提升
和var的变量声明提升类似,函数声明也会被提升到当前作用域的顶部,在函数声明之前可以调用该函数。不同的是,对于变量提升,只有变量名被提升,初始值不会被提升;而函数声明提升会提升整个函数的定义,包括函数体,即函数声明的初始化会立即完成
注意】将匿名函数赋值给一个变量(函数表达式)属于变量声明,不是函数声明,因此只提升声明,不提升函数体

1
2
3
4
5
6
7
8
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
// 说明循环变量i和循环体内部的i处于两个不同作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a=0;
function a() {
console.log(a);
let a = 1;
}
a();
// 执行的结果是ReferenceError,对应上面的第4条
// 要注意,即使外层环境有一个a,但是在函数a内部环境,是存在a变量的,引擎从函数的开始就知道函数块作用域内存在一个a变量,但是处于死区(存在,但不能使用),直到遇到let声明,因此并不会往外层环境继续寻找

// 如果是这样
function a() {
console.log(a);
}
a();
// 输出的是函数a的内容

全局对象

浏览器中的全局对象是windownode环境中全局对象是global

  • 全局环境下,this返回全局对象;在node模块中,this返回当前模块;ES6模块中this返回undefined
  • 单独的一个函数中的this指向全局对象,而作为一个对象的方法属性的函数中的this指向根据其被调用的场景不同而不同

变量解构赋值–模式匹配

等号两边的模式相同,变量将会被赋予对应的值,注意需要是数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let [a,b,c]=[1,2,3]
a //1
b //2
c //3

let [x, ,z]=[1,,3]
x //1
z //3

let [head, ...tail]=[1,2,3,4];
head //1
tail //[2,3,4]

const arr = [{name: 'alice', age: 11},{name: 'bob', age: 12}]
const [{name}] = arr // 将取出数组第一项的某个值
name // 'alice'
const [, bob] = arr // 可采用逗号占位的方式指定某个值
bob // {name: 'bob', age: 1}

解构不成功,变量的值是 undefined。解构可设置默认值,只有当数组内对应位置的成员严格等于(===undefined,才会使用默认值。对于表达式,比如函数,是惰性求值的(即在用到之前都不会执行)
解构在内部私用ToObject()把源数据转换成对象,原始值会被当成对象,根据ToObject的定义,nullundefined不能被解构,否则会抛出错误

1
2
3
4
let {length} = "foobar";
console.log(length); // 6
let {constructor: c} = 4;
console.log(c === Number); // true

如果是给事先声明的变量赋值,解构表达式必须包含在一对括号中

1
2
3
let name, age;
let person = {name: 'Bob', age: 24};
({name, age} = person);

多个属性的解构是一个输出无关的顺序化操作,当前一部分赋值成功但后面某个赋值失败,整个解构赋值会完成一部分,

箭头函数

  • 箭头函数没有自己的 this,它的 this定义该函数时所在的作用域this
  • 箭头函数没有arguments对象,没有原型对象
  • 箭头函数不可作为构造函数(因为它没有自己的this,并且没有prototype属性,这些都是构造函数必要的)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let 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
2
3
const a=[1,2,3,4]
Array.from(a, function(x){return x ** this.exp},{exp:2});
Array.from(a, x=>x**2);

==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
2
3
4
5
6
7
8
9
10
11
12
13
const buf = new ArrayBuffer(2);// 在内存中分配8字节
const view = new DataView(buf);

console.log(view.getInt8(0));
// 以无符号8位整数的格式从0位开始设置11111111,即255
view.setUint8(0, 255);

// 以有符号16位整数格式从第0位读取
console.log(view.getInt16(0)); // -256
/*
解释:上面set之后,buf的二进制位是 11111111 00000000
DataView默认使用大端字节序,1111111是最高有效字节,00000000是最低有效字节,有符号16位整数用补码表示法,最高位是1,表示负数,11111111 00000000取反加1后是00000001 00000000,即256
*/

定型数组与数组类似,数组的大部分方法也适用于定型数组,但是以下会改变数组大小的方法不适用:concat, pop, push, shift, unshift, splice


Promise

一些promise的理解:
promise用来描述一个异步操作,用生命周期来描述这个异步操作的状态。

then()

promise对象的then方法接受两个回调函数记为(onResolveed和onRejected)作为参数,第一个是promise被完成时要调用的函数,第二个参数是promise被拒绝时要调用的函数,这两个参数都是可选的,

1
2
3
4
let promise = readFile('example.txt');
promise.then((res) => {}, (rej) => {}); // 同时监听完成和拒绝状态
promise.then((res) => {}); // 只监听完成状态
promise.then(null, (rej) => {}); // 只监听拒绝状态

then方法也会返回一个promise(下面称之为pp),pp的状态和结果取决于传给then方法的回调函数的返回值,该返回值会通过Promise.resolve() 包装生成一个新的promise(不论是onResolved还是onRejected回调,其返回值都用Promise.resolve包装,onResolved好理解,onRejected可以理解为这个错误得到了处理,不再抛出异常):

  • 如果回调函数返回一个普通非promise对象,pp的状态为“fulfilled”,结果为此返回值
  • 如果回调函数内抛出了异常,pp的状态为rejected,结果为这个异常
  • 如果回调函数返回一个promisepp的状态和结果和这个promise一致
  • 如果没有传这个回调函数,则把上一个promise resolve的结果原样用promise.resolve()包装后向后传递

catch()

promise有一个catch方法,等同于只传递拒绝函数给then方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
asyncFun().then((res) => {
throw new Error('opps! error')
}).catch((err) => {
console.log();
})
/*
返回的promise为
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: undefined
*/

asyncFun().then((res) => {
throw new Error('opps! error')
}).catch((err) => {
console.log('something error')
return 'i am catch function return value'
})
/*
'something error'
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: "i am catch function return value"
*/
  • 上一个promise状态为resolve
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    asyncFn.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捕获和处理;如果正常被resolvedcatch不会做什么,返回一个原模原样的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:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let 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]
    })
    例2:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let 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对象

  • 当一个对象拥有一个能接受resolvereject参数的then方法时,该对象就会被认为是一个非promisethenable对象;
  • promise.resolve()promise.reject()都能接受非promisethenable对象作为参数,传入非promisethenable时,这两个方法会创建一个新的promise,这个promise会在then函数之后被调用。因此可以使用promise.resolevepromise.reject来将thenable转换成一个已决/已拒的promise
  • 这过程中,promise.resolve会调用thenablethen方法,根据then方法内代码来更新thenablepromise状态以及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
      5
      console.log(Promise.reject(Promise.resolve(1)));
      // Promise {<rejected>: Promise}
      // PromiseState: "rejected"
      // PromiseResult: Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try {
throw new Error('xx');
} catch (error) {
console.log('in catch', error); // in catch Error: xx
}

// 2
try {
Promise.reject(new Error('xxx'));
} catch (error) {
console.log('in catch', error);
}
// 不会被catch块捕获
// Uncaught (in promise) error: xxx

try {
await Promise.reject(new Error('333'))
} catch (error) {
console.log('in catch', error); // in catch Error: 333
}
// await 会暂停代码,直到promise执行完成,如果发现异常,await会同步抛出,由catch捕获

原因是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

Promise可以被解决(在then调用)或者被拒绝(在catch调用),promise可以放回一个promise,形成promise链。执行resolve后,promise的状态会变成fulfilled;执行reject后,promise的状态会变成rejected;promise中的throw相当于执行了reject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Promise {
constructor(executor) {
this.initValue();
this.initBind();
try {
executor(this.resolve, this.reject);
} catch (e){
// 错误时直接执行reject
this.reject(e)
}
}
initValue() {
// 初始化值
this.promiseResult = null; //promise结果预设值为null
this.promiseState = "pending"; //初始化状态为pending

// 在定时器的情况下,要把成功后的回调函数和失败的回调函数先保存起来,用数组保存
// 在给定的时间后再执行then函数里的回调
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
}
initBind(){
// 绑定resolve和reject的this值永远指向当前promise实例,不随函数环境的改变而改变
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
resolve(value) {
if(this.promiseState !== "pending") return; // 保证只改变一次状态
this.promiseState = "fulfilled";
this.promiseResult = value;
// 执行所有保存下来的回调函数,原本成功后的回调函数应该在then里执行,现在放到resolve里执行,
// 状态改成fulfilled后执行,和放在then里执行是一样的
while(this.onFulfilledCallbacks.length) {
this.onFulfilledCallbacks.shift()(this.PromiseResult);
}
}
reject(reason) {
if(this.promiseState !== "pending") return;
this.promiseState = "rejected";
this.promiseResult = reson;

while(this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(this.PromiseResult);
}
}

// then函数接受两个回调函数,成功回调和失败回调
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : val => val;
onRejected = typeof onRejected === "function" ? onRejected : reason => {throw reason};

var thenPromise = new Promise((resolve, reject) => {
const resolvePromise = cb => {
try {
const x = cb(this.PromiseResult);
if(x instanceof Promise) {
// 如果返回值是promise对象,返回值为成功/失败,新promise就是成功/失败
// then函数知道返回的promise是成功状态还是失败状态
x.then(resolve, reject);
} else {
// 如果返回值不是promise
resolve(x);
}
} catch (err) {
reject(err);
}
}
})

if(this.PromiseState === "fulfilled") {
onFullfilled(this.PromiseResult);
} else if (this.PromiseState === "rejected") {
onRejected(this.PromiseResult);
} else if (this.PromiseState === "pending") {
// 如果还是待定状态,即有定时器,把这些成功后的或失败后的回调函数先保存,放到resolve或reject函数里去执行
this.onFulfilledCallbacks.push(onFulfilled.bind(this));
this.onRejectedCallbacks.push(onRejected.bind(this));
}

return thenPromise;
}

}

Async和await

为什么引入Async和await

Asyncawait 建立在promise之上,减少了promise的样板,Asyncawait是原语,它们使代码看起来是同步的,实际上是异步的并且在后台无阻塞,

Async后面接一个会return promise的函数,并执行它,await只能放在Async函数里面,async函数执行完成后返回一个状态为fulfilledpromiseAsync用于声明一个异步的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
    11
    async 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
2
3
4
async function f3() {
await 1
}
f3()

【注】在forEach函数中,如果传入的回调函数用await修饰来等待异步操作完成,在实际执行中,forEach不会暂停等待异步,而是继续遍历,因为await只会阻塞async函数内的代码执行,将其加入微任务队列,然后继续下一次for循环。

1
2
3
4
5
6
7
8
9
10
let arr=[1,2,3]
arr.forEach(async(i) => {
await function() {setTimeout(()=>{}, 1000)};
console.log(new Date())
});
/* 输出的结果将会是:
Thu Mar 23 2023 14:41:37 GMT+0800 (中国标准时间)
Thu Mar 23 2023 14:41:37 GMT+0800 (中国标准时间)
Thu Mar 23 2023 14:41:37 GMT+0800 (中国标准时间)
*/

但在for in和for of循环中,会等待await异步函数执行完成后再进行下一次循环

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr=[1,2,3]
async function ff() {
for (let in arr){
await new Promise((resolve) => {setTimeout(()=>{resolve(1)}, 1000)});
console.log(new Date())
}
};
ff()
/* 输出结果
Thu Mar 23 2023 14:48:28 GMT+0800 (中国标准时间)
Thu Mar 23 2023 14:48:29 GMT+0800 (中国标准时间)
Thu Mar 23 2023 14:48:30 GMT+0800 (中国标准时间)
*/

对于一个处于pending状态的promise p来说,resolve的执行,会将p.then()中的内容移入到微任务队列
对于一个处于fullfilling状态的promise p来说,会立即将p.then()中的内容移入微任务队列
注:一个event loop内只会执行一个任务,注意每次循环的边界,什么时候开始进入下一次循环(

await平行加速,在使用多个await的时候,如果按顺序写下来,那么代码会在每个await处停止。如果这些异步操作直接没有依赖,顺序不是固定的,那么可以一次性初始化好所有promise,再分别等待结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function delay(id) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(id)
}, 1000)
})
}
// 无平行加速,每隔一秒控制台会打印
await delay(0);
await delay(1);
await delay(2);

// 平行加速,1秒后,控制台打印出2个值
const p0 = delay(0);
const p1 = delay(1);
await p0;
await p1;

待补充…….

async和await的原理

迭代器和生成器

padStart, padEnd

1
2
3
// padStart用于头部补全,padEnd用于尾部补全
'ssa'.padStart(9, '1234'); // 表示用'1234'在头部补齐至9位长度
String(0).padStart(2, 0); // '00'

JavaScript 严格模式

MDN中对严格模式的描述

使用严格模式:

为整个JS文件设置严格模式,在所有语句前放置一个特殊语句

1
2
'use strict'
// js codes bellow

为函数设置严格模式,在函数体内所有语句前声明特殊语句

1
2
3
4
function strict() {
'use strict'
// codes bellow
}

严格模式VS非严格模式

==过失错误转为异常

  1. 变量必须声明才能使用
  2. 静默失败的赋值操作将抛出异常,例如对NaN进行赋值、对不可写或只读属性赋值
  3. 禁止函数的参数名重名
    ==简化变量使用
  4. 禁用with语句,避免with引起的“块内任何名称d都可以映射到with传进来的对象属性,也可以映射到包围这个块的作用域内的变量”问题
  5. 不再为eval的上层范围(包围eval代码块的范围)引入新变量
  6. 禁止删除声明变量,即delete操作
    ==安全使用JavaScript
  7. 禁止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>textsearchurltelemailpassword 类型的 `` 元素进行编辑后)。

expression(表达式)和statements(语句)的区别

  • 表达式是可以解析为一个值的有效代码单元
  • 语句是执行一个操作的代码单元

任何需要表达式的地方都可以用语句来代替, 称为语句表达式,但需要语句的地方不能用表达式类代替

<script>的标识

使用type="module"修饰的script标签,表示,这是一个ES6模块,内部的代码有其特殊的作用域,不与全局作用域混淆,并且可以使用importexport关键词。例如:

1
2
3
4
<script type="module">
import {fun} from './module.js';
fun();
</script>
其他文章