基本类型和引用类型
- js变量可能包含两种不同的类型的值:基本类型值和引用类型值。基本类型值包括Undefined、Null、Boolean、Number和String。引用类型的值是保存在内存中的对象。js不允许直接访问内存中的位置,即不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。
动态属性
- 对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除属性和方法,但是这种操作对基本类型的值是无效的,即使它不会报错。
var person = new Object();
person.name = "Nicholas";
alert(person.name); //"Nicholas"
var name = "Nicholas";
name.age = 27;//不会报错,但是该属性不会保存
alert(name.age); //undefined
复制变量值
- 在从一个变量向另外一个变量复制基本类型和引用类型也存在不同。但复制基本类型时,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。
var num1 = 5;
var num2 = num1;
num1 = 4;
alert(num2);//5 改变num1不影响num2
- 复制引用类型时,同时也会将存储变量对象中的值复制一份放到新分配的内存空间中。不同的是,复制的只是引用(指针),而这两个引用存储的是堆上的同一个对象。
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "miaoch";
alert(obj2.name);//"miaoch"
传递参数
- js中所有函数的参数都是值传递的。也就是说你给一个函数传入引用类型的参数相当于该引用类型变量复制给函数的参数。所以说,传递基本类型参数在函数体内操作是不会影响在函数外部的原来的变量的。而传递引用类型参数是能够改变原来的变量的(其实只是改变了该变量所指向的对象,本身的指针是不会改变的)。看下面的例子:
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
- 在这里将person传递给setName,相当于复制了一份引用给局部变量obj。由于obj指向的对象和person一样,所以最终输出”Nicholas”(局部变量obj在执行后销毁)。但是在这里可能会误解该传递不是按值传递而是按引用传递,下面再给出一个例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "miaoch";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
- 这个例子说明了js函数的确是按值传递(而不是传递一个对象实体,也不是传递person的引用)。
执行环境和作用域
- 执行环境定义了变量或函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象(在web浏览器中,全局执行环境被认为是window对象)。环境中所有定义的变量和函数都保存在这个对象中。当所有代码执行完毕,该环境被销毁,保存的所有定义的变量和函数也随之销毁。(全局执行环境直到应用程序退出–例如关闭网页或浏览器–时才会销毁)
- 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当函数执行完毕会出栈,将控制权交给之前的执行环境。
- 作用域链:保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行代码所在环境的所有变量对象。如果这个环境是函数,则将其活动对象作为变量对象(一开始只有arguments对象)。全局执行环境的变量对象始终都是作用域链中的最后一个对象。
- 延长作用域链,通常执行环境类型总共有两种(全局和局部(函数))。但是以下2个语句能够延长作用域。(在作用域链前端添加一个变量对象)
- try-catch语句中的catch块。(catch语句会添加新的对象(放置被抛出的错误对象的声明))
- with语句。(括号中指定的对象)
//这里留一个疑问:
//var x = 30;
var obj = {
x: 10,
foo: function () {
with (this) {
var x = 20;//此处如果引掉,下面的log会报错
var y = 30;
}
console.log(x);//undefined 这里情况多变,
//以后会回头再来解决。
}
};
obj.foo();
//我回来解决了。这里涉及到变量提升。上面的代码相当于下面代码:
//var x = 30;
var obj = {
x: 10,
foo: function () {
var x;
var y;
with (this) {
//此处this代表调用foo的调用对象也就是obj。
//obj中有x,故obj中的x被修正为20;
//obj中无y,故y被放入foo的局部环境中
x = 20;
y = 30;
}
//此处会先访问foo的局部环境,找到了x,(不再向上查找)
//由于只定义无赋值,故弹出undefined。
//即使全局环境中有var x = 30;此处也不会再向上查找。
console.log(x);
}
};
obj.foo();
- 声明变量:使用var声明的变量会自动被添加到最近的环境中。在函数内部,最接近的执行环境是局部环境。在with语句中,最接近的环境是函数环境,如果初始化变量没用var声明,该变量会自动被添加入全局环境。来看下面这个例子:
//例子1:
function add(num1, num2) {
//用var声明,加入了add的执行环境,结束后被销毁。
var sum = num1 + num2;
return sum;
}
var result = add(10, 20); //30
alert(sum); //causes an error since sum is not a valid variable
//例子2:
function add(num1, num2) {
//不用var声明,加入了全局环境,结束后不会被销毁。
sum = num1 + num2;
return sum;
}
var result = add(10, 20); //30
alert(sum); //30
垃圾收集
- 垃圾收集就是对一些不再需要的变量进行释放。js具有自动垃圾收集机制(和java一致)。垃圾收集器必须跟踪哪个变量有用哪个没用,对于不再有用的变量打上标记,以便将来回收内存。下面是两种标记策略:
标记清除(mark-and-sweep)
- 当变量进入环境,就为这个变量打上标记。当变量退出环境,就会为这个变量去除标记。(或者说是打上类似于’退出’的标记)垃圾收集器在运行的时候会给所有存储在内存中的变量打上标记(打标记的具体方式不重要)。然后,再去掉环境中的变量以及被前者引用的变量的标记。剩下来的那些还有标记的变量将被视为准备删除的变量。这是js中最常用的垃圾收集方式。
引用计数(reference counting)
- 这是另外一种不太常用的垃圾收集策略。简单来说就是所有的对象会存储指向他的指针数量。当不再有指针指向该对象时,改对象内存即可回收。但是这个策略存在一个弊端:如果两个对象直接互相引用,那么这两个对象即使没办法由外部访问了,也无法使他们的引用次数为0。也就没办法回收。如下例子:
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.other = objectB;
objectB.other = objectA;
}
- 如果上述代码采用标记清除策略。objectA和objectB进入环境后,打上了’进入环境’的标记,当函数执行完毕。objectA和objectB都会打上’退出环境’的标记,也就会被垃圾收集器回收。这种方式是不会造成内存泄露的。
但如果采用了引用计数的方式。由于A引用了B,故B的引用次数不为0,那么B将无法回收。同理,B又引用了A,B因为无法回收,这个引用也不会消失,故A的引用次数也不会为0,这样AB都无法回收。当该函数多次被调用,就会造成巨大的内存泄露。正因如此,引用计数策略已经基本不再被使用了(IE中的COM对象(Component Object Model如DOM元素)依旧采用引用计数)。当采用引用计数策略时,可以手动将不再使用的对象对其他对象的引用置为null以避免循环引用的差错。
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.other = objectB;
objectB.other = objectA;
...
objectA.other = null;
objectB.other = null;
}