1、变量
变量的值的类型:基本类型值和引用类型值两种。
基本类型:Undefined、Null、Boolean、String、Number,这五类基本数据类型的值在内存中占有固定大小的空间,因此保存在栈内存(存放简单数据)中。
引用类型值:是指存放在堆内存中的对象,占用的大小不固定,变量中保存的只是指向对象的一个指针(内存中的一个存储位置,这个位置上存储的是具体的某个对象),指针即内存地址的大小是固定的,因此指针位置可以存储在栈内存中,但具体的对象存储在堆内存中。
基本类型值是“按值访问”,引用类型值是“按引用访问”。
2、复制变量值
复制基本类型值和引用类型值存在差异。
基本类型值:从一个变量向另一个变量复制基本类型值,会将该值复制一份到新变量的位置。因此,两个变量是互不相关的,操作其中一个不会影响另一个。
Var num=10;
Var num2=num;
引用类型值:修改其中一个会影响另一个,两者指向的是同一个对象。
Var obj1=new Object();
Var obj2=obj1;
3、传递参数:
按共享传递 call by sharing
准确的说,JS中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。
var obj = {x : 1}; function foo(o) { o = 100; } foo(obj); console.log(obj.x); // 仍然是1, obj并未被修改为100.
然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。
var obj = {x : 1}; function foo(o) { o.x = 3; } foo(obj); console.log(obj.x); // 3, 被修改了!
对于对象类型,由于对象是可变(mutable)的,修改对象本身会影响到共享这个对象的引用和引用副本。而对于基本类型,由于它们都是不可变的(immutable),按共享传递与按值传递(call by value)没有任何区别,所以说JS基本类型既符合按值传递,也符合按共享传递。
var a = 1; // 1是number类型,不可变 var b = a; b = 6;
据按共享传递的求值策略,a和b是两个不同的引用(b是a的引用副本),但引用相同的值。由于这里的基本类型数字1不可变,所以这里说按值传递、按共享传递没有任何区别。
4、垃圾收集
Javascript采用的是标记-清除策略
三种最基本的垃圾回收算法,
引用计数(Reference Counting)算法:这个可能是最早想到的方法。形象点说,引用计数可以这么理解,房子里放了很多白纸,这些纸就好比是内存。使用内存,就好比在这些纸上写字。内存可以随便使用,但是,有个条件,任何使用一张纸的人,必须在纸的一角写上计数1,如果2个人同时使用一张纸,那么计数就变成2,以此类推。当一个人使用完某张纸的时候,必须把角上的计数减1,这样,一旦当计数变为0,就满足了垃圾回收条件,等在一旁的机器人会立即把这张纸扔进垃圾箱。基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销;同时,还有个最大的问题,这个算法存在一个缺陷,就是一旦产生循环引用,内存就会被泄露。举个例子,我们new了2个对象a和b,这时,a和b的计数都是1,然后,我们把a的一个属性指向b,b的一个属性指向a,此时,由于引用的关系,a和b的计数都变成了2,当程序运行结束时,退出作用域,程序自动把a的计数减1,由于最后a的计数仍然为1,因此,a不会被释放,同样,b最后的计数也为1,b也不会被释放,内存就这么泄露了!
标记-清除(Mark-Sweep)算法:同样是房间和白纸的例子,这次规则有所修改。白纸仍然随便用,并且,一开始,不需要做什么记号,但是用到某个时候,机器人会突然命令所有人停下来,这时,需要每个人在自己仍然需要使用的白纸上做一个记号,大家都做完记号后,机器人会把那些没有记号的白纸全部扔进垃圾箱。正如其名称所暗示的那样,标记-清除算法的执行过程分为“标记”和“清除”两大阶段。这种分步执行的思路奠定了现代垃圾收集算法的思想基础。与引用计数算法不同的是,标记-清除算法不需要运行环境监测每一次内存分配和指针操作,而只要在“标记”阶段中跟踪每一个指针变量的指向――用类似思路实现的垃圾收集器也常被后人统称为跟踪收集器( Tracing Collector )。当然,标记-清楚算法的缺陷也很明显,首先是效率问题,为了标记,必须暂停程序,长时间进行等待,其次,标记清除算法会造成内存碎片,比如被标记清除的只是一些很小的内存块,而我们接下来要申请的都是一些大块的内存,那么刚才清除掉的内存,其实还是无法使用。解决方案,常见的有2种,一是清楚后对内存进行复制整理,就像磁盘整理程序那样,把所有还在使用的内存移到一起,把释放掉的内存移到一起。
第二种方案是不移动内存,而是按大小分类,建立一系链表,把这些碎片按大小连接并管理起来,(4个字节的内存一个链表,8个字节的内存一个链表……)如果我们需要4个字节的内存,就从4个字节的链表里面去取,需要16个字节,就从16字节的链表里面去取,只有到了一定时候,比如程序空闲或者大块的内存空间不足,才会去整理合并这些碎片。
ie对javascript的垃圾回收,采用的就是这种算法。
复制(copying)算法:mark-sweep算法效率低下,由此,又产生了一种新的奇思妙想,我们再把规则换一下:还是房间和白纸的例子,这次我们把房间分成左右2部分,一开始,所有人都在左边,白纸仍然随便用,一定时候,机器人又会叫大家停下来,这次不做记号了,你只要带着你还需要的白纸转移到右边去就可以了(相当于把现有的程序复制一份,无法使用的部分自然不会被复制),那些没用的纸自然就剩了下来,然后机器人会把左边所有的垃圾打扫干净(相当于把原先使用的那一半内存直接清空),下次执行垃圾回收的时候采用同样的方式,只不过这次从右边向左边迁移。这种算法的效率奇高,可惜,对内存的消耗太大,尤其是在1960年,内存可比黄金贵多了,直接砍掉一半的内存,显然是无法接受的。
了解万垃圾回收算法,再来看看IE下为什么会产生内存泄露。
在IE 6中,对于javascript object内部,javascript使用的是mark-and-sweep算法,这点前面也有提到,因此,纯粹的javascript对象的使用,不会造成内存泄露,但是对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用引用计数,这样一来,内存泄露就产生了。这点在犀牛书第八章函数部分有提到。
以下是常见的几种javascript内存泄露的情况:
一、循环引用:
<html> <head> < script language ="JScript"> var myGlobalObject; function SetupLeak() // 产生循环引用,因此会造成内存泄露 { // First set up the script scope to element reference myGlobalObject = document.getElementById("LeakedDiv"); // Next set up the element to script scope reference document.getElementById("LeakedDiv").expandoProperty = myGlobalObject; } </head> <body onload = "SetupLeak()"> <div id ="LeakedDiv" ></div> </body> </html>
我们可以看到,myGlobalObject指向了一个DOM对象,而这个DOM对象的一个属性又指向了myGlobalObject,循环引用出现,内存泄露,其原理如下:
解决方案很简单,在确保属性不再使用后,加入以下代码就可以了:
function BreakLeak(){ // 解开循环引用,解决内存泄露问题 document.getElementById( " LeakedDiv " ).expandoProperty = null ; }
说起来容易,不过当我们程序非常复杂的时候,发现和修改就没有这么容易了。