- 一、this
- call、apply与bind有什么区别?
- new
- 二、防抖节流
- 三、原型
- 四、继承
- 五、Promise
- 六、Generator生成器函数
- 七、async、await
- 八、js类型判断
- 九、闭包
- 十、观察者模式 vs 发布-订阅模式
- 十一、函数柯里化
- 十二、类数组和数组的区别
- 十三、事件循环机制(满分答案)
- node 事件循环整体理解
- 阶段概述
- 三大重点阶段
- node 版本差异说明
- timers 阶段执行时机的变化
- check 阶段的执行时机变化
- nextTick 队列执行时机的变化
- node 和 浏览器事件循环的主要区别
- 十四、进程和线程
- for in 和 for of 区别
- map 和 object 的区别,和 weakMap 的区别
一、this
默认绑定时this指向全局对象(非严格模式):
function fn1() {
let fn2 = function () {
console.log(this); //window
fn3();
};
console.log(this); //window
fn2();
};
function fn3() {
console.log(this); //window
};
fn1();
这个例子中无论函数声明在哪,在哪调用,由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window。
但需要注意的是,在严格模式环境中,默认绑定的this指向undefined,来看个对比例子:
function fn() {
console.log(this); //window
console.log(this.name);
};
function fn1() {
"use strict";
console.log(this); //undefined
console.log(this.name);
};
var name = '听风是风';
fn();
fn1() //TypeError: Cannot read property 'a' of undefined
如果在严格模式下调用不在严格模式中的函数,并不会影响this指向,来看最后一个例子:
var name = '听风是风';
function fn() {
console.log(this); //window
console.log(this.name); //听风是风
};
(function () {
"use strict";
fn();
}());
function Fn () {
this.name = '齐天大圣' //给对象添加属性要用this,不能用var
}
var b = new Fn;
console.log(b.name) //齐天大圣
var c = {
user: '追梦子',
fn: function () {
console.log(this.user) // 追梦子 this指向c
}
}
//c调用
c.fn() //
多层调用者 ,如果函数调用前存在多个对象,this指向距离调用自己最近的对象
var c = {
user: '追梦子',
b: {
fn: function () {
console.log(this.user) // undefined this指向c
}
}
}
//最终调用者是b
c.b.fn()
隐式丢失
在特定情况下会存在隐式绑定丢失的问题,最常见的就是作为参数传递以及变量赋值,先看参数传递:
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
};
function fn1(param) {
param();
};
fn1(obj.fn);//行星飞行
这个例子中我们将 obj.fn 也就是一个函数传递进 fn1 中执行,这里只是单纯传递了一个函数而已,this并没有跟函数绑在一起,所以this丢失这里指向了window。
第二个引起丢失的问题是变量赋值,其实本质上与传参相同,看这个例子:
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
};
let fn1 = obj.fn;
fn1(); //行星飞行
显式绑定是指我们通过call、apply以及bind方法改变this的行为,相比隐式绑定,我们能清楚的感知 this 指向变化过程
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
let obj3 = {
name: 'echo'
}
var name = '行星飞行';
function fn() {
console.log(this.name);
};
fn(); //行星飞行
fn.call(obj1); //听风是风
fn.apply(obj2); //时间跳跃
fn.bind(obj3)(); //echo
在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。
注意,如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
var name = '行星飞行';
function fn() {
console.log(this.name);
};
fn.call(undefined); //行星飞行
fn.apply(null); //行星飞行
fn.bind(undefined)(); //行星飞行
call、apply与bind有什么区别?
1.call、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为什么上方例子中bind后还加了一对括号 ()的原因。
2.bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
3.call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组。
Function.prototype.myBind = function (context = globalThis) {
const fn = this
const args = Array.from(arguments).slice(1)
const newFunc = function () {
const newArgs = args.concat(...arguments)
if (this instanceof newFunc) {
// 通过 new 调用,绑定 this 为实例对象
fn.apply(this, newArgs)
} else {
// 通过普通函数形式调用,绑定 context
fn.apply(context, newArgs)
}
}
// 支持 new 调用方式
newFunc.prototype = Object.create(fn.prototype)
return newFunc
}
// 测试
const me = { name: 'Jack' }
const other = { name: 'Jackson' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
const meSay = say.myBind(me)
meSay()
const otherSay = say.myBind(other)
otherSay()
Function.prototype.myCall = function (context = globalThis) {
// 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
const key = Symbol('key')
context[key] = this
// es5 可通过 for 遍历 arguments 得到参数数组
const args = [...arguments].slice(1)
const res = context[key](...args)
delete context[key]
return res
};
// 测试
const me = { name: 'Jack' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
say.myCall(me)
Function.prototype.myApply = function (context = globalThis) {
// 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
const key = Symbol('key')
context[key] = this
let res
if (arguments[1]) {
res = context[key](...arguments[1])
} else {
res = context[key]()
}
delete context[key]
return res
}
// 测试
const me = { name: 'Jack' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
say.myApply(me)
new
var Fn = {}
var fn = new Fn()
这个过程会执行如下步骤:
1.新创建一个空对象
var fn = new Object();
2.构造函数的显示原型等于实例对象的隐式原型,实例对象的constructor属性为构造函数的名称__
Fn.prototype = fn.proto
3.通过调用call、apply方法执行构造函数并改变this对象(绑定到实例对象上)
Fn.call(f)
4.如果没有手动返回其他任何对象或返回值是基本类型(Number、String、Boolean)的值__,会返回 this 指向的新对象,也就是实例,若返回值是引用类型(Object、Array、Function)的值,则实际返回值为这个引用类型。**
var New =function(fn){
//1.新建空对象
var obj={};
//2.实例对象的__proto__等于构造函数的prototype
obj.__proto__=fn.prototype;
//3. 将 arguments 对象转为数组
var args = [].slice.call(arguments);
//4.去除构造函数
args.shift();
//5. 通过调用call、apply方法执行构造函数并改变this对象
var result = fn.apply(obj, args);
//6. 判断返回值,如果是Object类型就直接返回,否则返回实例对象本身
if(Object.prototype.toString.call(result)=="[object Object]" ){
return result
}else{
return obj;
}
}
var Fun=function(sex){
this.name='hty';
this.sex=sex;
}
const fun=New(Fun,'123');
console.log(fun.name);//hty
console.log(fun.sex);//123
自己手写实现一个简单的new
function myNew (fun) {
return function () {
// 创建一个新对象且将其隐式原型指向构造函数原型
let obj = {
__proto__ : fun.prototype
}
// 执行构造函数
fun.call(obj, ...arguments)
// 返回该对象
return obj
}
}
function person(name, age) {
this.name = name
this.age = age
}
let obj = myNew(person)('li', 18) console.log(obj)
function fn() {
this.user = '追梦子';
return {};
}
var e = new fn;
console.log(e.user); //undefined
console.log(e) // {}
dsd
function fn() {
this.user = '追梦子';
return 1;
}
var f = new fn;
console.log(f.user); //undefined
console.log(f) // fn {user: "追梦子"}
sdsd
function fn() {
this.user = '追梦子';
return function () {};
}
var g = new fn;
console.log(g.user); //undefined
console.log(g) // ƒ () {}
总结:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。
在单位时间内,有些事件的触发频率会很高,比如scroll、resize、refresh、input框实时输入校验,这些情况会导致服务器请求跟不上,浏览器页面出现卡顿现象,同时给服务器造成负担,此时采用debounce(防抖)和throttle(节流)减少单位时间内的服务器请求次数,提升用户体验效果。
二、防抖节流
debounce(防抖)
简单说就是给事件设置一个时间段,如果这个函数在这个时间段内没有再次触发,就会执行一次,如果在这个设定时间到来之前再一次触发了事件,就重新开始计时,这个例子中只有当停止触发事件的1000毫秒后才会执行一次函数。
/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
clearTimeout(timer) //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时
}
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
写完会发现其实time = setTimeOut(fn,delay)
是一定会执行的,所以可以稍微简化下:
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timeout !== null) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
防抖的含义就是在某个时间段内如果不停止持续的触发事件,就永远不会执行函数,但如果我们期望的效果是即使在一个时间段内持续的触发事件也能在某个时间间隔后触发一次事件呢?这个时候节流可以帮我们实现这样的效果
throttle(节流)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数,下图。
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
return false
}
valid = false
setTimeout(() => {
fn()
valid = true;
}, delay)
}
}
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)
这种效果就是,即使一直在滚动,也会每间隔一秒输出滚动条的位置
内存溢出:一种程序运行出现的错误,当程序运行需要的内存超过了剩余内存空间、时,就会抛出内存溢出错误内存泄漏:占用的内存没有及时释放,内存泄漏积累过多就容易导致内存溢出,常见的内存泄漏:意外的全局变量、没有及时清理的计时器或回调函数、闭包
//内存溢出
var obj = {}
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(1000000)
console.log('-------')
console.log(obj)
}
//内存泄漏
//1.意外的全局变量
function fn () {
a = new Array(1000000) //全局变量
console.log(a)
}
fn() //正常如果是局部变量的话,在函数执行完成以后就会被释放,全局变量的话就一直存在于内存中
//2.定时器
var intervalid = setInterval(function () { //启动循环定时器后不清理
console.log('-----')
},1000)
//clearInterval(intervalId) //关闭
//3.闭包
function fun () {
var a = 3;
function fn1 () {
console.log(a)
}
return fn1
}
var f = fn1()
f()
三、原型
每个函数都有一个prototype属性,这个属性指向这个函数的原型对象,如下:
function Fn () {}
console.log(Fn.prototype)
用图表表示如下:
函数Fn有一个prototype属性,这个属性就指向它的原型对象即Fn.prototype,原型对象又有一个constructor构造器,这个构造器指向Fn函数本身。原型对象里面存放着所有实例对象需要共享的属性和方法!
Fn函数的实例化:
let fn = new Fn()
通过new操作符就可以实例化一个fn对象,画图来看一下他们之间的对应关系
实例对象fn会有一个proto属性,它指向构造函数Fn的原型。
上面我没看到了一个Fn.protottype里面有一个proto属性,那我们来看看它的指向吧
从图可以看到,Fn.prototype.proto指向Object.prototype,这就说明每个函数的原型都是Object的实例,也就是说每个函数的原型都是通过 new Object() 产生的。上面的构造函数和原型对象之间的关系汇成的了一条链。原型链诞生了!
原型链
作者为了解决js中的继承问题,于是就设计了原型链。画图表示如下:
通过原型链我们可以访问到实例对象上原本没有的方法,例如:toString、hasOwnProperty、valueOf等,
举栗子
这里test2就是共享方法,放在了原型对象里。
实例对象找方法,先从自身找,找不到的话就顺着原型链去原型对象找,原型对象找不到的话就去原型的原型里找,一直到Object为止。
例子:
JavaScript中的对象,都有一个内置属性[Prototype],指向这个对象的原型对象。当查找一个属性或方法时,如果在当前对象中找不到,会继续在当前对象的原型对象中查找;如果原型对象中依然没有找到,会继续在原型对象的原型中查找(原型也是对象,也有它自己的原型);直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。这个查找过程是一个链式的查找,每个对象都有一个到它自身原型对象的链接,这些链接组建的整个链条就是原型链。拥有相同原型的多个对象,他们的共同特征正是通过这种查找模式体现出来的。在上面的查找过程,我们提到了最顶层的原型对象,这个对象就是Object.prototype,这个对象中保存了最常用的方法,如toString、valueOf、hasOwnProperty等,因此我们才能在任何对象中使用这些方法。
大致总结一下就是:
1、Object是作为众多new出来的实例的基类 function Object(){ [ native code ] }
2、Function是作为众多function出来的函数的基类 function Function(){ [ native code ] }
3、构造函数的_proto _(包括Function.prototype和Object.prototype)都指向Function.prototype
4、原型对象的proto__都指向Object.prototype
5、Object.prototype.proto__指向null
四、继承
/**
* 原型链继承
* 优点:1、子类实例可继承的属性有:子类实例的构造函数的属性,父类构造函数属性,父类原型的属性。(子类实例不会继承父类实例的属性!)
缺点:1、子类实例无法向父类构造函数传参。
2、继承单一。
3、所有子类实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
*/
// 1.定义父类构造函数
function Father () {
this.property = true
}
// 2.给父类原型添加方法
Father.prototype.getFatherProperty = function () {
return this.property
}
// 3.定义字类构造函数
function Child () {
this.property = false
}
// 4.字类的原型等于父类的一个实例对象
Child.prototype = new Father()
//5.字类的构造器指向字类
Child.prototype.constructor = Child
// 6.给字类原型添加方法
Child.prototype.getChildProperty = function () {
return this.property
}
let child = new Child()
console.log(child.property)
/**
* 借用构造函数
* 通过call()方法借用了父类的属性。call方法可以将父类中通过this指定的属性和方法复制到子类的实例中
* call()方法借用了父类的属性,而方法都定义在构造函数中,
* 并没有从原型上继承到夫类的属性和方法,因此没有可复用性。
*/
function FatherFn(name, age) {
this.name = name
this.age = age
}
function ChildFn(name,age, height) { // 传参
FatherFn.call(this, name, age)
this.height = height
}
let child2 = new ChildFn('tom', 18, 180) // 传参
console.log(child2.name)
/**
* 组合继承:是一种将原型链和构造函数组合使用的方法。
* 本质:是通过call()方法借用了父类中通过this指定的属性,而原型链继承实现了对父类原型上属性和方法的继承
优点:融合两种模式的优点,既可以传参和复用
缺点:会调用两次父类型的构造函数,分别是在创建子类原型的时候和在子类构造函数内部。(对应上面的4,5步)。
*
*/
function Person(name,age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
function Student(name, age, height) { // 实现了传参
// 借用构造函数
Person.call(this, name, age)
this.height = height
}
// 原型链继承
Student.prototype = new Person()
Student.prototype.constructor = Student
Student.prototype.setHeight = function (height) {
this.height = height
}
原型式继承
其原理就是借助原型,可以基于已有的对象创建新对象。节省了创建自定义类型这一步(虽然觉得这样没什么意义)。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
在object()函数内部,先创建了一个临时性构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
ES5新增了Object.create()方法规范化了原型式继承。即调用方法为:Object.create(o);
适用场景
只想让一个对象跟另一个对象建立继承这种关系的时候,可以用Object.create();这个方法,不兼容的时候,则手动添加该方法来兼容。
es6继承
继承extends
和super
,class 子类名 extends 父类名
实现继承,当然还得在constructor里面写上super(父类的参数)
// es6继承
class Animal {
//构造函数,里面写上对象的属性
constructor(props) {
this.name = props.name || 'Unknown';
}
//方法写在后面
eat() {//父类共有的方法
console.log(this.name + " will eat pests.");
}
}
//class继承
class Bird extends Animal {
//构造函数
constructor(props,myAttribute) {//props是继承过来的属性,myAttribute是自己的属性
//调用实现父类的构造函数
super(props) //相当于获得父类的this指向
this.type = props.type || "Unknown";//父类的属性,也可写在父类中
this.attr = myAttribute;//自己的私有属性
}
fly() {//自己私有的方法
console.log(this.name + " are friendly to people.");
}
myattr() {//自己私有的方法
console.log(this.type+'---'+this.attr);
}
}
//通过new实例化
var myBird = new Bird({
name: '小燕子',
type: 'Egg animal'//卵生动物
},'Bird class')
myBird.eat()
myBird.fly()
myBird.myattr()
super里面的this是指向子类的构造函数的,super(this)相当于Animal.prototype.constructor.call(this)
ES5 / ES6 的继承除了写法以外还有什么区别?
- ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))
- ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this。
- ES5 的继承时通过原型或构造函数机制来实现。
- ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
- 注意 super 关键字指代父类的实例,即父类的 this 对象。
- 注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。function 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
以下不一定正确::::
1.class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}
const foo = new Foo();
//Cannot access 'Foo' before initialization
//初始化前不能访问Foo
//说明在这行代码下面的Foo的声明被提升了,只是还没有初始化
//与var变量的提升作对比,var变量的提升会初始化为undefined
class Foo {
constructor() {
this.foo = 42;
}
}
123456789101112131415
2.class 声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
baz = 42; // it's ok,调用后会自动声明为一个全局变量
}
const bar = new Bar();
class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();
123456789101112
3.class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
// 引用一个未声明的变量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']
class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
1234567891011121314151617181920212223242526
4.class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo(); // foo = 42
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
1234567891011121314151617181920
5.必须使用 new 调用 class。
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
1234567891011
6.class 内部无法重写类名。
function Bar() {
Bar = 'Baz'; // it's ok
this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42;
Foo = 'Fol'; // TypeError: Assignment to constant variable
}
}
const foo = new Foo();
Foo = 'Fol'; // it's ok
五、Promise
https://juejin.im/post/6844904077537574919#heading-16
介绍一下promise?有哪些API?
https://www.cnblogs.com/everlose/p/12950564.html
Promise 是异步编程的一种解决方案,解决了回调地狱问题。
Promise.reject 返回的 promise 对象的状态为 rejected。
Promise.resolve(value) 返回的状态由给它传入的参数决定。
.then
和.catch
都会返回一个新的Promise
。所以可以链式调用。
.then
或者 .catch
的参数期望是函数,传入非函数则会发生值透传。
Promise.all()
的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。当其中有一个被拒绝的话promise.all就会被拒绝,并且丢弃其他所有的结果。
.race()
的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
第一个then
和第二个then
中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将resolve(1)
的值直接传到最后一个then
里。
所以输出结果为:
1
.finally
方法也是返回一个Promise
,他在Promise
结束的时候,无论结果为resolved
还是rejected
,都会执行里面的回调函数。
.finally()
方法的回调函数不接受任何的参数,也就是说你在.finally()
函数中是没法知道Promise
最终的状态是resolved
还是rejected
的
它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise
对象。
在Promise
中,返回任意一个非 promise
的值都会被包裹成 promise
对象,例如return 2
会被包装为return Promise.resolve(2)
。
返回任意一个非 promise
的值都会被包裹成 promise
对象,因此这里的return new Error('error!!!')
也被包裹成了return Promise.resolve(new Error('error!!!'))
。
promise.all 传入了什么,返回了什么?
Promise中的all和race
.all()
的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
.race()
的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log(res))
输出:
1
2
3
[1, 2, 3]
有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
.all()
后面的.then()
里的回调函数接收的就是所有异步操作的结果。
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result: ', res))
.catch(err => console.log(err))
执行结果为:
1
'result: ' 1
2
3
这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作.
总结:
Promise.all()
的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。
.race()
的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
Promise.all().then()
结果中数组的顺序和Promise.all()
接收到的数组顺序一致。
all和race
传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then
的第二个参数或者后面的catch
捕获;但并不会影响数组中其它的异步任务的执行。
Promise.all()异常处理
该方法指当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。但是当其中任何一个被拒绝的话。主Promise.all([..])就会立即被拒绝,并丢弃来自其他所有promis的全部结果。
var p1 = Promise.resolve(3);
var p2 = Promise.reject(2);
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // 永远走不到这里
}).catch(function(err) {
console.log(err); // 2
});
这不是我们想要的。所以在使用这个方法的时候要记住为每个promise关联一个错误的处理函数.
var p1 = Promise.resolve(3).catch(function(err) {
return err;
});
var p2 = Promise.reject(2).catch(function(err) {
return err;
});
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
}).catch(function(err) {
return err;
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 2, "foo"]
}).catch(function(err) {
console.log(1); //不会走到这里
});
实现promise.all
1) 核心思路
- ①接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
- ②这个方法返回一个新的 promise 对象,
- ③遍历传入的参数,用Promise.resolve()将参数”包一层”,使其变成一个promise对象
- ④参数所有回调成功才是成功,返回值数组与参数顺序一致
- ⑤参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
2)实现代码
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了~
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if(!Array.isArray(promises)){
throw new TypeError(`argument must be a array`)
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value=>{
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter == promiseNum) {
return resolve(resolvedResult)
}
},error=>{
return reject(error)
})
}
})
}
// test
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1)
}, 1000)
})
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(2)
}, 2000)
})
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(3)
}, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
console.log(res) // [3, 1, 2]
})
promise.then的实现原理
promise的优缺点
- 优点: 解决回调地狱, 对异步任务写法更标准化与简洁化
- 缺点: 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消;
- 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部;
- 第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成).
怎么取消掉 Promise
1.Promise.race竞速方法
利用这一特性,也能达到后续的Promise不再执行
let p1 = new Promise((resolve, reject) => {
resolve('ok1')
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {resolve('ok2')}, 10)
})
Promise.race([p2, p1]).then((result) => {
console.log(result) //ok1
}).catch((error) => {
console.log(error)
})
2.当Promise链中抛出一个错误时,错误信息沿着链路向后传递,直至被捕获。
利用这一特性能跳过链中被捕获前的函数的调用,直至链路终点。
Promise.resolve().then(() => {
console.log('ok1')
throw 'throw error1'
}).then(() => {
console.log('ok2')
}, err => {
// 捕获错误
console.log('err->', err)
}).then(() => {
// 该函数将被调用
console.log('ok3')
throw 'throw error3'
}).then(() => {
// 错误捕获前的函数不会被调用
console.log('ok4')
}).catch(err => {
console.log('err->', err)
})
// 输出
ok1
err-> throw error1
ok3
err-> throw error3
Promise {<resolved>: undefined}
https://juejin.im/post/6844903830119792647
https://www.jianshu.com/p/7ec1272831fa
promise异常捕获
Promise在执行过程中,try..catch只能处理同步,对于异步的错误没有办法捕获,当出错时,会将错误抛给reject函数,所以直接throw new Error,会被catch所捕获。在不主动 catch 的情况下,如果使用 throw new Error() 或者 reject(‘error’),都会变成 Uncaught (in promise) Error 而不会被 window.onerror 捕获。 这个时候可以使用window.onunhandledrejection
进行捕获。
Promise.reject('出错了')
// 有catch就会在这里处理
// .catch(err =>{
// console.log(err)
// })
// 没有对应的catch处理,就会被unhandledrejection捕获
window.addEventListener('unhandledrejection', e =>{
console.log(e)
})
使用setTimeout()实际上可以算是异步过程,在异步过程中抛出的错误无法被try..catch所捕获,最终错误被window.onerror捕获,
需要注意的是,当resolve调用之后,再throw new Error也不会导致onRejected函数被调用。
在 Promise 中发生的未捕获异常不会被 window.onerror 捕获,如果你的页面使用了 前端异常监控平台 这样的东西,会导致无法自动收集未捕获的 Promise 异常。
比如你在 Promise 中,在不主动 catch 的情况下,如果使用 throw new Error() 或者 reject(‘error’),都会变成 Uncaught (in promise) Error 而不会被 window.onerror 捕获。
六、Generator生成器函数
ES6 提供的一种异步编程解决方案, 调用Generator 会返回一个interator遍历器,调用遍历器的next方法会执行一个异步任务。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
调用后返回指向内部状态的指针, 调用next()才会移向下一个状态, 参数:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
使用方式:
function* genDemo() {
console.log("开始执行第一段")
yield 'generator 2'
console.log("开始执行第二段")
yield 'generator 2'
console.log("开始执行第三段")
yield 'generator 2'
console.log("执行结束")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
输出结果:
main 0
开始执行第一段
generator 2
main 1
开始执行第二段
generator 2
main 2
开始执行第三段
generator 2
main 3
执行结束
generator 2
main 4
观察输出结果,你会发现函数 genDemo 并不是一次执行完的,全局代码和 genDemo 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。
- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
- 外部函数可以通过 next 方法恢复函数的执行。
原理:
生成器函数是基于协程实现的。
协程:
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
上面那段代码的执行图示:
从图中可以看出来协程的四点规则:
- 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
- 要让 gen 协程执行,需要通过调用 gen.next。
- 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
- 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。
V8是如何切换协程的调用栈的?
- 第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。
- 第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
使用使用生成器和 Promise 来改造一段 Promise 代码:
fetch('https://www.geekbang.org')
.then((response) => {
console.log(response)
return fetch('https://www.geekbang.org/test')
}).then((response) => {
console.log(response)
}).catch((error) => {
console.log(error)
})
改造后的代码:
//foo函数
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})
- 首先执行的是let gen = foo(),创建了 gen 协程。
- 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
- 父协程恢复执行后,调用 response1.then 方法等待请求结果。
- 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。
七、async、await
使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。而且可以通过try catch捕获错误。
其实 async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。要搞清楚 async 和 await 的工作原理,我们就得对 async 和 await 分开分析。
async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
async function foo() {
return 2
}
console.log(foo()) // Promise {<resolved>: 2}
await
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
当执行到await 时,会默认创建一个 Promise 对象,并调用promise.resolve()函数,
let promise_ = new Promise((resolve,reject){
resolve(100)
})
await后面的代码相当于是promise.then(),JavaScript 引擎会将该任务提交给微任务队列。
然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise 对象返回给父协程。主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise.then 来监控 promise 状态的改变。接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来 3。随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数,
promise_.then((value)=>{
//回调函数被激活后
//将主线程控制权交给foo协程,并将vaule值传给协程
})
该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。
总结:
使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。
(以前一直以为promise.then就是添加微任务,原来真的的微任务是promise.resolve/reject。then函数只是resolve/reject执行的副产品)
- Generator函数的语法糖,将*改成async,将yield换成await。
- 是对Generator函数的改进, 返回promise。
- 异步写法同步化,遇到await先返回,执行完异步再执行接下来的.
- 内置执行器, 无需next()
八、js类型判断
数据类型
基本数据类型:bigInt、symbal、boolean、 String、Number、null、undefined
复杂数据类型:Object 、Array、 Function1.bigInt
**BigInt**
是一种内置对象,它提供了一种方法来表示大于253 - 1
的整数。**BigInt**
可以表示任意大的整数。2.Symbol
Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。声明方法:
let id = Symbol(“id“);
1
Symbol 数据类型的特点是唯一性,即使是用同一个变量生成的值也不相等。
let id1 = Symbol(‘id’);
let id2 = Symbol(‘id’);
console.log(id1 == id2); //false
123
Symbol 数据类型的另一特点是隐藏性,for···in,object.keys() 不能访问
let id = Symbol(“id”);
let obj = {
[id]:’symbol’
};
for(let option in obj){
console.log(obj[option]); //空
}
1234567
但是也有能够访问的方法:Object.getOwnPropertySymbolsObject.getOwnPropertySymbols 方法会返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
let id = Symbol(“id”);
let obj = {
[id]:’symbol’
};
let array = Object.getOwnPropertySymbols(obj);
console.log(array); //[Symbol(id)]
console.log(obj[array[0]]); //‘symbol’
1234567
虽然这样保证了Symbol的唯一性,但我们不排除希望能够多次使用同一个symbol值的情况。为此,官方提供了全局注册并登记的方法:Symbol.for()
let name1 = Symbol.for(‘name’); //检测到未创建后新建
let name2 = Symbol.for(‘name’); //检测到已创建后返回
console.log(name1 === name2); // true
123
通过这种方法就可以通过参数值获取到全局的symbol对象了,反之,能不能通过symbol对象获取到参数值呢?是可以的 ,通过Symbol.keyFor()
let name1 = Symbol.for(‘name’);
let name2 = Symbol.for(‘name’);
console.log(Symbol.keyFor(name1)); // ‘name’
console.log(Symbol.keyFor(name2)); // ‘name’
1234
最后,提醒大家一点,在创建symbol类型数据 时的参数只是作为标识使用,所以 Symbol() 也是可以的。3.null与undefined
null == undefined 会返回 true
1、undefined
undefined 的字面意思就是未定义的值,这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:
【1】声明了一个变量,但没有赋值
var foo;
console.log(foo); //undefined
访问foo,返回了undefined,表示这个变量自从声明了以后,就从来没有使用过,也没有定义过任何有效的值,即处于一种原始而不可用的状态。
【2】访问对象上不存在的属性
console.log(Object.foo); // undefined
访问Object对象上的 foo 属性,同样也返回 undefined , 表示Object 上不存在或者没有定义名为 “foo” 的属性。
【3】函数定义了形参,但没有传递实参
//函数定义了形参 a
function fn(a) {
console.log(a); //undefined
}
fn(); //未传递实参
函数 fn 定义了形参a, 但 fn 被调用时没有传递参数,因此,fn 运行时的参数 a 就是一个原始的、未被赋值的变量。
【4】使用 void 对表达式求值
void 0 ; // undefined
void false; //undefined
void []; //undefined
void null; //undefined
void function fn(){} ; //undefined
ECMAScript 规范 void 操作符 对任何表达式求值都返回 undefined ,这个和函数执行操作后没有返回值的作用是一样的,JavaScript中的函数都有返回值,当没有 return 操作时,就默认返回一个原始的状态值,这个值就是undefined,表明函数的返回值未被定义。
因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。
2、null
null 的字面意思是 空值 ,这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象,即:
当一个对象被赋值了null 以后,原来的对象在内存中就处于游离状态,GC 会择机回收该对象并释放内存。因此,如果需要释放某个对象,就将变量设置为null,即表示该对象已经被清空,目前无效状态。试想一下,如果此处把 null 换成 undefined 会不会感到别扭? 显然语义不通,其操作不能正确的表达其想要的行为。
与 null 相关的另外一个问题需要解释一下:
typeof null == ‘object’
null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
类型判断
前言:判断JS类型,有以下几种方法:1、typeof 2、object.property.toString.call 3、instance of。
(一)JS的类型
JS的基本类型共有七种:bigInt(bigInt是一种内置对象,是处symbol外的第二个内置类型)、number、string、boolen、symbol、undefined、null。复杂数据类型有对象(object)包括基本的对象、函数(Function)、数组(Array)和内置对象(Data等)。
(二)判断JS的类型
方法一、typeof方法
基本数据类型除了null外都返回对应类型的字符串。
typeof 1 // "number"
typeof 'a' // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
注:判断一个变量是否被声明可以用(typeof 变量 === “undefined”)来判断
null返回“object”
typeof null // “object” 因为历史遗留的原因。typeof null 尝试返回为null失败了,所以要记住,typeof null返回的是object。
特殊值NaN返回的是 “number”
typeof NaN // "number" 而复杂数据类型里,除了函数返回了"function"其他均返回“object”
typeof({a:1}) // "object" 普通对象直接返回“object”
typeof [1,3] // 数组返回"object"
typeof(new Date) // 内置对象 "object"
函数返回"function"
typeof function(){} // "function"
所以我们可以发现,typeof可以判断基本数据类型,但是难以判断除了函数以外的复杂数据类型。于是我们可以使用第二种方法,通常用来判断复杂数据类型,也可以用来判断基本数据类型。
方法二、object.property.toString.call方法 ,他返回”[object, 类型]”,注意返回的格式及大小写,前面是小写,后面是首字母大写。
基本数据类型都能返回相应的类型。
Object.prototype.toString.call(999) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(42n) // "[object BigInt]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]
复杂数据类型也能返回相应的类型
Object.prototype.toString.call({a:1}) // "[object Object]"
Object.prototype.toString.call([1,2]) // "[object Array]"
Object.prototype.toString.call(new Date) // "[object Date]"
Object.prototype.toString.call(function(){}) // "[object Function]"
这个方法可以返回内置类型
方法三、obj instanceof Object ,
可以左边放你要判断的内容,右边放类型来进行JS类型判断,只能用来判断复杂数据类型,因为instanceof 是用于检测构造函数(右边)的 prototype 属性是否出现在某个实例对象(左边)的原型链上。
[1,2] instanceof Array // true
(function(){}) instanceof Function // true
({a:1}) instanceof Object // true
(new Date) instanceof Date // true
obj instanceof Object方法也可以判断内置对象。 缺点:在不同window或者iframe间,不能使用instanceof。
其他方法:除了以上三种方法,还有constructor方法 和 duck type方法,具体在文章就不介绍了,个人觉得吃透分清上面三种方法已经足够了。
方法四:constructor
我们可以通过constructor来判断数据的类型,但是除了null、undefined,因为他们不是由对象构建。
数字、布尔值、字符串是包装类对象,所以有constructor
数字
var num = 1;
num.constructor
ƒ Number() { [native code] }
布尔值
true.constructor
ƒ Boolean() { [native code] }
字符串
“”.constructor
ƒ String() { [native code] }
函数
var func = function(){}
func.constructor
ƒ Function() { [native code] }
数组
[].constructor
ƒ Array() { [native code] }
对象
var obj = {}
obj.constructor
ƒ Object() { [native code] }
== 与 === 与 object.is
== 只比较值的大小,在比较之前会进行类型转换
=== 比较值和类型,两者都相等了才是相等,但是 ===
运算符 (也包括 ==
运算符) 将数字 -0
和 +0
视为相等 ,而将[Number.NaN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/NaN)
与[NaN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/NaN)
视为不相等.
所以有了object.is
九、闭包
1)什么是闭包
函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。
可以在内部函数访问到外部函数作用域。
使用闭包,一可以读取函数中的变量,二可以将函数中的变量存储在内存中,保护变量不被污染。而正因闭包会把函数中的变量值存储在内存中,会对内存有消耗,所以不能滥用闭包,否则会影响网页性能,造成内存泄漏。当不需要使用闭包时,要及时释放内存,可将内层函数对象的变量赋值为null。
2)闭包原理
函数执行分成两个阶段(预编译阶段和执行阶段)。
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
3)优点
- 可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
4)缺点
- 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
- 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
- 可能获取到意外的值(captured value)
4)应用场景
应用场景一: 典型应用是模块封装,在各模块规范出现之前,都是用这样的方式防止变量污染全局。
var Yideng = (function () {
// 这样声明为模块私有变量,外界无法直接访问
var foo = 0;
function Yideng() {}
Yideng.prototype.bar = function bar() {
return foo;
};
return Yideng;
}());
应用场景二: 在循环中创建闭包,防止取到意外的值。
如下代码,无论哪个元素触发事件,都会弹出 3。因为函数执行后引用的 i 是同一个,而 i 在循环结束后就是 3
for (var i = 0; i < 3; i++) {
document.getElementById('id' + i).onfocus = function() {
alert(i);
};
}
//可用闭包解决
function makeCallback(num) {
return function() {
alert(num);
};
}
for (var i = 0; i < 3; i++) {
document.getElementById('id' + i).onfocus = makeCallback(i);
}
理解:
闭包是有权限访问其他函数作用域内的变量的一个函数。这是《JavaScript高级程序设计》
解决了哪些问题:
由于闭包可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。以平时使用的Ajax成功回调为例,这里其实就是个闭包,由于上述的特性,回调就拥有了整个上级作用域的访问和操作能力,提高了极大的便利。开发者不用去写钩子函数来操作上级函数作用域内部的变量了。
应用场景:
闭包随处可见,一个Ajax请求的成功回调,一个事件绑定的回调方法,一个setTimeout的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影。
闭包有哪些
原理比较深奥:要想完全掌握闭包,一定要清楚函数作用域、内存回收机制、作用域继承等,然而闭包是随处可见的,很可能开发者在不经意间就写出了一个闭包,理解不够深入的话很可能造成运行结果与预期不符。
代码难以维护:闭包内部是可以缓存上级作用域,而如果闭包又是异步执行的话,一定要清楚上级作用域都发生了什么,而这样就需要对代码的运行逻辑和JS运行机制相当了解才能弄明白究竟发生了什么。
一.什么是闭包
高级程序设计三中:闭包是指有权访问另外一个函数作用域中的变量的函数.可以理解为(能够读取其他函数内部变量的函数)
闭包的作用: 正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的 变量,在函数执行完之后依旧保持没有被垃圾回收处理掉
二. 闭包的实例
// 创建闭包最常见的方式函数作为返回值
function foo() {
var name = "kebi";
return function() {
console.log(name);
};
}
var bar = foo();
bar(); //打印kebi --外部函数访问内部变量
接下来通过一个实例来感受一下闭包的作用:
接下来实现一个计数器大家肯定会觉得这不是很简单吗
var count = 0;
function add() {
count = count + 1;
console.log(count);
}
add(); //确实实现了需求
//但是如果需要第二个计数器呢?
//难道要如下这样写吗?
var count1 = 0;
function add1() {
count1 = count1 + 1;
console.log(count1);
}
add1(); //确实实现了需求
当我们需要更多地时候,这样明显是不现实的,这里我们就需要用到闭包.
function addCount() {
var conut = 0;
return function() {
count = count + 1;
console.log(count);
};
}
这里解释一下上边的过程: addCount() 执行的时候, 返回一个函数, 函数是可以创建自己的作用域的, 但是此时返回的这个函数内部需要引用 addCount() 作用域下的变量 count, 因此这个 count 是不能被销毁的.接下来需要几个计数器我们就定义几个变量就可以,并且他们都不会互相影响,每个函数作用域中还会保存 count 变量不被销毁,进行不断的累加
var fun1 = addCount();
fun1(); //1
fun1(); //2
var fun2 = addCount();
fun2(); //1
fun2(); //2
三.常见面试题
1. for 循环中打印
for (var i = 0; i < 4; i++) {
setTimeout(function() {
console.log(i);
}, 300);
}
上边打印出来的都是 4, 可能部分人会认为打印的是 0,1,2,3
原因:js 执行的时候首先会先执行主线程,异步相关的会存到异步队列里,当主线程执行完毕开始执行异步队列, 主线程执行完毕后,此时 i 的值为 4,说以在执行异步队列的时候,打印出来的都是 4(这里需要大家对 event loop 有所了解(js 的事件循环机制))
如何修改使其正常打印:(使用闭包使其正常打印)
//方法一:
for (var i = 0; i < 4; i++) {
setTimeout(
(function(i) {
return function() {
console.log(i);
};
})(i),
300
);
}
// 或者
for (var i = 0; i < 4; i++) {
setTimeout(
(function() {
var temp = i;
return function() {
console.log(temp);
};
})(),
300
);
}
//这个是通过自执行函数返回一个函数,然后在调用返回的函数去获取自执行函数内部的变量,此为闭包
//方法发二:
for (var i = 0; i < 4; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, 300);
})(i);
}
// 大部分都认为方法一和方法二都是闭包,我认为方法一是闭包,而方法二是通过创建一个自执行函数,使变量存在这个自执行函数的作用域里
2.真实的获取多个元素并添加点击事件
var op = document.querySelectorAll("p");
for (var j = 0; j < op.length; j++) {
op[j].onclick = function() {
alert(j);
};
}
//alert出来的值是一样的
// 解决办法一:
for (var j = 0; j < op.length; j++) {
(function(j) {
op[j].onclick = function() {
alert(j);
};
})(j);
}
// 解决办法二:
for (var j = 0; j < op.length; j++) {
op[j].onclick = (function(j) {
return function() {
alert(j);
};
})(j);
}
//解决方法三其实和二类似
for (var j = 0; j < op.length; j++) {
op[j].onclick = (function() {
var temp = j;
return function() {
alert(j);
};
})();
}
//这个例子和例子一几乎是一样的大家可以参考例子一
3.闭包的缺陷:
通过上边的例子也发现, 闭包会导致内存占用过高,因为变量都没有释放内存
十、观察者模式 vs 发布-订阅模式
观察者设计模式:
我认为大多数人都会同意观察者模式是学起来最好入门的,因为你从字面意思就能知道它主要是做什么的。
观察者模式 在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。
看吧,即使是维基百科的定义 ,也不是很难, 对吧? 如果你还不清楚,那让我用通俗易懂的来解释。
我们假设你正在找一份软件工程师的工作,对“香蕉公司”很感兴趣。所以你联系了他们的HR,给了他你的联系电话。他保证如果有任何职位空缺都会通知你。这里还有几个候选人也你一样很感兴趣。所以职位空缺大家都会知道,如果你回应了他们的通知,他们就会联系你面试。
所以,以上和“观察者模式”有什么关系呢?这里的“香蕉公司”就是Subject,用来维护Observers(和你一样的候选人),为某些event(比如职位空缺)来通知(notify)观察者。
是不是很简单!?
所以,如果你想在你的软件或者应用中实现观察者模式,你可以遵循上图这个流程。(我不想在我的文章中写任何代码,因为网上有数不清的例子)
代码实现
// 一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。
class Subject {
constructor() {
this.observers = []
}
// 添加观察者
add( observe) {
this.observers.push(observe)
}
// 通知观察者
notify(...arg) {
this.observers.forEach(observe => observe.updata(arg))
}
}
class Observer{
updata(...arg) {
console.log(arg) // do something
}
}
const sub = new Subject() /* 系统 */
sub.add(new Observer()) /* 张三点了预约 */
sub.add(new Observer()) /* 李四点了预约 */
sub.notify() /* 双十一了,通知所有点了预约的人来抢货了 */
发布-订阅设计模式
在观察者模式中的Subject就像一个发布者(Publisher),观察者(Observer)_完全和订阅者(Subscriber)*关联。subject通知观察者就像一个发布者通知他的订阅者。这也就是很多书和文章使用“发布-订阅”概念来解释观察者设计模式。但是这里还有另外一个流行的模式叫做*_发布-订阅设计模式。它的概念和观察者模式非常类似。最大的区别是:
在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者。
意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。
那么如何过滤消息的呢?事实上这里有几个过程,最流行的方法是:基于主题以及基于内容。好了,就此打住,如果你感兴趣,可以去维基百科了解。
发布-订阅模式(图片来源: MSDN 博客)
所以,我用下图表示这两个模式最重要的区别:
图片来源: developers-club
有感觉了吗?
我们把这些差异快速总结一下:
- 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
- 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
- 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
- 观察者 模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
尽管它们之间有区别,但有些人可能会说发布-订阅模式是观察者模式的变异,因为它们概念上是相似的。
ok,我想说的说完了,希望你get到了,感谢您的阅读,如果发现有什么错误或者需要修改的地方,请在下方留言,谢谢!
发布-订阅模型 API 设计思路
通过前面的讲解,不难看出发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布),这两个动作自然而然地对应着两个基本的 API 方法。
- on():负责注册事件的监听器,指定事件触发时的回调函数。
- emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。
最后,只进不出总是不太合理的,我们还要考虑一个 off() 方法,必要的时候用它来删除用不到的监听器:
off():负责监听器的删除。
问题一:事件和监听函数的对应关系如何处理?
提到“对应关系”,应该联想到的是“映射”。在 JavaScript 中,处理“映射”我们大部分情况下都是用对象来做的。所以说在全局我们需要设置一个对象,来存储事件和监听函数之间的关系。
- 问题二:如何实现订阅?
所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 eventMap 里面去
- 问题三:如何实现发布?
订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,我们需要做的就是找到这个事件对应的监听函数队列,将队列中的 handler 依次执行出队
代码实现
class Observer {
event = {}
// 订阅
on(eventName, eventFn) {
if(!this.event[eventName]) {
this.event[eventName] = []
}
this.event[eventName].push(eventFn)
}
// 发布
emit(eventName, eventMsg) {
if(this.event[eventName]) {
this.event[eventName].forEach(element => {
element[eventMsg]
});
}
}
// 取消
off(eventName, fn) {
if(this.event[eventName]) {
// 若fn不传, 直接取消该事件所有订阅信息
const newEvent = fn ? this.event[eventName].filter( item => item !== fn) : []
this.event[eventName] = newEvent
}
}
}
const eventEmitter = new Observer()
eventEmitter.on('fileSucess', function(eventMsg){
console.log('1查看数据库数据')
})
eventEmitter.on('fileSucess', function(eventMsg){
console.log('2查看数据库数据')
})
eventEmitter.on('fileSucess', function(eventMsg){
console.log('3查看数据库数据')
})
class myEventEmitter {
constructor() {
// eventMap 用来存储事件和监听函数之间的关系
this.eventMap = {};
}
// type 这里就代表事件的名称
on(type, handler) {
// hanlder 必须是一个函数,如果不是直接报错
if (!(handler instanceof Function)) {
throw new Error("哥 你错了 请传一个函数");
}
// 判断 type 事件对应的队列是否存在
if (!this.eventMap[type]) {
// 若不存在,新建该队列
this.eventMap[type] = [];
}
// 若存在,直接往队列里推入 handler
this.eventMap[type].push(handler);
}
// 别忘了我们前面说过触发时是可以携带数据的,params 就是数据的载体
emit(type, params) {
// 假设该事件是有订阅的(对应的事件队列存在)
if (this.eventMap[type]) {
// 将事件队列里的 handler 依次执行出队
this.eventMap[type].forEach((handler, index) => {
// 注意别忘了读取 params
handler(params);
});
}
}
off(type, handler) {
if (this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1);
}
}
}
是无符号按位右移运算符。考虑 indexOf 返回-1 的情况:splice方法喜欢把-1解读为当前数组的最后一个元素,这样子的话,在压根没有对应函数可以删的情况下,不管三七二十一就把最后一个元素给干掉了。而 >>> 符号对正整数没有影响,但对于-1来说它会把-1转换为一个巨大的数(可以本地运行下试试看,应该是一个32位全是1的二进制数,折算成十进制就是 4294967295)。这个巨大的索引splice是找不到的,找不到就不删,于是一切保持原状,刚好符合我们的预期。
十一、函数柯里化
所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3十二、类数组和数组的区别
- 类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。
- 区别本质:类数组是简单对象,它的原型关系与数组不同。
// 原型关系和原始值转换
let arrayLike = {
length: 10,
};
console.log(arrayLike instanceof Array); // false
console.log(arrayLike.proto.constructor === Array); // false
console.log(arrayLike.toString()); // [object Object]
console.log(arrayLike.valueOf()); // {length: 10}
let array = [];
console.log(array instanceof Array); // true
console.log(array.proto.constructor === Array); // true
console.log(array.toString()); // ‘’
console.log(array.valueOf()); // []
类数组转换为数组
- 转换方法
1. 使用 `Array.from()`
2. 使用 `Array.prototype.slice.call()`
3. 使用 `forEach()` 进行属性遍历并组成新的数组
- 转换须知
答题大纲
- 先说基本知识点:宏任务哪些,微任务哪些
- 说说事件循环机制过程,边说边画
- 说说 async / await 执行顺序注意,可以把 chrome 的优化,做法其实是违反了规范的,V8 团队的 PR 这些自信点说出来,会显得很好学,理解的很详细,很透彻
- 把 node 的事件循环也说一下,重复 1、2、3点,并且着重说 node v11 前后事件循环的变动
浏览器中的事件循环
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序以外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为 macro-task(宏任务)和 micro-task(微任务),在最新标准中,他们分别被称为 tasks 和 jobs。
macro-task(宏任务) 大概包括:
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- I / O
- UI render(dom渲染)
micro-task(微任务) 大概包括:
- process.nextTick
- Promise.then
- async / await (等价于 Promise.then)
- MutationObserver(HTML5 新特性)
setTimeout、setInterval
这两个函数内部运行机制完全一致,区别在于前者一次性执行,后者反复执行。
两者产生的任务都是 异步任务,也属于 宏任务。
setTimeout 接收两个参数,第一个是回调函数,第二个是延迟执行的毫秒数。
如果第二个参数设置为0或者不设置,意思 并不是立即执行,而是指定某个任务在主线程最早可得到的空闲时间执行,也就是说,尽可能早得执行。他在 任务队列 的尾部添加一个事件,因此要等到同步任务和 任务队列 现有的事件都处理完,才会得到执行。
所以说,setTimeout 和 setInterval 第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的,简单来说,如果当前代码执行的时间超出了推迟执行的时间,那么 setTimeout(fn, 100) 就和 setTimeout(fn, 0) 没有区别了。
Promise
Promise 相对来说比较特殊,在 new Promise() 中传入的回调函数是会 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,它属于 微任务。
process.nextTick
process.nextTick 是 Node.JS 提供的一个与 任务队列 有关的方法,它产生的任务是放在 执行栈的尾部,并不属于 宏任务 和 微任务,因此它的任务 总是发生在所有异步任务之前。
setImmediate
setImmediate 是 Node.js 提供的与 任务队列 有关的方法,它产生的任务追加到 任务队列 的尾部,它和 setTimeout(fn, 0) 很像,但是优先级低于 setTimeout。
有时候,setTimeout 会在 setImmediate 之前执行,有时会在之后执行,这是因为虽然 setTimeout 第二个参数设置为 0 或者不设置,但是 setTimeout 源码中,会指定一个具体的毫秒数(nodejs 为 1ms,浏览器为 4ms),而由于当前代码执行时间收到执行环境的影响,执行时间有所起伏。
总体结论就是:
- 执行宏任务
- 然后执行宏任务产生的微任务
- 若微任务在执行过程中产生了新的微任务,则继续执行微任务
- 微任务执行完毕,再回到宏任务中进行下一轮循环
async / await 执行顺序
我们知道 async
会隐式返回一个 Promise 作为结果的函数,那么可以简单理解为:await 后面的函数在执行完毕后,await 会产生一个微任务(Promise.then 是微任务)。但是我们要注意微任务产生的时机,它是执行完 await 后,直接跳出 async 函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到 async 函数去执行剩下的代码,然后把 await 后面的代码注册到微任务队列中。例如:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
分析这段代码:
- 执行代码,输出
script start
- 执行
async1()
,调用了async2()
,然后输出async2 end
,此时会保留 async1 的上下文,然后跳出 async1 - 遇到 setTimeout,产生一个宏任务
- 执行 Promise,输出 Promise,遇到 then,产生第一个微任务,继续执行代码,输出
script end
- 当前宏任务执行完毕,开始执行当前宏任务产生的微任务,输出
promise1
,然后又遇到 then,产生一个新的微任务 - 执行微任务,输出
promise2
,此时微任务队列已清空,执行权交还给 async1 - 执行 await 后的代码,输出
async1 end
- 所有微任务队列均已执行完毕,开始执行下一个宏任务,打印
setTimeout
注意
新版的 chrome 并不是像上面那样的执行顺序,它优化了 await 的执行速度,await 变得更早执行了,输出变更为:
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
但是这种做法其实违反了规范,但是规范也可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。知乎上也有相关的 讨论 。
我们可以分两种情况进行讨论
- 如果 await 后面直接跟的为一个变量,比如
**await 1**
。这种情况相当于直接把 await 后面的代码注册为一个微任务,可以简单理解为**Promise.then(await 后面的代码)**
,然后跳出函数去执行其他的代码。 ```javascript console.log(‘script start’)
async function async1() { await 1 console.log(‘async1 end’) } async1()
setTimeout(function() { console.log(‘setTimeout’) }, 0)
new Promise(resolve => { console.log(‘Promise’) resolve() }).then(function() { console.log(‘promise1’) }).then(function() { console.log(‘promise2’) })
console.log(‘script end’)
1. script start<br />Promise<br />script end<br />async1 end<br />promise1<br />promise2<br />setTimeout
2. 如果 await 后面跟的是一个异步函数的调用,则会跳出函数执行完其它再来注册await后面的函数为微任务,再去执行,比如上面的代码修改为:
```javascript
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
输出为:
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
此时 执行完 await 并不会把 await 后面的代码注册到微任务对立中,而是执行完 await 之后,直接跳出了函数,执行其他同步代码,直到其他代码执行完毕后,再回到这里将 await 后面的代码推倒微任务队列中执行。注意,此时微任务队列中是有之前注册的其他微任务,所以这种情况会先执行其他的微任务。可以理解为 await 后面的代码会在本轮循环的最后被执行。
MutationObserver
MutationObserver 是用来监听 DOM 变化的一套方法 ,将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。
MutationObserver 采用了“异步 + 微任务”的策略。
- 通过异步操作解决了同步操作的性能问题;
- 通过微任务解决了实时性的问题。
node 中的事件循环
同样是使用 V8 引擎的 Node.js 也同样有事件循环。事件循环是 Node 处理非阻塞 I / O 操作的机制,Node 中实现事件循环依赖的是 libuv 引擎。由于 Node 11 之后,事件循环的一些原理发生了改变,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。
宏任务和微任务
node 中也分为宏任务和微任务,其中,
macro-task(宏任务)包括:
- setTimeout
- setInterval
- setImmediate
- script(整体代码)
- I / O 操作
micro-task(微任务)包括:
- process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
- Promise.then 回调
setTimeout 和 setImmediate
二者非常相似,区别主要在于调用时机不同。
- setImmediate 设计在poll阶段完成时执行,即check阶段;
- setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
- 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。
- 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
- 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
node 事件循环整体理解
图中每个框被成为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但是通常情况下,当事件循环进入特定的阶段时,它将执行特性该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。
因此,上图可以简化为以下流程:
- 输入数据阶段(incoming data)
- 轮询阶段(poll)
- 检查阶段(check)
- 关闭时间回调阶段(close callback)
- 定时器检测阶段(timers)
- I / O 事件回调阶段(I / O callbacks)
- 闲置阶段(idle,prepare)
- 轮询阶段…
阶段概述
- 定时器检测阶段(timers):本阶段执行 timers 的回调,即 setTimeout、setInterval 里面的回调函数
- I / O 事件回调阶段(I / O callbacks):执行延迟到下一个循环迭代的 I / O 回调,即上一轮循环中未被执行的一些 I / O 回调
- 闲置阶段(idle,prepare):仅供内部使用
- 轮询阶段(poll):检索新的 I / O 事件;执行与 I / O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些计时器和 setImmediate 调度之外),其余情况 node 将在适当的时候在此阻塞
- 检查阶段(check):setImmediate 回调函数将在此阶段执行
- 关闭事件回调阶段(close callback):一些关闭的回调函数,如
socket.on('close', ...)
三大重点阶段
日常开发中绝大部分异步任务都在 poll、check、timers 这三个阶段处理,所以需要重点了解这三个阶段
timers
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只是尽快执行。
poll
poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:
如果当前已经存在定时器,而且有定期到时间了,拿出来执行,事件循环将会到 timers 阶段
如果没有定时器,回去看回调函数队列
- 如果 poll 队列不为空,会遍历回到队列并同步执行,直到队列为空或达到系统限制
- 如果 poll 队列为空,会有两件事发生
- 如果 setImmediate 回调需要执行,poll 阶段将会停止并进入 check 阶段执行回调
- 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置,防止一直等待下去,一段时间后自动进入 check 阶段
check
check 阶段,这是一个比较简单的阶段,直接执行 setImmediate 的回调
process.nextTick
process.nextTick
是独立于事件循环的任务队列
在每一个事件循环阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。
看一个例子:
setImmediate(() => {
console.log('timeout1')
Promise.resolve().then(() => console.log('promise resolve'))
process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
在 node11 之前,因为每一个事件循环阶段完成后都会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为:
// timeout1 -> timeout2 -> timeout3 -> timeout4 -> next tick1 -> next tick2 -> promise resolve
在 node11 之后,
process.nextTick
被视为是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,在执行下一个宏任务及其微任务队列,因此输出为:// timeout1 -> next tick1 -> promise resolve -> timeout2 -> next tick2 -> timeout3 -> timepout4
node 版本差异说明
这里主要说明 node11 前后的差异,因为 node11 之后一些特性向浏览器看齐,总的变化一句话来说就是:
如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout、setInterval、setImmediate)就会立刻执行对应的微任务队列
timers 阶段执行时机的变化
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
如果是 node11以后的版本一旦执行到一个阶段内的一个宏任务(setTimeout、setInterval 和 setImmediate)就会立刻执行微任务队列,这和浏览器的运行方式是一致的,最后输出为:
// timer1 -> promise1 -> timer2 -> promise2
如果是 node11 之前的版本,要看第一个定时器执行完,第二个定时器是否在完成队列中
如果第二个定时器还未在完成队列中,最后的结果为
// timer1 -> promise1 -> timer2 -> promise2
如果第二个定时器已经在完成队列中,最后结果为
// timer1 -> timer2 -> promise1 -> promise2
check 阶段的执行时机变化
setImmediate(() => console.log('immediate1'));
setImmediate(() => {
console.log('immediate2')
Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
如果是 node11 后的版本,会输出
// immediate1 -> immediate2 -> promise resolve -> immediate3 -> immediate4
如果是 node11 前的版本,会输出
// immediate1 -> immediate2 -> immediate3 -> immediate4 -> promise resolve
nextTick 队列执行时机的变化
setImmediate(() => console.log('timeout1'));
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
如果是 node11 后的版本,会输出
// timeout1 -> timeout2 -> next tick -> timeout3 -> timeout4
如果是 node11 前的版本,会输出
// timeout1 -> timeout2 -> timeout3 -> timeout4 -> next tick
node 和 浏览器事件循环的主要区别
两者主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而 nodejs 中的微任务则是在不同阶段之间执行的。
十四、进程和线程
首先我们了解一下进程和线程的概念和关系:
- 进程:运行的程序就是一个进程,比如你正在运行的浏览器,它会有一个进程;
- 线程:程序中独立运行的代码段。一个进程 由单个或多个 线程 组成,线程是负责执行代码的。
单线程与多线程的区别:
- 单线程:从头执行到尾,一行行执行,如果其中一行代码报错,那么剩下的代码将不再执行。同时容易代码阻塞。
- 多线程:代码运行的环境不同,各线程独立,互不影响,避免阻塞。
为了保证 JavaScript 的主要运行环境(浏览器),JavaScript 被设计成了单线程语言。
为了充分利用多核处理器的计算能力,HTML5 提出了 Web Worker 标准,允许 JavaScript 脚本创建多个子线程,但是子线程完全受控于主线程,且不得操作 DOM。因此,新标准并没有改变 JavaScript 单线程的本质。
for in 和 for of 区别
map 和 object 的区别,和 weakMap 的区别
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。通过Set集合可以快速访问其中的数据,更有效地追踪各种离散值
Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名和它对应的值,Map集合经常被用于缓存频繁取用的数据。在标准正式发布以前,开发者们已经在ES5中用非数组对象实现了类似的功能
在 Object 中, key 必须是简单数据类型(整数,字符串或者是 symbol),而在 Map 中则可以是 JavaScript 支持的所有数据类型,也就是说可以用一个 Object 来当做一个Map元素的 key。
Map 想要访问元素,可以使用 Map 本身的原生方法:
map.get(1) // 2
Object 可以通过 . 和 [ ] 来访问
obj.id;
obj['id'];
Map 元素的顺序遵循插入的顺序,而 Object 的则没有这一特性。
Map 继承自 Object 对象。
Map 自身支持迭代,Object 不支持。
console.log(typeof obj[Symbol.iterator]); // undefined
console.log(typeof map[Symbol.iterator]); // function
Map 自身有 size 属性,可以自己维持 size 的变化。
Object 则需要借助 Object.keys() 来计算
console.log(Object.keys(obj).length);
Map 有原生的 delete 方法来删除元素:
Map 在存储大量元素的时候性能表现更好,特别是在代码执行时不能确定 key 的类型的情况。
ES6中Map相对于Object对象有几个区别:
1:Object对象有原型, 也就是说他有默认的key值在对象上面, 除非我们使用Object.create(null)创建一个没有原型的对象;
2:在Object对象中, 只能把String和Symbol作为key值, 但是在Map中,key值可以是任何基本类型(String, Number, Boolean, undefined, NaN….),或者对象(Map, Set, Object, Function , Symbol , null….);
3:通过Map中的size属性, 可以很方便地获取到Map长度, 要获取Object的长度, 你只能用别的方法了;
Map实例对象的key值可以为一个数组或者一个对象,或者一个函数,比较随意 ,而且Map对象实例中数据的排序是根据用户push的顺序进行排序的, 而Object实例中key,value的顺序就是有些规律了, (他们会先排数字开头的key值,然后才是字符串开头的key值);
另: 关于 Array 和 Set, 区别就是 Set 不可以有重复元素, 而 Array 可以有
weakMap 和 Map 的区别
weakMap 弱映射,其键只能是 Object 类型,值可以是任意类型,
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
注意的是,WeakMap弱引用的只是键名,而不是值。值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo : 1};
wm.set(key,obj);
obj = null;
console.log(wm.get(key)); //{foo : 1}
WeakMap的用途
WeakMap应用的典型场合就是Dom节点作为键名。
let myElement = document.getElementsByTagName('div')[0];
let myWeakMap = new WeakMap();
myWeakMap.set(myElement,{timesClicked: 0});
myElement.addEventListener('click',function(){
let logoData = myWeakMap.get(myElement);
console.log(logoData.timesClicked ++);
},false)
//上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,
//对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
WeakMap的另一个用处是部署私有属性。
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter,action) {
_counter.set(this,counter);
_action.set(this,action);
}
dec(){
let counter = _counter.get(this);
if(counter < 1){
return ;
}
counter --;
_counter.set(this,counter);
if(counter === 0){
_action.get(this)();
}
}
}
const c = new Countdown(2,() => console.log('DOWM'));
c.dec();
c.dec(); //DOWN