JavaScript中变量、作用域和内存问题 | StriveZs的博客

JavaScript中变量、作用域和内存问题

JavaScript学习笔记。 Github同步链接: click here

变量、作用域和内存问题

基本类型和引用类型的值

JavaScript变量可能包含两种不同的数据类型的值:基本类型值和引用类型值。

  • 基本类型值指的是简单的数据段
  • 引用类型值指的是那些可能由多个值构成的对象

引用类型的值是保存在内存中的对象。JavaScript不允许直接访问内存中的位置,即不能直接操作对象的内存空间。

动态的属性

定义基本类型值和引用类型值的方法是类似的:创建一个变量并为该变量赋值。但是当这个值保存到变量中以后,对不同类型值可以执行的操作是不同的。对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法,如下例子:

1
2
3
var person = new Object();
person.name = "Niko";
alter(person.name);

以上代码创建了一个对象并将其保存在了变量person中,然后我们为该对象添加了一个名为name的属性,并将字符串“Niko”赋给了它。这个person就属于引用类型。而对于基本类型则无法添加属性,比如:

1
2
var name = "Niko";
name.age = 13; // 会报错

引用类型一般指的是对象,基本类型值的是之前提到的基本数据类型

复制变量值

除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型时,也存在不同。如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到新的变量分配的位置上。

1
2
var num1 = 5;
var num2 = num1;

figure.1

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象,因此,改变其中一个变量,就会影响另一个变量

figure.2

传递参数

JavaScript中所有的函数的参数都是按值传递的。即把函数外部的值赋值给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制,引用类型的值,则如同引用类型变量的值一样。

检测类型

要检测一个变量是不是基本数据类型,使用typeof操作符是最佳的工作。如果变量的值是一个对象或null,则typeof操作符会如下返回object:

1
2
3
4
5
6
7
8
9
10
11
12
13
var s = "123";
var b = true;
var i = 22;
var u;
var n = null;
var m = new Object();

typeof(s); // string
typeof(b); // number
typeof(i); // boolean
typeof(u); // undefined
typeof(n); // object
typeof(m); // object

在检测基本数据类型时,typeof十分好用,但是在检测引用类型的值时,这个操作符用处则不大。为了知道某个值是什么类型的对象,我们可以使用instanceof操作符:

1
2
3
4
result = variable instanceof constructor;

person instanceof Object; //变量person是Object类型的对象吗
colors instanceof Array; //变量colors是Array吗?

根据规定,所有引用类型的值都是Object的实例。因此,在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回True,如果用instanceof检测基本类型的值,则始终会返回false。因为基本类型不是对象。

执行环境及作用域

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的一个执行环境。根据JavaScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完之后,该环境会被销毁,保存在其中的所有变量和函数定义也会随之销毁。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。标识符解析是沿着作用域链一级一级地搜索标识符的过程,搜索过程始终从作用域链的前段开始,然后逐级地向后回溯,直到找到标识符位为止。

1
2
3
4
5
6
7
8
9
10
11
12
var color = "blue";
function changeColor(){
var temp = "yellow";
if (color == "blue"){
color = "red";
}
else{
color = "blue";
}
}

changeColor();

上述代码中changColor()的作用域链包含两个对象:他自己的变量对象(temp)和全局环境的变量对象(color)。可以在函数内部访问全局变量color。
同理看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var color = "blue";
function changeColor(){
var temp = "yellow";
if (color == "blue"){
color = "red";
}
else{
color = "blue";
}
}

changeColor();
alter(color); // 显示red
alter(temp); // 会报错

这里temp为changColor的局部变量,只可以在函数的局部环境中访问,而不能在全局环境中访问,而color则是全局变量,既可以在全局环境中访问,又可以在局部环境中访问。

延长作用域链

执行环境类型总共有两种——全局和局部,但是还是有其他办法来延长作用于链。当执行流进入下列任何一个语句时,作用域链就会得到加长:

  • try-catch语句的catch块
  • with语句

