基本原理
在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'