执行环境
- 执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个变量对象。解析器在处理数据的时候,会在后台使用它。在所有的代码执行完毕后,执行环境就会被销毁。
- 全局执行环境是最大的一个执行环境,在web浏览器中,window被认为是最大的执行环境,所有的变量和函数都是作为window对象的属性和方法存在的。当应用程序退出(关闭网页或者浏览器),window就会被销毁。
- 每个函数也都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中,而在函数执行完之后,栈会将其环境弹出,恢复到上一个执行环境中。
作用域链(scope chain)
- 当代码在一个环境中执行时,会创建变量对象的一个作用域链。用途:保证对执行环境有权访问的所有变量和函数的有序访问。
- 作用域链的前端,始终都是当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其活动对象(activation object)作为变量对象,活动对象最开始的时候只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。全局执行环境中的变量始终都是作用域链中的最后一个对象。
- 标识符的解析是沿着作用域链一级一级的搜索标识符的过程。
- 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
var color = 'red';
function changeColr(){
var another = 'blue';
function swapColpr() {
var temp = 'yellow';
console.log(color); //red 可以访问祖先执行环境
console.log(another); //blue 可以访问父执行环境
console.log(temp); //yellow 可以访问自身执行环境
}
swapColpr();
console.log('------------------');
console.log(color); //red 可以访问父执行环境
console.log(another); //blue 可以访问自身执行环境
console.log(temp); // error not defined 不能访问子执行环境
}
console.log('------------------');
console.log(color); //red 可以访问自身执行环境
console.log(another); //error not defined 不能访问子执行环境
console.log(temp); //error not defined 不能访问子执行环境
changeColr();
延长作用域链
- 原因:有些语句可以在作用域链的前端临时增加一个变量,该变量对象会在代码执行后被移除。所以通过此方法可以延长作用域链。
- 方法:当执行流进入到下列任何一个语句时,作用域链就会被延长
-
try-catch
语句的catch块
。会创建一个新的变量,其中包含抛出错误对象的声明。 -
with
语句。会将指定对象添加到作用域链中。
function bulidUrl(){
var qs = "debug = true";
with(location){
var url = href + qs;
}
return url
}
var result = bulidUrl();
console.log(result);
TIM图片20190118103029.png
以上代码假设with不能延长作用域链的话,那么url就是with函数内的局部变量,他的父执行环境bulidUrl是无法访问的,那么return url就会报错,但是with可以延长作用域链,他接收到的是location对象,因此其变量对象中就包含了location对象所有的属性和方法,并且把这个变量对象添加到作用域链的前端,当在with语句中引用变量href时(实际上引用的是location.href),可以在当前执行环境的变量对象中找到,当变量引用qs时,引用的是在bulidUrl()中定义的那个变量,至于with语句内部,则定义了一个名为url的变量,因此url就成了函数执行环境的一部分,可以作为函数值被返回。
没有块级作用域
if(true){
var color = "blue";
}
console.log(color)
TIM图片20190118103912.png
在其他强类型语言,例如C语言中,color是定义在if语句内的变量,if是具有块级作用域的,当if执行完,color就会被销毁,所以最后输出undefined,但是在JavaScript语言中,if语句中的变量声明会将变量添加到当前的执行环境(此时当前的执行环境就是全局执行环境)中。for语句也是这样的道理。
垃圾收集
JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存,所需内存的分配以及无用内存的回收完全实现了自动化管理。
原理:垃圾回收器按照固定的时间间隔找出那些不在继续使用的变量,然后释放其占用的内存。
- 实现方式:
- 标记清除
先给即将使用的变量标记---清除标记---标记使用后的变量,便于删除 - 引用计数:跟踪标记记录每个值被引用的次数。
当声明了一个变量a并且将一个引用类型的值Q赋值给a,Q的引用就+1,如果Q又被b引用,Q的引用在+1,此时为2。当a不引用Q而引用T的时候,Q的引用就-1,此时Q的引用为1,当Q的引用为0时,就将Q占用的内存空间收回来。 - 引用计数的缺点:循环引用
function fn(){
var obj1 = new Object();
var obj2 = new Object();
obj1.someOtherObject = obj2;
obj2.anotherObject = obj1;
}
例如以上代码,obj1通过someOtherObject引用obj2,obj2通过anotherObject引用obj1,那么obj1和obj2的引用都是2,永远不会变成0,所以使用引用计数永远无法回收,假设这个函数被重复多调用,那么就会导致大量的内存得不到回收。
在IE浏览器中,为了避免DOM对象和JavaScript原生对象之间循环引用问题,最好在不使用它们的时候,手动断开它们之间的联系。
myObject.element = null;
element.somObject = null;
设置为null就断开了它们之间的联系,垃圾回收机制就会对他们进行内存回收。
原代码:
/*原生对象与DOM对象是循环使用的*/
var element = document.getElementById('some_element'); //DOM对象
var myObject = new Objrct(); //JavaScript原生对象
myObject.element = element; // 原声对象与DOM对象的联系
element.someObject = myObject; // DOM对象与原声对象的联系
为了解决上诉问题,IE9把DOM和BOM对象都转成了真正的JavaScript对象。
- 性能问题
垃圾收集器是周期性运行的,确定垃圾收集的时间间隔是一个非常重要的问题,JavaScript垃圾回收机制:触发垃圾收集的变量分配,字面量和数组元素的临界值是动态修正的。
在IE中调用window.ClooectGrabage()方法
会立即执行垃圾收集。
在Opera7 + 版本中,调用window.opera.collect()
会启动垃圾回收机制。
管理内存
分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少,目的:防止运行JavaScript的网页耗尽全部系统内存造成系统崩溃。内存限制的问题不仅会影响给变量分配内存,同时还会影响调用栈以及一个在线程中能够同时执行的语句数量。
- 优化内存的方法:解除引用
为执行中的代码保存必要的数据,一旦数据不使用,通过将其值设置为null来释放其引用。
解除引用不意味着自动回收该值所占用的内存,解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行的时候将其收回。
function fnname){
var person = new Object();
person.name = name;
return person; //person为局部变量,当函数执行完毕后,自动离开执行环境,不需要设置null。
}
var resut = fn('liqi');
resut = null //手动解决resut的引用
以上内容小结:
JavaScript变量可以用来保存两种类型的值:基本类型和引用类型。基本类型的值包含5种:undefined,null。number,Boolean,string。基本类型与引用类型具有以下特点:
- 基本类型的值在内存中占据固定大小的空间,因此被保存在栈内存中;
- 从一个变量向另外一个变量copy基本类型的值,会创建这个值的一个副本;
- 引用类型的值是一个对象,保存在堆内存中;
- 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
- 从一个变量向另一个变量copy引用类型的值,实际上copy的是指针,因此两个变量最终指向同一个对象;
- 确定一个值是哪种基本类型可以使用
typeof
操作符,而确定一个值是哪种引用类型可以使用instanceof
操作符。
所有变量都存在与一个执行环境中(也成为作用域),这个执行环境决定了变量的声明周期,以及那一部分代码可以访问其中的变量,以下是关于执行环境的总结: - 执行环境有全局执行环境(也成为全局环境)和函数执行环境之分;
- 每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链;
- 函数的局部环境不仅有权访问函数作用域中的变量,还可以访问其父环境以及全局环境;
- 全局环境只能访问在全局下定义的变量和函数,不能访问局部环境中的;
- 变量的执行环境有助于确定如何释放内存。
JavaScript是一门具有自动垃圾回收机制的编程语言,开发人员不必关心内存分配和回收的问题,垃圾回收机制总结: - 离开作用域的值将自动标记为可以回收,因此在垃圾收集期间被删除;
- 标记清除是目前主流的垃圾回收法,这种算法的思想是给当前不使用的值加上标记,然后在回收其内存;
- 另一种垃圾回收算法是引用计数,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不在使用这种算法,但在IE浏览器中访问非原生JavaScript对象时,这种算法仍然可能导致问题;
- 当代码中存在循环引用时,引用计数法就会存在问题;
- 解除变量的引用不仅有助于消除循环引用现象,而且对于垃圾收集也有好处,为了确保有效的回收内存,应该及时解除不在使用的全局对象,全局对象属性以及循环引用变量的引用。
代码解析示例1
- 代码解析示例
var x = 10
bar()
function foo() {
console.log(x)
}
function bar(){
var x = 30
foo() // 输出什么
}
当声明的时候,会出现下面的情况
globalContext = {
AO: {
x: 10
foo: function
bar: function
},
Scope: null
}
//声明 foo 时 得到下面
foo.[[scope]] = globalContext.AO
//声明 bar 时 得到下面
bar.[[scope]] = globalContext.AO
注意: 在当前的执行上下文内声明的函数,这个函数的[[scope]]就执行当前执行上下文的 AO
当调用 bar() 时, 进入 bar 的执行上下文
barContext = {
AO: {
x: 30
},
Scope: bar.[[scope]] //globalContext.AO
}
当调用 foo() 时,先从 bar 执行上下文中的 AO里找,找不到再从 bar 的 [[scope]]里找,找到后即调用
当调用 foo() 时,进入 foo 的执行上下文
fooContext = {
AO: {},
Scope: foo.[[scope]] // globalContext.AO
}
所以 console.log(x)是 10
代码解析示例2
- 代码解析示例
var x = 10;
bar() // 输出什么
function bar(){
var x = 30;
function foo(){
console.log(x)
}
foo();
}
进行声明的时候,会出现
globalContext = {
AO: {
x: 10
bar: function
},
Scope: null
}
//声明 bar 时 得到下面
bar.[[scope]] = globalContext.AO
注意: 在当前的执行上下文内声明的函数,这个函数的[[scope]]就执行当前执行上下文的 AO
当调用 bar() 时, 进入 bar 的执行上下文
barContext = {
AO: {
x: 30,
foo: function
},
Scope: bar.[[scope]] //globalContext.AO
}
//在 bar 的执行上下文里声明 foo 时 得到下面
foo.[[scope]] = barContext.AO
当调用 foo() 时,先从 bar 执行上下文中的 AO里找,找到后即调用
当调用 foo() 时,进入 foo 的执行上下文
fooContext = {
AO: {},
Scope: foo.[[scope]] // barContext.AO
}
所以 console.log(x)是 30
相关概念
- 执行上下文 executionContext
- 活动对象 AO
- Scope 属性
- 执行顺序
笔试题
var a = 1;
function fn(){
console.log(a); //undefined
var a = 5;
console.log(a); // 5
a++; //a = 6
var a; // a= 6
fn3();
fn2();
console.log(a); // 20
function fn2(){
console.log(a); //6
a = 20;
}
}
function fn3(){
console.log(a) //a = 1
a = 200;
}
fn();
console.log(a); //200
补充说明
- JS普通变量的作用域是词法作用域:词法分析,不需要执行就可以知道是什么。
- 词法作用域只能确定他是哪一个,不能确定他具体的取值。
-
this
不是词法作用域,必须调用才能确定this。 - 就近原则,是作用域进,不是代码行数少。
网友评论