这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中,对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

1
2
3
4
5
6
7
function buildUrl(){
var qs = "?debuge=true";
with(location){
var url = href + qs;
}
return url;
}

对于with语句内部,定义了一个url变量,这个url就会成为函数执行环境的一部分,所有可以作为函数的值被返回。

没有块级作用域

JavaScript没有块级作用域,比如下面代码:

1
2
3
4
if (true){
var color = "blue";
}
alter(color);

在C++、C和Java中,color会在if语句执行完毕后被销毁。但是JavaScript中,if语句中的变量声明会将变量添加到当前的执行环境(上文的代码指的是全局环境中)。在使用for语句时一定要记住这一点差异。因为要定义i:

1
2
3
4
for (var i=0; i<10; i++){
doSomething(i);
}
alter(i); // i=10

这里i在for中被声明后,当退出for的块之后,这个变量i还是会存在于外部的执行环境中的。这里适合C++、Java等语言不同的地方。

声明变量

使用var声明的变量会自动被添加到最接近的环境中,在函数内部,最接近的环境就是函数的局部环境。在with语句中,最接近的环境是函数环境。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。

1
2
3
4
5
6
function add(num1,num2){
var sum = num1 + num2;
return sum;
}
var result = add(10,20);
return sum; // 会报错,因为sum在函数环境中

但是如何在函数中sum省略了var声明,则sum会被自动添加到全局环境中。但是不建议这样使用!

1
2
3
4
5
6
function add(num1,num2){
sum = num1 + num2; // 声明未使用var,sum被添加到全局环境中
return sum;
}
var result = add(10,20);
return sum; // 30

这种声明方式不被推荐使用,应该按照严格的方式来声明变量。

查询标识符

当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前段开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止,变量就绪,如果在局部环境中没有找到变量名,则继续沿着作用域链向上搜索。搜索过程将会一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个变量,则意味着该变量未声明。(先找局部环境,局部环境没有再找全局环境)

1
2
3
4
5
var color = "blue";
function getColor(){
return color;
}
alter(getColor()); // blue

上述过程,因为函数环境中没有blue,因此搜索了全局环境中的color。

1
2
3
4
5
6
var color = "blue";
function getColor(){
var color = "red";
return color;
}
alter(getColor()); // red

上述过程,因为函数局部环境中有red,因此不需要在搜索全局环境了。

垃圾收集

JavaScript具有自动垃圾收集机制,即执行环境会负责管理代码执行过程中使用的内存。垃圾收集机制的原理:找出那些不再继续使用的变量,然后释放其占用的内存。因此垃圾收集机制会按照固定的时间间隔周期性的执行操作。

标记清除

JavaScript中最常用的垃圾收集方式是标记清除。当环境进入环境时,就将这个变量标记为进入环境。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为执行流可能要用到它们,当变量离开环境时,则将其标记为离开环境。
可以使用任何方法来标记变量,垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,他们会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,最后垃圾收集器完成内存清楚工作,销毁那些带有标记的值并且回收他们所占用的内存空间。

目前主流的浏览器都是采用这种垃圾收集策略的。

引用计数

另一种不太常见的垃圾收集策略叫做引用计数。引用技术的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。同样对于其他的变量也是类似的加1。而如果包含对这个值引用变量又取得了另外一个值,则这个值的引用次数就减1.当这个值的引用次数变成0之后,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

性能问题

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是十分重要的。后面JavaScript引擎的垃圾收集例程改变了工作方式:触发垃圾收集的变量分配、字面量和数组元素的临界值被调整为动态修正。

内存管理

一般来说,出于安全考虑分配给浏览器的内存数量通常要不分配给桌面应用的内存少。目的是防止运行JavaScript的网页耗尽系统全部内存而导致系统崩溃。
因此确保占用最少内存可以让页面获得更好地性能,一般来说对于不使用的数据,最好将其设置为null来释放其引用,这个做法叫做解决引用。

StriveZs wechat
Hobby lead  creation, technology change world.