JavaScript高级进阶(更新中)-javascript-gao-ji-jin-jie--geng-xin-zhong-(一)https://developer.aliyun.com/article/1469502
JS运行原理
深入了解V8引擎原理
浏览器内核是由两部分组成的,以webkit为例:
- WebCore:负责HTML解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行JavaScript代码;
官方对V8引擎的定义:
- V8是用C ++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等
- 它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理
器的Linux系统上运行。 - V8可以独立运行,也可以嵌入到任何C ++应用程序中。
V8引擎的架构很复杂 ,我们可以先了解它庞大引擎的一些模块
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码
- 如果函数没有被调用,那么是不会被转换成AST
- Parse的V8官方文档:https://v8.dev/blog/scanner
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition会解释执行ByteCode
- Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
- 如果一个函数被多次调用,那么就会被标记为热点函数,它会被TurboFan转换成优化的机器码,提高代码的执行性能
- 机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执
行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码 - TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
V8架构解析图 来自官方
解析代码的步骤:
- 获得到代码之后 V8用流输入通过词法分析,分析成token
- 解析/预解析 来生成一个一个执行节点
- 生成 AST 树
- 转成字节码 如果有热点方法就会走turbofan编译器优化成机械码提升性能
全局代码执行过程
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等
- 其中还有一个window属性指向自己
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈,
他执行的式全局代码块,它的作用就是:
- 为了执行代码构建一个 Global Execution Context GEC 全局上下文
- 将这个构建的上下文加入到执行栈中 也就是将 GEC 放入 ECS中
GEC被放入到ECS中里面包含两部分内容:
- 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值
- 在代码执行中,对变量赋值,或者执行其他的函数;
每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中,当全局代码被执行的时候,VO就是GO对象了
全局上下文三个关键:
- VO(go)
- 作用域链
- This
执行以下代码过程
var message = "Global Message" function foo() { var message = "Foo Message" } var num1 = 10 var num2 = 20 var res = num1 + num2 console.log(res);
全局代码执行前
执行代码后
函数代码执行过程
在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中
- 当进入一个函数执行上下文时,会创建一个AO对象(Activation Object)
- 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数
- 这个AO对象会作为执行上下文的VO来存放变量的初始化
如下函数执行过程
执行前
执行后
流程为:
- 执行前创建FEC 也就是函数执行上下文
- 创建 AO 对象 name为函数名
- 创建作用域链
- 生成函数对象存放代码
- thisbing(暂无)
- 之后从上到下执行代码
- 执行完成后将name 变为 undefined
作用域和作用域链
当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象
PS : 作用域会提升 在本身vo没有情况下 会去上层寻找,我们先输出后声明会输出undefined, 这里也印证了
作用域提升小练习
var n = 100 function foo(){ n=200 } foo() console.log(n)
N =200
顺序内存查找图如下 :
- 全局代码创建函数 找到 n放入到函数vo中 之后调用foo()
- 在函数调用后找到GO中的n复制
- 函数结束,之后输出n
作用域链也是我们JS闭包的一个重点, js中闭包就是通过作用域链的方式来完成变量可以跨作用域访问的,为我们加快提升了开发的效率 也省去很多麻烦
JS内存管理
内存原理:
任何变成语言在执行的时候都需要操作系统来分配内存,只是有些语言需要手动管理分配的内存有些语言有专门来管理内存的方式 如 JVM
了解以上的概念之后,我们再来了解一下大致的内存周期
- 分配需要的内存
- 使用内存
- 在不使用的时候释放内存
JS 属于自动管理内存的语言
在我们定义数据的时候 JS 会给我们分配内存,但是内存分配的方式有区别
- 对于原始数据内存分配在执行的时候 直接放在栈空间进行分配
- 对于复杂的数据类型 会在堆内存中开辟一块空间 并且将这块空间的指针返回值变量引用
垃圾回收机制算法
概念:
因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
对比手动管理内存释放语言 对于开发者的技术要求非常高,一旦操作不但 效果反而会变得很差,这个也形成了高手可以做到性能很高 但是苦于进阶的选手,所以现在大部分高级语言都实现了GC也就是垃圾回收机制/垃圾回收算法
GC怎么知道哪些对象是不再使用的呢?
对于GC的实现是百花齐放的 设计语言的人总能整出花活,这里介绍几个常见的GC算法
常见GC - 引用计数(Reference counting)
- 当一个对象有一个引用指向它时,那么这个对象的引用就+1;
- 当一个对象的引用为0时,这个对象就可以被销毁掉;
PS: 这个算法的弊端就是会产生循环引用 就是加入 a b之间互有属性引用 会出现两个对象哦都无法销毁的问题
常见的GC算法 – 标记清除(mark-Sweep)
这个算法的核心思想是实现可达性
设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象
PS:这个算法可以很好的解决循环引用的问题
- 他会从一个根对象去不断查找确认查找之后就会标记对象
- 如果发现找不到 就等于无法引用 那么就会去销毁(如下图)
- 前提是 RO 对象不会被删除 其实就代表我们 js 中的 window对象
拓展
其他的GC算法
- 标记整理算法(Mark-Compact) 回收的时候保留存储对象搬运到灰级连续的内存空间,整合空闲空间,避免内存碎片化
- 分代收集(Generational collection) 对象分为旧 新 两组 有很多对象在完成工作后就会销毁 长期存活的对象变为
老旧
同时他们的检查频次不会那么频繁 - 增量收集(Incremental collection)
- 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟
- 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟
- 闲时收集(Idle-time collection)垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
闭包概念
闭包是JavaScript中一个非常容易让人迷惑的知识点
JS 作为高级语言 是支持函数式编程的,这意味着在js中
- 函数操作和使用都非常灵活
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用
所以JavaScript存在很多的高阶函数,我们可以自己编写高阶函数,也可以使用内置的函数
在未来开源框架中也都是趋向于函数式编程
闭包的定义
- 最早出现的闭包是 Scheme
- 闭包实际上是一种存储了函数和关联环境的结构体
- 他和函数最大的区别就是闭包被捕捉的时候,他的自由变量会被锁定 即使脱离了捕捉时的上下文也可以照常运行
他的作用就是让我们可以在函数中访问到外围的变量,替我们省去了很多繁杂的变量处理
闭包小案例
function createAdder(count){ funtion adder(num){ return count+num } return adder } var adder5 = createAdder(5) adder(100) // 100+5
这个例子可以很容易的看出闭包的使用和带来的好处
PS: 使用闭包的时候最好是可以将不需要的函数或者属性置为 null 来帮助GC回收释放对象 ,否则内存泄露会加大内存的占用
浏览器对于闭包的优化: 使用闭包的时候 浏览器会将我们没有使用的多余属性释放来增加性能
JS增强
JS函数增强
函数属性
JavaScript中函数也是一个对象,那么对象中就可以有属性和方法,他有一些默认的属性
- name 函数名
- length 函数参数个数(ES6
...
语法不会被算在内) - arguments 类似数组对象 可以i用索引来获取对象
- rset
PS: 箭头函数不绑定 Arguments 对象
arguments 转为数组对象常见方法
普通的方法 就是将内容一个一个迭代到新数组了
let newArray = [] // arguments function foo1(m, n) { for (var arg of arguments) { newArray.push(arg) } // arguments类似数组的对象(它可以通过索引来获得对象) console.log(newArray) } foo1(1, 2)
ES6 中的方法
- Array.form() 传入一个可迭代对象就可以转为数组
- 对象结构
...
的方式来复制
// 方法2 var newArray1 = Array.from(arguments) // 方法3 var newArray = [...arguments]
rset
如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
function foo1(m, n, ...arg)
- arguments 对象包含了传给函数的所有实参但是不是数组对象 需要转换
- rest参数是一个真正的数组,可以进行数组的所有操作
- arguments是早期为了方便去获取所有的参数提供的数据结构,rest参数是ES6中提供并且希望替代arguments的方案
纯函数理解和应用
副作用:
执行函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
纯函数的理解
- 输入 相同值的时候产生同样的输出 所以纯函数不能通过闭包的特性调用上层属性,因为会随着上层属性变化函数输出内容
- 函数的输出和输入值以外的信息无关和设备的外部输出也无关
- 这个函数不能有语义上可观察到的 “副作用”
纯函数辨别案例
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改
var names = ["abc", "nba", "nbc", "cbd"] var newNames = names.slice(0, 2) var newNames1 = names.splice(0, 2) console.log(newNames); console.log(newNames1);
纯函数的优势
- 稳定,可以放心使用
- 保证函数的纯度 简单的实现自己的业务逻辑,和外置的各种因素依赖关系少
- 用的时候需要保证输入的内容不被任意篡改,并且需要确定输入一定会有确定的输出
柯里化的理解和应用
函数式编程重要概念,他是一个作用于函数的高阶技术,在其他的编程语言也有使用
只传递函数部分参数来调用,让它返回一个函数去处理剩余的参数这个过程就被成为柯里化
// 普通的函数 function foo(x, y, z) { console.log(x + y + z); } foo(10, 20, 30) // 柯里化的结果 function kelifoo(x) { return function (y) { return function (z) { console.log(x + y + z); } } } kelifoo(10)(20)(30) //箭头函数写法 var foo2 = x => y => z => { console.log(x + y + z) }
自动柯里化函数
// 需要转化的例子 function sum(num1, num2) { console.log(num1 + num2); return num1 + num2 } // 自动柯里化函数 function hyCurrying(fn) { // 1 继续返回一个新的函数 继续接受函数 // 2 直接执行 fn 函数 function curryFun(...args) { if (args.length >= fn.length) { // 执行第二种操作 return fn.apply(this, args) } else { return function (...newArgs) { return curryFun.apply(this, args.concat(newArgs)) } } } return curryFun } // 对其他函数柯里化 var sumCurry = hyCurrying(sum) sumCurry(10)(5) sumCurry(10, 5)
柯里化函数只有在某些特殊的场景才需要使用。他得性能并不高也可能引起闭包的内存泄漏所以使用的时候需要注意。
组合函数理解和应用
当我们需要嵌套调用两个函数的时候,为了方便复用,我们可以写一个组合函数
var sum = pow(double(12))
我们可以编写一个通用的组合函数来让我们使用组合函数更加的便捷,其实思路就是很简单的将函数放入数组判断边界顺序执行
function sum(num) { return num * 2 } function pow(num) { return num ** 2 } function composeFn(...fns) { // 边界判断 var length = fns.length if (length < 0) { return } for (let i = 0; i < length; i++) { var fn = fns[i] if (typeof fn != "function") { throw new Error(`index postion ${i} must be function`) } } //轮流执行函数 返回结果对象 return function (...args) { var result = fns[0].apply(this, args) for (let i = 1; i < length; i++) { var fn = fns[i] result = fn.apply(this, [result]) } return result } } var newfn = composeFn(sum, pow) console.log(newfn(5)); //100
with语句、eval函数(拓展知识)
with
语句 扩展一个语句的作用域链,不推荐使用有兼容性问题
eval
允许执行一个代码字符串。他是一个特殊函数可以将传入的字符串当作js代码执行
- 可读性差
- 有注入风险
- 必须经过解释器 不会得到引擎的优化
严格模式的使用
js的局限性 :
- JavaScript 不断向前发展且并未带来任何兼容性问题;
- 新旧代码该新模式对于向下兼容有帮助但是也有问题出现
- 就是创造者对于js的不完善之处会一直保留
ES5标准中提出了严格模式的概念,以更加严格的方式对代码进行检测和执行
只需要在代码的开头或者函数的开头 加入use strict
就可以开启严格模式
JS对象增强
数据属性描述符
我们的属性一般定义在对象的内部或者直接添加到对象内部,但是这种方式我们就不能对属性进行一些限制,比如这个属性是否是可以通过delete删除,是否可以for-in遍历的时候被遍历出来等等
PS: 一个属性进行比较精准的操作控制,就可以使用属性描述符。
- 通过属性描述符可以精准的添加或修改对象的属性
- Object.defineProperty 来对属性进行添加或者修改
这个方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty()
属性描述符分类
分为两类:
- 数据属性
- 存取属性
数据属性描述符
Configurable
:表示属性是否可以通过delete删除属性,是否可以修改它的特性
- 使用对象定义属性的时候为true
- 使用属性描述符来定义的时候 默认为false
Enumerable
:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 直接对象内定义的时候 为true
- 通过属性描述符定义为false
Writable
:表示是否可以修改属性的值;
- 直接对象内定义的时候 为true
- 通过属性描述符定义为false
value
:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值是undefined
使用案例
var obj = { name: "whit", age: 12 } Object.defineProperty(obj, "name", { configurable: false, enumerable: false, writable: false, value: "1234" })
存取属性描述符
Configurable&Enumerable
也是存取属性描述符
get
:获取属性时会执行的函数。默认为undefined
set
:设置属性时会执行的函数。默认为undefined
同时定义多个属性
// 多个属性调用 Object.defineProperties(obj, { name: { configurable: false, enumerable: false }, age: { enumerable: false, writable: false, } })
对象方法补充
获取对象的属性描述符:
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
禁止对象扩展新属性:preventExtensions
- 给一个对象添加新的属性会失败(在严格模式下会报错);
密封对象,不允许配置和删除属性:seal
- 实际是调用preventExtensions
- 并且将现有属性的configurable:false
冻结对象,不允许修改现有属性: freeze
- 实际上是调用seal
- 并且将现有属性的writable: false
代码案例
// 阻止对象的拓展 Object.preventExtensions(obj) obj.address = 12 //密封对象 不能进行配置 Object.seal(obj) delete obj.name // 冻结对象 Object.freeze(obj) obj.name = "ske"
JavaScript高级进阶(更新中)-javascript-gao-ji-jin-jie--geng-xin-zhong-(三)https://developer.aliyun.com/article/1469506