前端基础进阶(一):内存空间详细图解

ECMAscript 变量可能包含两种不同数据类型的值: 基本类型值 引用类型的值 。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

数据类型:

  • 基本类型值:Boolean、Number、String、Undefined、Null;
  • 引用类型值,也就是对象类型:Object、Array、Function、Date 等;

声明变量时不同的内存分配

  • 基本类型值:存储在栈(stack)中的简单数据段,它们的值直接存储在变量访问的位置。这是因为这些基本类型占据的空间是固定的,所以可以将它们存储在较小的内存区域 - 栈中。这样存储更便于迅速查寻变量的值。
  • 引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在中,否则会降低变量查寻的速度。相反,放在变量的空间中的值是该对象存储在中的地址。地址的大小是固定的,所以把它存储在中对变量性能无任何负面影响。

不同的内存分配机制也带来了不同的访问机制

在javascript中是不允许直接访问保存在堆内存中的对象的,也就是说不能直接操作对象的内存空间。所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。而原始类型的值则是可以直接访问到的。

注意:当复制保存着对象的某个变量时,操作的事对象的引用。但在为对象添加属性时,操作的是实际的对象

复制变量的不同

  • 基础类型值:在将一个保存着基础类型值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
1
2
3
4
5
6
7
8
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
console.log(count); //20 没有变化
console.log(result); //30
  • 引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)
1
2
3
4
5
6
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
console.log(person.name); //"Nicholas"

参数传递的不同

首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。但是为什么涉及到基础类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。

  • 基础类型值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
  • 引用类型值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象呀。或许我这么说了以后你对书上的例子还是有点不太理解,那么请看图吧:

所以,如果是按引用传递的话,是把第二格中的内容(也就是变量本身)整个传递进去(就不会有第四格的存在了)。但事实是变量把它里面的值传递(复制)给了参数,让这个参数也指向原对象。因此如果在函数内部给这个参数赋值另一个对象时,这个参数就会更改它的值为新对象的内存地址指向新的对象,但此时原来的变量仍然指向原来的对象,这时候他们是相互独立的;但如果这个参数是改变对象内部的属性的话,这个改变会体现在外部,因为他们共同指向的这个对象被修改了呀!来看下面这个例子吧:(传说中的call by sharing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var obj1 = {
value:'111'
};

var obj2 = {
value:'222'
};

function changeStuff(obj){
obj.value = '333';
obj = obj2;
return obj.value;
}


var foo = changeStuff(obj1);

console.log(foo);// '222' 参数obj指向了新的对象obj2
console.log(obj1.value);//'333'

可以把ECMAScript函数的参数想象成局部变量

检测类型

如果变量的值是一个对象null,则typeof操作符会返回"object".
通常我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。为此,ECMAScript提供了instanceof操作符;
如果对象是给定引用类型的实例,那么instanceof操作符就会返回true

1
2
3
console.log(person instanceof Object);  //变量person是Object吗?
console.log(colors instanceof Array); //变量colors是Array吗?
console.log(pattern instanceof RegExp); //变量pattern是RegExp吗?

根据规定,所有引用类型的值都是Object的实例。在检查一个引用类型值Object构造函数时,instanceof操作符始终会返回true。

执行坏境和作用域

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。

每个环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它

当代码在一个环境执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

声明变量

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

注意:在编写JavaScript中,不声明而直接初始化变量时一个错误的做法,因为这样可能会导致意外。在严格模式下,初始化未经声明的变量会导致错误。

查询标识符

搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境找到,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域向上搜索。搜索过程将一直追溯到全局环境的变量对象。在全局环境也没找到的话则说明该变量尚未声明。

1
2
3
4
5
6
7
var color = "blue";

function getColor() {
return color;
}

console.log(getColor()); //"blue";

垃圾回收(gc)

JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

变量生命周期

什么叫不再使用的变量?不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后再函数中使用这些变量,直至函数结束(闭包中由于内部函数的原因,外部函数并不能算是结束

一旦函数结束,局部变量就没有存在必要了,可以释放它们占用的内存。貌似很简单的工作,为什么会有很大开销呢?这仅仅是垃圾回收的冰山一角,就像刚刚提到的闭包,貌似函数结束了,其实还没有,垃圾回收器必须知道哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来回收。用于标记无用的策略有很多,常见的有两种方式

1.标记清除

这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等,这些并不重要,重要的是使用什么策略,原则上讲不能够释放进入环境的变量所占的内存,它们随时可能会被调用的到。

垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了,因为环境中的变量已经无法访问到这些变量了,最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

2.引用计数

在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。

性能问题

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。

事实上,在有的浏览器中可以触发垃圾收集过程,但我们不建议这样做。在IE中调用window.CollectGarbage()方法会立即执行垃圾收集。在Opera7及更高版本中,调用window.opera.collect()也会启动垃圾收集例程。

管理内存

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要数据。一旦数据不再可用,最好通过将其值设置为null来释放其引用——这个方法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象属性。局部变量会在它们离开执行环境时自动被解除引用。

1
2
3
4
5
6
7
8
9
10
function createPerson(name) {
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Nicholas");

//手工解除globalPerson的引用

globalPerson = null;

小结

  • 基本类型值和引用类型值具有以下特点:
  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值得一个副本;
  • 引用类型的值是对象,保存在堆内存中;
  • 包含引用类型值得变量实际上包含的并不是对象本身,而是指向该对象的指针;
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
  • 确定一个值是哪种基本类型可以使用typeof操作符,而确定一个值是哪种引用类型可以使用instanceof操作符。所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。