原型&继承

摘要:关于原型,学过js的应该都知道这个概念,记得3月份有一次在面试的时候面试官叫我跟他讲一下js的原型,当时是电话面试,我上来就是懵逼,原型这个东西,画图还好讲点,直接口述,而且还要让对方听懂,确实有点。。于是我就讲得有点乱,而且讲得我自己都懵逼了。为了揭开原型那神秘的面纱,现在我们来分析一下它,弄清晰之后,再也不怕面试官考原型啦~

原型

先看个简单的例子,构造函数与原型与对象实例的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构造函数
function Animal(type){
// this绑定到当前对象实例上
this.type = type;
}
// 给构造函数的原型对象设置属性
Animal.prototype.species = "动物";
// new一个实例对象
var cat = new Animal('cat');

cat.type; //cat
cat.species; // 动物
Animal.prototype.species //动物
cat.constructor; // Animal(){}

Animal为构造函数,构造函数可以设置当前对象(this)的属性/方法
也可通过原型对象设置属性/方法
实例化对象时,构造器函数中的属性方法会为每个实例分配一套自己的属性/方法
而原型的话则只有一套
如果每个实例对象的该属性/方法都不一样,可以使用this设置属性
如果所有实例的该属性/方法都一样,可以使用prototype设置属性
prototype属性每个实例的属性都是同一个,修改后所有的实例都会跟着改变

利用构造函数实现继承

1.利用实例对象

把子类的prototype指向父类的一个实例,实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 父类Animal
function Animal(type){
this.type = type;
}
Animal.prototype.species = "动物";
// 子类Cat
function Cat(name,color){
this.name = name;
this.color = color;
}

// 使Cat的原型指向Animal的一个实例
Cat.prototype = new Animal('cat');

Cat.prototype.constructor // Animal
// 这里改变了Cat.prototype,此时Cat.prototype.constructor也跟着改变
// 因为Cat的实例的构造器是Cat,但是这里变成了Animal,不符合实际
// 所以重新改变其构造器的指向为Cat
Cat.prototype.constructor = Cat;

// new一个Cat的实例,此时该实例就继承自Animal
var cat1 = new Cat('小黄','yellow');

首先我们使Cat的原型指向了一个Animal的对象实例
为什么这么做呢?因为对象实例可以直接继承原型对象的属性和方法
然后由于继承机制是往原型链走的
当我们创建一个Cat的实例的时候
Cat的实例的原型为Cat.prototype
此时Cat.prototype又指向Animal的一个实例
Animal的这个实例又直接继承Animal.prototype
所以实现了Cat继承Animal
这个情况下实现的继承比较好的就是Cat.prototype与Animal.prototype互不影响
Animal.prototype新增属性也可以被Cat继承
(注意:改变原型链时,子类原先的原型对象直接被替换,所以建议设置子类原型属性应在改变原型链后再设置)

2.利用prototype对象

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(type){
this.type = type;
}
Animal.prototype.species = "动物";
function Cat(name,color){
this.name = name;
this.color = color;
}
// 使Cat.prototype指向Animal.prototype
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat('小黄','yellow');

同样是上面的例子
只是这个时候是把Cat.prototype指向Animal.prototype
这样做的目的很明显
但是有一个很明显的缺点就是Cat.prototype与Animal.prototype指向了同一个引用
当其中一个发生变化时,另一个也会发生变化
所以这个用法我不建议使用
这里只做简单的介绍

3.利用空对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal(type){
this.type = type;
}
Animal.prototype.species = "动物";
function Cat(name,color){
this.name = name;
this.color = color;
}
// F为一个空的构造函数
function F(){}
// 使F.prototype指向Animal.prototype
// 再使Cat.prototype指向F的一个实例对象
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
var cat1 = new Cat('小黄','yellow');

这种方法其实是结合了1、2两种方法
为什么要这样做呢?
其实第一种方法就能解决我们的问题
但是第一种方法的话new一个新实例的时候很可能会占用内存
而第二种方法的话又导致两个prototype指向同一个引用
所以利用空构造函数使其原型与父构造函数原型指向同一个引用
然后再使子构造函数的原型指向空构造函数的实例对象
这样就实现了继承并且几乎不占用内存

1
2
3
4
5
6
7
// 这个方法我们可以封装成一个函数来实现继承
function extend(child,father) {
function F(){}
F.prototype = father.prototype;
child.prototype = new F();
child.prototype.constructor = child;
}

非构造函数实现的继承

1.利用Object.create函数

1
2
3
4
5
6
7
8
9
var a = {
m:0,
n:8
}
// 为a创建一个prototype属性使其指向a ,模仿构造器
// 因为对像没有prototype属性
a.prototype = a;
// 利用Object.create函数创建一个对象b使其继承a
var b = Object.create(a);

2.利用空对象构造object方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
m:0,
n:8
}
// 传入一个对象(因为构造函数的原型也是一个对象)
// 把空函数的prototype指向该对象
// 返回一个对象实例
// 调用该方法后b即为返回的对象实例
// 同样模仿构造器把a.prototype指向a
function object(o){
function F(){}
F.prototype = o;
o.prototype = o;
return new F();
}
var b = object(a);

3.浅拷贝继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
m:0,
n:8,
x:[1,2,3],
y:{
a:7
}
}
function extend(a){
var result = {};
for(var i in a) {
result[i] = a[i];
}
return result;
}
var b = extend(a);

这种情况下的拷贝是浅拷贝
当拷贝的属性为数组/对象时,只拷贝其引用
此时a,b属性引用的是相同的地址
所以当改变其中结构的时候,两个属性都会随之改变

4.深拷贝继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function extend(a,result){
var result = result || {};
for(var i in a) {
if(typeof a[i] === 'object') {
result[i] = (a[i].constructor === Array) ? [] : {};
extend(a[i],result[i]);
} else {
result[i] = a[i];
}
}
return result;
}
var b = extend(a);

这种情况下的拷贝是深拷贝
当拷贝的对象是数组/对象时,再进行深一层的拷贝
这是在浅拷贝的基础上进行递归

其实,原型这个东西,真的要多接触,多画图,多敲代码,才能更好的理解它,毕竟多敲代码会发现更多我们学理论知识的发现不到的问题~