3 JavaScript
(==)在比较中会先进行类型转换,再确定操作数是否相等 ( === )只有两个操作数在不转换的前提下相等才返回 true。即类型相同,值也需相同
JavaScript的组成
JavaScript的组成
- ECMAScrip:JavaScript的语法标准,规定了基本语法、数据类型、操作符等。
- BOM(浏览器对象模型):提供与浏览器交互的接口,如
window、location、navigator等。 - DOM(文档对象模型):将HTML文档表示为树形结构,提供操作文档的接口。
BOM的API
window:全局对象,提供与窗口相关的操作,如window.alert()、window.location等。location:提供当前页面的URL信息,如location.href、location.reload()等。navigator:提供浏览器信息,如navigator.userAgent。
DOM的API
document:代表整个HTML文档,提供查找和操作元素的方法,如document.getElementById()、document.createElement()等。 提供操作元素属性、样式、事件等方法。
Cookie
- 存储位置:属于DOM,通过
document.cookie提供了一种方式来访问由浏览器为当前网页管理的cookie,这更接近于浏览器的行为层面,即BOM的部分功能。因为从根本上说,cookie的管理和存储是由浏览器控制的, - 用途:存储用户身份信息、会话状态等。
for in和for of
for in
用于遍历对象的可枚举属性名(键)。
- 遍历的是对象的 键(key)。
- 可以遍历普通对象、数组、字符串等所有可枚举属性。
- 不保证顺序(尤其在对象中),不建议用于遍历数组(顺序可能不符合预期)。
- 包括原型链上的可枚举属性(除非使用
hasOwnProperty()判断)。
for of
用于遍历可迭代对象的值(value)。
- 遍历的是 值(value)。
- 支持的类型包括:
Array、Map、Set、String、TypedArray、arguments对象、生成器(Generator)等。 - 不适用于普通对象(因为普通对象不可迭代)。
- 顺序是可预测的(如数组按索引顺序遍历)。
事件委托
事件委托(Event Delegation) 是 JavaScript 中一种高效的事件处理机制,它利用了 事件冒泡 的特性,将事件监听器绑定到父元素上,而不是每个子元素单独绑定。
事件委托的原理
- 在 DOM 中,事件会从目标元素向上传播(冒泡),途中经过它的所有祖先节点。
- 利用这一特性,我们可以在父级元素统一监听事件然后根据事件对象中的
target属性判断具体是哪个子元素触发了事件。
事件委托的优点
- 性能优化
- 减少绑定的事件监听器数量,尤其适用于动态内容或大量子元素。
- 支持动态添加的元素
- 不需要为新添加的子元素重新绑定事件监听器。
- 代码更简洁、易维护
- 更少的事件绑定逻辑,提升可维护性。
事件委托就是“以父治子”,通过父元素来管理子元素的事件,提高性能和灵活性。
| 方式 | 优点 | 缺点 |
|---|---|---|
| 直接绑定事件 | 逻辑清晰,控制粒度细 | 性能差,不易维护动态内容 |
| 事件委托 | 性能好,兼容动态内容 | 逻辑稍复杂,需判断事件源 |
事件冒泡
事件冒泡 是 JavaScript 中 事件传播机制的一部分,它指的是:当一个元素上的事件被触发时,该事件会从当前元素(目标元素)开始,向上传播到其父元素、祖父元素,一直到 window 对象。
事件冒泡的应用
- 事件委托:利用冒泡机制,在父元素监听子元素的事件。
- 统一处理嵌套结构中的交互逻辑
- 阻止冒泡:使用
event.stopPropagation()来阻止事件继续向上传播。
| 特性 | 事件捕获(Capture Phase) | 事件冒泡(Bubbling Phase) |
|---|---|---|
| 执行顺序 | 从外层向内层(window → target) | 从内层向外层(target → window) |
| 默认行为 | 否 | 是 |
| 是否支持 | 支持 | 支持 |
| 使用方式 | addEventListener(type, fn, true) | addEventListener(type, fn, false) 或不写第三个参数 |
this的指向
谁调用指向谁 全局环境下的this指向window
箭头函数:没有this和arguments,捕捉外层的执行环境,继承了该外层的this
两个对象如何比较
- 使用JSON.stringifyO方法转化成json字符串比较
- 使用es6的object.is0方法
- 使用递归的方式
展开运算符
迭代的对象展开到单独的元素中
对一维数组深拷贝 对多维数组第,一维深拷贝,其它为浅拷贝 对象的拷贝:一层深拷贝,二层浅拷贝
console.log
在打印一个数组或对象时,我们在控制台看到的是他的最终状态而不是打印时的庄状态 因为对于这些复杂数据类型,打印的是指向对象的引用,当在控制台展开对象时就会执行get函数获取对象的值,而此时对象的数据已经改变 在打印数组或对象时最好使用JSON.stringify打印或者打印其中具体的值这样就能得到打印时的值了
暂时性死区原理
函数在执行时会有自己的执行上下文,在自己的执行上下文中会初始化变量环境和词法环境,var声明的变量会被放在变量环境中,同时,由于变量提升,会默认初始化为undefined。而let声明后会放在词法环境中,与变量环境相互隔离,也不会自动初始化,如果内层还有作用域则会在词法环境中再生成一个内层作用域,外层作用域中无法访问到内层作用域的变量,但内层作用域中可以访问到外层作用域中的内容
function foo(){
var a_var=1
let a_let=1
{
var b_var=2
let b_let=2
{
var c_var=3
let c_let=3
}
}
}对应的环境上下文如图 
垃圾回收
代际假说与分代收集
为了达到最好的回收效果,V8会根据对象的生存周期的不同来应用不同的回收算法,所以在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收
- 副垃圾回收器,主要负责新生代的垃圾回收
- 主垃圾回收器,主要负责老生代的垃圾回收
垃圾回收器的工作流程
V8的内存结构
- 新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。
- 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。
- 大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。
- 代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。
- map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单
垃圾回收的过程一般主要出现在新生代与老生代。
垃圾回收策略
标记( Mark-Sweep )清除
目前在 JavaScript引擎里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。 此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
引擎在执行 GC(使用标记清除算法)时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们称之为一组根对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象、文档DOM树等。
标记清除算法大致过程:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0;
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1;
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间;
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收;
优点: 实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点: 在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题
引用计( Reference Counting )数
是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,但因为它的问题很多,目前很少使用这种算法了。
它的策略是跟踪记录每个变量值被使用的次数
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1;
- 如果同一个值又被赋给另一个变量,那么引用数加 1;
- 如果该变量的值被其他的值覆盖了,则引用次数减 1;
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存;
优点:
- 引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾;
- 标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了;
缺点:
- 需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限;
- 无法解决循环引用无法回收的问题;
工作流程
不论什么类型的垃圾回收器,它们都有一套相同的执行流程
- 第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
- 第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
- 第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。
副垃圾回收器
主要负责新生区的垃圾回收。而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。
新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记 - 清除的算法进行垃圾回收的。
原理:
- 首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,对一块内存多次执行标记 - 清除算法后,可能会产生大量不连续的内存碎片。
而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
全停顿(Stop-The-World)
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。
在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大,但老生代就不一样了。如果在执行垃圾回收的过程中,占用主线程时间过久,将会造成页面卡顿。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking) 算法。
js为什么是单线程的
- UI交互敏感性:用户点击、滚动等操作需要即时响应
- DOM操作安全性:避免多线程同时操作DOM导致的竞态条件
- 轻量级特性:作为网页脚本语言需要快速启动和执行 而为了避免长任务长时间占据资源导致短任务过长等待,所以也必须区分微任务和宏任务设定优先队列
JIT
js并不是严格意义上的解释型语言 由于JIT的优化,它成为一门即时编译的语言
js代码的执行有两种状态:
- 解释器:快速开始执行
- JIT编译器:热代码编译成机器码
JIT的优化
- 类型推断:jit通过关键函数之前的调用情况推断类型,并提前生成对应的机器码逻辑,如果突然类型不对(如:数字相加变成字符串的拼接)会降级回解释器执行
// 函数定义
function add(a,b){
return a+b
}
//观察调用
add(1,2)
add(3.4)- 内联缓存:对象属性访问优化,当经历过某个对象则可以记录对象的结构以及其内部属性的偏移地址,在后续访问对象的属性时可以直接读取到其对应的地址
const person={name:'tom'}
console.log(person.name)- 函数内联:对于较短的函数可以直接将函数内联(甚至直接常量折叠),避免上下文切换的开销
function square(x){
return x*x
}
const result=square(2)
// 优化后
const result=2*2 //甚至直接得出4- 死区代码消除:对于永远不会进入的分支会直接不生成机器码
if(0===1){
console.log('不会执行')
}- 逃逸分析:如果对象只在函数内部用到,JIT会分析变量没有逃逸,直接用栈或者寄存器存储,避免堆分配
function makePoint(x,y){
return {x,y}
}- 循环优化:JIT 会将其展开、简化,甚至 SIMD 化,提升执行效率。
for(let i=0;i<10;++i){
sum+=1
}- 闭包优化:JIT 编译器会分析闭包中捕获的变量是否“逃逸”了作用域:没有逃逸:寄存器分配、甚至常量折叠。逃逸了:只能慢路径堆分配
function outer(){
let counter=0
return ()=>{
coutner++
return counter
}
}
// 优化后
function() { return 20; }贡献者
版权所有
版权归属:wynnsimon
