前置知识
JS中动态加载JS脚本
1 | let script = document.createElement('script') |
Rest Parameters
define a function like function sum(...nums){}
invoke this function: sum(1, 2, 3)
,
nums will be a list: [1, 2, 3]
spread
operator
1 | var nums = [1, 2, 3] |
when invoking function, using sum(…nums) equals sum(1, 2, 3)
Default parameter value
like python.
NT_1_V8 JS Engine And JS Scope
流程
- Stream: 编码转换
- Scanner: 词法分析
- Preparse: 将不必要的函数(如Outer func内没用到的inner func)预解析。LazyParsing。
- Parser模块转换为AST(抽象语法树)https://astexplorer.net/
- Ignition解释器将AST解释成字节码(如果函数只执行一次的话),并收集关键信息用于TurbleFan优化器
- TurbleFan编译器将字节码编译成优化后的机器码(,并缓存下来 ?),用于多次执行相同函数。这样可以节省Ignite过程耗费的资源。当函数参数类型变化了,就会Deoptimation,逆向转换为字节码。
GO
GO,AO都是VO
代码解析(Parse)阶段,Parser会产生GlobalObject(GO)对象,存放类、函数、window(指向自己)、属性(没使用的话值将为undefined)
ECStack、GEC、FEC
ECStack是上下文栈!GEC,FEC上下文按照调用顺序都存在这里。
执行上下文
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文
编译阶段
遇到变量: 加入GO并赋值为undefined;
遇到函数:开辟空间,空间有parent scope、函数执行体,然后将空间地址加入GO。
执行阶段
从全局域开始自上而下执行。
创建全局执行上下文GEC,遇到调用函数就创建函数执行上下文FEC。
执行上下文会分别创建VO,建立scope chain,以及确定this指向。
全局执行上下文GEC
- 执行代码:赋值到VO(GO)、执行代码
函数执行上下文FEC
2. 执行代码:赋值到VO(AO),沿着作用域链找、执行代码
3. 执行完毕:FEC从ECS中销毁
变量对象的创建
创建VO的过程发生在进入执行上下文的过程,也就是编译阶段。
- 建立arguments对象
- 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
- 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
VO与AO
变量对象与活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。
不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。(这句话对吗??对的,只有在执行该函数的时候才会创建AO对象)
注意,函数A内的函数B在全局初次解析的时候只会做预解析,只有当函数A被执行时才会正式解析函数B(在堆中开辟空间)。
ES6新特性
ES6下,使用Variable Environment概念而不单单是VO。
也说明可以向上兼容。
作用域链
FEC中有VO(AO)之外,还有scope chain。
scope chain = 当前上下文的VO + parent scope(这个在编译阶段就被确定了。)
作用域链本质上是一个指向当前环境与上层环境的一系列变量对象的指针列表(它只引用但不实际包含变量对象),作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。
按照scope chain来进行逐层查找
在非全局域内定义属性而不使用标识符(let\var)V8会默认将其直接加入/更新到GO中
在非全局域内定义属性使用var a=b=10,相当于var a=10;b=10
this
this在代码执行的时候才被绑定。
在node中,全局下的this是{}
在浏览器中,全局下的this是GO
绑定规则
- 默认绑定:直接调用函数,函数内的this绑定为GO。**只要是xxx()**,不管在哪里定义,不管函数在哪,不管这个函数是不是被另一个函数作为返回值返回,其内部this都绑定为GO
- 隐式绑定:通过一个对象去调用这个函数。如obj.xxx(),那么this绑定的就是obj
- 显式绑定:func.apply()/func.call(),func中的this会绑定到apply的参数。apply和call的区别:当func有参数需要传参时,func(this要绑定的东西, func参数1, func参数2, …),func(this要绑定的东西, [func参数1, func参数2, …]). bind()也可以,func.bind()返回一个this为bind参数的新函数
- new绑定: new func(“xxx”, “xxx”),func中的this绑定为调用这个构造器时创建出来的对象.
- 优先级: new > 显式 > 隐式 > 默认
- bind > call
- new和call\apply不允许同时使用
- apply、call、bind的第一个参数为undefined或者null时,调用的函数的this为GO
- 箭头函数的this不进行绑定。直接找上层作用域的this。注意,只有函数给或者全局才能产生作用域,对象并不能
- setTimeout等GO内的直接函数传入的非箭头函数的this为GO
call()原理
1 | Function.prototype.hycall = function(thisArg, ...args) { |
apply()原理
类似
bind()原理
1 | Function.prototype.hybind = function(thisArg, ...argArray) { |
箭头函数
简写:
var newns = nums.filter(item => item%2 === 0)
filter: 符合条件的(true)过滤出来,不符合的过滤掉
NT_2_JS Memory Operation
Basic Info
定义变量时分配内存。
基本数据类型:栈空间分配
复杂数据类型(对象、列表等):堆内存开辟,并返回这块空间的指针地址。
Normal GC Algorithm
引用计数
对象隐式Property:retain_count。
当有其他对象引用之时,retain_count++。当retain_count == 0时,回收。
弊端:循环引用导致的Memory leak。
标记清除
设置根对象(How to set? GO?),GC会定期从根开始找对象。对于没有引用到的,会认为是不可用的对象。
很好地解决循环引用的问题。
被较广泛使用。
NT_3 JS Closure And Parameter
方法:一个对象中的函数称作这个对象的方法。(当然也可以称为函数)
数组的函数
1 | // 过滤 |
闭包
Define
闭包的定义,分成两个∶在计算机科学中和在JavaScript中。在计算机科学中对闭包的定义(维基百科):
- 闭包(英语:Closure) ,又称词法闭包(Lexical Closure)
函数闭包(function closures) - 是在支持头等函数的编程语言中,实现词法绑定的一种技术;
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
- 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
捆绑的东西只是持有其引用,而不是深复制一份
闭包 = 函数+一些自由变量。
在执行完foo()后,foo对应的上下文会被销毁(但是AO不会!!!),但是最后bar()还是输出了name=”foo”。
提醒:作用域链(parentScope)在词法解析的时候就已经被确定。也就是说,一个函数在此法解析的时候就已经知道了他的自由变量。
因此,闭包在函数创建时就会被确定然后被创建出来(*MDN)
Memory Leak of Closure
如上图,当执行完foo()后,foo()对应的FEC会被销毁,其对foo()的AO的引用也就没了,但是此时是AO没有被销毁的,因为GO中有fn,而此时fn得到了foo()的返回值bar(),而bar()中对应内存中的parentScope是有对foo的AO的引用的,根据JS的垃圾回收机制,foo()的AO是不会被销毁的。
所以,如果bar()此时候的函数对象不被销毁,foo()的AO是不会被销毁的。
这就造成了内存泄漏——该被GC的东西不能被GC。
怎么解决呢?
简单,fn = null(0x0)
V8变量销毁
闭包没有使用到的变量,V8会将其销毁。
参数
本质上是类数组对象。
- argument是函数内的隐藏参数
- 获取参数长度arguments.length
- 获取参数arguments[0]
- 获取当前函数argument.callee
- 转array: Array.prototyope.slice.call(argument) 或者 [].slice.call(argument) 或者 Array.from(argument)
- 当函数的实参个数小于调用函数的形参个数的时候,可以用argument获得全部的参数
- 箭头函数中没有arguments
JavaScript—Pure Func And Currying And Compose Func
Pure Function
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等.
Currying
好处:
- 逻辑复用 灵活!
- 单一职责原则
AutoCurrying
1 | function lwlcurring(fn) { |
Compose Func
So elegant!
1 | //只对单返回值单参数函数有效 |
JavaScript NT_5—with_eval_strict AND OOP
Eval函数
var str = ‘console.log(‘OK’)’
eval(str)
不建议使用。
- 可读性
- 恶意篡改
- 必须经过JS解释器,且不被V8优化
Strict mode严格模式
- 严格模式下,默认绑定(自执行函数)this会指向undefined,可以使用window
OOP面向对象
对对象和其属性的控制
1 | //加_表示是私有的属性/方法,实际上不是。是一个约定。 |
当然也可以同时定义多个。
1 | Object.defineProperties(obj, { |
更简便的方法设置getter/setter
1 | obj = { |
获取属性描述符
1 | Object.getOwnPropertyDescriptors(obj, 'address') |
禁止对象添加新属性
1 | Object.preventExtensions(obj) |
禁止对象配置/删除属性configurable
1 | Object.seal(obj) |
禁止对象修改属性writable
1 | Object.freeze(obj) |
创建对象
Factory Mode
工厂模式,生产对象。
用函数。
缺点:不能抽象出具体的类型,只知道他是Object
new构造函数
var p1 = new Person()
函数Person一般称为构造函数
发生了什么?
p1的类型:Person
p1.__proto__.constructor.name
Person
【IMP】注意第二点,所有该构造函数构造出来的对象的隐式原型都指向
为该构造函数的显式原型
Prototype原型
构造函数每次都会创建一个单独的对象出来,占用空间大。
原型是一个对象
获取
obj.__proto__
隐式原型, 浏览器提供
Object.getPrototypeOf(obj)
隐式原型, ES5
func.__proto__
隐式原型, 浏览器提供
func.prototype
显式原型, ECMA
函数的显式原型默认自带一个constructor属性,指向该函数。constructor属性是对象才拥有的,函数其实也是一个对象,但是函数本身还是一个函数, constructor的重点是Function(),
用途
获取对象属性时,如果目标对象没有该属性,会去prototype内找
因此,可以将类中需要共有的东西(如某个函数)都用函数显式原型去定义。
Prototype Chain原型链
获取对象属性时,如果目标对象没有该属性,会去prototype内找,prototype也是一个对象,也有prototype。会逐层查找。
原型链的顶层:Object的原型
例一
1 | obj = { |
例二
对象继承
使用Student和Person构造函数来记录。
原型链实现继承
Student.prototype = new Person()
弊端
可能会造成相互影响。
1 | stu1.friends = [] //这是对friends的set操作 |
借用构造函数实现继承
1 | function Person(name, age, friends){ |
弊端
父类会被调用两次,生成了一个无意义的p
对象
原型式继承
创建一个新的对象,使得新对象的原型指向指定的对象(这样就可以实现子类继承父类)。
本质上和上面的方法相同,但是上面的方法生成的对象有
杂质
1 | obj= {name: "ok"} |
寄生式继承
也有弊端。不建议
⭐Better method of Inheritance——寄生组合式继承
1 | function Person(name, age, friends){ |
这样就实现了继承。由Student构造函数创建的对象在直接对其增删改的时候不会影响到其父类,Student的原型也是没有杂质的(只有指向父类原型的原型)
其他补充1
obj.hasOwnProperty
只检测本对象的属性,不包括原型内
xxx in obj
检测对象的属性,包括原型内
obj1 instanceOf obj2
检测obj2(构造函数的prototype)是否在obj1(实例对象)的原型链上
类
- ES6新增
1 |
|
super.xxx()可以执行父类中的函数。
JavaScript—ES6-ES13
多态
当对不同的数据类型执行同一个操作时,变现出来的行为不一样,就是多态的表现
增强对象字面量(Enhanced object literals)
1 | var name = "" |
解构
1 | //数组 |
let/const
- var有作用域提升,let和const无, 但不代表在代码执行前就不创建let和const标识的变量,只是不可以访问他们,直到被赋值 c——作用域提升:能被提前访问
- let、const不允许重复声明
- const变量之后,该变量不能被赋值到其他内存地址上,可以修改内存地址上的一些数据。
为什么var用的越来越少?
var会直接添加到window中,window这个对象在新ECMA中不做实现,V8也不做实现,由浏览器为了保持向下兼容而多出来的东西。
新ECMA中,GO变成了由C++的HashMap(ZoneHashMap)实现的VariableMap类型的variables_,所有(let var const)声明的变量都会放到这里面。
在用var时,会同时添加到window和variables_中(二者保持双向同步)。可能会产生一些bug。
暂时性死区
只要在一个代码块中用let或const声明过一个变量,那么在他被赋值之前都不能够被访问(无论全局作用域用没用var定义过相同名字的变量)
for…of
1 | o = [1, 2, 3] |
模板字面量
1 | let name = "ok" |
展开
1 | let num = [1, 2] |
ES9中,对对象也可以展开。在对象中,如果对数组进行展开,则会返回’0’:xxx, ‘1’:xx,如果对对象,那就直接展开
对对象用展开是浅拷贝!只会完全复制一份原对象给自己,如果修改原对象中所新建的对象,那么新对象也会改变!
Symbol
在ES6,可以使用字符串和Symbol作为对象key。(不能用对象!)
Symbol创建出来的是独一无二的值(当然也可以自己定义值),因此可以解决对象内容覆盖的问题。
1 | let s1 = Symbol(), s2 = Symbol() |
Set
- let set = new Set()
- 对对象强引用
- delete、add、clear
WeakSet
- 只能存放对象,不能存放基本数据类型
- 对对象弱引用
强引用和弱引用
GC在回收的时候,会区分强弱引用。GC将弱引用当成没有引用。
1 | info = new WeakRef(obj) |
Map
- 相比对象,可以用更多数据类型来作为key
- let map = new Map(), 可接收Entries作为参数
- get、delete、set、clear
- 遍历:map.forEach((item, key) => {}) 或 for..of
WeakMap
- key只能是对象
- key 弱引用
Vue3响应式原理
每一个都会生成一个render函数
1 | let obj = {name: "OK", age: 19} |
监听name的变化,当name变化后,通过weakmap得到obj1map得到其要调用的函数。
当用户销毁obj时,由于weakMap对obj相当于没有引用,所以obj内存会被垃圾回收。
Array Includes
- ES7
和 arr.indexOf != -1 差不多,但是includes可以判断NaN。
平方运算
- ES7
3 ** 3
Entries
- ES7
将对象转换为可迭代的类型,Entries类型可以用forEach。
padding
- ES8
1 | let cardnum = "1234567890" |
flat
- ES10
arr.flat(depth)
对数组降维。默认depth=1。
fromEnrtries
ES10
将Entries转为Obj
OptionalChaining可选链
ES11
1 | info = {name: "lwl"} |
Global
ES11
node和浏览器获取全局方式不同。
用globalThis
FinalizationRegistry
检测对象是否被GC回收。
1 | let fr = new FinalizationRegistry((val)=>{ |
Proxy
mipad5
Reflect
mipad5
Reflect.get(target, key, receiver), 改变target(也就是obj)中getter/setter的this指向。指向receiver(也就是proxObj)
在Proxy内使用Reflect有什么用?
1 | let user = { |
Reflect.construct
1 | Reflect.construct() |
Promise 原理和使用
传统success、failcallback回调缺点:
- 不统一
- 要自己写
promise.then()可以接收两个参数,第一个是resolve的第二个是reject的。
调用了resolve后,还可以继续执行后面的代码,reject后不能。
resolve
会调用then中的第一个参数hook
- 如果resolve中的参数是一个新的promise,则状态会转移到新的promise上,也就是说最终结果会看新的promise
- 如果resolve中的参数是一个对象,对象中有then方法,那么会调用该对象的then方法(实现了thenable接口)并传resolve和reject进去,最终结果会看这个then里面的情况。
then
传入的回调函数的返回值会被一个新的Promise的Resolve包装然后作为then的返回值。
reject
throw new Error和reject都会调用then的第二个参数hook
catch
传入的回调函数的返回值会被一个新的Promise的Resolve包装然后作为then的返回值。
finally
无参数
特例!
1 | promise.then(res => { |
这个catch捕获的是promise的异常!不是then回调函数中返回值产生的Promise
1 | promise.then(res => { |
这个catch捕获的是then回调函数中返回值产生的Promise的异常!
all(类方法)
Promise.all([p1, p2, p3, "aaa"]).then(...)
当所有promise都fulfilled之后,执行then。
当有一个rejected,那么就直接执行catch
allsettled(类方法)
Promise.allsettled([p1, p2, p3]).then(...)
当所有promise都有结果后,执行then,将结果以对象数组的形式返回。
race(类方法)
Promise.race([p1, p2, p3]).then(...)
竞赛,返回最先一个promise的结果(fulfilled或rejected)
any(类方法)
至少等到有一个fulfilled之后才执行then。全部rejected之后就会执行catch
Vue响应式原理
简单来说,定义了一个Depend类来收集依赖(一些函数,函数当中用到了要监听并响应的一些数据),Depend类有一个数组(承载依赖函数),和add函数和notify函数。
Vue3使用Proxy来实现响应式。Vue2使用Object.defineProperty.
下面展示Vue3的响应式 Step1:
1 | class Depend{ |
然而,实际开发中会有多个对象,每个对象有多个属性。这样就是要生成多个depend、watcher,如何对这些depend进行管理呢?
用map存。每个map是专属于一个对象的,map内key为obj的key名,value为depend。
然后用weakmap存各个map,weakmap的key为各个对象,value为map
修改后:
1 | class Depend{ |
Vue2响应式:
1 | class Depend{ |