伪类与原型继承原型继承是javascript中继承最传统的做法,通过操作伪类和prototype,你很容易就能实现继承的效果:将父类的实例赋给子类的prototype,再创建子类实例即可。
如果你不知道原型的基础知识,我强烈推荐你阅读我的上一篇文章《轻松理解javascript原型》,文章中,作者把原型部分的知识近乎完美的剥离了出来,目的是为这篇文章做好铺垫。
但实话实说,我和《javascript语言精粹》的作者“老道”有着近乎相同的想法——我们希望能够避免new关键字的出现和伪类带来的种种问题,而采取另一种产生对象的设计模式——让函数直接返回对象。
这篇文章会为大家详细的介绍原型继承的应用、问题和需要注意的地方,更会花上一定的篇幅为大家推荐应用模块差异化继承。
一个例子
你知道,在javascript中,没有纯粹的类的概念,就像“老道”说的那样:javascript只关心对象能做什么,并不关心它是怎么来的。
所谓原型继承,其实就是利用js访问对象属性(或方法)的时候会去查找它的原型链这一特点:你把父类对象的实例赋值给子类构造函数的原型属性,这样子类的所有实例在创建时都会通过__proto__
与子类构造函数.prototype
的隐秘连接“获得”那个对象的所有属性和方法——子类对象从父类对象继承的属性并不真正直接存在于子类对象上,而是存在于子类构造函数的prototype(原型)对象中或者说子类实例的__proto__
属性上,你可以在子类对象上通过原型链访问到它们。如果你还不知道是原型链查找是怎么运作的,《轻松理解javascript原型》里讲得很清楚。
下面的例子会更好地帮助你理解:
// 动物类
var Animal = function () {
this.kind = "animal";
};
// 哺乳动物类
var Mammal = function (name) {
this.name = name;
this.sayHello = function () {
alert('hello I am' + this.name + " and I am a kind of" + this.kind);
}
};
Mammal.prototype = new Animal();
var mammal = new Mammal('Tom');
mammal.sayHello();
输出:
hello I am Tom and I am a kind of animal
当Mammal的实例mammal调用sayHello()方法时,里面需要找到this.kind,但是对象本身并没有this.type,于是javascript开启"大招"——原型链查找。去查找mammal构造函数Mammal的prototype对象,结果在这里面发现了“赃物”属性——kind,返回结果,游戏结束。
弄丢了什么
这么做并不是完美无缺的,不知道你还记不记得我们曾经说过的关于构造器的事,我不介意让这段代码再次呈现在这里:
this.prototype = {constuctor : this}
是不是想起什么来了,(构造)函数的prototype并非一个任何意义上的空对象——在这个函数对象被创建的时候,它就被它的构造器Function强行刻下了烙印,一个名叫constructor的属性,指的正是它自己。
如果用另一个实例去替换函数的prototype的话,显然,对象会丢失原本的constuctor属性,我们输出一下mammal的constuctor:
function () {
this.kind = "animal";
}
是Animal的构造函数,不难想象,Mammal的prototype被Animal的实例替换掉了,原本的constuctor属性也随之丢失了。而Animal构造器的prototype里也有一个叫做construcor的属性,值是上面输出的结果。
所以,如果需要的话,为了保留住真正的构造器,你可以在替换了Mammal的prototype之后再做些额外的工作:
Mammal.prototype.constructor = Mammal;
此时再访问mammal的constructor属性,会返回Mammal构造函数。
更新
当构造器的prototype对象更新时,之前产生的所有实例对象都会做出立即更新。我们把上面动物的例子稍作改变:
// 动物类
var Animal = function () {};
// 哺乳动物类
var Mammal = function (name) {
this.name = name;
this.sayHi = function () {
console.log('hello I am ' + this.name + " and I am from " + this.country);
}
};
Mammal.prototype = new Animal();
var mammal = new Mammal('Tom');
mammal.sayHi();
Mammal.prototype.country = 'USA'; // 显然 Animal.prototype.country = 'USA'效果相同
mammal.sayHi();
先后输出:
hello I am Tom and I am from undefined
hello I am Tom and I am from USA
但是,你要注意我说的是更新,如果你完全将prototype换成了别对象,那么引用关系将被破坏,之前生成的实例对象将会不为所动。
伪类的缺陷
伪类加原型的方式的的确确能够解决对象与继承的问题,但这种方式存在缺陷:
(1) 首先,它没有私有环境,所有属性都是公开的
function Cat() {
this.name='Tom';
this.age = 2;
}
var cat = new Cat();
打开控制台,输入cat.name,就可以轻而易举的知道我们的小猫叫做Tom,在控制台输入cat.name = 'Jerry',糟糕,我们小猫的名字被修改了!!
(2)其次,它无法像java那样通过super.属性
轻易的访问父类名字相同的属性和方法,除非使用cat.__proto__
这样不友好的方式,这是应该禁止的。(事实上,直接访问属性都是应该被禁止的)
function Animal () {
this.name = "animal"
}
function Cat() {
this.name='Tom';
this.age = 2;
}
Cat.prototype = new Animal();
var cat = new Cat();
console.log(cat.name); // =>Tom
console.log(cat.__proto__.name); // =>animal
(3)最可怕的如果你在调用构造器函数时忘记了在前面加上new前缀,那么this将不会被绑定到那个新对象上,反倒被绑定到全局对象上,从而破坏了全局环境。既没有编译时警告,也没有运行时警告。
或许,我是说或许,是时候换一个设计模式以实现相同并更强大的功能。
应用模块模式继承差异化继承
差异化继承的本质是先复制一份与父对象含有相同属性特征的对象(注意是深复制,不是简单的复制一个引用),然后再在这个基础上创建我们自己的对象:
var animal = {
name : "animal",
age : 15
};
var cat = Object.create(animal);
cat.name = "Tom";
cat.sayMeow = function () {
console.log('meow')
};
console.log(cat.name);
console.log(cat.age);
cat.sayMeow();
Object.create是ES5的新特性,作用是创建一个具有指定原型且可选择性地包含指定属性的对象(我们不讨论它的第二个作为属性描述符集合的可选参数)。在父对象的这个基础上,我们再为子对象添加新属性。
在ES3的那个原始的时代,Object.create()大概是像下面这样工作的:
function create (obj) {
var F = function () {};
F.prototype = obj;
return new F();
}
实际上,父对象的属性与伪类继承模式类似,被放置在了原型上,而不是真正的存在与子对象上。
函数化
想想伪类模式的缺陷吧,访问父元素的同名属性的问题解决了(直接父对象.属性就可以),遗漏new关键字造成的威胁解决了,但私有属性仍然暴漏无疑。我们使用函数化的方式解决这个问题,先看一个简单的例子:
function animal() {
var name = 'animal';
var getName = function () {
return name
};
return {
getName : getName
}
}
var myAnimal = animal();
console.log(myAnimal.name); // =>undefined
console.log(myAnimal.getName()); //=>animal
这里我们利用闭包的特性来解决私有变量的问题。我在想或许我应该写一篇叫做《轻松理解javascript闭包的文章》。
现在好了,除非调用我们的getName特权方法,谁也别想知道name是什么,更别想修改它。看那,animal()已经不是new的小跟班,它自己返回一个对象,它的首字母再也不用大写了。
应用模块继承
干得不错,现在,我们试着融合两种方法,实现模块化继承。为了更明显些,我们为animal构造器增加一个food属性和eat方法。嗯,我已经尽力把情况做的丰富些:
function animal(info) {
var that = {};
that.getName = function () {
var name = info.name|| 'animal';
console.log('(super method) my name is ' + name);
return name ;
};
that.eat = function () {
console.log('I am eating ' + info.food);
};
return that;
}
function cat(info) {
var that = animal(info), // 获得父级对象,并在此基础上创建子对象
super_getName = that.getName; // 直接获得父级对象的方法
that.sayMeow = function () { // 子对象特有的方法
console.log('Meow')
};
that.super_getName = super_getName;
that.getName = function () { // 子对象与父对象重名的方法
var name = info.name|| 'Tom';
console.log('(child method) my name is ' + name);
return name ;
};
return that;
}
var Tom = cat({name:'Tom', food:'fish'});
Tom.sayMeow(); // =>Meow
Tom.eat(); // =>I am eating fish
Tom.getName(); // =>(child method) my name is Tom
Tom.super_getName();// =>(super method) my name is Tom
“构造函数”接收一个参数info,存放对象的所有数据信息。这种设计模式下,私有变量受到保护,可以轻易的访问父级对象的方法(需要时使用apply改变this),更解决了new关键字容易造成的问题。
小结通过今天的学习,不知道你有没有走出原型、继承的谜团呢? 如果还是有疑惑,我推荐你再读一遍。《javascript语言精粹》、《javascript启示录》中对相关的知识点有更细致的讲解,没事的话就去看看这些书吧,的确挺好的。