JS对象引用与继承
js中的对象引用
ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型,也有其他的叫法,比如原始类型和对象类型。
基本类型
JS中的基本数据类型有:undefined,boolean,number,string,null。基本类型的访问都是按值访问,我们可以直接操作保存在变量中的实际的值。基本还有一些相同的特点:
-
基本类型的值是不可变的。
任何方法都无法改变一个基本类型的值,比如说:var name = 'lee'; name.toUpperCase(); // 输出 'LEE' console.log(name); // 输出 'lee'
我们可以看到原始的name的值并没有发生任何改变,调用这两个方法都只是返回了一个新的字符串。
再比如说:var person = 'lee'; person.age = 22; person.method = function(){//...}; console.log(person.age); // undefined console.log(person.method); // undefined
由上面代码我们还可以知道,我们不能给基本类型添加属性和方法,再次说明了基本类型是不可变的。
-
基本类型的比较是对值的比较。
当两个基本类型在做比较时,只有在他们的值相等时他们才会想等。 -
基本类型的变量是存放在栈中的。
假如有以下的几个基本类型的变量:var name = 'lee'; var city = 'chengdu'; var age = 22;
那么他的存储结构就会是这样的:
栈(标识符) 栈(值) name lee city chengdu age 22 栈区包括了 变量的标识符和变量的值。
引用类型
javascript中除了上面的基本类型(number,string,boolean,null,undefined)之外就是引用类型了,也可以说是就是对象了。对象是属性和方法的集合。也就是说引用类型可以拥有属性和方法,属性又可以包含基本类型和引用类型。来看看引用类型的一些特性:
-
引用类型的值是可变的。
我们可为为引用类型添加属性和方法,也可以删除其属性和方法,如:var person = {};//创建对象 --引用类型 person.name = 'lee'; person.age = 22; person.sayName = function(){console.log(person.name);} person.sayName();// 'lee' delete person.name; //删除person对象的name属性 person.sayName(); // undefined 上面代码说明引用类型可以拥有属性和方法,并且是可以动态改变的。
-
引用类型的值是同时保存在栈内存和堆内存中的对象。
javascript和其他语言不同,其不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,那我们操作啥呢? 实际上,是操作对象的引用,所以引用类型的值是按引用访问的。
准确地说,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,栈区内存保存变量标识符和指向堆内存中该对象的指针,也可以说是该对象在堆内存的地址。
假如现在有以下一个对象:var person1 = {name:'lee'}; var person2 = {name:'lee1'}; var person3 = {name:'lee2'};
则这三个对象的在内存中保存的情况如下图:
-
引用类型的比较是引用的比较
我们来看一下这个例子:var person1 = {}; var person2 = {}; console.log(person1 == person2); // false
我们可以看出虽然两个对象的值似乎是相当的,但为什么两个对象的比较却不相同呢?这就是因为引用类型是按引用访问的,换句话说就是在比较两个对象时,比较的是两个对象在堆中的地址,所以person1和person2是不相同的,是两个不同的对象。
简单复制
在从一个变量向另一个变量赋值基本类型时,会在该变量上创建一个新值,然后再把该值复制到为新变量分配的位置上:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10
此时,a中的值为10,当使用a来向b赋值时,b中的值也为10,但b的值和a的值是完全独立的,此后这两个变量可以进行任何操作而相互不会受到影响。
对象引用
当一个变量向另一个变量赋引用类型的值时,同样也会将存储在变量中的对象的值复制一份放到为新变量分配的空间中。前面讲引用类型的时候提到,保存在变量中的是对象在堆内存中的地址,所以,与简单赋值不同,这个值的副本实际上是一个指针,而这个指针指向存储在堆内存的一个对象。那么赋值操作后,两个变量都保存了同一个对象地址,则这两个变量指向了同一个对象。因此,改变其中任何一个变量,都会相互影响:
var a = {}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象
a.name = 'lee';
console.log(a.name); // 'lee'
console.log(b.name); // 'lee'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true
因此,引用类型的赋值其实是对象保存在栈区地址指针的赋值,因此两个变量指向同一个对象,任何的操作都会相互影响。
对象的继承
众所周知,在ES6之前都没有类的概念,所以不能像JAVA中一样一个extends关键字就搞定了继承关系,而是靠一种原型链的一级一级的指向来实现继承。
prototype对象
我们需要学习原型链上的第一个属性prototype,这个属性是一个指针,指向的是原型对象的内存堆。prototype是为了解决构造函数的属性和方法不能共享的问题而提出的,下面我们先实现一个简单的继承:
function constructorFn (state, data) {
this.data = data;
this.state = state;
this.isPlay = function () {
return this.state + ' is ' + this.data;
}
}
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
此时,实例1 和实例2 都有自己的data属性、state属性、isPlay方法,造成了资源的浪费,既然两个实例都需要调用isPlay方法,便可以将isPlay方法挂载到构造函数的prototype对象上,实例便有了本地属性方法和引用属性方法,如下:
function constructorFn (state, data) {
this.data = data;
this.state = state;
}
constructorFn.prototype.isPlay = function () {
return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
console.log(instance1.isDoing); // nonono!
console.log(instance2.isDoing); // nonono!
我们将isPlay方法挂载到prototype对象上,同时增加isDoing属性,既然是共享的属性和方法,那么修改prototype对象的属性和方法,实例的值都会被修改,如下:
constructorFn.prototype.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!
那如果修改实例1的isDoing属性的原型,实例2的isDoing会不会受到影响?
instance1.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // nonono!
问题又又来了,为什么修改实例1的__proto__属性上的isDoing的值就会影响到构造函数的原型对象的属性值?
我们先整理一下,未解决的三个问题:
-
为什么实例会取到prototype对象上的属性和方法?
-
为什么修改实例1的isDoing属性,实例2的实例没有受到影响?
-
为什么修改实例1的__proto__属性上的isDoing的值就会影响到构造函数的原型对象的属性值?
这时候不得不说new操作符,同样是面试最火爆的问题之一,new操作符干了什么?以 Var instance1 = new constructorFn();为例,就是下面三行代码:
var obj = {};
obj.__proto__ = constructorFn.prototype;
constructorFn.call(obj);
第一行声明一个空对象,因为实例本身就是一个对象。 第二行将实例本身的__proto__属性指向构造函数的原型,obj新增了构造函数prototype对象上挂载的属性和方法。 第三行将构造函数的this指向替换成obj,再执行构造函数,obj新增了构造函数本地的属性和方法。
理解了上面三行代码的含义,那么三个问题也就迎刃而解了。问题1:实例在新建的时候,本身的__ptoto__指向了构造函数的原型。问题2:实例1和实例2 在新建后,有了各自的this,修改实例1的isDoing属性,只是修改了当前对象的isDoing的属性值,并没有影响到构造函数。问题3:修改实例1的__proto__,即修改了构造函数的原型对象的共享属性。
__ proto__
那__proto__又是什么?简单来说,__proto__是对象的一个隐性属性,同时也是一个指针,可以设置实例的原型。 实例的__proto__指向构造函数的原型对象。
我们继续用上面的例子来说明:
function constructorFn (state, data) {
this.data = data;
this.state = state;
}
constructorFn.prototype.isPlay = function () {
return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
console.log(instance1.__proto__ === constructorFn.prototype); // true
构造函数的原型对象也是对象,那么constructor.prototype.__proto__指向谁呢?
定义中说对象的__proto__指向的是构造函数的原型对象,下面我们验证一下constructor.prototype.__proto__的指向:
console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
几种继承的方式
既然要实现继承,那么首先我们得有一个父类,代码如下:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型链继承
将父类的实例作为子类的原型。
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
2、构造继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)。
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
3、组合继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
4、寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性。
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());