第四章 作用域与内存
原始值与引用值
ECMAScript的变量可以包含两种不同的数据类型:
原始值:简单的数据
引用值:由多个值构成的对象
在把一个值赋值给变量时,JavaScript引擎必须确定这个值是原始值还是引用值
原始值和引用值具有不同的访问方式:
原始值,按值访问,因为我们操作的就是存储在变量中的实际值
引用值,按引用访问,js不允许直接访问内存位置,因此不能直接操作对象所在的内存空间,变量上存储的是引用值的引用地址而非实际值,因此访问的时候是按引用访问的
动态属性
原始值和引用值的在定义上基本没有差异,但是在定义之后的使用行为上,内在表现大为不同
对于引用值而言,可以随时增删改查属性和方法,而原始值不能有属性,只有引用值可以动态的添加后面可以使用的属性
// 原始值无法动态添加属性
let str = 'str';
str.name = 'n';
console.log(str.name);// undefined
// 引用值可以动态添加属性
let obj = {};
obj.name = 'n';
console.log(obj.name);// n
注意,原始类型的初始化可以只使用原始字面量形式
let str = 'abc';
如果使用new关键字创建,则会创建一个Object类型的实例,但其行为与原始值类似。
let str1 = 'a';
let str2 = new String('b');
str1.value = 'avalue';
str2.value = 'bvalue';
console.log(str1.value);// undefind
console.log(str2.value);// bvalue
console.log(typeof str1);// string
console.log(typeof str2);// object
复制值
原始值和引用值除了存储方式不同,通过变量复制值的方式也有所不同
原始值通过变量复制值:将开辟新的内存空间,这个空间用于存入复制值,复制值和原值完全独立,互不干扰
引用值通过变量复制值:存储在变量中的值也会被复制到新的变量的位置,区别在于,这里复制的实际上是一个指针,它指向存储在堆内存中的对象。由于两个变量存储的指针是相同的,意味着一个变量对对象做出了修改也会体现在另一个变量上
传递参数
ECMAScript中所有的函数的参数都是通过按值传递的
这表示,调用函数时传递的值,不会直接把这个值拿到内部使用,而是通过复制值的方式,如果传递的是原始值,就复制原始值,如果传递的是引用值,就复制引用值
两种不同传递方式的概述:
在按值传递参数的过程中,值会被复制到一个局部变量(即一个命名参数,ECMACscript描述为arguments对象中的一个槽位)
在按引用传递参数的过程中,值在内存中的位置会被保存在一个局部变量中,这意味着对本地变量的修改会反映到函数外部
在原始值的传递中,ES按值传递的概念非常明显:
function demo(str){
return str = 'abc';
}
const argumentStr = 'argument';
console.log(demo(argumentStr));//abc
console.log(argumentStr);// argument
str是一个局部变量,在调用时,相当于把argumentStr的值复制给str,因此str和argumentStr互不干扰
在引用值的传递中, 按值传递的概念比较模糊,不要误认为是按引用传递参数:
一个比较容易误解的例子:
function setName(obj){
obj.name = 'newName';
}
let group = {}
setName(group);
console.log(group.name);// newName
调用setName()后,原本是空对象的group拥有了name属性,这样的行为看起来和按引用传递值所描述的过程非常相似,实际上,在上例中加上两行代码即可破解其中谬论:
function setName(obj){
obj.name = 'newName';
obj = new Object();// 创建新对象
obj.name = 'newObjectName';
}
let group = {}
setName(group);
console.log(group.name);// newName
按照引用传递参数的概念,传入的obj参数在setName内部的行为会影响到外部的group,但实际上,group.name的值并没用发生改变,这证明传入setName函数内部的对象只是一个group的复制值,并非其本身
这里是说,相当于我们用new Object()更改了obj的引用,如果是按引用传递,那么外部传递的值group的引用也会被更改,然鹅并没有发生这种情况,证明了obj和group没有关系,只是在传递参数时,把相同的引用给到了obj参数
注意,ECMAScript中函数的参数就是局部变量
确定类型
typeof可以确定任何原始值的类型(null是一个意外),但是无法满足引用值的类型确定,比如Date类型的变量会被确定为Object,理论上来说,Object是所有对象的基类,typeof的返回值并不算是错误,但是这不是一个准确并且我们需要的结果
因此,ECMAScript提供了instanceof操作符,用于判定引用值是什么类型的对象
语法:result = variable instanceof constructor
如果variable是给定引用类型的实例,则instanceof会返回true,否则返回false
按照定义,所有引用值都是Object对象的实例,因此用instanceof检测任何对象和Object对象的关系都会返回true,函数也会返回true,这证明了虽然typeof函数时会返回Function,但是实际上,函数是一个特殊的对象
ECMA-262规定,任何实现内部[[cell]]方法的对象都应该在typeof检测时返回Function。在 Safari(直到 Safari 5)和 Chrome(直到 Chrome 7)中的正则表达式因为实现了[[cell]]这个方法,typeof的返回值是function
其他几个确认引用值类型的方法:
Array.isArray(val):专用于确定val是否是数组,返回true or false
Object.prototype.toString.call(obj):用于检测引用值是什么类型的,传入任意引用值类型的参数obj,会返回形如'[object type]'的引用值的字符串表示
执行上下文与作用域
执行上下文是js中程序执行顺序和数据可访问性的机制,也就是说,在js中,执行顺序和数据可访问性是由当前上下文决定的
变量对象:每个上下文都关联着一个变量对象,在当前上下文中定义的所有变量和函数都会存储在这个变量对象上
变量对象补充:其实技术实现上,上下文是对象,变量对象就是上下文对象上的一个属性
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。全局上下文在应用程序退出时才会被销毁,比如关闭网页或浏览器
执行上下文的种类有三种:全局上下文、函数上下文和eval上下文
全局上下文
全局上下文是最外层的上下文。不同的ECMAScript实现的宿主环境,表示全局上下文的对象不一样,浏览器环境中是window对象,node环境中是global对象
所有通过var声明的全局变量和函数都会成为window对象的属性和方法。但使用let和const的顶级声明不会,不过在作用域链解析的效果上是一致
函数上下文
js通过上下文栈来管理上下文的执行,当一个函数发生调用时,它会被推到栈顶,当函数执行完毕后,会从栈顶弹出,然后将控制权返回给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的
作用域链
作用域链是建立在变量对象上的,将不同上下文的变量对象“串联”起来,就形成作用域链,当前上下文的变量对象始终位于作用域链的前端,如果上下文是函数,其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments(全局上下文中没有这个变量)
作用域链的下一个变量对象来自包含上下文,相当于当前上下文的父上下文,下下个依次类推,直至全局上下文,全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域最前端(当前上下文的变量对象)开始,然后逐级往后,直到找到标识符。
上下文之间的连接是线性的、有序的,也就是说,任何上下文都可以到上一级去搜索变量和函数,但是无法到下一级去搜索变量和函数
注意,函数参数被认为是当前上下文的变量,因此与上下文中其他变量一样有着相同的访问规则
作用域链增强
所谓增强,逻辑上就是对数据访问性的增强,底层描述其在作用域链上增加变量对象,使得作用域链的访问范围变大,但是这个增强不一定和作用域链上其他变量对象具有关联性,它只是在作用域链最前端增加了一个变量对象
通常有两种情况会导致作用域链增强
try/catch语句的catch块
with语句
这两者都会在作用域链最前端添加一个变量对象,不同的是,with语句添加的是指定对象,catch块添加的是一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明
function buildUrl() {
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}
这个示例中的href引用的是作用域最前端被添加的location对象的href属性,而qs引用的则是定义在buildUrl()的qs,由于块级作用域的原因,这里会显示url未定义,如果改成var,将会失去块级作用域的限制,就可以正常返回
注意,catch作用域增强在IE8之前是有偏差的,实现上,IE将捕获的错误添加到了上下文的变量对象上,而不是catch块的变量对象上,这会导致在catch外部也能访问到捕获的错误
除了这两种方式外,理论上来说闭包、IIFE、模块化等一定层面上也可以被视为作用域增强,因为作用域增强的核心概念就是增强数据的访问性
变量声明
ES5.1之前,var是变量声明的唯一关键字,但在ES6之后,出现了let和const这两个更合理的变量声明关键字
1.使用var的函数作用域声明
使用var声明变量时,变量会被自动添加到最接近的上下文中,规则如下:
如果是函数上下文,最接近的上下文就是函数的局部上下文
with语句中,最接近的上下文也是函数上下文
如果变量未声明就被初始化了,那么它就会自动的添加到全局上下文中
// 遇到过的面试题
function add(num1,num2){
var sum= num1 + num2;
return sum;
}
console.log(sum);// 报错,无法访问sum
// 如果未使用var定义
function addNoVar(num1,num2){
sum = num1 + num2;
return sum;
}
console.log(sum);// 可以访问到sum
由于var对于变量的作用域规则的影响,add()函数中sum变量被添加到最近的上下文中,也就是add()函数上下文中,因此,根据作用域链的描述,外部无法访问到sum变量.而addNoVar()函数未使用var声明sum变量,根据规则,变量未声明就被初始化时,它会自动添加到全局上下文中,因此在addNoVar()函数外部可以访问到sum
注意,未经声明就初始化的变量会导致很多问题,并且在严格模式下,未经声明就初始化的行为会抛出错误
====一些知识点补充====
js引擎执行代码的简单过程:
词法分析阶段,引擎会对关键字、变量、操作符等进行标记,构建标记序列,忽略注释和空格
语法分析阶段,主要是将标记序列转化为抽象语法树(AST)
解释和编译阶段,抽象语法树被解释或编译成更底层次的字节码等文件
hoisting特性理解
一段代码在被真正的执行前,会有个专门用来声明变量的过程,俗语常把这个过程称为预解析/预处理。无论是用 var 还是用 let/const 声明的变量,都是在这个过程里被提前声明好的,俗语常把这种表现称为 hoisting。只是 var 和 let/const 有个区别,var 变量被声明的同时,就会被初始化成 undefined,而后两者不会
TDZ是如何产生的
ES规范定义var/let声明变量的过程:使用createMutableBinding()声明变量,然后通过InitializeBinding()初始化变量,setMutableBinding()为变量赋值,getBindingValue()引用一个变量
在执行完 CreateMutableBinding() 后没有执行 InitializeBinding() 就执行 SetMutableBinding() 或者 GetBindingValue()就会产生错误,这种现象有个专门的非规范术语Timporal Dead Zone(TDZ)
var由于声明和初始化都是在“预处理”阶段完成的,因此永远不会触发TDZ。
let的声明和初始化是分开的,只有真正执行到let语句才会初始化。初始化有两种情况,如果只声明不赋值,变量将会被初始化为undefined,如果,如果有赋值,只有=等号右边的表达式求值成功,才会初始化成功。并且=等号执行的是SetMutableBinding(),并不会执行InitializeBinding(),如果错过这两种调用初始化的情况,变量将会永远被困在TDZ中
====补充完成====
var声明提升:var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个现象叫做“提升”(hoisting)
var test = 'test';
// 等同于
var test;
test = 'test';
// 等同于
(functiuon(){
test = 'test';
var test;
return test;
})()// 返回字符串test
这里使用IIFE是为了区分在全局作用域下,不声明变量会把变量变成全局属性的情况
2.使用let的块级作用域声明
let是ES6之后出现的。块级作用域由最近的一对包含花括号{}界定,也就是说,if块、while块、function块,甚至单独的花括号块也是let声明的作用域
if(true){
let i = 1;
}
console.log(i); // i未定义
while(true){
let j = 2;
}
console.log(j); // j未定义
{
let k = 3;
}
console.log(k); // k未定义
var和let声明的其中之一不同之处是let在同一作用域中不能声明两次,会抛出SyntaxError错误,而var声明的这种行为则会被忽略
var a;
var a;
// 不会报错
let b;
let b;
// SyntaxError
由于let的块级作用域的声明行为,它非常合适在循环中声明迭代变量。使用var作为迭代变量会导致变量泄漏到循环外部
严格来说,let声明也会被提升,但是由于“时间死区”的存在,实际编写代码时不能在声明前使用变量
3.使用const的常量声明
使用const声明的变量必须同时初始化为某个值,一经声明,在其生命周期内任何时候都不能再赋予新值
const a;// SyntaxError语法错误
const b = 1;
b = 2; // Assignment to constant variable分配常数变量
除了这个特性外,const的其他行为与let一致
块级作用域
不能重复声明
注意,const声明只应用到顶级原语或者对象,换句话说,赋值为对象的const变量不能再被赋予其他引用值,但对象的键不受影响
const a = {};
a = {}; // 报错
const obj = {
name:'obj'
}
obj.name = 'oobj';// 不会报错,只应用到顶级原语
使用Object.freeze(),可以让整个对象都不能修改,使用freeze()不会报错,但是会静默失败
由于const声明暗示变量的值是单一类型且不可修改,JavaScript运行时编译器可以将其所有实例都替换为实际的值,而不会通过查询表进行变量查找,谷歌的v8引擎就执行了这种优化,这说明,如果在声明变量时,这个变量在将来不会发生改变,就应该使用const声明,同时这样也可以避免重新赋值导致的bug
4.标识符查找
当在特定上下文中为读取或写入而引入一个标识符时,必须通过搜索确定这个标识符表示什么。
搜索过程如下:
从作用域链最前端开始,也就是当前上下文的变量对象上搜索,搜到就停止
如果在前一作用域没有搜索到,则继续沿作用域链搜索,注意,作用域链中的对象也是一个原型链,因此搜索可能会涉及每个对象的原型链
一直持续到搜索至全局上下文的变量对象
如果全局上下文的变量对象也没有搜索的标识符,则表示未声明
var color = 'blue';
function getColor() {
return color;
}
console.log(getColor()); // blue
作用域链:getColor()函数上下文的变量对象->全局上下文的变量对象
搜索分为两步,在getColor()函数上下文的变量对象搜索名为color的标识符,没找到,继续搜索下一级也就是全局上下文的变量对象,找到名为color的标识符,搜索结束
对于这个搜索过程而言,引用局部变量会让搜索自动停止,而不继续搜索下一级变量对象。如果父级上下文和当前上下文中有同名标识符,因为搜索停止,所以只会引用当前上下文的标识符
var color = 'red';
function getColor(){
let color = 'green';
return color;
}
注意,使用块级作用域不会改变搜索流程,但是可以给词法层级添加额外的层次:
let color = 'red';
function getColor (){
let color = 'blue';
{
let color = 'green';
return color;
}
}
console.log(getColor());// green
标识符的搜索在块级作用域搜索到值为'green'的变量color后就结束了,不会再往上一层及搜索,自然也无法访问全局变量color,除非使用完全限定的写法window.color;
注意:标识符的查找是有性能代价的,访问局部变量比访问全局变量要快得多,因为不用切换作用域。不过js引擎在优化标识符查找上做了很多工作,这个差异将会微不足道
垃圾回收
JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。
在JS中,通过自动内存管理实现了内存分配和闲置资源回收。因此不用跟踪内存的使用情况
基本思路:周期性的自动检测,哪个变量不会再使用,然后释放掉它占用的内存。
JavaScript使用自动内存管理进行垃圾回收的优劣势:
优势:减轻程序员负担,避免人为的内存泄露问题
劣势:内存的使用变成了“不可判定”问题,无法通过算法解决,存在一定的性能问题
现代浏览器引擎中使用的是分代垃圾回收策略,即把内存分为老生代内存和新生代内存
新生代内存使用的是复制算法,这个算法将内存分为两个相等大小的区域:一个是存活对象的区域,通常称为 "From 区" 或 "Eden 区";另一个是空闲区,通常称为 "To 区" 或 "Survivor 区"
新生代完整的复制算法执行垃圾回收的过程:
1.分配对象到Form区,所有创建的对象都会被分配到Form区
2.到达阈值进行垃圾回收,这个阈值通常由引擎动态决定。
3.标记存活对象,使用标记-扫描(Mark-Sweep)或增量标记(Incremental Marking)等算法标记 From 空间中的存活对象,同时复制存活对象,将标记出的存活对象复制到to区
4.清理from区,清理掉 From 空间中不再被引用的对象
5.from区和to区角色互换,为下一轮垃圾回收做准备
在新生代中通过多次复制算法存活下来的对象,就会被放到老生代,由于老生代的对象存活时间较长,因此对其进行垃圾回收时,可能采用更为复杂的算法,例如标记-清除(Mark and Sweep)和标记-整理(Mark and Compact)
分代垃圾回收的核心思想是基于"弱保守策略"
追踪变量的使用情况是通过标记变量是否还在使用来实现的。浏览器发展史上用到过的两种主要的标记策略:
标记清理:标记清理是js垃圾回收策略中最为常用的,策略描述当进入上下文时,对当前上下文的变量做标记,标记的方式不是统一的,可能通过反转一个位,也可能通过维护一个变量在上下文中和不在上下文中的表。当上下文退出时,当前上下文中的变量会被标记为不再使用,js引擎每隔一段时间就会回收这些标记为不再使用的变量
引用计数:引用计数是一个并不常用的垃圾回收策略。其思路是,对每一个值都记录它被引用的次数。声明变量并赋予引用值时,这个值的引用为1。如果同一个值(引用地址)又被赋予两外一个变量,值的引用+1,如果保存该引用值的变量被其他值覆盖了,那么值的引用-1,当一个值的引用次数为0时,说明无法再访问到这个值了,此时就可以安全的回收了。
引用计数的问题:
循环引用,当A、B对象以属性的方式相互引用,就会引发循环引用,导致A、B的引用计数为2
循环引用问题补充,在IE8以及更早版本的IE中,并非所有对象都是原生对象,BOM和DOM中的对象是使用c++实现的组件对象模型(COM),而COM使用的是引用计数实现垃圾回收,也就是说,即便这些版本的IE中使用的是其他垃圾回收策略,在引用COM时依然会出现循环引用问题
let el = document.getElementById('el_id');
let obj = new Object();
obj.el= el;
el.obj= obj;
// IE8之前的版本,即使js引擎使用其他垃圾回收机制,由于COM使用的始终是引用计数,因此这里也会出现循环引用问题
由于存在循环引用,因此 DOM 元素的内存永远不会被回收,即使它已经被从页面上删除了也是如此
解决这种情况的方法是,通过手动来清除他们之间的“连接”
obj.el = null;
el.obj = null;
这个问题在IE9得到了修复,IE9把DOM和BOM对象都改成了JavaScript对象
注意,这里说到的垃圾回收策略,实际上是回收引用值,而不是都进行回收
原始值(基本类型)通常是在栈上分配内存,其生命周期与其所在上下文(作用域)绑定,一旦离开了该上下文,就会被销毁
引用值(对象、数组)通常在堆上分配内存,其生命周期更为复杂,多个变量可以引用同一个对象,而对象的生命周期不仅仅受限于当前作用域
function createObject() {
let obj = { value: 42 };
function getValue() {
console.log(obj.value);
}
return getValue;
}
let getValueFunction = createObject();
getValueFunction(); // 输出 42
// 现在,createObject 函数的执行上下文已经结束了,但是 getValueFunction 仍然能够访问 obj
其中奥妙在于引用值和原始值复制值的方式不同,由于函数传递参数说通过值复制传递的,当原始值复制传递时,和原来值并没有关系,是独立的,但是引用值不同,虽然引用值不是同一个变量,但是复制结果是相同的,如果内部不进行参数的引用变更,那么这个参数的修改就会影响到外部
性能
垃圾回收程序会周期性的进行,如果内存中分配了很多变量,则可能导致性能损失,因此垃圾回收机制的调度很重要
如果频繁的运行垃圾回收程序是非常消耗内存的。因此作为开发者,我们不能确定垃圾回收程序什么时候运行,但是我们可以做到尽快让它结束回收
现代垃圾回收机制会基于JavaScript运行时环境的探测来决定何时运行。探测机制因引擎而异,但是都是根据已分配的对象的数量和大小来决定的。
V8团队2016年博文:在进行一次完整的垃圾回收机制之后,V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次运行垃圾回收程序
IE饱受诟病的垃圾回收机制:IE7之前。垃圾回收程序运行的判断:
256个变量
4096个对象/数组字面量和槽位数(slot)
64KB字符串
只要满足以上的任意一个条件,IE的垃圾回收程序就会运行,这导致如果分配这么多变量的脚本,可能整个生命周期都在运行,这导致了垃圾回收机制的频繁运行
IE7修改策略:
一次回收的内存如果低于已分配的内存的15%,说明这是这些内存将会常常被使用,因此翻倍的增加上面判断规则的阈值,让垃圾回收机制不至于频繁的运行
一次如果回收的内存到达已分配内存的85%,则会重置阈值
内存管理
在使用垃圾回收机制的编程环境中,开发者通常无需关心内存管理。不过js运行在一个很特殊的环境。浏览器能分到的内存相比于其他桌面程序是非常少的。这是处于安全性考虑,避免运行大量js的网页耗尽系统内存导致系统崩溃
从内存管理来优化性能的方式:
执行代码时只保持必要的数据,将内存占用控制在较小的范围
解除引用,如果数据不在必要,应该主动将它设置为null,这叫做解除引用
解除引用比较合适在全局上下文中的全局对象和全局属性。局部作用域在超出作用域后都会自动解除
function createPerson(name){
let localP = new Object();
localP.name = name;
return loaclP;
}
let globalP = createPerson("name");
globalP = null;
localP是一个createPerson中的对象,在createPerson执行完毕后,它会被自动解除,但是createPerson函数返回的对象会被赋值给globalP,这是一个全局变量,除非网页关闭或浏览器退出,否则将不会自动解除,此时就需要手动的解除引用
解除引用不会导致内存被回收,或者立即执行垃圾回收机制,他只是告诉垃圾回收程序,下一次执行垃圾回收的时候把这个变量占用的内存回收掉
1.通过const和let声明提升性能
let和const都是以块为作用域,相比于var,使用这两个值可能会更早的让垃圾回收程序介入,尽早回收应该回收的内存。在块级作用域比函数作用域更早的终止的情况下,这就有可能发生
2.隐藏类和删除操作
隐藏类是由v8引擎开发者提出和实现的,在运行期间,v8会将创建的对象与影藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好
function Article(){
this.title = 'titleName';
}
const a = new Article();
const b = new Article();
a和b共用一个隐藏类
b.next = 'next';
此时,a和b就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,对性能可能产生明显影响
解决方案就是避免JavaScript的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性。
function Test(){
this.name = 'test';
this.title = 'title;
}
const a1 = new Test();
const a2 = new Test();
这样两个实例基本就一样了(不考虑hasOwnperty的返回值)。因此可以共享同一个隐藏类,从而带来潜在的性能提升
注意,使用delete关键字动态删除属性和动态添加属性一样会让实例不在共享隐藏类,最佳实践是把不需要的属性设置为null,即可以达到垃圾回收的效果,还不会影响隐藏类的共享
内存泄漏
JavaScript中的内存泄漏大部分是由不合理的引用导致的
1.意外声明全局遍历:如果不使用var、let、const等关键字声明变量,这个变量就会变成全局变量(无关嵌套深度)
function test(){
(function test1(){
num = 112;
})()
}
console.log(window.num) // undefined
test()
console.log(window.num)// 112
num = 112 相当于 window.num = 112,解决方案就是使用变量声明关键字声明
2.定时器会导致内存泄漏:定时器会形成闭包,闭包会导致变量在内部函数执行期间一直存在,而定时器又会一直运行,所以会导致被引用的变量无法被回收
let name = 'Jake';
setInterval(()=>{
console.log(name);
},100)
只要定时器不停止,name变量就不会被清理。解决办法:把定时器赋值给一个变量,在不需要定时器时对其赋值为空对象引用,清理定时器
3.闭包也会导致内存泄漏
let outer = function(){
let name = 'Jake';
return function(){
return name;
}
}
解决办法:主动释放outer函数,可以设置为null
静态分配与内存池
理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因为释放内存而损失的性能
浏览器决定何时运行垃圾回收程序的其中一个标准就是对象的更替速度
如果有很多对象被初始化,然后又一下子都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行。
一个例子计算二维矢量加法:
function addVector(a,b){
let r = new Vector();
r.x = a.x + b.x;
r.y = b.x + b.y;
return r;
}
这个二位矢量加法的特征是涉及对象的属性更替多,调用一次,会创建一个对象并且修改x和y属性。如果这个函数被多次调用,就会触发大量的对象更替操作,从而引发垃圾回收机制频繁的回收
解决方案:不要动态的创建对象,通过静态分配,保持操作已有的对象
function addVector(a,b,r){
r.x = a.x + b.x;
r.y = b.x + b.y;
return r;
}
实际上,这里只是简单的做了抽离创建对象的操作,这样修改是不够的,真正要做的是在什么时候创建对象不会让垃圾回收机制盯上
已有方案是使用对象池:在程序初始化某个时刻完成对象池的构建,后续的操作只是针对对象池中的对象进行操作,就不会额外的创建对象
// vectorPool 是已有的对象池
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate();
let v3 = vectorPool.allocate();
v1.x = 10;
v1.y = 5;
v2.x = -3;
v2.y = -6;
addVector(v1, v2, v3);
console.log([v3.x, v3.y]); // [7, -1]
vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);
// 如果对象有属性引用了其他对象
// 则这里也需要把这些属性设置为 null
v1 = null;
v2 = null;
v3 = null;
如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存,因此必须选择一个符合的数据结构来充当对象池,数组是个不错的选择,但是应该注意数组也会引发垃圾回收机制
let vectorList = new Array(100);
let vector = new Vector();
vectorList.push(vector);
由于js的数组是动态可变的,引擎会删除大小为100的数组,创建一个大小为200的数组。为了避免这种情况,应该在数组创建时就给定足够的内存空间
注意:这里对比较疑惑的数组即为对象,但是为何会拥有原始值那样创建行为做一个解释:
数组是一个特殊的对象,同时在js中它也是一个数据结构,它可以存放任何值,并且动态变化,由于这样的特殊性,在js引擎中,当添加了超过最初设置的大小的元素时,js引擎可能会为了更灵活的管理数组而复制原数组,并创建一个更大的数组,再把原数组复制值赋值给新数组,这种行为和原始值的修改很类似,但实际只是对数组这个特殊的对象进行的操作。(目前理解来说)
注意:静态分配属于一种极端的方式,是一种过早优化,除非程序被垃圾回收机制严重拖后腿,否则不考虑这种优化
第四章总结
JavaScript变量可以保存两种类型的值:原始值和引用值,原始值包括以下六种Undefined、Null、Boolean、String、Number、Symbol。
原始值和引用值有以下特点:
原始值大小固定,因此保存在栈内存上
从一个变量到另外一个变量的复制原始值会创建该值的副本,也就是两者相互独立
引用值是对象,存储在堆内存上
赋值为引用值的变量实际上只是包含引用值的指针,而不是引用值本身
从一个变量到另一个变量的复制引用值只会复制指针,两个变量相互影响,不独立
typeof用于确定原始值的类型,而instanceof用于确定引用值的类型
执行上下文决定变量的生命周期,以及它们可访问代码的哪些部分,执行上下文总结:
执行上下文分为全局上下文、函数上下文和块级上下文
代码执行流每进入一个上下文就会创建一个作用域链,用于搜索变量和函数
函数或块级上下文不仅可以访问当前上下文中的变量,还可以访问包含上下文乃至全局上下文的变量
全局上下文只能访问全局上下文中的变量,不能直接访问任何局部上下文中的任何数据
变量的执行上下文用于确定什么时候释放内存
JavaScript是使用垃圾回收的编程语言,开发者不需要操作性。垃圾回收总结如下:
离开作用域的值会被自动标记为可回收。然后在垃圾回收期间被回收
主流的垃圾回收算法是标记算法,即先给当前不需要使用的值加上标记,再回来回收它们
引用计数是另外一种垃圾回收策略,需要记录引用次数,为0时被回收,这种算法存在循环引用等问题,因此不是主流策略,另外一些老版本的IE浏览器中,即使引擎不使用这种算法也会有循环引用问题,原因是Javascript会访问非原生的对象(DOM等)
解除引用可以解决循环引用问题,并且不光可以解决循环引用问题,还可与促进垃圾回收。全局对象、全局对象的属性和循环引用都应该在不需要使用是解除引用
评论区