一、js代码在浏览器上是如何执行的(V8)
Parser模块:会将 Javascript代码转换成AST(抽象语法树),这是因为解释器并不直接认识 javascript 代码 如果函数没有被调用,那么是不会被转换成AST的 Parse的V8官方文档v8.dev/blog/scanne…
lgnition:是一个解释器,会将AST转换成 Byte Code(字节码) 同时会收集 Turbo Fan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算) 如果函数只调用一次, Ignition会执行解释执行 Byte Code Ignition的V8官方文档:v8.dev/blog/igniti… Turbofan:是一个编译器,可以将字节码编译为CPU可以直接执行的机器码(0101) 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 Turbofan 转换成优化的机器码,提高代码的执行性能 如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是 numbera类型,后来执行变成了 string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码 TurboFan的V8官方文档:https://8dey/blog/turbofan-it
以下内容为个人口述:
Parse: 对js代码进行解析(词法分析,语法分析)
语法分析生成抽象语法树
抽象语法树通过Ignition转化成字节码(字节码是跨平台的)
lgnition还会收集执行次数较多的函数并标记(如果收集只执行一两次的函数那么就比较浪费空间了)
转换成汇编代码
转换成对应平台的机器指令执行
TurboFan会将lgnition标记的代码直接转换成对应平台的机器指令并保存下来,直接跳过了生成字节码到汇编再到机器码过程,但是如果函数的调用发生了变化(比如一个 sum(10,20) 函数一直传递的参数是number类型,但是有一次 我传入了字符串类型 sum('aaa','bbb') 那么会做一个反向优化 (Deoptimization) 将它再次转化成字节码到汇编再到机 器码)
二、JS 中的垃圾回收机制
必要性:由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。
JavaScript 程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
这段话解释了为什么需要系统需要垃圾回收,JS 不像 C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。
JavaScript 的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:
var a = "hello world";
var b = "world";
var a = b; //这时,会释放掉"hello world",释放内存以便再引用
垃圾回收的方法:标记清除、计数引用。
标记清除:
这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。
垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
引用计数法:
引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为 1,
相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0 的这些值。
用引用计数法会存在内存泄露
下面来看原因:
function problem() {
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB;
objB.anOtherObject = objA;
//解决办法
objA = null
objB = null
}
在这个例子里面,objA 和 objB 通过各自的属性相互引用,这样的话,两个对象的引用次数都为 2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存泄露。
特别是在 DOM 对象中,也容易存在这种问题:
var element=document.getElementById('root');
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;
这样就不会有垃圾回收的过程。
三、预编译的过程
1、创建AO对象
2、找形参和变量的声明并且作为AO对象的属性名值为undefind
3、实参和形参相统一
4、找函数声明并且覆盖变量的声明
function fn(a,c){
console.log(a)
var a = 123
console.log(a)
console.log(c)
function a(){}
if (false){
let d = 678
}
console.log(d)
console.log(b)
var b = function(){}
console.log(b)
function c() {}
console.log(c)
}
fn(1,2)
AO: {
a:undefined // 1 // function a(){}
b:undefined
c:undefined // 2 function c() {}
d:undefined
}
//控制台输出
ƒ a(){}
123
ƒ c() {}
undefined
undefined
ƒ (){}
ƒ c() {}
四、闭包
闭包就是能够读取其他函数内部变量的函数。
只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。
在本质上,闭包是将函数内部和函数外部连接起来的桥梁
五、this的指向问题
1、普通函数
(1)、直接调用(fun())指向window function.call(window,参数)
(2)、对象调用指向调用的对象 object.function.call(object,参数)
2、箭头函数
1、概念:箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
2、箭头函数的this指向是指向他的父级
六、对象的原型、原型链
1、隐式原型
在javascript当中每个对象都有一个特殊的内置属性 [[prototype]] (隐式原型), 这个特殊的对象可以指向另外一个对象。
早期的ECMA是没有范如何去查看 prototype但是浏览器给对象中提供了ー个属性,可以让我们查看ー下这个原型对象
__proto__
var obj = {
name: 'lmw'
}
console.log(obj.__proto__)
在ECMA5之后提供了一个方法来查看对象的原型
Object.getPrototypeOf()
var obj = {
name: 'lmw'
}
console.log(Object.getPrototypeOf(obj))
当我们ー个对象中获某一个属性时它会触发 [[get]] 操作
1、在当前对象中去查找对应的属性,如果找到就直接使用
2、如果没有找到,那么会沿着它的原型链去查找 __proto__
var obj = {
name: 'lmw'
}
obj.__proto__.age = 18
console.log(obj.age) // 18
2、函数的原型
1、函数也是个对象
函数作为对象来说,它也是有 [[prototype]] (隐式原型) 的
函数作为一个函数,还会有一个显示原型: prototype (在ECMA标准中早期就定义了的 没有兼容性问题) foo.prototype
2、函数原型上的属性constructor
prototype.constructor = 构造数本身
function foo(){}
console.log(foo.prototype.constructor) // [Function: foo]
3、原型链
var obj = {
name: 'lmw'
}
obj._proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
address: '江西'
}
// 如果在obj里面找不到会沿着__proto__一层一层的查找
// .__proto__.__proto__.__proto__就被称为原型链
console.log(obj.address) // 江西
// 原型链的最顶层是Object.prototype
console.log(obj.__proto__) // [Object: null prototype] {} 其实并非空对象,由于设置了不可枚举所以看不到可以通过函数Object.getOwnPropertyDescriptors(Object.prototype)来查看
console.log(Object.prototype) // [Object: null prototype] {}
console.log(obj.__proto__ === Object.prototype) // true
console.log(Object.getOwnPropertyDescriptors(Object.prototype)) // 很多东西 去浏览器自己看
console.log(Object.prototype.__proto__) // null
4、方法
1、Object.hasOwnProperty()
// 判断对象是否拥有某个属性 不包括原型链上的属性
let obj = {
name: 'lmw'
}
// 获取对象的隐式原型
Object.getPrototypeOf(obj).info = {
age: 18
}
console.log(obj.hasOwnProperty('info')) // false
2、in操作符
// 判断对象是否拥有某个属性 包括原型链上的属性
let obj = {
name: 'lmw'
}
Object.getPrototypeOf(obj).info = {
age: 18
}
console.log(obj)
console.log('info' in obj) // true
七、手写bind, apply, call, new,Object.create
// call和apply中为了防止obj中有fn属性所以不用obj.fn = fn
Function.prototype.newCall = function (obj, ...args) { // call实现
var fn = this
obj = obj ? Object(obj) : window
// obj.fn = fn
var onlyFn = Symbol('onlyFn')
obj[onlyFn] = fn
args = args || []
var res = obj[onlyFn](...args)
delete obj[onlyFn]
return res
}
Function.prototype.newApply = function (obj, arr) { // apply实现
var fn = this
obj = obj ? Object(obj) : window
arr = arr || []
if (!Array.isArray(arr)) {
throw 'TypeError: CreateListFromArrayLike called on non-object'
}
var onlyFn = Symbol('onlyFn')
obj[onlyFn] = fn
var res = obj[onlyFn](...arr)
delete obj[onlyFn]
return res
}
Function.prototype.newBind = function (obj, ...args) { // bind实现
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function`)
}
var fn = this
obj = obj ? Object(obj) : window
var foo = function (...arg) {
var totalArgs = [...args, ...arg]
var res = null
if (this instanceof foo) {
res = fn.apply(this, totalArgs)
} else {
res = fn.apply(obj, totalArgs)
}
return res
}
foo.prototype = Object.create(fn.prototype)
return foo
}
// new过程
// 1.在内存中创建一个新的对象(空对象)
// 2.将新对象的__proto__ (隐式原型) 属性指向构造函数的prototype (显式原型)
// 3.构造函数内部的this,会指向创建出来的新对象
// 4.执行函数的内部代码(函数体代码)
// 5.如果构造函数没有返回非空对象,则返回创建出来的新对象
let Parent = function (name, age) {
this.name = name;
this.age = age;
};
let newMethod = function (Parent, ...rest) { // new实现
// 1.以构造器的prototype属性为原型,创建新对象;
let child = Object.create(Parent.prototype);
// 2.将this和调用参数传给构造器执行
let result = Parent.apply(child, rest);
// 3.如果构造函数没有手动返回对象,则返回创建出来的对象也就是child
return typeof result === 'object' ? result : child;
};
const child = newMethod(Parent, 'echo', 26);
child.sayName() //'echo';
// Object.create
// 创建一个新的对象
// 新对象的__proto__指向传入的对象
Object.prototype.newCreate = function(o){ // Object.create实现
function Fn(){}
Fn.prototype = o
var obj = new Fn()
return obj
}
八、类的继承
一、原型链继承
function Person() {
this.name = 'lmw'
this.friend = []
}
Person.prototype.running = function () {
console.log(this.name + ' is running')
}
function Student() {
this.title = '学生'
}
var p = new Person()
Student.prototype = p
Student.prototype.constructor = Student
Student.prototype.getTitle = function () {
console.log('titlt: ' + this.title)
}
var stu = new Student()
console.log(stu.name) // lmw
stu.getTitle() // title: 学生
stu.running() // lmw is running
// 缺点
// 1、打印子类无法看到继承到的属性
console.log(stu) // Student { title: '学生' }
console.log(stu.name) // lmw
// 2、获取引用值、修改引用值、会相互影响
var stu2 = new Student()
stu.friends.push('jjh')
console.log(stu.friends) // [ 'jjh' ]
console.log(stu2.friends) // [ 'jjh' ]
// 3、Student传递参数比较复杂
二、call继承
function Person(name, friends) {
this.name = name
this.friends = friends
}
Person.prototype.running = function () {
console.log(this.name + ' is running')
}
function Student(name, friends, title) {
Person.call(this, name, friends)
this.title = title
}
var p = new Person()
Student.prototype = p
Student.prototype.getTitle = function () {
console.log('titlt: ' + this.title)
}
var stu = new Student()
// 优点
// 解决了原型链继承的所有缺点
// 缺点
// 1、Person至少被执行两次
// 2、stu的原型对象上会多出一些属性,但是这些属性是没有存在的必要
三、寄生组合继承
function Person(name, friends) {
this.name = name
this.friends = friends
}
Person.prototype.running = function () {
console.log(this.name + ' is running')
}
function Student(name, friends, title) {
Person.call(this, name, friends)
this.title = title
}
Student.prototype = Object.create(Person.prototype)
// Student.prototype.constructor = Student
Object.defineProperty(Student.prototype, 'constructor', {
value: Student,
writable: true,
enumerable: false,
configurable: true,
})
Student.prototype.getTitle = function () {
console.log('stu titlt: ' + this.title)
}
function Teacher(name, friends, title) {
Person.call(this, name, friends)
this.title = title
}
Teacher.prototype = Object.create(Person.prototype)
Object.defineProperty(Teacher.prototype, 'constructor', {
value: Student,
writable: true,
enumerable: false,
configurable: true,
})
Teacher.prototype.getTitle = function () {
console.log('teacher title: ' + this.title)
}
var stu = new Student('lmw', ['xxx', 'yyy'], '学生')
var teacher = new Teacher('xh', ['zzz', 'eee'], '老师')
stu.getTitle()// stu titlt: 学生
teacher.getTitle()// teacher title: 老师
缺点:
无法继承原生js的构造函数的属性,例如
Object()
Number()
String()
Boolean()
Array()
Data()
Error()
示例:
function Foo(){
Array.call(this)
}
Foo.prototype = Object.create(Array.prototype)
Object.defineProperty(Foo.prototype, 'constructor', {
value: Foo,
writable: true,
enumerable: false,
configurable: true,
})
let arr = new Foo()
arr[0] = 'lmw'
console.log(arr.length) // 0
四、ES6继承
class A {
constructor(a) {
this.a = a
}
getA() {
return this.a
}
}
class B extends A {
constructor(b) {
super(10)
this.b = b
}
getb() {
return this.b
}
}
完美
九、暂停死区
在代码块内,使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”
十、ES7新增
1、Array.includes
用于判断数组中是否包含某个属性
let arr = [1, 2, 3, 4, 5, NaN]
console.log(arr.includes(1)) // true
// 第二个参数可以传入数组的索引 表示从第几个开始
console.log(arr.includes(1,1)) // false
console.log(arr.includes(1,0)) // true
console.log(arr.indexOf(NaN)) // -1 indexOf是无法判断NaN的
console.log(arr.includes(NaN)) // true
2、数字的指数操作
let foo = 2
console.log(foo ** 3) // 8
十一、ES8新增
1、Object.values
在ES8中提供了 Object values来获取所有的 value值
let obj = {
name: 'lmw',
age: 18,
}
console.log(Object.values(obj)) // [ 'lmw', 18 ]
console.log(Object.keys(obj)) // [ 'name', 'age' ]
console.log(Object.values('adsd')) // [ 'a', 'd', 's', 'd' ]
2、Object.entris
通过 Object.entries可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
let obj = {
name: 'lmw',
age: 18,
}
console.log(Object.entries(obj)) // [ [ 'name', 'lmw' ], [ 'age', 18 ] ]
// 如果传入的是数组那么会取下标 字符串也是
console.log(Object.entries(['aa', 'bb', 'cc']))
// [ [ '0', 'aa' ], [ '1', 'bb' ], [ '2', 'cc' ] ]
3、padStart、padEnd
const userName = 'lmw'
// 第一个参数是指填充完之后制字符的长度
// 第二个参数是填充的内容
const res = userName.padStart(10, '-')
const fina = res.padEnd(15, '*')
console.log(res) // -------lmw
console.log(fina) // -------lmw*****
4、Object.getOwnPropertyDescriptors
获取对象的所有属性描述符
let obj = {
age: 18,
}
console.log(Object.getOwnPropertyDescriptors(obj))
// {
// age: { value: 18, writable: true, enumerable: true, configurable: true }
// }
5、Aysnc、await
十二、ES9
1、对象的展开运算
const obj = {
name: 'lmw',
age: 18,
}
const { name, age } = { ...obj }
console.log(name, age)
2、Promise的finally
3、Async iterators 迭代器
十三、ES10
1、flat和flatMap
//flat 对数组进行降维 纯函数
let arr = [1, [1, 2, [33]], [22, [33, [44]]]]
console.log(arr.flat(4))
// Infinity 无穷大
// 传入Infinity,数组会被降成一维
arr.flat(Infinity)
// [
// 1, 1, 2, 33,
// 22, 33, 44
// ]
// flatMap 使用映射函数映射每个元素,然后将结果压缩成一个新数组,降维的深度是1
let arr = ['lmw and', 'www admin']
const res = arr.flatMap((item) => {
return item.split(' ')
})
console.log(res) // [ 'lmw', 'and', 'www', 'admin' ]
2、Object.fromEntries
方法把键值对列表转换为一个对象
let obj = {
name: 'lmw',
age: 18,
}
let res = Object.entries(obj)
console.log(res) //[ [ 'name', 'lmw' ], [ 'age', 18 ] ]
let bar = Object.fromEntries(res)
console.log(bar) // { name: 'lmw', age: 18 }
// 应用场景
let query = `name=lmw&age=18`
let url = new URLSearchParams(query)
console.log(url) // URLSearchParams { 'name' => 'lmw', 'age' => '18' }
let res = Object.fromEntries(url)
console.log(res) // { name: 'lmw', age: '18' }
十四、ES11
1、BigInt
const num = Number.MAX_SAFE_INTEGER // 获取最大安全数字
console.log(num) //9007199254740991 超过这个值做运算可能会出错
let bigInt = 9007199254740991111n
console.log(bigInt) // 9007199254740991111n
console.log(bigInt + 11n) // 9007199254740991122n
const n = 1000
console.log(bigInt + BigInt(n)) // 9007199254740992111n
2、??
let res = 0
let bar = res || '100'
console.log(bar) // 100 这是||(逻辑或)的弊端 0为false并且""也是false
// ??运算符解决了这个问题
bar = res ?? '100'
console.log(bar) // 0
3、可选链
let obj = {
name: 'lmw',
age: 18,
friend: {
name: 'jjh',
},
}
// 如果有就继续执行后面的,没有就返回undefined
console.log(obj.sex?.friend) // undefined
console.log(obj.friend?.name) // jjh
4、获取全局对象
console.log(globalThis)
5、for-in标准化
在ES11之前,虽然很多浏览器支持for.in来遍历对象类型,但是并没有被ECMA标准化。
在ES11中,对其进行了标准化,for.in是用于遍历对象的key的
十五、ES12 (2021)
1、监听对象销毁(一个类)
const finalRegistry = new FinalizationRegistry((key) => {
console.log(key, '被销毁')
})
let obj = {
name: 'lmw',
}
let foo = {
age: 18,
}
finalRegistry.register(obj, 'obj')
finalRegistry.register(foo, 'foo')
obj = null
foo = null
// foo 被销毁
// obj 被销毁
2、WeakRef(类)
// WeakRef对象允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被GC回收
let obj = {
name: 'lmw',
age: 18,
}
const foo = new FinalizationRegistry((key) => {
console.log(key, '被销毁')
})
foo.register(obj, 'obj对象')
// 如果使用 bar = obj 那么obj = null 时对象是不会被销毁的 因为还有一个bar指向这个对象(强引用)
// 如果使用WeakRef那么指向会变成弱引用,这时如果obj = null时,对象会被销毁
const bar = new WeakRef(obj)
// WeakRef.prototype.deref()
// 返回当前实例的WeakRef对象所绑定的target对象,如果该target对象已被GC回收则返回undefined
console.log(bar.deref()?.name) // lmw
obj = null
// obj对象被销毁
3、逻辑赋值运算
// 逻辑或赋值运算 ||
let bar = undefined
// bar = bar || 'lmw'
bar ||= 'lmw'
console.log(bar) // lmw
// 逻辑与赋值运算符(不常见) &&
let obj = {
age: 18,
}
// obj = obj && obj.age //18
obj &&= obj.age
console.log(obj) // 18
// 逻辑空赋值运算符 ??
// ?? 于 || 的区别在于 || 会把 ""和 0当成false
let foo = ''
foo ??= 'lmw'
console.log(foo) // ''
4、String.replaceAll
替换匹配到的所有字符串,是个纯函数(不会改变原来的值)
let str = 'axsbdxna'
let foo = str.replaceAll('a', 'e')
console.log(foo) // exsbdxne
console.log(str) // axsbdxna
5、数字分隔符
const bar = 111_222_333_444
console.log(bar) // 111222333444
十六、Proxy-Reflect
1、defineProperty的缺陷
1、Object.defineProperty设计的初衷,不是为了去监听截止个对象中所有的属性的、是为了定义属性描述符 2、如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object. defineproperty是无能为力的,所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。
2、Proxy、Reflect
let obj = {
_name: 'lmw',
get name() {
return this._name
},
set name(newValue) {
this._name = newValue
},
age: 18,
}
let foo = {
sex: '男',
}
const proxyObj = new Proxy(obj, {
/**
* 拦截对象获取值的操作
* 拦截 Reflect.get()
* @param { Object } target - 对象本身: obj
* @param { String|... } key - 对象的键
* @param { Object } receiver - 代理对象
*
*/
// receiver的作用:会把原对象 (obj) 中的 this 替换成代理对象 (proxyObj) 就是改变 this 指向
// Reflect参数
/**
* @param target 需要取值的目标对象
* @param propertyKey 需要获取的值的键值
* @param receiver 如果target对象中指定了getter,receiver则为getter调用时的this值。
*
*/
get: function (target, key, receiver) {
console.log('get被调用')
return Reflect.get(target, key, receiver)
},
/**
* 拦截对象的属性赋值的操作
* 拦截 Reflect.set()
* @param { any } newValue - 新的值
*/
set: function (target, key, newValue, receiver) {
console.log('set被调用')
Reflect.set(target, key, newValue, receiver)
},
// 拦截in操作
// 拦截Reflect.has() 方法
has: function (target, key) {
console.log('in方法被调用')
return Reflect.has(target, key)
},
// 拦截delete操作
// 拦截Reflect.deleteProperty() 方法
deleteProperty: function (target, key) {
console.log('拦截到delete操作')
// delete target[key]
Reflect.deleteProperty(target, key)
},
// 拦截 Object.getPrototypeOf() 方法 返回指定对象原型
// 拦截 Reflect.getPrototypeOf() 方法 返回指定对象的原型
// 拦截 对象.__proto__ 属性 已废除,不建议使用
// 拦截 Object.prototype.isPrototypeOf() 方法 用于测试一个对象是否存在于另一个对象的原型链上
// 拦截 instanceof 运算符 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
getPrototypeOf: function (target) {
console.log('获取原型对象 target.__proto__')
return Object.getPrototypeOf(target)
},
/**
*
* 拦截 Object.setPrototypeOf() 方法 设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象
* 拦截 Reflect.setPrototypeOf() 和上面一样
* @param { any } proto - 要赋值的原型
* @returns {Boolean}
*/
setPrototypeOf: function (target, proto) {
console.log('拦截到setPrototypeOf')
Object.setPrototypeOf(target, proto)
return true
},
// 拦截 Object.isExtensible() 方法 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)
// 拦截 Reflect.isExtensible() 方法 判断一个对象是否可扩展 (具体区别等阅读详细资料再进行拓展)
// Object.isExtensible(proxy) 必须同Object.isExtensible(target)返回相同值。
// 也就是必须返回true或者为true的值, 返回false和为false的值都会报错。
isExtensible: function (target) {
console.log('拦截到isExtensible')
return true
},
// 拦截 Object.preventExtensions() 方法 让一个对象变的不可扩展,也就是永远不能再添加新的属性
// 拦截 Reflect.preventExtensions() 方法 阻止新属性添加到对象 (具体区别等阅读详细资料再进行拓展)
// 如果目标对象是可扩展的,那么只能返回 false
preventExtensions: function (target) {
console.log('拦截到preventExtensions')
Object.preventExtensions(target)
return true
},
/**
* 拦截 Object.getOwnPropertyDescriptor() 方法 返回指定对象上一个自有属性对应的属性描述符 (自有属性就是排除原型链的属性)
* 拦截 Reflect.getOwnPropertyDescriptor() 方法 返回指定对象上一个自有属性对应的属性描述符
* @param { String|... } prop - 对象的属性名
* @returns { Object }
*/
getOwnPropertyDescriptor: function (target, prop) {
console.log('prop', prop) // name
console.log('拦截到getOwnPropertyDescriptor')
return Object.getOwnPropertyDescriptor(target, prop)
},
/**
* 拦截 Object.defineProperty() 方法 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
* 拦截 Reflect.defineProperty() 方法 基本等同于 Object.defineProperty() 方法,唯一不同是返回 Boolean 值
* 拦截 proxy.property='value'
* @param { String|... } prop - 待检索其描述的属性名
* @param { Object } descriptor - 待定义或修改的属性的描述符
* @returns {Boolean}
*/
defineProperty: function (target, prop, descriptor) {
console.log('拦截到defineProperty')
console.log(prop) // sno
console.log(descriptor) // { value: 10, enumerable: true, configurable: true }
return true
},
// 拦截 Reflect.ownKeys() 方法 以数组的类型返回所有键,包括Symbol和不可枚举类型
// 拦截 Object.getOwnPropertySymbols() 方法 以数组的类型返回Symbol类型的key
// 拦截 Object.getOwnPropertyNames() 以数组的类型返回所有键(包括不可枚举类型,但是还是不包括Symbol类型)
// 拦截 Object.keys() 方法 以数组的类型返回所有键(不包括不可枚举的类型以及Symbol类型)
// 返回一个可枚举的对象
ownKeys: function (target) {
console.log('拦截到ownKeys')
return Reflect.ownKeys(target)
},
/**
* 拦截 Function.prototype.apply() 和 Function.prototype.call()
* 拦截 Reflect.apply()
* @param { Function } target - 函数本身
* @param { Object } thisArg - 被调用时的上下文对象
* @param { Array } argumentsList - 被调用时的参数数组
* @returns {any}
*/
apply: function (target, thisArg, argumentsList) {
Reflect.apply(target, thisArg, argumentsList)
},
/**
* 拦截 Reflect.construct()
* 拦截new 操作符
* @param { any } target - 目标对象
* @param { any } argumentsList - constructor的参数列表
* @param { Function } newTarget - 最初被调用的构造函数
* @returns {Object}
*/
construct: function (target, argumentsList, newTarget) {
Reflect.construct(target, argumentsList, newTarget)
return {}
},
})
console.log(proxyObj.name) // lmw
proxyObj.age = 17
console.log(proxyObj.age) // 17
console.log(obj.age) // 17
console.log('name' in proxyObj) // true
delete proxyObj.name
console.log(proxyObj) // { age: 18 }
// getPrototypeOf 返回指定对象的原型(内部[[Prototype]]属性的值)。
console.log(Object.getPrototypeOf(proxyObj))
// setPrototypeOf 设置一个指定的对象的原型 (即, 内部[[Prototype]]属性)到另一个对象
Object.setPrototypeOf(proxyObj, foo)
// isExtensible() 方法判断一个对象是否是可扩展的 (是否可以在它上面添加新的属性)
console.log(Object.isExtensible(proxyObj)) // true
// Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性
Object.preventExtensions(proxyObj)
proxyObj.sex = 'ss'
console.log(proxyObj.sex) // undefined
// Object.getOwnPropertyDescriptor() 方法返回指定对象上的一个自有属性对应的属性描述符。
//(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
console.log(Object.getOwnPropertyDescriptor(proxyObj, 'name'))
//{ value: 'lmw', writable: true, enumerable: true, configurable: true }
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty(proxyObj, 'sno', {
configurable: true,
enumerable: true,
value: 10,
})
// Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组 (所有键都可以返回)。
// 它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。
// Object.getOwnPropertySymbols() (对象Symbol类型的键 并且组成数组返回)
// Object.keys() (不包括不可枚举的类型以及Symbol类型的键组成数组返回)
// Object.getOwnPropertyNames() (包括不可枚举类型,但是还是不包括Symbol类型的键组成数组返回)
console.log(Reflect.ownKeys(proxyObj)) // [ 'name', 'age' ]
3、Reflect的construct()
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
class Hero {}
// 如果想用Person类, 但是又想创建出来对象是 Hero 类型的
const hero = Reflect.construct(Person, ['lmw', 18], Hero)
console.log(hero) // Hero { name: 'lmw', age: 18 }
十七、Promise 类
then、 catch、 finally方法都属于 Promise的实例方法,都是存放在 Promise的 prototype上的
1、状态
new Promise((resolve, reject) => {
console.log('pending') // pending状态
resolve('111') // resolved 状态
reject('000') // rejected 状态
}).then(
(res) => {
console.log(res)
},
(err) => {
console.log(err)
}
)
2、resolve 其他参数问题
1、如果resolve中传入的是一个 Promise 那么进入成功还是失败的回调会由传入的 Promies 决定
let p = new Promise((resolve, reject) => {
// resolve('由这个决定是进入resolve还是reject')
reject('还是会进入reject')
})
/**
* 如果resolve中传入的是一个 Promise 那么进入成功还是失败的回调会由传入的 Promies 决定
*/
new Promise((resolve, reject) => {
resolve(p)
}).then(
(res) => {
console.log('resolve')
console.log(res)
},
(err) => {
console.log('reject')
console.log(err)
}
)
2、如果传入的是一个对象,并且这个对象有实现then方法,那么会执行这个then方法,并且后续的状态也会由这个then方法来决定
new Promise((resolve, reject) => {
let obj = {
then: function (resolve, reject) {
resolve('进入resolve') // 后续会进入resolve
// reject('进入reject') // 后续会进入reject
},
}
resolve(obj)
}).then(
(res) => {
console.log('resolve')
console.log(res)
},
(err) => {
console.log('reject')
console.log(err)
}
)
3、then特性
1、同一个 promise可以被多次调用then方法,当resolve方法被回调时,所有的then方法传入的回调函数部会被调用
let promise = new Promise((resolve, reject) => {
resolve('aaa')
})
promise.then((res) => {
console.log(res, '1')
})
promise.then((res) => {
console.log(res, '2')
})
promise.then((res) => {
console.log(res, '3')
})
2、then方法传入的回调函数可以有返回值,如果我们返回的是一个普通值,那么这个普通的值被作为一个新的 Promise 的resolve值,这个新的Promise会当成then函数的返回值进行返回
promise
.then(() => {
return '111'
})
.then((res) => {
console.log(res) // 111
})
// 等同于
promise
.then(() => {
return new Promise((resolve, reject) => {
resolve('111')
})
})
.then((res) => {
console.log(res) // 111
})
// 如果返回的还是一实现了then方法的对象那么也是由这个对象来决定Prmoise后续的状态
promise
.then(() => {
return {
then: function (resolve, reject) {
resolve('admin')
},
}
})
.then((res) => {
console.log(res) // admin
})
4、catch操作
catch返回的也是一个Promise,和then是一样的
let promise = new Promise((resolve, reject) => {
// reject('111')
throw Error('zczxc')
})
// 1、可以使用.catch来捕获reject或者抛出的异常
promise
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
console.log('-------')
})
// 当然在then里面也是可以捕获异常的
promise.then(
(res) => {
console.log(res)
},
(err) => {
console.log(err)
console.log('-------')
}
)
// 2、catch是有传递性的
let promise1 = new Promise((resolve, reject) => {
resolve('aaa')
})
promise1
.then((res) => {
return new Promise((resolve, reject) => {
reject(222)
})
})
.catch((err) => {
// 如果 promise1并没有异常,但是在then中返回的 Promise 有异常那么 catch 也会进行捕获
console.log(err) // 222
})
5、finally操作(ES9)
不论返回的是成功状态还是失败状态,finally都是会执行的,无参数,返回的也是一个Promise
let promise = new Promise((resolve, reject) => {
// resolve('111')
reject('222')
})
promise
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
console.log('finally')
})
6、Promise.resolve()
1、参数是一个普通的值或者对象
2、参数本身是 Promise
3、参数是一个 tenable
参数的三种情况都是和then一样的
let obj = {
name: 'lmw',
}
// 如果想吧obj转换成Promise形式就可以使用Promise.resolve方法
let objPromise = Promise.resolve(obj)
objPromise.then((res) => {
console.log(res) // { name: 'lmw' }
})
7、Promise.reject()
注意reject的参数不分情况,不论传入的是什么类型的都会给err
let obj = {
name: 'lmw',
}
let objReject = Promise.reject(obj)
objReject.catch((err) => {
console.log(err) // { name: 'lmw' }
})
8、Promise.all()
当传入的所有Promise的状态都是resolved的时候才回执行后续代码,有一个错误都会执行catch或者error回调
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('111')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('222')
}, 2000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('333')
}, 3000)
})
// 如果三个Promise中有一个变成了reject状态那么就不会继续往下执行,直接返回这个reject
Promise.all([p3, p1, p2, 'aaa'])
.then((res) => {
console.log(res) // [ '333', '111', '222', 'aaa' ]
})
.catch((err) => {
console.log(err)
})
9、Promise.allSettled() (ES11)
不论中间是否失败都会继续执行,会返回一个带有状态的结果数组
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('111')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('222')
}, 2000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('333')
}, 3000)
})
Promise.allSettled([p3, p1, p2, 'aaa']).then((res) => {
console.log(res)
// [
// ({ status: 'fulfilled', value: '333' },
// { status: 'fulfilled', value: '111' },
// { status: 'rejected', reason: '222' },
// { status: 'fulfilled', value: 'aaa' })
// ]
})
10、Promise.race()
当传入的Promise的数组中有一个变成执行完毕 就会结束
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('111')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('222')
}, 2000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('333')
}, 3000)
})
Promise.race([p3, p1, p2])
.then((res) => {
console.log(res) // 111
})
.catch((err) => {
console.log(err)
})
11、Promise.any() (ES12)
any方法会等到一个fulfilled状态,オ会決定新 Promise的状态
如果所有的 Promise都是 reject的,那么也会等到所有的 Promise都变成 rejected状态;
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('111')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('222')
}, 500)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('333')
}, 3000)
})
Promise.any([p3, p1, p2])
.then((res) => {
console.log(res)
})
.catch((err) => { // 如果全是reject那么才会进入catch里面
console.log(err) // [AggregateError: All promises were rejected]
console.log(err.errors) // [ '333', '111', '222' ]
})
12、Promise实现
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'
function useTryCatch(fun, value, resolve, reject) { // 用于处理then有返回值的情况
try {
const res = fun(value)
if (res instanceof lmwPromise) { // 为了确定then返回的是不是new lmwPromise
res.then(
(res) => {
resolve(res)
},
(err) => {
reject(err)
}
)
} else {
if(res)resolve(res)
}
} catch (err) {
reject(err)
}
}
class lmwPromise {
constructor(executor) {
this.value = undefined
this.reason = undefined
this.status = PROMISE_STATUS_PENDING
this.onFulfilledFns = []
this.onRejected = []
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
queueMicrotask(() => {
// 为什么要进行判断?
// 如果在new lmwPromise中同时使用了resolve和reject那么resolve和reject都会被执行,也就是说都会被加到微任务队列里面
// 例如 let promise = new lmwPromise((resolve,reje)=>{
// resolve('111')
// reject('222')
// })
// 那么这两个都会被执行
if (this.status !== PROMISE_STATUS_PENDING) return
this.status = PROMISE_STATUS_FULFILLED
this.value = value
this.onFulfilledFns.forEach((fn) => {
fn()
})
})
}
}
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
queueMicrotask(() => {
if (this.status !== PROMISE_STATUS_PENDING) return
this.status = PROMISE_STATUS_REJECTED
this.reason = reason
this.onRejected.forEach((fn) => {
fn()
})
})
}
}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
//为了catch
// 因为当我们.then之后再掉用catch,那么这么catch是在catch .then的返回值,并不是在catch当前的promise
onRejected =
onRejected ??
((error) => {
throw error
})
onFulfilled =
onFulfilled ??
((res) => {
return res
}) // 为了finally
return new lmwPromise((resolve, reject) => {
// 如果在then调用的时候已经确定了是FULFILLED状态
// 例如在主线中已经执行了promise.then,但是在settimeout中我又调用了promise.then那么在settimeout中执行then方法时promise的状态已经被 确定了。
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
useTryCatch(onFulfilled, this.value, resolve, reject)
}
// 如果在then调用的时候已经确定了是REJECTED状态
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
useTryCatch(onRejected, this.reason, resolve, reject)
}
if (this.status === PROMISE_STATUS_PENDING) {
if (onFulfilled) {
this.onFulfilledFns.push(() => {
useTryCatch(onFulfilled, this.value, resolve, reject)
})
}
if (onRejected) {
this.onRejected.push(() => {
useTryCatch(onRejected, this.reason, resolve, reject)
})
}
}
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
finally(onFinally) {
return this.then(
() => {
onFinally()
},
() => {
onFinally()
}
)
}
static all(prmoises) {
// 全部执行完毕或者其中一个走reject
let values = []
return new lmwPromise((resolve, reject) => {
prmoises.forEach((promise) => {
promise.then(
(res) => {
values.push(res)
if (values.length === prmoises.length) {
resolve(values)
}
},
(err) => {
reject(err)
}
)
})
})
}
static allSettled(promises) {
// 全部执行完毕 不论走resolve还是reject
let values = []
return new lmwPromise((resolve, reject) => {
promises.forEach((promise) => {
promise.then(
(res) => {
values.push({ status: 'fulfilled', value: res })
if (values.length === promises.length) {
resolve(values)
}
},
(err) => {
values.push({ status: 'rejected', reason: err })
if (values.length === promises.length) {
resolve(values)
}
}
)
})
})
}
static resolve(value) {
return new lmwPromise((resolve) => {
resolve(value)
})
}
static reject(reason) {
return new lmwPromise((resolve, reject) => {
reject(reason)
})
}
static any(promises) {
// 有一个执行了resolve就立马结束 如果传入的promise执行的都是reject才会执行reject
let reason = []
return new lmwPromise((resolve, reject) => {
promises.forEach((promise) => {
promise.then(
(res) => {
resolve(res)
},
(err) => {
reason.push(err)
if (reason.length === promises.length) {
reject(new AggregateError(reason))
}
}
)
})
})
}
static reace(promises) {
// 那个先执行完就返回那个 不论reject还是resolve
return new lmwPromise((resolve, reject) => {
promises.forEach((promise) => {
promise.then(
(res) => {
resolve(res)
},
(err) => {
reject(err)
}
)
})
})
}
}
十八、迭代器
概念:迭代器是帮助我们对某个数据结构进行遍历的对象
1、在js中需要实现一个特定的方法next()
一个无参数或一个参数的函数,返回个应当拥有以下两个属性的对象
1、done (boolean) 如果迭代器可以产生序列中的下个值,则为 false。(这等价于没有指定done这个属性。) 如果迭代器已将序列迭代完毕,则为true。这种情况下, value是可选的,如果它依然存在,即为迭代结東之后的默认返回值。 2、value 迭代器返回的任何 Javascript值。done为true时可省略。
2、生成迭代器的函数
let names = ['lmw', 'ccc', 'ddd']
function iterator() {
let index = 0
return {
next: () => {
if (names.length > index) {
return {
done: false,
value: names[index++],
}
} else {
return {
done: true,
value: undefined,
}
}
},
}
}
const obj = iterator()
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())
// { done: false, value: 'lmw' }
// { done: false, value: 'ccc' }
// { done: false, value: 'ddd' }
// { done: true, value: undefined }
3、可迭代对象
let iterator = {
names: ['lmw', 'ccc', 'ddd'],
[Symbol.iterator]: function () {
let index = 0
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names[index++] }
} else {
return { done: true, value: undefined }
}
},
}
},
}
// 普通的对象是不能使用for of方法的
for (const item of iterator) {
console.log(item)
}
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的
如:String、Array、Map、Set、 arguments对象、 Nodelist集合;
4、类的迭代器
class Room {
constructor(name, rno, person) {
this.name = name
this.rno = rno
this.person = person
}
addPerson(person) {
this.person.push(person)
}
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.person.length) {
return {
done: false,
value: this.person[index++],
}
} else {
return {
done: true,
value: undefined,
}
}
},
return: () => {
// 可以监听提前停掉的迭代
console.log('提前停止')
return { done: true, value: undefined }
},
}
}
}
let r = new Room('aaa', '2111', [1, 2, 3, 4, 'lmw', 5, 6])
for (const item of r) {
console.log(item)
if (item === 'lmw') break
}
十九、生成器
1、next传递参数
function* foo() {
console.log(111)
const num = yield 100
console.log(num) //2
yield
}
const generator = foo()
console.log(generator.next()) // { value: 100, done: false }
generator.next(2) // 会把这个2传入到上一个yield的返回值
2、return()
function* foo() {
console.log(111) // 不执行
const num = yield 100
console.log(num) // 不执行
yield
}
const generator = foo()
console.log(generator.return(2)) // { value: 2, done: true }
generator.next(3) // 不会再执行
3、抛出异常
function* foo() {
let num
try {
// 如果没进行捕获那么不会执行下面代码
num = yield 100
} catch (error) {
console.log(error) // err message
num = yield 100
}
console.log(num) // 345
yield
}
const generator = foo()
generator.next()
generator.throw('err message')
generator.next(345)
4、引出async/await
function getUrl(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url)
}, 1000)
})
}
// 1、一直调then
getUrl('lmw')
.then((res) => {
return getUrl(res + 'aaa')
})
.then((res) => {
return getUrl(res + 'bbb')
})
.then((res) => {
console.log(res)
})
// 2、使用生成器
function* generatorGeturl() {
let url1 = yield getUrl('aaa')
let url2 = yield getUrl(url1 + 'bbb')
let url3 = yield getUrl(url2 + 'ccc')
console.log(url3)
}
// 调用生成器
const generator = generatorGeturl()
generator.next().value.then((res) => {
generator.next(res).value.then((res) => {
generator.next(res).value.then((res) => {
console.log(res) // aaabbbccc
})
})
})
// 封装一个函数递归调用生成器函数
function exeGenerator(geFun) {
let generate = geFun()
function exe(res) {
let result = generate.next(res)
if (result.done) {
return result.value
}
result.value.then((res) => {
exe(res)
})
}
exe()
}
exeGenerator(generatorGeturl)
// co库替代自己写的封装函数
const co = require(co)
co(generatorGeturl)
// 3、ES8之后出现了async/await
async function url() {
let url1 = await getUrl('aaa')
let url2 = await getUrl(url1 + 'bbb')
let url3 = await getUrl(url2 + 'ccc')
console.log(url3)
}
url()
二十、async/await
1、概念
async关键字用于声明一个异步函数
2、执行顺序
async function foo() {
console.log(222)
console.log(333)
}
console.log(111)
foo()
console.log(444)
// 111
// 222
// 333
// 444
// 异步代码在没做什么特殊操作的时候执行的顺序是和同步代码没什么区别的
3、和普通函数的区别
1、async函数的返回值是一个Promise
async function foo() {
return 1 // 返回普通数据会转换成promise的resolve
}
foo().then((res) => {
console.log(res) // 1
})
async function foo() {
return new Promise((resolve, reject) => { // 返回Promise
resolve(1)
})
}
foo().then((res) => {
console.log(res) // 1
})
async function foo() {
return { // 返回一个实现了thenable的对象
then: function (resolve, reject) {
resolve(1)
},
}
}
foo().then((res) => {
console.log(res)
})
2、用new Throw抛出异常并且在reject中捕获
async function foo() {
throw new Error('错误')
}
foo().then(
(res) => {},
(err) => {
console.log(err)
}
)
4、await
// 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用
const promise = new Promise((resolve, reject) => {
resolve('lmw')
})
async function foo() {
const res = await promise
console.log('结束', res) // 结束 lmw
}
foo()
二十一、javaScript中的线程
1、进程和线程的概念
进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
线程:每个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程; 所以我们也可以说进程是线程的容器
2、浏览器的进程
目前多数的浏览器其实都是多进程的,当我们打开个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出; 每个进程中又有很多的线程,其中包括执行 Javascript代码的线程;
3、javaScript的线程
javaScript是单线程的,但是 javaScript 的线程应该有自己的容器进程:浏览器或者Node
javaScript的代码执行是在一个单独的线程中执行的,这就意味着 javaScript 的代码,在同一个时刻只能做一件事如果这件事是非常耗时的,就意味着当前的线程就会被阻塞
所以真正耗时的操作,实际上并不是由 javascript线程在执行的 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作,比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可
二十二、js中EventLoop(事件循环机制)
js是一门单线程语言,它的异步和多线程的实现是由事件循环机制来实现
EventLoop由三个部分组成
1、调用栈 (call stack)
2、消息队列 (Message Queue)
3、微任务队列 (Microtack Queue)
4、宏任务队列
函数在执行的时候会被压入调用栈中,被压入的函数叫做帧 (Frame) , 执行完函数中的代码才会被弹出栈
宏任务
| # | 浏览器 | Node |
|---|---|---|
| I/O | ✅ | ✅ |
| setTimeout | ✅ | ✅ |
| setInterval | ✅ | ✅ |
| setImmediate | ❌ | ✅ |
| requestAnimationFrame | ✅ | ❌ |
| Ajax | ✅ | ✅ |
| DOM监听 | ✅ | ❌ |
微任务
| # | 浏览器 | Node |
|---|---|---|
| process.nextTick(优先执行) | ❌ | ✅ |
| MutationObserver | ✅ | ❌ |
| Promise.then catch finally | ✅ | ✅ |
| queueMicrotask | ✅ | ✅ |
只有主函数执行完后才会开始执行微任务,然后再是宏任务
二十三、Node中的事件循环
Node中的事件循环主要靠LIBUV实现的
事件循环像是一个桥梁,是连接着应用程序的 javascript和系统调用之间的通道
无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函 数放到事件循环(任务队列)中 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
但是一次完整的事件循环Tick分成很多个阶段
1、定时器( Timers):本阶段执行已经被 settimeout ( ) 和 setinterval ( ) 的调度回调函数。 2、待定回调( Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到 ECONNREFUSED。 3、idle, prepare:仅系统内部使用。 4、轮询(Poll):检索新的I/O事件;执行与I/O相关的回调 5、检测(check); setimmediate( ) 回调函数在这里执行。 6、关闭的回调函数:一些关闭的回调函数,如: socket.on( close',...)
执行队列的顺序
1、ticks队列(process.nextTick)
2、微任务队列 (promise.then queueMicrotask)
3、timer队列(setTimeout/setInterval)
4、io队列
5、setImmediate队列 (setImmediate)
6、close队列
为什么有的时候setImmediate会比setTimeout先执行?
因为setTimeout在执行的时候会把里面的回调函数插入到一个树结构中,之后再把这个回调函数加入到调用栈中,但是这两步操作时需要时间的,我们比如这个时间需要10m s(时间是不固定的),当我们的mian script(主线程)执行完毕后会初始化事件循环,这个初始化也是需要时间的,这里假设初始化的时间是5ms(时间是不固定的),那么在第一次tick的时候setTimeout里面的回调函数还没被放入到调用栈,这样这个回调函数是不会被执行的,但是setImmediate中的回调函数是没有插入到树结构再到调用栈这一步操作的,他是会立马被放到调用栈中执行。等到第二次tick的时候setTimeout中的回调函数才会被执行。
三十四、ESModule的解析过程
阶段一:构建( Construction)根据地址查找 js 文件,井下载,将其解析成模块记录( Module Record); 阶段二:实例化( Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。 阶段三:运行( Evaluation),运行代码,计算值,并且将值填充到内存地址中
三十五、npm
1、npm install执行过程
检测是有 package-lock.json 文件:
1、没有lock文件
分析依赖关系,这是因为要install的包可会依赖其他的包,并且多个包之间会产生相同依赖的情况;
从 registry仓库中下载压缩包(如果我们设置了镜像,那么会从镜像服务器下载压缩包) ;
获取到压缩包后会对压缩包进行缓存(从npm5开始有的);
将压缩包解压到项目的node_ modules文件夹中
2、有lock文件 检测 lock 中包的版本是否和 package.json 中一致(会按照 server版本规范检测); 不ー致,那么会重新构建依赖关系,直接会走顶层的流程; 一致的情况下,会去优先查找缓存 没有找到,会从 registry仓库下载,直接走顶层流程; 查找到,会获取缓存中的压缩文件,并且将压缩文件解压到 node_modules 文件夹中
2、npx
npx是npm5.2之后自带的一个命令
npx的作用非常多,但是比较常见的是使用它来调用项目中的某个模块的指令。
三十六、模块化规范
1、AMD规范
(1)介绍
AMD主要是应用于浏览器的一种模块化规范:
AMD是Asynchronous Module Definition(异步模块定义)的缩写;
它采用的是异步加载模块:
事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
(2)使用
AMD实现的比较常用的库是require.js和curl.js;
第一步:下载require.js 下载地址:qithub.com/requirejs/r… 找到其中的require.js文件;
第二部:使用
在index.html中加入如下代码
// data-main的作用是在加载完src中的代码后立刻执行data-main中的代码
<script src="./lib/require.js" data-main="./index.js"></script>
index.js
(function () {
require.config({
baseUrl: '',
// 注意不能加后缀
paths: { bar: './bar', foo: './foo.js' },
})
// 第一个参数是要加载的模块,加载了会自动执行模块中的代码
// 第二个参数是要对模块进行的操作
require(['foo'], function (foo) {})
})()
bar.js
define(function () {
const name = 'lmw'
const age = 18
const todo = function () {
console.log(1111)
}
return {
name,
age,
todo,
}
})
foo.js
define(['bar'], function (bar) {
console.log(bar.name)
console.log(bar.age)
console.log(bar.todo())
})
浏览器控制台打印
lmw
18
1111
2、CMD规范
CMD也是异步的
CMD常用的库为sea.js 在github上也可以下载
html中
<script src="./lib/sea.js"></script>
<script>
seajs.use('./index.js')
</script>
foo.js
define(function (require, exports, module) {
const name = 'lmw'
const age = 20
const todo = function () {
console.log(2222)
}
module.exports = {
name,
age,
todo,
}
})
Index.js
define(function (require, exports, module) {
const foo = require('./foo')
console.log(foo.name)
console.log(foo.age)
console.log(foo.todo())
})
3、commomJs规范
commonJs规范最常见的是在node.js中
注意:node中的commonJs的加载是同步的!!!
加载时(require)会自动执行加载模块中的代码
导出方法
1、exports
bar.js
let bar = 222
setTimeout(() => {
bar = 111
}, 300)
exports.bar = bar
2、module.exports
foo.js
function dosth() {
console.log('doSth')
}
module.expotrs = {
dosth,
}
导入方法
main.js
let bar = require('./bar.js')
let foo = require('./foo')
console.log(bar) // 222
foo.dosth() // doSth
其实node.js的实现中看似有两种导出方式,其实内部实现中都是由module.exports来导出的。内部做了module.exports = exports这步操作。
在node.js实现中导出的是一个exports对象,对象里面包裹了要导出的值,我们在导入的时候也是在拿exports对象,所以当导出的值不是引用类型的时候我们修改他的值是不会影响原来的值的例如:
bar.js
let bar = 222
setTimeout(() => {
console.log(bar) // 222
}, 300)
exports.bar = bar
main.js
let bar = require('./bar.js')
setTimeout(() => {
bar = 333
}, 200)
即便我在main.js中修改了bar的值也不会影响原来的值
但是当我们导出的值是引用类型的时候我们在导入的地方修改是会影响到导出模块中的值的。例如:
bar.js
let info = {
mes: 'lmw',
}
setTimeout(() => {
console.log(info.mes) // ccc
}, 300)
exports.info = info
main.js
let bar = require('./bar.js')
bar.info.mes = 'ccc'
可以简单的理解为node.js实现commonJs为导出的值做了一层浅拷贝
4、EsModule
EsModule是在ES6(ES2015)中才出现的。
EsModule的是静态分析 ,模块导入也是异步的,加载时(import)会自动执行加载模块中的代码。
导出:
module1.js
let info = 'lmw'
let baz = 'hahaha'
export let age = 18
export { info }
// 取别名导出
export { info as i } //导入的时候用
// import { i } from './module1.js'
export default { // 采用此导出方式在导入的时候可以以任意名字导入,但是这种方法在一个模块中只能使用一次
baz,
}
导入:
main.js
import bar, { info, age } from './module1.js'
console.log(info) // lmw
console.log(age) // 18
console.log(bar) // {baz: 'hahaha'}
// 导入并取别名
import bar, { info as i, age as a } from './module1.js'
console.log(i) // lmw
console.log(a) // 18
console.log(bar) // {baz: 'hahaha'}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./main.js" type="module"></script>
</head>
<body></body>
</html>
EsModule是不支持file协议的所以打开html文件必须启动一个服务,不能直接本地文件打开。
EsModule在被js引擎解析的时候就必须要知道模块之间的依赖关系(注意是解析parse(词法分析语法分析),还没到执行代码),所以我们是不能在代码逻辑当中使用import进行导入的。例如:
let flag = true
if (flag) {
import bar from './module1.js'
} else {
import bar from './module2.js'
}
// 这样写是不被允许的
如果我们真的想使用上面的写法那么可以用import函数(import函数是异步加载,返回promise)
let flag = true
if (flag) {
import('./module1.js')
.then((res) => {
console.log(res.info) // lmw
console.log(res.age) // 18
console.log(res.default) // {baz: 'hahaha'}
})
.catch((err) => {
console.log(err)
})
} else {
import('./module2.js')
}
重点:
为什么修改导出的非引用类型的值的时候会报错Uncaught TypeError: Assignment to constant variable,例如:
bar.js
let info = 'lmw'
export {
info
}
main.js
import { info } from './bar.js'
info = 'lcl'
// 这个时候就会报错 Uncaught TypeError: Assignment to constant variable
为什么会出现这个问题?这个就涉及到了内部对模块解析的过程
如图所示,js在解析的时候开辟一个叫模块环境记录的空间,这个空间会做一个实时绑定的操作,当我们bar.js中的name修改时,这个空间会重新声明const info = info所以当我们在bar.js中修改值的时候,在main.js中拿值会是最新的值,但是我们在main.js中修改info的值,由于info是由const定义的,所以是不可以被修改的。
三十七、防抖与节流
1、防抖
当持续触发事件一定时间内没有再触发事件事件处理函数才会执行一次如果设定的时间到来之前又一次触发了事件就重新开始延时
/**
* @param {Function} fn 要防抖的函数
* @param {Number} time 防抖时间
* @param {Boolean} now 是否立即执行传入的函数
* @param {Function} callback 接收传入函数的返回值的回调
* @returns
*/
function antiShake(fn, time = 1000, now = false, callback) {
//防抖
let timer
let isNow = false
let debounce = function (...args) {
if (timer) clearTimeout(timer)
if (!isNow && now) {
const res = fn.apply(this, args)
if (res) {
callback(res)
}
isNow = true
} else {
timer = setTimeout(() => {
const res = fn.apply(this, args)
if (res) {
callback(res)
}
isNow = false
}, time)
}
}
debounce.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
isNow = false
}
return debounce
}
2、节流
当持续触发事件的时候保证一段时间内 只调用一次事件处理函数一段时间内 只做一件事情
/**
*
* @param {Function} fn 要节流的函数
* @param {Number} time 节流的时间
* @param {Object} options leading: 是否立即执行,trailing: 如果时间没到但是还有事件触发是否执行, resultCallBack: 用于拿到传入的函数返回值
* @returns
*/
function throttle(fn, time, options = { leading: true, trailing: false }) {
// 上一次的开始时间
let lastTime = 0
let timer = null
let { leading, trailing, resultCallBack } = options
let _throttle = function (...arg) {
// 事件触发时的时间
let nowTime = new Date().getTime()
if (!leading && !lastTime) {
lastTime = nowTime
}
// 计算还剩多少时间出发函数
const remainTime = time - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
const res = fn.apply(this, ...arg)
if (resultCallBack) resultCallBack(res)
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
lastTime = !leading ? 0 : new Date().getTime()
const res = fn.apply(this, ...arg)
if (resultCallBack) resultCallBack(res)
}, remainTime)
}
}
// 用于取消后续的执行
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
加我微信:qcsjin888,邀你进群,一起学习前端,成为更优秀的工程师~,毕竟我也是有趣的前端,认识我也不赖🌟~