一、封装
在es6之前,javascript是没有类(class)的概念的,但是又确实是一种基于对象的语言,所以通常情况下我们都是把属性和方法封装成一个对象,可以直接从对象生成一个实例对象,下面我们来看看具体实现的方法。
1、生成实例对象的原始模式
假设我们把人看作是一个原型对象,他有名字和年龄两个属性。
var People = {
name:'',
age:''
}
现在我们根据这个原型对象生成两个实例
var people1 = {};
people.name = 'people1';
people.age = 12;
var people2 = {};
people.name = 'people2';
people.age = 13;
这个就是最原始的封装,把两个属性封装在一个对象里面。但是这样的写法有两个缺点,一是生成实例太多,写起来就很麻烦。二是实例与原型之间看不出来有任何联系。
2、原始模式的改进
写一个函数解决代码重复的问题
function People(name,age){
return {
name:name,
age:age
}
}
然后生成实例对象,等于是在调用函数:
var people1 = People('people1',12);
var people2 = People('people2',13);
但是这种方法问题是,people1和people2之间没有什么联系,看不出来是同一个原型的实例。
3、构造函数的模式
为了解决从原型对象生成实例的问题javascript提供了一个构造函数,使用new运算符就能生成实例,并且this变量会绑定到实例对象上
比如:上面人的原型对象现在就可以这样写了。
function People (name,age){
this.name = name;
this.age = age;
}
现在就可以生成实例对象了:
var people1 = new ('people1',12);
var people2 = new('people2',13);
console.log(people1.name); // people1
这时people1和people2会自动含有一个constructor属性,指向它们的构造函数。
console.log(people1.constructor == People) // true
console.log(people2.constructor == People) // true
4、构造函数模式的问题
构造函数很好用,但是存在一个消耗内存的问题。
下面为People对象添加一个不变的属性height,那么People原型对象就变成这样:
function People (name,age){
this.name = name;
this.age = age;
this.height = 170;
}
还是采用同样的方法生成实例:
var people1 = new ('people1',12);
var people2 = new('people2',13);
console.log(people1.height); // 170
表面上看没什么问题,但是实际上这样做有一个很大的弊端。那就是对于每一个实例对象,height属性都是一样的内容,每生成一个实例都会为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
console.log(people1.height == people2.height) // false
能不能让height属性在内存中只生成一次,然后所有实例都指向那个内存地址呢?答案是可以的。
5、Prototype模式
javascript规定,每一个构造函数都有prototype属性,指向另外一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这就意味着,我们可以把那些不变的属性和方法直接定义在prototype对象上。
function People (name,age){
this.name = name;
this.age = age;
}
People.prototype.height = 170;
People.prototype.say = function(){console.log('hello)};
生成实例。
var people1 = new ('people1',12);
var people2 = new ('people2',13);
console.log(people1.height); // 170
console.log(people1.say()); // hello
这时所有实例的height属性和say方法,其实都是一个内存地址,指向prototype对象,因此就提高了运行效率。
console.log(people1.height == people2.height) // true
6 、Prototype模式的验证方法
为了配合prototype属性,javascript定义了一些方法辅助我们使用它们。
6.1、isPrototypeOf()
这个方法用来判断,某个prototype对象和某个实例对象之间的关系
console.log(People.prototype.isPrototypeOf(people1)); //true
console.log(People.prototype.isPrototypeOf(people2)); //true
6.2、 hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
console.log(people1.hasOwnProperty("name")); // true
console.lot(people1.hasOwnProperty("height")); // false
二、构造函数的继承
我们已经知道了如何封装属性和方法,以及如何从原型对象上生成实例,
但是如何实现对象之间的继承呢?
比如现在有一个‘人’对象的构造函数。
function People (){
this.name = 'people';
}
还有一个‘中国人’对象的构造函数。
function Chinese (height) {
this.height = height ;
}
怎么才能使‘中国人’继承‘人’呢?
1.构造函数绑定
第一种方法也是最简单的办法,使用apply或者call方法,将父对象的构造函数绑定在子对象上。
function Chinese (height) {
People.call(this, arguments);
this.height = height ;
}
var chinese1 = new Chinese();
console.log(chinese.name); // people
2、prototype模式
这种方法更常见,使用prototype属性
如果‘中国人’的prototype对象指向People实例,那么所有的‘中国人’的实例,就能继承People了。
Chinese.prototype = new People();
Chinese.prototype.constructor = Chinese;
var chinese1 = new Chinese();
console.log(chinese.name); // people
代码的第一行,我们将Chinese的prototype对象指向一个People实例。
Chinese.prototype = new People();
它相当于完全删除了prototype对象原先的值,然后赋了一个新的值。第二行代码又是在干什么呢?
Chinese.prototype.constructor = Chinese;
原来任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有‘Chinese.prototype = new People()’这一句话,Chinese.prototype.constructor是指向Chinese的,现在是指向People的。
console.log(Chinese.prototype.constructor == People); // true
一次在加上Chinese.prototype.constructor = Chinese这句以后,Chinese.prototype.constructor指向了Chinese;
console.log(Chinese.prototype.constructor == Chinese); // true
更重要的是每一个实例也有constructor属性,默认调用prototype对象的constructor属性。
console.log(chinese1.constructor == Chinese.prototype.constructor); // true
因此在运行‘Chinese.prototype = new People();’之后,chinese1.constructor也指向了People。
console.log(chinese1.constructor == People); //true
这显然会造成继承链的混乱,people1明明是People生成的,因此我们必须手动纠正,将Chinese.prototype对象的constructor值改为Chinese。这是很重要的一点,编程时一定要遵守。
3、直接继承prototype
第三种方法是对第二种方法的改进。在People对象中,不变的属性可以直接写入People.prototype。所以我们可以让Chinese跳过People直接继承People.prototype.
实现方式如下:
function People(){};
PeoPle.prototype.name = 'people';
然后将将Chinese的prototype对象指向People.prototype,这样就完成了继承。
Chinese.prototype = Chinese.prototype;
Chinese.prototype.constructor = Chinese;
var chinese1 = new Chinese();
console.log(chinese.name); // people
与前一种方法相比,这样做的优点是效率高(不用执行和建立People实例),就比较省内存,缺点就是Chinese.prototype和People.prototype都指向了同一个对象,那么任何对Chinese.prototype的修改也同时反映到了People.prototype上。
所以上面的这一段代码是有问题的。
Chinese.prototype.constructor = Chinese;
这一句话实际上也把People.prototype对象的constructor属性也改了。
console.log(People.prototype.constructor ); // Chinese
4、利用空对象作为中介
由于直接继承prototype存在上述的缺点,所以我们可以用一个空对象作为中介。
var F = function(){};
F. prototype = People.prototype;
Chinese.prototype = new F();
Chinese.prototype.constructor = Chinese;
F 是一个空对象,几乎不占内存。这时修改Cat的prototype对象就不会影响到People的prototype对象。
console.log(Chinese.prototype.constructor ); // Chinese
我们将上面的方法封装成一个方法,便于调用。
function extend(Child,Parent){
var F = function(){};
F. prototype = Parent.prototype;
Chind.prototype = new F();
Child.prototype.constructor = Chind;
Child.uber = Parent.prototype;
}
使用方式如下
extend(Chinese,People);
var chinese1 = new Chinese();
console.log(chinese.name); // people
另外说明一点,函数最后一行
Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
5、拷贝继承
上面采用prototype对象实现继承,我们可以换一种思路,纯粹采用拷贝方法实现继承。简单说如果把父对象的所以属性和方法拷贝进子对象,也能实现继承。
function extend2(Child,Parent){
var p = Parent.prototype;
var c = Child.prototype;
for(var i in p){
c[i] = p[i];
c.uber = p;
}
}
这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:
extend(Chinese,People)
var chinese1 = new Chinese();
console.log(chinese.name); // people
三、非构造函数的继承
前面提到的都是构造函数实现的继承,那么不使用构造函数,怎么能实现继承呢?
1、什么是非构造函数的继承
比如我有个对象叫“中国人”。
var Chinese = {
nation:'中国'
}
还有一个对象叫“医生”。
var Doctor = {
career:'医生'
}
那要怎样才能让“医生”去继承“中国人”,也就是说,要怎样才能去生成一个中国医生。这两个都是普通对象不是构造函数,无法用构造函数的方式实现继承。
2、object()方法
function object(o){
function F (){}
F.prototype = o;
returen new F();
}
这个object函数只做一件事,就是把子对象的prototype指向父对象,从而使得子对象与父对象连在一起。使用的时候先在父对象的基础上生成子对象:
var Doctor = object(Chinese);
在加上子对象本身的属性
Doctor.career = '医生';
这时子对象已经继承了父对象的属性了。
console.log(Doctor.nation); // 中国
3、浅拷贝
除了使用‘prototype链’以外,我们可以把父对象的属性全部拷贝给子对象,也能实现继承。
请看下面这个函数 :
function extendCopy(p){
var c = {};
for (var i in p){
c[i] = p[i]
}
c.uber = p;
return c;
}
使用的时候,这样写:
var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
console.log(Doctor.nation); // 中国
但是这样拷贝有一个问题。那就是如果父对象的属性等于数组或是另外一个对象,那么实际是,子对象获取的只是一个内存地址,而不是真正的拷贝,因此存在父对象被篡改的可能。
请看,现在给Chinese添加一个‘出生地’属性,它是一个数组。
Chinese.birthPlaces = ['成都','上海','北京'];
通过extendCopy()函数,Doctor继承了Chinese。
var Doctor = extendCopy(Chinese);
然后我们为Doctor的出生地添加一个城市:
Doctor.birthPlaces.push('厦门');
发生了什么事?Chinese的‘出生地’也被改掉了!
console.log(Doctor.birthPlaces); // '成都','上海','北京','厦门';
console.log(Chinese.birthPlaces); // '成都','上海','北京','厦门';
所以,extendCopy()函数只是拷贝基本类型的数据,我们把这种拷贝叫做浅拷贝。
4、深拷贝
所谓深拷贝,就是能够实现真正意义上的数组和对象的拷贝。实现并不难,只要递归调用浅拷贝就行了。
function deepCopy(p,c){
var c = c || {};
for(var i in p){
if(typeof p[i] == 'object'{
c[i] = (p[i].constructor == Array) ? [] : {};
deep(p[i],c[i]);
}else {
c[i] = p[i];
}
}
return c;
}
使用的时候这样写:
var Dcotor = deepCopy(Chinese);
现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:
Chinese.birthPlaces = ['成都','上海','北京'];
Doctor.birthPlaces.push('厦门');
这时父对象就不受影响了。
console.log(Doctor.birthPlaces);// '成都','上海','北京','厦门';
console.log(Chinese.birthPlaces); // '成都','上海','北京';