《红宝书》 |执行上下文与垃圾回收
执行上下文
变量或函数的上下文决定了它们可以访问那些数据以及它们的行为。下面是一个例子:
let first='box'
function change(){
let second='bottle'
function swap(){
let third=second
second=first
first=third
}
}
上下文类别
执行上下文分为全局上下文和局部上下文(函数上下文)。
全局上下文是最外层的上下文。根据ECMAScript实现的宿主环境的不同,全局上下文的对象可能不一样。在浏览器中,全局上下文是window对象,所有通过var定义的全局变量或函数都会成为window对象的属性和方法。上下文在其所有代码都执行完毕后销毁,包括所有在其上面的所有变量和函数。(全局上下文在应用程序退出前才会被销毁,如关闭浏览器)
例子包含三个上下文:全局上下文、change()局部上下文、swap()局部上下文
变量对象
每个上下文都有一个的变量对象,它储存了上下文中定义的所有变量和函数。
作用域链
-
上下文的代码在执行的时候,会创建变量对象的作用域链,决定各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量始终位于作用域链的最前端。
-
内部上下文可以通过作用域链访问外部上下文,外部上下文无法访问内部上下文。
-
在特定上下文中引用一个标识符时,必须通过搜索来确定这个标识符代表什么。搜索开始于作用域最前端,如果该上下文中找不到标识符,就会沿作用域链继续在其他上下文中搜索,如果找到该标识符,搜索停止。这个过程持续到搜索至全局作用域,如果仍然没找到,说明未声明。
作用域链增强
除了常规的全局上下文和函数上下文,还可以通过下面方式在作用域前端临时添加一个上下文,这个上下文在代码执行后会删除:
- try/watch语句的catch块:创建一个新的变量对象,会包含要抛出的错误对象的声明
- with语句:添加指定的变量对象
function buildUrl(){
let qs="?debug=true"
with(location){
var url=href+qs//with将location作为上下文,此时href实际上引用的是location.href
}
return url
}
//注意,这里的url使用var声明的,var采用采用的是函数作用域,会成为buildUrl()上下文的一部分,可以作为函数的值被返回
//而如果改用let声明,它就被限制在块级作用域内了,这时with以外是无法访问该变量的
垃圾回收
js是使用垃圾回收的语言,即执行环境负责在代码执行时管理内存,这个过程是每隔一段时间就会自动运行。基本思路是:确定哪个变量不会再使用,然后释放它占用的内存。下面是一个例子(标记清理):
垃圾回收程序会跟踪哪个变量还会使用,现在有以下两种策略来标记未使用变量:
-
标记清理(常用)
该策略会标记内存中储存的所有变量,接着将所有上下文的变量,以及被上下文变量引用的变量的标记去掉。最后垃圾回收程序做一次内存清理,销毁带标记的所有值并释放他们的内存。
-
引用计数(不常用)
思路是记录每个值的引用次数。声明引用值时,该值的引用数为1;如果该值被赋给另一个值,引用数加1;引用该值的变量如果被其他值覆盖了,引用数减1;当一个数的引用数为0时,就无法访问该值。垃圾回收程序每次运行时会释放引用数为0的值的内存。
该策略有一个问题:如果两个引用值相互引用,就会形成循环:
function(){ let obj1=new Object(); let obj2=new Object(); obj1.a=obj2; obj2.a=obj1; }
在上面例子中,obj1和obj2的引用数都是2。根据引用计数的思路,在函数结束后,
obj1
和obj2
依旧会存在,如果函数被多次调用,会导致大量内容永远不会被释放。通过下面方法可以清除引用:obj.property=null
提高性能
垃圾回收程序是周期性运行的。如果内存中分配了很多变量,可能会造成性能损失。尤其是在移动设备上,垃圾回收可能会拖慢渲染速度和帧速率,因此垃圾回收的时间调度很重要。因此最好做到:无论什么时候开始收集垃圾,都能让它尽快结束掉。
js运行在一个内存管理与垃圾回收的环境中。分配给浏览器的内存通常较少,将内存占用量保持在一个较小的值可以让页面性能更好。
优化内存的最佳手段就是保证在执行代码时只保存必要数据。如果数据不再必要,那么把它设置为null,从而释放引用,这也叫解除引用。这适用于全局变量和全局对象的属性。而局部变量在超出作用域后会自动解除引用。下面是几种提高性能的方法:
const
和let
它们都以块{}
为作用域,相较于var
,它们会更早的让垃圾回收介入,尽快回收内存。
隐藏类
根据js所在的运行环境,有时候需要根据浏览器使用的js引擎来采取不同的性能优化策略。以Chrome的V8引擎为例,它在将js代码编译为实际的机器码时会利用“隐藏类”。下面是一个例子:
function Artical(){
this.title='hello'
}
let a1=new Artical()
let a2=new Artical()
V8会在后台配置,让两个类实例共享相同的隐藏类(因为两个实例共享同一个构造函数)。然而,如果进行如下改变:
a1.author='小明'
delete a1.title
上面动态添加属性和删除属性的方式会导致它们不再共享同一个隐藏类,这样会降低性能。所以写代码的时候应该避免“先创建后补充”。下面是提高性能的解决方案:
function Artical(opt_author){
this.title='hello'
this.author=opt_author
}
let a1=new Artical()
let a2=new Artical('小明')//动态添加属性
a2.author=null//动态删除属性
内存泄露
js中内存泄露主要是不合理的引用造成的。
-
意外声明全局变量是最常见的内存泄露问题,下面没有使用关键字声明,解释器会把name当成是window的属性,这时候只要window本身不被清理该属性就不会消失:
function setName(){ name='jack' }
在变量前面加上
var
、let
或const
可以解决上述问题。 -
定时器也会导致内存泄露。只要定时器一直运行,回调中引用的变量就会一直占用内存:
let name='jack' setInterval(()=>{ console.log(name) },100)
window.clearInterval()
可以解决上述问题。 -
闭包也很容易造成内存泄露。下面例子会导致name的内存泄露,只要outer函数存在就不能清理name,因为闭包一直在引用它。
let outer=funtion(){ let name='jack' return function(){ return name } }
静态分配与对象池
减少执行垃圾回收的次数也很重要。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。
对象更替速度影响何时运行垃圾回收程序。如果有很多对象被初始化,然后一下子跳出作用域,那么浏览器会更加频繁的运行垃圾回收程序,从而影响性能。下面由一个例子解读这段话:
function addVector(a,b){
let result=new Vector()
result.x=a.x+b.x
result.y=a.y+b.y
return result
}
调用上面的函数,会创建并修改对象,最终返回给调用者。如果返回的对象生命周期很短,所有对它的引用将很快失去,进而成为被回收的值。假设这个函数被频繁调用,那么垃圾回收程序就会频繁的运行。
解决方法:不要动态创建对象。这就需要在其他地方实例化该对象,进而作为参数传递给函数。
function addVector(a,b,result){
result.x=a.x+b.x
result.y=a.y+b.y
return result
}
在哪里创建对象可以不让垃圾回收盯上呢?一个策略是使用对象池。
在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收就不会检测到对象更替,因此不会频繁地运行。https://docs.cocos.com/creator/manual/zh/scripting/pooling.html