执行上下文(作用域)
任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称作用域)。
以上是红宝书中的原话。(之前一直搞不懂上下文和作用域有什么区别。。。其实就是同一个东西)
作用域决定了变量的生命周期,以及他们可以访问代码的哪些部分。
JavaScript
有3种作用域:
- 全局作用域:最外层作用域,即
window
对象的作用域 - 函数作用域:函数内的作用域,也是最复杂的一个作用域。
var
的声明范围即为函数作用域
- 块级作用域:
{}
包裹的作用域,如for
、while
、if
等块。单独的{}
块也算- 函数作用域也算块级作用域
let
和const
的声明范围是块级作用域
每个作用域都有一个关联的变量对象,保存该作用域下定义的所有变量和函数。
如果是函数作用域,其一开始就有个定义对象arguments
作为变量对象
函数作用域
每个函数调用都有一个专属的函数作用域,其所有代码都执行完毕后会销毁。
当代码执行流进入函数,函数的作用域被推到一个作用域栈(上下文栈)上(此时原本的作用域比如全局作用域就被压在了下面),执行完毕后弹出,继续执行原来的作用域的代码。
如下代码:
let color = 'blue';
function changeColor () {
let a = 1;
color = 'red';
console.log(a);
}
changeColor();
console.log(color);
- 一开始代码执行流在全局作用域,作用域栈内只有一个全局作用域(既是堆顶,也是堆底)
- 在第1行创建了一个
color
变量并赋值blue
- 在第3行定义了一个
changeColor
的Function
对象
- 在第1行创建了一个
- 在第7行执行
changeColor()
,代码执行流进入函数changeColor()
的作用域- 将**
changeColor
作用域**压入作用域栈,成为堆顶 - 全局作用域被压在
changeColor
作用域下
- 将**
changeColor()
的代码执行完毕,弹出栈顶,所有都被销毁(如临时创建的a
以及arguments
)- 代码执行流重新进入全局作用域,继续往下执行
作用域链
作用域链:决定各级上下文访问变量和函数的顺序
作用域中的代码被执行的时候,会创建变量对象的一个作用域链。正在执行的作用域的变量对象始终位于作用域链的最前端。
作用域链中的下一个变量对象来自包含上下文(外面一层),再下一个对象来自再下一个包含上下文,以此类推至全局上下文。
上面例子中的changeColor()
的作用域链包含2个变量对象,一个是他本身的变量对象(包含arguments
和a
),另一个是全局作用域的变量对象(包含color
)。因为能在作用域链中找到color
,因此在changeColor()
中能访问color
在作用域链上能找到的变量,均能被访问或修改。(一般来说,外层作用域的变量对象均在作用域链上)
看一个例子:
var color = 'blue';
function changeColor() {
let anotherColor = 'red';
function swap(){
let temp = anotherColor;
anotherColor = color;
color = temp;
}
swap();
}
changeColor();
红宝书上的作用域链图是下侧为最前端。不过我感觉有些别扭,下面画了些以顶部为最前端的简图
-
代码流执行到第1行的作用域链如下:
- 全局作用域链:
- 全局作用域链:
-
执行到第2行:
- 全局作用域链:
- 全局作用域链:
-
执行到第11行,进入
changeColor
作用域:红色代表changeColor
函数作用域-
执行到
changeColor
作用域内第1行的代码(第3行的位置let anotherColor = 'red';
)-
全局作用域链:
-
anotherColor
作用域链:
-
-
执行到
changeColor
作用域内第2行的代码(第4行的位置function swap()
)-
全局作用域链:
-
anotherColor
作用域链:
-
-
-
执行到
changeColor
作用域内第9行的代码,进入swap
作用域:这个新的颜色代表swap
函数作用域- 执行到
swap
作用域内第1行的代码(第5行的位置let temp = anotherColor;
)-
全局作用域链:
-
anotherColor
作用域链:
-
swap
作用域链:
-
- 执行到
-
到
changeColor
作用域内第9行的代码执行完毕,释放swap
作用域 -
第11行执行完毕,释放
changeColor
的作用域
JavaScript
引擎在查找变量时顺着作用域链找的,先找最前端的正在执行的作用域的变量对象,没找到再一层层往下找,直到全局作用域。因此,访问局部变量比访问全局变量要快得多。
内存
垃圾回收
JavaScript
会自动释放内存,其基本思路是:
- 确定哪些变量不会再被使用,然后释放他的内存
- 周期性:垃圾回收程序是每隔一段时间运行一次的
但是单靠算法也无法完美避免内存的泄露,因此需要理解垃圾回收机制,注意避免。
回收算法
前面说过,回收程序在确定变量不会再被使用时,释放他的内存,那么怎么确认呢,就需要跟踪记录哪些变量还有可能使用。
一般来说,局部变量在其作用域执行时,不会被删除。此时,内存会给他分配空间以保存相应的值。当作用域中的代码执行完后,里面的变量基本就不会被使用了,此时就会释放该作用域占用的所有内存。但有一种情况例外,就是当局部变量被其他作用域的变量引用(在其他作用域被使用)时,回收程序会认为他扔需要使用,就不会释放内存。
如何标记未使用的变量主要有以下两种方式:标记清理和引用计数
标记清理(常用)
标记清理是针对变量(变量名)的
-
当变量进入上下文时(如函数内声明一个变量),这个变量会被加上一个存在于上下文的标记。
标记方法有很多(这都不重要)
- 如反转某一位
- 或维护“上下文中”和“不在上下文中”的变量列表
-
当变量离开上下文时(上下文内的代码执行完),也会被加上一个离开上下文的标记。
-
垃圾回收程序运行时,会标记(待删除)内存中存储的所有变量。然后将
- 所有上下文中的变量
- 被在上下文中的变量引用的变量
的标记去掉,剩下的还带着(待删除)标记的变量就是待删除的了。(因为不会再被访问了)
let a = [1, 2, 3]; let b = a; // 此时,a和b均在上下文中,且a被b引用
let method = function () { let a = [1, 2, 3]; return function () { let b = a; return b; } } let method1 = method(); // 此时,a已经离开上下文,但a仍在被b引用
-
到了回收周期进行清理待删除标记的变量,并回收他们的内存。
引用计数
引用计数是针对值(变量值)的
-
当声明一个变量并为它赋予一个引用值时,这个值的引用数为1
-
如果这个值又被赋给了另一个变量,那么引用数+1
let a = [1, 2, 3];// [1, 2, 3]的引用数为1 let b = a; // [1, 2, 3]的引用数为2
-
-
如果保存该值的变量被覆盖了,则引用数-1
a = []; // [1, 2, 3]的引用数为1
引用计数有一个严重的缺陷,就是当a引用b,而b又引用a时,会造成循环引用,导致这两个值一直不被释放
总结
- 标记清理是针对变量(变量名)的
- 引用计数是针对值(变量值)的
- 标记清理比引用计数更常用,也更好
回收周期
垃圾回收程序周期运行,当垃圾累积到一定数量会影响性能,因此回收周期(时间调度)很重要。
回收频率太低会导致垃圾积压,频率太高会到处小内存片泛滥。
IE7后,JavaScript引擎的垃圾回收程序为动态改变分配变量、字面量或数组槽位等阈值。
-
设一个初始阈值。
-
如果有一次回收内存不到已分配的15%,则阈值会翻倍。
-
如果有一次回收内存达到85%,则阈值重置为默认值。
(有点像网络拥塞控制的慢开始和快重传)
内存管理
手动释放内存
JavaScript
是自动回收内存的,但也可以通过解除引用的方式手动释放内存。
如果数据不再必要,那么把他设置成
null
,从而释放引用
这种方法最适合全局变量,因为局部变量在超出作用域会被自动接触引用,而全局变量会一直存在直到浏览器关闭
注:解除引用不是立即回收相关内存,而是在下一次垃圾回收的时候释放
多用let和const
let和const以块级作用域为单位,比var更有助于被回收
善用隐藏类
能够共享相同隐藏类的对象性能更好。
算是属于V8JavaScript引擎(Chrome使用这个引擎)的特性。
通过下面这个例子来解释:
function Article(){
this.title="title";
}
let a1=new Article();
let a2=new Article();
这时候a1
和a2
同属于一个类(Article
)
a2.author='jake';
这时候a1
和a2
还是同属于一个类(Article
),但他们不属于同一个隐藏类。其中,a2
的隐藏类比a1
多了一个author
属性
再看下例:
function Article(author){
this.title="title";
this.author=author;
}
let a1=new Article();
let a2=new Article("jake");
同样的,此时2者属于同一个类,也属于同一个隐藏类
delete a1.author;
现在,a1
和a2
又不属于同一个隐藏类了。
能够共享隐藏类的对象性能更好,即尽可能少的制造隐藏类
因此,我们可以把delete a1.author
改成下面这样
a1.author=null;//用这个代替delete
对象池
开发者无法控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。
-
**标准1:**对象更替速度。
如果很多对象初始化,然后一下子又都超出了作用域(不再用到),那么浏览器更频繁地垃圾回收
function addVector(){ let result=new Vector(); result.x=1; result.y=1; return result; }
如上面这个例子,如果多次调用
addVector()
,那么Vector
的实例对象被频繁的创建,又很快超出作用域,生命周期很短。垃圾回收程序会发现此处对象更替速度快,则会更频繁地安排垃圾回收。解决:不要频繁的创建对象,可以通过对象池来实现。
-
**对象池:**创建一个对象池,用来管理一组可回收的对象
//vectorPool:假设这是一个已经创建好的对象池对象,先不管是怎么实现的 let v1=vectorPool.allocate();//从对象池中取一个vector对象 let v2=vectorPool.allocate(); console.log(addVector(v1)); console.log(addVector(v2)); vectorPool.free(v1); vectorPool.free(v2);//用完把result对象还给对象池 v1=null; v2=null; function addVector(result){//函数改成这样 result.x=1; result.y=1; return result; }
这样垃圾回收检测就不会发现有对象更替,也就不会频繁的垃圾回收。
一般对象池都是用数组实现的,JavaScript的数组大小是动态可变的,Array.push()
操作有可能会导致不必要的垃圾回收,因此最好一开始就初始化创建一个大小够用的数组作为对象池。
原话是这样的,但不是很理解。
let list=new Array(100); let vector=new Vector(); list.push(vector);
引擎会删除大小为100的数组,再创建一个大小为200的数组。垃圾回收程序看到这个删除操作,可能会跑过来进行一次垃圾回收。
不知道这个是不是和C++
的capacity一样,当数组长度超过capacity
时,加长capacity
,然后之后的push
就不需要再花大力气分配新的内存了。但我看JavaScript好像并不是这样,所以不是很理解,以下是CSDN中说的
JavaScript据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现
内存泄漏
JavaScript
的内存泄漏大部分是由不合理的引用导致的。
-
意外声明全局变量是最常见也是最容易修复的内存泄漏
function setName(){ name='jake'; }
这里
name
一不小心定义在了全局作用域(window
),即使setName
的上下文执行完毕不再需要,也不会释放解决:只要在
name
前面加上var
、let
、const
即可。(最好不要用var
) -
定时器可能会导致内存泄漏。(本质上是闭包引起的)
let name = 'Jack'; setInterval(() => { console.log(name); },100);
只要定时器一直运行,回调函数中引用的
name
就会一直占内存。因而就不会清理外部变量(外面那个name
) -
关于闭包的泄漏,在下面会详细讲。其实节流和防抖函数也是利用这个,使计时器一直不被回收
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数
解析一下上面这句话:
-
引用:这个引用很关键,闭包不仅仅是函数内的函数,还有个重要条件是引用了另一个函数作用域的变量
function outer() { let name = 'jack' return function() {// 这是一个闭包 return name; } } function outer2() { return function() {// 这不是 let name = 'joker'; return name; } }
-
另一个函数域:指的一般是外部域。因为只有外层的作用域的变量是可以被引用的(变量对象在作用域链后端可以被找到)
-
的函数:指的是里面那层函数,上面例子中被
return
的函数是闭包,而不是外层的outer
闭包的内存泄漏
看以下例子
function outer() {
let name = 'jack'
return function() {// 这是一个闭包
return name;
}
}
let method = outer();// 通过引用outer的返回值定义一个method函数
method(); // 执行method函数
在定义let method = outer();
这段代码,代码执行流进入outer
的函数作用域,执行完毕后本来应该释放局部变量name
,但因为返回了一个匿名函数(闭包),而这个匿名函数对name
有所引用(需要用到),因此name
实际上与这个匿名函数共生死
而这个匿名函数(闭包)作为返回值被method
引用,又导致匿名函数与method
绑定在了一起,此时method
→闭包→name
,只要箭头左侧的变量/值存在,右侧的就不会被释放(反之不会)。
在这个例子中,method
因为是全局变量,因此直到浏览器窗口被关闭前都不会被销毁,因此只要method
不被别的值覆盖,name
也会一直在内存中。
注:匿名函数≠闭包,只是这个例子中是同一个
不过我们可以减少闭包的内存泄漏,如下例
function outer(obj){
return function(){
return obj.a;
}
}
let temp=outer({'a': 1});
这里闭包引用了obj.a
,从而使得obj
无法释放。当obj
大到一定程度的时候,会严重影响性能,而我们仅仅是需要obj.a
的属性,得不偿失。
可以改成下面这样:
function outer(obj){
let a = obj.a;
obj = null;
return function(){
return a;
}
}
let temp=outer({'a': 1});
这样一来,当执行到obj = null
时,obj
会被解除引用(本质上是解除了obj
对其他变量的引用,而obj.a
由于还存在被a
的引用,不会被释放),在下一次垃圾回收时会被释放。而a
则会一直存在在内存中,但大大降低了内存的泄漏。
闭包的this指向
如果闭包不是使用箭头函数定义,那么其this
会在运行(被调用)时绑定到执行函数的上下文(调用闭包函数的对象的作用域)。也就是说,这种情况下,闭包的this
指向调用该闭包函数的对象。即:
- 如果在全局函数调用:
- 非严格模式下:
this
等于(指向)window
- 严格模式下:
this
为undefined
- 非严格模式下:
- 如果作为某个对象的方法调用:
this
等于(指向)这个对象
如下例:
window.color = 'red';
let obj = {
color: 'blue',
getColor() {
return function() {
return this.color;
}
}
}
console.log(obj.getColor()());// red
// 相当于
// let c = obj.getColor();
// c();
这里解析一下:
-
闭包是函数声明式定义,非箭头函数,因此与就看谁调用的了
-
在
obj.getColor()()
这块,我们可以把他看成是以下2行代码let c = obj.getColor(); // 先对返回的闭包函数进行一个赋值 c(); // c()实际上是window.c()
这样就很容易看出,闭包是在全局对象上被调用的
那么该如何通过闭包调用到obj.color
呢?
每个函数在被调用时会自动创建2个变量:this
和arguments
,但内部函数永远无法直接访问外部函数的这2个变量。因此我们只能通过创建一个临时变量引用来保存这2个变量的值,如下:
window.color = 'red';
let obj = {
color: 'blue',
getColor() {
let self = this;
return function() {
return self.color;
}
}
}
console.log(obj.getColor()());// blue
防抖、节流
防抖和节流是闭包的一个很经典的例子,具体的防抖节流介绍这里就不详细讲了,就列举几个例子
// 节流函数(计时器版)
function throttle(func, wait) {
var timeout;
return function() {// 这是一个闭包
var context = this;
var args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
这里在debounce
函数域中定义了计时变量timeout
,
- 为了阻止
timeout
被自动释放,使用闭包来对其引用。
计时器版也是如此
function throttle(func, wait) {
var previous = 0;
return function () {
var now = Date.now();
var context = this;
var args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
主要参考《JavaScript高级程序设计》,由于本人水平有限,可能会有理解不到位的地方,欢迎各位指正