JS高级程序设计笔记(三)变量、作用域和内存问题

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

引用类型的值是保存在内存中的对象。与其他语言不同,js不允许直接访问内存中的位置,也就是说不能直接操对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。

只能给应用类型值动态的添加属性

var person = new Object();
preson.name = "Nicholas";
alert(person.name);   //"Nicholas"
//创建对象,为对象添加一个名为name的属性,并将字符串值赋值给这个属性。如果对象不被销毁,或这个属性被删除,则这个属性将一直存在。


//我们不能给基本类型的值添加属性,尽管这样不会报错
var name = "Nicholas";
name.age = 27;
alert(name.age);//undefined

复制变量值

从一个变量向另一个变量复制基本类型的值,会在变量上创建一个新值,然后把该值复制到为新变量分配的位置上。

var num1 = 5;
var num2 = num1;

//num1与num2 是完全对立的,相互不影响

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

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name);//"Nicholas"

传递参数

函数的参数传递是按值传递的。

向参数传递基本类型的值时,被传递的值会被复制给一个局部变量。

function addTen(num) {
	num +=10;
	retrun num;
	// body...
}
var count = 20;
var result = addTen(count);
alert(count); //20 没有变化
alert(result); //30

 在像参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化反映在函数的外部。

function setName(obj){
	obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);//"Nicholas"

在以上代码中创建一个对象,并将其保存在了变量person中。然后,这个变量被传递到setName()函数中之后就被复制给了obj。在这个函数内部,obj和person引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj也会按引用来访问同一个对象。于是,当在函数内部为obj添加name属性后,函数外部的person也将有所反映;因为person指向的对象在堆内存中只有一个,而且是全局对象。

function setName(obj)
{
	obj.name = "Nicholas";
	obj = new Object();
	obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name);//"Nicholas"

检测类型

检测是什么类型的对象  result = variable instanceof constructor

如果变量是给定引用类型的实例,那么instanceof操作符就会返回true。

执行环境及作用域

执行环境是js中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。 

全局执行环境是最外围的一个执行环境。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

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

当代码在一个环境中执行时,会创建变量的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象。作用域链中的下一耳光变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标示符解析是沿着作用域链一级一级地搜索标识符的过程。搜索始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止。

var color = "blue";
function changeColor(){
	if(color ==="blue")
	{
		color = "red";
	}
	else
	{
		color = "blue";
	}
}
changeColor();
alert("Color is now"+color);

函数changeColor()作用域链包含两个对象:它自己的变量对象(其中定义着argument对象)和全局环境的变量对象。可以再函数内部访问color,就是因为可以在这个作用域中找到它。

在局部作用域中定义的变量可以再局部环境中与全局变量互换使用,

var color = "blue";
function changeColor(){
	var anotherColor = "red";
	function swapcolor(){
		var tempColor = anotherColor;
		anotherColor = color;
		color = tempColor;
		//这里可以访问color、anotherColor和tempColor
	}
	//这里可以访问color和anotherColor,但不能访问tempColor
	swapColors();
}
//这里只能访问color
changeColor();

延长作用域链

try-catch语句的catch块

with语句

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

function buildUrl()
{
	var qs = "?debug=true";
	with(location){
		var url = href + qs;
	}
	return url;
}

在此,with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的前端。buildUrl()函数中定义了一个变量qs。当在with语句中引用变量href()时(实际上引用的是location.href),可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在buildUrl()中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句内部,则定义了一个名为url的变量,因而utl就成了函数执行环境的一部分,所以可以作为函数的值被返回。

没有块级作用域

var声明的变量会自动被添加到最接近的环境中。如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境。

垃圾收集

js中最常用的垃圾收集方式是标记清除。当变量进入环境时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为"离开环境"。

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

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

引用计数

不常用。跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋值给该变量时,这个值得引用次数就是1.如果同一个值又被赋值给另一个变量,则该值得引用次数加1.相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值得引用次数减一。当这个值得引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存回收回来。

计数引用会到遇到一个很严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。

function problem()
{
	var ObjectA = new Object();
	var ObjectB = new Object();

	ObjectA.someOtherObject = objectB;
	ObjectB.anotherObject = ObjectA;
}

objectA 和 objectB 通过各自的属性相互引用;也就是说,这两个对象的引用次数都是 2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题。但在采用引用计数策略的实现中,当函数执行完后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。假如这个函数被重复多次调用,就会导致大量内存得
不到回收。

性能问题

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

管理内存

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

function problem()
{
	var ObjectA = new Object();
	var ObjectB = new Object();

	ObjectA.someOtherObject = objectB;
	ObjectB.anotherObject = ObjectA;
}

function createPerson(name){
	var localPerson = new Object();
	localPerson.name = name;
	return localPerson;
}
var globalPerson = createPerson("Nicholas");
//手工解除globalPreson引用
globalPerson = null;

解除一个值得引用并不意味着自动回收该值所占的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行的时将其回收。

猜你喜欢

转载自blog.csdn.net/baidu_29474379/article/details/84345203