首页 > WEB开发 > JavaScript开发 > JavaScript中变量的存储方式
2014
11-11

JavaScript中变量的存储方式

基本原理

前面文章提到过,在js中变量包括5中基本类型以及一个复杂数据类型Object,当然常用的函数和数组都是对象。对于基本类型和复杂类型,对应着两种不同的存储方式–栈存储和堆存储。为什么要实现两种存储方式的理由很简单,就是基本类型一旦初始化则内存大小固定,访问变量就是访问变量的内存上实际的数据,称之为按值访问。而对象类型说不定什么时候就会增加自身的大小,内存大小不固定。比如动态添加对象的属性、动态增加数组的大小等等都会使变量大小增加,无法在栈中维护。所以js就把对象类型的变量放到堆中,让解释器为其按需分配内存,而通过对象的引用指针对其进行访问,因为对象在堆中的内存地址大小是固定的,因此可以将内存地址保存在栈内存的引用中。这种方式称之为按引用访问。 嗯,理解这一点很重要,在以后的编程中可以避免很多问题。 我们来看下如下的代码:

var a = 'I am a string.';   //a,b,c的变量中保存的都是实际的值,因为他们是基本类型的变量
var b = 1010;
var c = false;

var d = a;    //d中保存着和“a值一样的副本,它们互不影响”
a = 'I am different from d';
alert(d);    //输出'I am a string'

以上代码很好理解,就是说按值访问的变量复制“你的就是你的,我的就是我的,咱们都有副本,互不影响。”而对于按引用访问则稍有不同:

var e = {
	name : 'I am an object',
	setName : function(name){
		this.name = name;
	}
};
var f = e;    //赋值操作,实际上的结果是e,f都是指向那个对象的引用指针
f.setName('I am different from e,I am object f.');
alert(e.name);    //对f进行操作,e的值也改变了!

对于引用类型的赋值,说白了,就是把那个对象的指针复制了过去,两个指针指向的都是同一个实体对象,不存在副本,原本的对象还是只有一个!好。以上就是基本类型和引用类型的最大最根本的差别!我用一张图来形象的表示下:

基本类型和引用类型的区别

*栈内存中存放基本类型变量,以及对象的指针;堆中存放对象实体

复制前后栈和堆中的情况

*复制前后栈和堆中的情况

引用类型引发的问题

1.使用原型模型创建对象的问题

我们都知道,在JavaScript OO(Object Oriented)中,使用原型模式创建对象的最大的好处就是可以让对象实例共享原型(prototype)所包含的属性和方法。这样就避免了构造函数模式的缺陷,即每个对象都会有每个方法的副本,每个方法都会在每个实例上重新创建一遍,方法重用无意义的问题。

嗯,使用原型模式是为所有实例共享了方法,但是当原型中有引用类型值的属性的时候,问题就来了:

var Person = function(){
};
Person.prototype = {
	constructor : Person,
	name : 'Hanzongze',
	hobby : ['basketable', 'swiming', 'running'],    //注意,这里包含着一个引用类型的属性
	sayName : function(){
		alert(this.name);
	}
};
var person1 = new Person();
var person2 = new Person();
person1.hobby.push('music');
alert(person2.hobby);    //输出为'basketable', 'swiming', 'running','music'
alert(person1.hobby === person2.hobby);    //true

由于hobby属性是引用类型的值,所以由Person构造函数创建出来的实例的hobby属性,都会指向这一个引用实体,实例对象间的属性互相干扰。这不是我们想要的结果,为避免这类问题,解决方案就是组合使用构造函数模型和原型模型:

var Person = function(){
	this.name = 'Hanzongze';
	this.hobby = ['basketable', 'swiming', 'running'];    //对引用类型的值使用构造函数模式
};
Person.prototype = {
	constructor : Person,
	sayName : function(){
		alert(this.name);
	}
};
var person1 = new Person();
var person2 = new Person();
person1.hobby.push('music');
alert(person2.hobby);   //输出 'basketable', 'swiming', 'running',说明对person1的修改没有影响到person2
alert(person1.hobby === person2.hobby);    //false

2.原型继承中的问题

这个问题与上一个的本质其实是一样的,只不过是发生在了原型继承的背景中。看一个原型链继承的问题:

var Person = function(){
	this.name = 'Hanzongze';
	this.hobby = ['basketable', 'swiming', 'running'];
};
Person.prototype = {
	constructor : Person,
	sayName : function(){
		alert(this.name);
	}
};
//子类型Student		
function Student(){
}
Student.prototype = new Person();    //Student继承了Person
var student1 = new Student();
var student2 = new Student();
student1.hobby.push('music');    //对子类实例student1的引用属性做了改动
var student3 = new Student();
alert(student2.hobby);    //输出'basketable', 'swiming', 'running', 'music'
alert(student3.hobby);    //输出'basketable', 'swiming', 'running', 'music'

在这段代码中,可以看到,子类Student继承自父类Person。但由于使用的是原型继承,也就是说父类的实例作为了子类的原型,那么实例中的引用类型属性也就继承在了子类的原型prototype中去了。则子类的实例共享该引用属性,相互影响。

解决方案,那就是使用借用构造函数方案(但是也不是理想的方案,理想的方案是组合使用原型链和借用构造函数。涉及到了比较多的继承模式,这里简单描述,以后会写一篇详细的文章):

var Person = function(){
	this.name = 'Hanzongze';
	this.hobby = ['basketable', 'swiming', 'running'];
};
Person.prototype = {
	constructor : Person,
	sayName : function(){
		alert(this.name);
	}
};
function Student(){
	//借用构造函数,继承了Person
	Person.call(this);
}

var student1 = new Student();
var student2 = new Student();
student1.hobby.push('music');
alert(student2.hobby);    //输出'basketable', 'swiming', 'running', 'music'

编程技巧