一、基本类型和引用类型
ECMAScript中包含有两种不同数据类型的值:
- 基本类型值:简单的数据段。基本数据类型有:number、string、boolean、null、undefined
- 引用类型值:可能由多个值构成的对象
对于引用类型,我们可以为引用类型动态地
添加属性。但是对于基本类型,是不能添加属性的(尽管这么做不会报错,但是没有任何效果),如:
var person = new Object;
person.name = 'RuphiLau';
person.name; // RuphiLau
var name = 'Ruphi';
name.age = 18;
name.age; // undefined
1、变量值的复制
对于基本类型的变量,我们在执行复制操作的时候。会在内存空间中复制一块新的区域,所以基本类型的变量进行复制后,所得到的是一个副本。被复制变量和复制出的变量,是彼此独立的,不会相互影响。如:
var a = 1;
var b = a;
这个过程如:
而对于引用类型,那么情况就不一样了。对于引用类型,在执行复制的时候,复制的也是一个值,只不过,这个值不是对象的内容。而是一个指向对象的指针
,对象的内容,实际上是存储在堆内存中的,复制对象,只是创建了一个指向同一块堆内存的新的指针。所以,两个变量的操作,会互相影响。这个过程如:
var a = new Object;
var b = a;
b.name = 'Ruphi';
a.name; // Ruphi
2、传递参数
在JS中,所有函数的参数,都是按值传递
的。即,当把函数外部的值复制给函数内部的参数的时候,和发生在变量之间的复制是一样的:基本类型值的传递和基本类型变量的复制一样,引用类型值的传递,和引用类型变量的复制一样。
有以下例子:
function fn(x) {
x += 10;
return x;
}
var count = 20;
var result = fn(count);
count; // 还是20,没变化
result; // 30
这说明,对于基本类型,变量值的传递是独立的,互不相影响的。再看如下例子:
function fn(o) {
o.name = 'Ruphi';
}
var obj = new Object;
fn(obj);
obj.name; // Ruphi
这难道是说明,引用类型的值,是按引用传递的呢?其实不是的。
在这里,当我们传入实参obj的时候,会创建一个新的内存单元,然后参数o的值,就会拷贝obj的值(即对同一块对象内存单元的指针),所以本质上,还是按值传递
(只不过,这个值是一个指针),如:
其实,我们可以通过以下例子来佐证:
function fn(object) {
object = new Object;
object.name = 'Ruphi';
}
var obj = new Object;
fn(obj);
obj.name; // undefined
在函数内,我们把实参object指向了另一个新创立的对象,然后对这个对象添加name
属性。假如对引用类型的数据,是引用传递的话,那么,应该是发生如图所示的情况:
也就是object和obj是同一块内存单元,object只是obj的别名。那么,这时候我们让一个新创建的对象赋给object的时候,obj的内容也应该发生改变,那么执行object.name = 'Ruphi'
后,obj也应该获得name
这一属性。但是实际上,并没有,obj.name
的执行结果是undefined
。这就说明,引用类型在函数内也是按值传递的。当我们新建一个对象赋给object后,object这个指针就断开了原来对obj所指向的那一块堆内存的引用,而指向了新建的对象的堆内存,所以自然的,两个对象是独立的,就不会互相影响。
二、执行环境与作用域
1、理解作用域链
执行环境,是JavaScript中最为重要的一个概念。
- 每一个执行环境,都有一个与之关联的 变量对象(VO,Variable Object),VO中保存着环境中所有的变量和函数。我们无法直接访问VO,但是解析器需要使用到它
全局执行环境,是最外围的一个执行环境。全局执行环境,会因ECMAScript实现时的宿主环境不同而不同。如在浏览器环境中,全局执行环境是window对象
,而在NodeJS中,全局执行环境是global对象
,所有全局的变量、函数都是作为全局对象的属性和方法所创建的。 - 执行环境中的代码执行完毕后,该执行环境就会被销毁,执行环境中保存的变量、函数定义也随之销毁
- 每个函数都拥有一个
执行环境
,当执行流进入一个环境后,函数的环境就会被推入环境栈
中,而函数执行完毕后,环境栈
就会弹出其环境,把控制权交给先前的执行环境 - 每一个执行环境,都会创建一个
作用域链(SC,Scope Chain)
。作用域链的作用是:保证对执行环境有权访问的变量、函数的访问是有序的,只能向上访问,而不能向下访问
。SC的最前端,保存的是当前执行代码所对应的环境的VO。如果执行环境是一个函数,那么就将该函数的活动对象(AO,Activation Object)作为VO。对AO而言,它一开始只包含一个变量(arguments对象) - SC中的下一个VO来自于包含环境,再下一个VO来自于下一个包含环境,如此类推一直延续到全局执行环境(全局执行环境是SC的最后一个对象)
在有了SC后,标识符解析就可以沿着这个SC一级一级地搜索。标识符的查找,从SC的前端开始,一直逐级地向后查找,直到找到标识符为止(如果找不到,就会报错)。如:
var color = 'blue';
function fn() {
color = 'red';
}
fn();
color;
例子中,fn()的SC包含了:fn的AO(保存着arguments对象)、全局环境的VO。在查找color
这一标识符的时候,首先在AO中查找,因为没有找到,所以就沿着SC读取到了全局环境的VO,在VO就中找到了color
这一标识符。
再如以下的例子:
var a = 1;
function fn() {
var b = 2;
function foo() {
var c = 3;
b = a;
a = c;
}
foo();
}
fn();
以上代码,涉及了三个执行环境:全局环境、fn的局部环境、foo的局部环境。每一个执行环境,都有自己的VO。
- 在全局环境中,有一个变量
a
和一个函数fn
,它的SC为:全局环境的VO - fn的局部环境中,有一个变量
b
和一个函数foo
。但是它可以访问到全局环境的a
变量。因为fn的SC为:fn的AO -> 全局环境的VO - foo的局部环境中,有一个变量
c
。但是它可以访问到fn的b
变量、全局的a
变量。此时,foo的SC为:foo的AO -> fn的VO -> 全局环境的VO
在搜索的时候,总是会沿着SC搜索,当在当前VO中找不到对应的标识符的时候,就会寻找下一个VO。但是,在作用域链中,只能是往父级VO查找,不能往子级VO查找(在SC中是往后查找,而不是往前查找)
2、延长作用域链
在JS中,有两个语句,可以在SC的前端临时加入一个VO,该VO会在代码执行后被移除。这两个语句就是:
try-catch
中的catch
块with
语句
以with语句为例子
,如:
function buildUrl() {
var qs = '?debug=true';
with(location) {
var url = href + qs;
}
return url;
}
在这里,with接收了location
对象,于是就创建了一个VO,VO中包含了location
对象中的所有属性和方法,所以在访问href
的时候,就可以在当前VO中找到。而在访问qs
的时候,当前VO中找不到,就需要在SC中往后查找,于是就在buildUrl的VO中找到了。然后,with语句内部又定义了一个url
变量,url
变量因而成为了函数执行环境的一部分,被添加到了AO中,所以可以作为函数的返回值而返回。
3、没有块级作用域
在ES6之前,是没有块级作用域的说法的。能够创建作用域的,就只有函数。所以,会有以下的情况发生:
if(true) {
var color = 'blue';
}
color; // blue,因为没有块级作用域
for(var i=0; i<5; ++i) {}
i; // 5
三、垃圾回收机制
在JavaScript中,无需开发人员手动管理内存。执行环境便会自己负责追踪并管理内存,而这种管理内存机制的实现,主要是通过垃圾回收机制
来实现的。垃圾回收机制的基本原理为:找出不再使用的变量,回收其内存。
在JavaScript中,主要有两种垃圾回收机制:
1、标记清除
标记清除,是目前JavaScript实现中广泛采用的方法。基本原理为:当一个变量进入环境的时候,就将该变量标为“进入环境”,而当变量离开环境的时候,就把变量标为“离开环境”,具体而言,可以一开始就对所有的变量打一个标记,然后去除掉环境中的变量以及被环境引用的变量,最后那些还存在标记的变量,就可以被回收了。
目前,IE、Firefox、Opera、Chrome、Safari的JavaScript实现所采用的垃圾回收机制,都是标记清除。
2、引用计数
引用计数,就是当一个变量声明了并将一个引用类型赋给该变量时,引用次数便为1。当同一个值又赋给其他变量,那么引用次数就+1,如果包含这个值引用的变量指向了其他的值,那么引用次数-1。如此一来,当引用次数变为0的时候,就可以被垃圾回收机制给回收。但是引用计数,会存在循环引用
问题,从而导致内存泄漏。如:
function foo() {
var a = {};
var b = {};
a.x = b;
b.y = a;
}
在IE中,BOM和DOM并不是原生的JavaScript对象,而是使用COM对象的形式实现的,而COM对象采用的垃圾收集机制,便是引用计数。所以,在IE中设计COM对象的时候,就存在循环引用
问题。解决办法:使用完成后,手动断开引用:
function foo() {
var a = {};
var b = {};
a.x = b;
b.y = a;
// ...
a.x = null;
b.y = null;
}
四、管理内存
优化内存占用,有一个很好的方式,即为解除引用
。所谓解除引用,是为:会执行中的代码只保留必要的数据,一旦数据不再可用,最好将值设为null
来释放引用。
但是,解除引用并不是意味着马上执行GC,而是让值脱离执行环境。下一次GC触发时,得以回收