深入浅出JS → 易混淆点解析
JS基础模块
数据类型
- 原始数据类型
- 原始数据类型存储的都是值,是没有函数可以调用的,如
undefined.toString()
- 因为存在
强制类型转换
,会存在原始类型的函数调用的情况,但是是因为强制类型转换
的缘故;如'1'.toString()
- 因为存在
- 原始数据类型
null
的typeof
会输出Object
- JS最初版本使用的是32位的系统,为了性能考虑使用低位存储变量的类型信息,000开头的代表是对象,而null表示为全0,因此被错误的判断为object
- 原始数据类型存储的都是值,是没有函数可以调用的,如
- 类型转换
- JS中的类型转换的三种情况
- 转换为Boolean
- 在类型判断中,除了undefined、null、false、NaN、''、0、-0外其他所有值都转换为true,包括所有对象
- 转换为Number
- 转换为String
- 转换为Boolean
- JS对象转原始类型
- 对象在转换类型的时候,会调用内置的
[[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般是:- 如果是原始类型,就不需要转换了
- 调用
x.valueOf()
,如果转换为基础类型,就返回转换的值 - 调用
x.toString()
,如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
- 也可以重写
Symbol.toPrimitive
,该方法在转原始类型时调用优先级最高 - 上述优先级为:
[Symbol.toPrimitive]()
>valueOf()
>toString()
let a = { valueOf() { return 1 }, toString() { return '2' }, [Symbol.toPrimitive]() { return 3 } } let b = {} 1 + a // 优先级输出 1 + b //1[object Object]
- 对象在转换类型的时候,会调用内置的
- JS四则运算
- 加法运算
- 运算中一方是字符串,那么会将另一方也转换为字符串
- 如果一方不是字符串或数字,那么会将其转换为数字或字符串
4 + [1,2,3,4] //41,2,3,4``1 + {a:2} // 1[object Object]
,触发第二条,将数组通过toString
转换为字符串,然后执行四则运算
- 在连加的情况下会先将后续的运算执行
+
数据类型转换,如+ 'a' // NaN
、+ '1' // 1
,因此'a' + 'b' → aNaN
- 除加法外的运算
- 只要一方是数字,另一方都会转换为数字
Number([]) === Number([0]) //true
Number([2]) === Number(['2']) // true
Number([1,2]) //NaN
Number([true]) //NaN
Number(true) //1
- 只要一方是数字,另一方都会转换为数字
- 比较运算
- 如果是对象,就通过
toPrimitive
转换为对象 - 如果是字符串,就通过
unicode
字符串索引来比较
- 如果是对象,就通过
- 加法运算
- JS中的类型转换的三种情况
this问题
- 常规判断是:
谁调用了函数,谁就是this
- 通过
new
处理后的:this永远被绑定到了实例上,不会被任何方式改变this
- 箭头函数中的this
- 箭头函数是没有this的,箭头函数的this只取决于包裹箭头函数的第一个普通函数的this;
- 箭头函数使用
bind
这类函数是无效的
- bind问题
- 当一个函数被多次
bind
时,对应函数中的this永远由第一次bind决定
- 当一个函数被多次
- 同时多条件出现时对应优先级
- new的优先级最高 → bind类的函数 → 对象打点调用 → 函数单独自调用
- 箭头函数的this被绑定后,不会有任何方式可以改变this
比较问题
- === 和 == 的区别
- 先判断两者类型是否相同,相同的话就比较大小
- 类型不同时,会进行类型转换
- 先判断是否是null和undefined对比,是的话就返回true
- 判断两者类型是否为string和number,是的话就将字符串转换为number
- 判断其中一方是否为Boolean,是的话就将Boolean转换为number再进行对比
- 判断其中一方是否是object且另一方是否是string、number或者Symbol,是的话就会将object转换为原始类型再进行判断
- 案例:
[] == ![] // true
=>[] == ![]
→[] == false // !的优先级大于==
→0 == 0
→true
- 案例:
- Object.assign()
- Object.assign()是浅拷贝,只会拷贝所有的属性值到新的对象中,当属性值是对象时,拷贝的该属性值是地址
- 浅拷贝只解决了第一层的问题,当有多层嵌套时就会有问题
变量提升和暂时性死区问题
- 函数提升优先于变量提升
- 函数提升会将整个函数挪动到作用域顶部,变量提升只会将声明挪动到作用域顶部
- var存在提升,可以在声明之前调用,let和const存在暂时性死区的原因,不可在声明前使用
- vary在全局作用域下声明的变量会挂载到window上,let和const不会
- let和const作用基本一致,但是后者声明的变量不可再次赋值
继承问题
组合式继承
组合式继承的核心在于:在子类的构造函数中通过
Parent.call(this)
继承父类的属性
、然后通过改变子类的原型
为new Parent()来继承父类的方法函数
;
- 优缺点
- 优点:在于构造函数可以传参,并不会与父类引用属性共享,可以复用父类的函数
- 缺点:在继承父类函数的时候调用了父类的构造函数,导致子类的原型上多了不需要的父类属性,存在内存浪费
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child('Lbxin')
child.val
child.getValue() // Lbxin
child instanceof Parent // true
寄生组合继承
该方式是对组合继承进行了优化,修复了组合继承存在的
继承父类函数时调用了构造函数
其核心在于:将父类的原型赋值给了子类,并将构造函数设置为子类,这样既可以解决了无用的父类属性问题,还可以正确的找到子类的构造函数;
function Parent(value) {
this.val = value;
}
Parent.prototype.getValue = function () {
console.log(this.val);
};
function Child(value) {
Parent.call(this, value);
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true,
},
});
const child = new Child('Lbxin');
child.getValue(); // Lbxin
child instanceof Parent; // true;
class继承
class实现继承的核心在于使用
extends
表明继承自哪个父类,并且在子类构造函数中必须使用super关键字,super(value) => Parent.call(this,value)
class的本质就是函数,JS中并不存在类的概念
class Parent {
constructor(value) {
this.val = value;
}
getValue() {
console.log(this.val);
}
}
class Child extends Parent {
constructor(value) {
super(value);
this.val = value;
}
}
let child = new Child('Lbxin');
child.getValue(); // Lbxin
child instanceof Parent; // true
拓展 construct内部放和外部放的区别
主要区别就是:一个是添加到实例上,一个是添加到原型的方法上 定义到construct内部的方法,在进行new关键字实例化时会改变this指向,因此每个实例的construct内部的方法是不一样的
- 在construct内部声明的都会添加到每个实例上
- 在construct外部声明的会添加到原型方法上
class parent {
constructor(val) {
this.val = val
this.showIn = () => {
console.log(this.val,'this.val========= showIn')
}
}
showOut(){
console.log(this.val,'this.val=========, showOut')
}
}
// parent 等价于 parentEs5
// function parentEs5(){
// this.show = (val)=> {
// this.val = val
// console.log(this.val,'this.val========= showIn')
// }
// }
// parentEs5.prototype.showOut = () => {
// console.log(this.val,'this.val=========, showOut')
// }
class child extends parent {
constructor(){
super()
}
}
class child2 extends parent {
constructor(){
super()
}
}
let a1 = new child()
let a2 = new child()
let a3 = new child2()
console.log(a1.showIn==a2.showIn,'a1.showIn==a2.showIn=========',a1.showOut===a2.showOut) // false true
console.log(a1.showIn==a3.showIn,'a1.showIn==a3.showIn=========',a1.showOut == a3.showOut) // false true
并发(concurrency)和并行(parallelism)
并发:是宏观概念,可以在一段时间内通过任务建的切换完成了多个任务,这种情况就可以称为
并发
并行:是微观概念:可以理解为在多核CPU中同时
完成多个任务的情况称为并行
回调地狱
- 回调地狱的根本问题
- 嵌套函数存在耦合性,一旦有所改动,就会出现较大的影响
- 嵌套函数一多,很难处理错误问题
- 不可以使用try catch进行捕获错误
- 不能直接return
- 解决上述问题的方式
- Generator
- 基本概念:Generator最大的特点就是
可以控制函数的执行
,与普通函数的区别在于返回了一个迭代器
- 基本执行逻辑
- 第一次执行
next
时,传参会被忽略,且会停留在第一个yield
定义的地方 - 第二次执行
next
时,传入的参数等于上一个yield
的返回值,不传参时,yield
永远返回undefined
- 后续的
next
都是按照上述的逻辑进行类推的
function* foo(x) { let y = 2 * (yield x + 1); let z = yield y / 3; return x + y + z; } let it = foo(5); console.log(it.next()); // => {value: 6, done: false} console.log(it.next(12)); // => {value: 8, done: false} console.log(it.next(13)); // => {value: 42, done: true}
- 第一次执行
- Generator解决回调地狱的方法
async await 是内部实现了generator,await就是generator加上Promise的语法糖,且内部实现了自动执行的generatorfunction *fetch() { yield ajax(url, () => {}); yield ajax(url1, () => {}); yield ajax(url2, () => {}); } let it = fetch(); let result1 = it.next(); let result2 = it.next(); let result3 = it.next();
- 基本概念:Generator最大的特点就是
- Promise
- 在构造
Promise
的时候,构造函数内部的代码是立即执行的 → 构造Promise
时内部的逻辑比js的调用栈先执行 - Promise实现了链式调用,即每次调用
then
后返回的都是一个Promise,且是一个全新的Promise,原因是Promise内部的三个状态变化后是不可变的 - 当Promise的
then
中使用了return
返回值,那么该值会被Promise.resolve()
包裹
Promise.resolve(1) .then((res) => { console.log(res); // => 1 return 2; // 包装成 Promise.resolve(2) }) .then((res) => { console.log(res); // => 2 }) (async () => { let as = await Promise.resolve(1).then((res) => { console.log(res); // => 1 return 2; // 包装成 Promise.resolve(2) }); console.log(as, "as========="); })(); // 1 2 as========= new Promise((resolve, reject) => { console.log("new Promise"); resolve("success"); }); console.log("finifsh"); // new Promise -> finifsh
- Promise解决回调地狱的问题
ajax(url) .then((res) => { console.log(res); return ajax(url1); }) .then((res) => { console.log(res); return ajax(url2); }) .then((res) => console.log(res));
- 在构造
- Generator
JS高级模块
JS面相对象浅析
Event Loop
进程和线程
进程描述了CPU在运行指令及加载和保存上下文所需要的时间,在应用维度上来说就代表了一个程序 线程是进程中更小的单位,描述了执行一段指令所需的时间
JS是单线程执行的,JS引擎线程的执行会阻止渲染进程的执行,这样可以避免不能安全的渲染UI的问题,也是单线程的好处,其得益于JS是单线程运行的,可以达到节省内存、节约上下文切换的时间,没有锁的问题
- 多任务执行时的解决方案
- 排队:一个进程一次只能执行一个任务,只能等前面的任务执行完了,再执行后面的任务
- 新建进程:用
fork
命令,为每个任务新建一个进程 - 新建线程:因为进程太耗资源、因此一般都是一个进程包含多个线程,由线程去完成任务
- 备注:多线程需要共享资源,且有可能资源间会修改彼此的运行结果,因此JS的单线程是为了让浏览器尽量简单化
- Event Loop就是为了解决上述问题的
- 在程序中设置两个线程
- 一个负责程序本身的运行,成为
主线程
- 另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为Event Loop线程(消息线程)
- 上图中,当主线程遇到如I/O等操作时,主线程就让Event Loop线程去执行相应的逻辑,然后主线程继续执行其他任务,因此不存在等待的过程,等到I/O操作完成时,Event Loop线程再将结果返回到主线程,主线程执行相应事先设计好的回调函数,完成整个任务
- 一个负责程序本身的运行,成为
- 在程序中设置两个线程
- 在浏览器维度来说当打开一个tap时,其实就是创建了一个进程,一个进程可以有多个线程,比如渲染线程、JS引擎线程、HTTP请求线程等
执行栈
可以理解为存储函数调用的栈结构、遵循
先进后出
的原则;但在某些情况下(如递归),因为栈可以存放的函数是有限的,一旦存放过多的函数且没有得到释放就会出现暴栈的问题
Event Loop
Event Loop即事件循环,是指浏览器或Node的一种解决javascript单线程运行时不会阻塞的一种机制,即经常使用的异步原理;
- 执行机制
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context Stack)
- 主线程之外,还存在一个
任务队列(task queue)
,只要异步任务有了运行结果,就在任务队列
中放置一个事件 - 一旦
执行栈
中所有同步任务执行结束,系统就会读取任务队列
,看看里面有哪些事件。那些对应的异步任务会结束等待状态,进入执行栈,开始执行 - 主线程不断重复上面的第三步
任务被分为两种:同步任务(synchronous)和异步任务(asynchronous:又分为宏任务和微任务)
- 同步任务(会立即执行的任务):函数返回 => 拿到预期结果
- 异步任务(不会立即执行的任务):函数返回 => 拿不到预期结果,还要通过一定手段获得结果
- 宏任务:由宿主对象发起的任务(settimeout)
- 微任务:由JS引擎发起的任务(promise)
浏览器中的Event Loop
当浏览器在执行JS代码时本质上就是往执行栈中放入函数,当遇到异步代码时,会被挂起并在需要执行的时候加入到任务队列(Task)中,一旦当前执行栈被清空,Event Loop就会从Task队列中拿出需要执行的代码并且放入到执行栈中执行,因此本质上来说JS中的异步还是同步的行为; 不同的任务源会被分配到不同的Task队列中,任务源可以分为
微任务(microtask:也被称为jobs)
和宏任务(macrotask:也被称为task)
console.log('script start')
async function async1() {
await async2()
// 当遇到await时 会让出当前占用的线程 开始执行该函数作用域之外的逻辑代码 => await可以当成是「让出线程」的标志
// 去掉await时 执行完async2后就会执行下面的log
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')
// node/chrome sudo sudo
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// setTimeout
虽然settimeout定义在Promise之前,但因为Promise属于微任务而settimeout属于宏任务,因此settimeout会在最后执行
- Event Loop执行顺序
- 先执行同步代码,属于宏任务
- 当执行完所有同步代码后,当前执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有需要会进行页面渲染
- 开始下一轮Event Loop,执行宏任务中的异步代码,即settimeout中的回调函数
- 微任务包括
- Promise.nextTick、Promise、MutationObserver
- 宏任务包括
- script、settimeout、setInterval、setImmediate(Node)、I/O、UI rendering、postMessage、MessageChannel
node中的Event Loop
node 中的Event Loop分为6个阶段,他们会按照顺序反复运行,每当进入到某一个阶段时,都会从对应的回调函数中取出函数去执行,当队列为空或者执行的回调函数数量达到系统设定的阈值时就会进入到下一阶段
- node Event Loop执行顺序浅析
- timer阶段:会执行settimeout、setinterval回调,并且是由poll阶段控制的
- I/O:I/O阶段会处理一些上一轮循环中的
少数未执行
的I/O回调 - idel,prepare:仅在内部使用
- poll:
- 回到timer阶段执行回调
- 执行I/O回调
- 。。。。。。
Promise深入
Promise A+规范
琐碎拓展
JS值类型和引用类型
JS设计出值类型和引用类型的原因是:按值传递的类型,复制一份传入栈内存,这类类型一般不占用太多的内存,而且按值传递保证了其访问速度;按共享传递的类型,是复制其引用,而不是整个复制其值,保证过大的对象等不会因为不停复制内容而造成内存的浪费。
原型和原型链
- 原型注意点
- 所有引用类型(数组、对象、函数),都具有对象的特性,都可自由拓展属性(null除外)
- 所有引用类型(数组、对象、函数),都有一个
__proto__
属性,属性值是一个普通的对象 - 所有函数都有一个
prototype
属性,属性值也是一个普通的对象 - 所有的引用类型(数组、对象、函数),
__proto__
属性值指向他的构造函数的prototype
属性值
- 原型遵循的原则
- 当试图找到一个对象的某个属性时,如果这个对象本身没有这个属性,则会去它的
__proto__(即构造函数)
中寻找 f.__proto__
中没有找到toString
,就会去f.__proto__.proto__
上寻找,f.__proto__
是指向Function.prototype
,Function.prototype
就是一个普通对象,因此f.__proto__.proto__
=>Function.prototype.__proto__
=>Object.prototype
,找到这一层就可以调用toString()
方法了,原型链的最上层就是Object.prototype.__proto__ === null
- 所有从原型或更高级原型中得到执行的方法,其中的this在执行时
- JS没有块级作用域(ES6中加入了块级作用域,即let和const定义的变量),只有全局作用域和函数作用域,在函数作用域外部是无法常规的拿到函数作用域内部的值的,通常情况下,会将对应的数据放在函数作用域下,避免污染全局变量,如jQuery中的
(function(){......})()
- 当试图找到一个对象的某个属性时,如果这个对象本身没有这个属性,则会去它的
new浅析
- new关键字创建实例后的影响
- 会执行构造函数中的代码
- 会生成一个该构造函数的实例对象
- this指向创建后的实例对象,
this === 实例对象 // true
- 当new实例化时的构造函数有返回值时,情况会有不一致的,当构造函数返回了对象(引用类型)或函数时,实例化的新对象就是该返回的值,其他情况都是对应的新实例对象
- 调用new的过程浅析
- 生成一个新的空对象
- 链接到原型
- 绑定this
- 返回新对象
- 具体实现过程
function myNew() {
let obj = {}; // 底层还是 new Object()
// arguments是类数组对象,并没有数组中的方法,因此需要call()将数组的shift方法绑定给arguments
let Con = [].shift.call(arguments); // 获取构造函数
obj.__proto__ = Con.prototype; //设置空对象原型
let result = Con.apply(obj, arguments); // 绑定this并执行构造函数
return result instanceof Object ? result : obj; // 确保返回值为对象
}
// 通过Object.create()方法实现
function myNew(Func, ...args) {
// 创建一个空的对象,将实例化对象的原型指向构造函数的原型对象
const instance = Object.create(Func.prototype);
// 将构造函数的this指向实例化对象
const res = Func.apply(instance, args);
//判断返回值
return typeof res === "object" ? res : instance;
}
Object.create(null)会造成创建的对象其__proto__
指向空
- 对于对象来说其实都是通过new创建的,无论是function fn()还是let a = { test: 1 }
- 对于创建一个对象来说,推荐使用字面量的方式创建对象(无论是性能还是可读性),当使用new Object()的方式创建对象需要通过作用域链一层一层的找到Object,使用字面量的方式就没有这个问题了
instanceof浅析
instanceof用于实例和构造函数的对应,可以正确的判断对象的类型,内部机制是通过判读对象的原型链中是不是可以找到类型的prototype;例如
[]
的构造函数是Array
,因此[1,2] instanceof Array // true
,通过new
关键字构造函数产生的实例其实例是instanceof构造函数的
- 实现过程浅析
- 首先获取类型的显示原型
- 然后获取对象的隐式原型
- 然后一直循环判断对象的原型是否等于类型的原型,直到对象的原型为null,因为原型链最终为null
function myInstanceof(left, right) { // 基本数据类型以及 null 直接返回 false if (!["function", "object"].includes(typeof source) || source === null) return false; // let leftProto = left.__proto__; // Object.getPrototypeOf是Object对象自带的一个方法,可以拿到参数的原型对象 let as = {} as.__proto__ === Object.getPrototypeOf(as) // true let leftProto = Object.getPrototypeOf(left); while (true) { if (leftProto === null || leftProto === undefined) { return false; } if (right.prototype === leftProto) { return true; } leftProto = Object.getPrototypeOf(leftProto); } }
call、apply和bind函数
- 主要特点
- 不传入第一个参数,那么上下文默认是Window
- 通过new来实例化一个构造函数的对象,则this指向该构造函数
- 改变了this指向,让新的对象可以执行该函数,并可以接收参数
- 异同分析
- 相同点:都是JS函数的共有内部方法,都是重置函数的this,改变函数的执行环节
- 不同点:
- bind是创建一个新的函数,而call和apply是用来调用函数的
- call和apply作用一样,只不过call为函数提供的参数是一个一个列出来的,而apply为函数提供的参数是一个数组
- call浅析
- 常规的函数调用,如fn()直接函数名执行,只不过是语法糖,本质是fn.call()
- 应用:子函数要想继承父函数需要在子函数中写
Father.call(this)
,这个this是指向子函数,那么就实现了子函数的实例调用父函数的属性或方法
function Father() { this.age = 56 } // 方式一 // function Son() { // Father.call(this) // } // let son = new Son() // console.log(son.age,'son.age=========') // 56 'son.age=========' // 方式二 function Son(){} let son = new Son() Father.call(son) console.log(son.age,'son.age=========') // 56 'son.age========='
- call实现
Function.prototype.myCall = function(context) { if(typeof this !== 'function') { throw new TypeError("error") } context = context || window context.fn = this const args = [...arguments].slice(1) // call中的参数是可以为多个 因此需要剥离出来调用时的入参 const result = context.fn(...args) delete context.fn return result }
- apply浅析
- apply除了后面的参数形式与call有区别之外,其他的都一样,上面的call的地方都可以用apply来实现,不同点就是参数上是使用数组的方式,在某些情况下这种方式更适合于call
Math.max.call(null,1,2,3,4,5)
Math.max.apply(null,[1,2,3,4,5])
- apply实现
Function.prototype.myApply = function(context) { if(typeof this !== 'function') { throw new TypeError("error") } context = context || window context.fn = this let result // 与call的区别在于参数的处理 有参数时带参执行 没参数时直接执行 if(arguments[1]){ result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
- bind浅析
- bind与前两者的唯一区别就是前两者是直接改变函数的this指向,而bind是生成一个新函数,该函数改变了this指向
- bind实现
- 相对前两者的实现,bind略微复杂一些,因为bind需要返回一个函数,需要判断一些边界问题
- bind返回了一个函数,对于函数的调用方式来说有两种,一种是直接调用,一种是通过new的方式
Function.prototype.myBind = function(context) { if(typeof this !== 'function') { throw new TypeError("error") } const _this = this const args = [...arguments].slice(1) // 对于函数的调用有两种方式 直接调用和new 实例化调用 return function F(){ // new 的方式进行调用 对于new 的方式 不会被任何方式改变this if(this instanceof F) { return new _this(...args,...arguments) } // 直接调用 bind的调用方式例如fn.bind(obj,1)(2) 因此需要将新旧入参都进行拼接整合 return _this.apply(context,args.concat(...arguments)) } }
JSON.parse和JSON.stringify实现深拷贝的问题
JS中可以通过JSON.parse(JSON.stringify(object))来实现深拷贝,但该方法也有局限性
- 局限性包括
- 会忽略undefined
- 会忽略Symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 解决办法(当拷贝的对象有内置类型并且不包含函数时)
- 可以通过
MessageChannel
来实现
- 可以通过
MessageChannel拓展
是一种用于在不同的浏览器上下文之间传递数据的机制。该API允许两个不同的脚步运行在同一个文档的不同浏览器上下文(如两个iframe,两个不同的窗口、文档主体和一个iframe,使用sharedWorker的两个文档,两个Worker)来直接通讯,在每端使用一个端口(port)通过双向频道(channel)来彼此传递消息; 在之前的版本中可以通过
postMessage
和localStorage
,他们同样也是用在不同浏览器之间传递数据的方式,与上述两者不同的是MessageChannel支持更高特性
- MessageChannel的高级特性
- 多路传输:一个MessageChannel实例可以同时传输多个消息
- 双向传输:MessageChannel双向传输,即可以在两个端口之间发送和接收消息
- 接收发送的消息不需要特定的数据结构
- 可以传输更大的数据
- 基本用法
- 使用
MessageChannel()
构造函数来创建通信通道,获取两个端口MessagePort对象port1和port2 - 一个端口使用
postMessage
发送消息 - 另一个端口通过
onmessage
接收消息 - 当端口收到无法反序列化的消息时,使用
onmessageerror
进行处理 - 停止发送消息时,调用close关闭端口
const { port1, port2 } = new MessageChannel(); port1.onmessage = (event) => { console.log("收到来自port2的消息:", event.data); }; port1.onmessageerror = (event) => {}; port2.onmessage = function (event) { console.log("收到来自port1的消息:", event.data); port2.postMessage("我是port2"); }; port2.onmessageerror = (event) => {}; port1.postMessage("我是port1");
- 通过
addEventListener
方式实现- 该方式需要手动调用
portX.start()
方法消息才可以进行流动,原因是初始化的时候是暂停的 - 而
onmessage
已经隐式的调用了start()
方法
- 该方式需要手动调用
const { port1, port2 } = new MessageChannel(); port1.addEventListener("message", (event) => { console.log("收到来自port2的消息:", event.data); }); port1.addEventListener("messageerror", (event) => {}); port1.start(); port2.addEventListener("message", (event) => { console.log("收到来自port1的消息:", event.data); port2.postMessage("我是port2"); }); port2.addEventListener("messageerror", (event) => {}); port2.start(); port1.postMessage("我是port1");
- 使用
- 在Event Loop中的执行顺序
- 同步任务 > 微任务 > requestAnimationFrame > DOM渲染 > 宏任务
setTimeout(() => { console.log('setTimeout') }, 0) const { port1, port2 } = new MessageChannel() port2.onmessage = e => { console.log(e.data) } port1.postMessage('MessageChannel') requestAnimationFrame(() => { console.log('requestAnimationFrame') }) Promise.resolve().then(() => { console.log('Promise1') }) // Promise1 // 微任务先执行 // requestAnimationFrame // setTimeout // 宏任务,先定义先执行 // MessageChannel // 宏任务,后定义后执行
- 使用场景
- 同一个document的上下文通信
var channel = new MessageChannel(); var para = document.querySelector('p'); var ifr = document.querySelector('iframe'); var otherWindow = ifr.contentWindow; ifr.addEventListener("load", iframeLoaded, false); function iframeLoaded() { otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]); } channel.port1.onmessage = handleMessage; function handleMessage(e) { para.innerHTML = e.data; }
-
结合Web Worker实现多线程通信
- 在Web Worker的计算复杂算法,并将结果发送回主线程,这样可以减少主线程负担,从而提高页面响应的时间,通过
MessageChannel
可以将这一过程简单实现
// 主进程中创建MessageChannel实例,将其中的一个端口发送给Web Worker const worker = new Worker('yourworker.js'); const channel = new MessageChannel(); worker.onmessage = (e) => { // 来自worker内部的postMessage API触发 } worker.postMessage({type: "connect"}, [channel.port2]); channel.port1.onmessage = function(evt) { console.log(evt.data); }
// yourWorker.js // 在Web Worker中创建与发送端口相匹配的onmessage处理程序,以便在主线程发送消息时,可以接收结果 self.onmessage = function(evt) { const channel = new MessageChannel(); evt.ports[0].postMessage({data: 'response'}, [channel.port2]); channel.port1.onmessage = function(evt) { console.log(evt.data); postMessage(data) // 直接通过该API进行相关事件信息交互 } };
- 多个work间可以直接通过
postMessage()
API进行消息传递,外部主页面通过对应的new Worker()
的实例的onmessage
方法进行监听对应消息事件 - worker内部可以通过
MessageChannel
API透传port
进行worker间的消息传递
- 在Web Worker的计算复杂算法,并将结果发送回主线程,这样可以减少主线程负担,从而提高页面响应的时间,通过
-
多窗口/Iframe之间的通信
- 具体过程
- 主页面通过
XXIframe.contentWindow.postMessage("message data from index page","*",[port-X])
传递到iframe页面内部中 - iframe内部通过
window.addEventListener('message',(e) => {const {data,type,ports} = e})
获取到data、ports等信息,然后通过ports.postMessage('data from iframe')
进行多个窗口或iframe间通信
- 主页面通过
- 具体过程
-
实现深拷贝
- 大部分深拷贝的场景都可以通过
JSON.parse(JSON.stringify(object))
,但该方法会忽略undefined、function、Symbol和循环引用的对象 - 使用MessageChannel实现的深拷贝只可以解决undefined和循环引用对象的问题,对于Symbol和function依然束手无策
- 大部分深拷贝的场景都可以通过
// 深拷贝函数 function deepClone(val) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = e => resolve(e.data) port1.postMessage(val) }) }
const deepClone = (obj) => { return new Promise((resolve, reject) => { const { port1, port2 } = new MessageChannel(); port1.postMessage(obj); // 触发下面的onmessage方法 port2.onmessage = (e) => { resolve(e.data); // 不执行下列注释 控制台将处于挂起状态 port1.close(); port2.close(); }; }); }; const obj = { a: 1, b: { c: 1, d: { f: 1, g: 1, } }, }; deepClone(obj).then((obj2) => { console.log(obj2, 22222); // { a: 1, b: { c: 1, d: { f: 1, g: 1 } } } 22222 obj.b.d.f = 2; console.log(obj, obj2, 33333); // { a: 1, b: { c: 1, d: { f: 2, g: 1 } } } { a: 1, b: { c: 1, d: { f: 1, g: 1 } } } 33333 });
CSS选择器及其权重优先级
- 常见选择器及对应权重
- 内联样式,如
style
,权重为1000 - ID选择器:权重为100
- 类选择器、伪类和属性选择器:权值为10
- 伪类和伪元素的设计是为了解决
在文档树中有些信息是无法被充分描述的
,如css没有「段落第一行」、「文章首字母」之类的选择器 - 伪类:为处于某个状态的已有元素添加对应的样式,该状态是随用户的行为而变的
- 伪类分为:结构伪类和状态伪类
- 结构伪类:选择第几个元素的那些API、元素状态的API(如checked、expty、disabled、enabled、valid等)
- 状态伪类:
link、hover、active、visited、focus
- 需要注意的是「顺序问题」,通常按照
link-visited-focus-hover-active
的顺序,这样可以满足大多数的场景,但是并不是不能改的 - 如link和visited是互斥的(这两个选择器又称为静态伪类,只应用于超链接),且当这两个选择器放置在其他三个之后时,其他三个是不生效的
- 需要注意的是「顺序问题」,通常按照
- 伪类和伪元素的设计是为了解决
- 元素选择器和伪元素选择器:权值为1
- 伪元素:创建一些不在文档树中的元素,并为其添加样式
- 常见的有:
after、before、first-letter、first-line、selection
- first-letter和first-line仅用于块级元素
- 常见的有:
- 伪元素:创建一些不在文档树中的元素,并为其添加样式
- 注意:
- 通用选择器(*)、子选择器(>)和相邻同胞选择器(+)并不在这四个等级中,其权值为0
- 权重值大的选择器其优先级也高,权重相同的遵循后定义覆盖前定义
- 内联样式,如
转载自:https://juejin.cn/post/7251182084672225336