Javascript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是JavaScript的继承,其他两个容后再讲。
JavaScript的继承和C++的继承不大一样,C++的继承是基于类的,而JavaScript的继承是基于原型的。
现在问题来了。
function Animal(name) { this.name = name;}Animal.PRototype.setName = function(name) { this.name = name;}var animal = new Animal("wangwang");我们可以看到,这就是一个对象Animal,该对象有个属性name,有个方法setName。要注意,一旦修改prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如
function Animal(name) { this.name = name;}var animal = new Animal("wangwang");这时animal只有name属性。如果我们加上一句,
Animal.prototype.setName = function(name) { this.name = name;}这时animal也会有setName方法。
var animal = Animal("wangwang");animal将是undefined。有人会说,没有返回值当然是undefined。那如果将Animal的对象定义改一下:
function Animal(name) { this.name = name; return this;}猜猜现在animal是什么?此时的animal变成window了,不同之处在于扩展了window,使得window有了name属性。这是因为this在没有指定的情况下,默认指向window,也即最顶层变量。只有调用new关键字,才能正确调用构造器。那么,如何避免用的人漏掉new关键字呢?我们可以做点小修改:
function Animal(name) { if(!(this instanceof Animal)) { return new Animal(name); } this.name = name;}这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用instanceof来判断,但instanceof在继承的时候对祖先对象跟真正对象都会返回true,所以不太适合。constructor在new调用时,默认指向当前对象。
console.log(Animal.prototype.constructor === Animal); // true我们可以换种思维:prototype在函数初始时根本是无值的,实现上可能是下面的逻辑
// 设定__proto__是函数内置的成员,get_prototyoe()是它的方法var __proto__ = null;function get_prototype() { if(!__proto__) { __proto__ = new Object(); __proto__.constructor = this; } return __proto__;}这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor是可以修改的,后面会讲到。
function Animal(name) { this.name = name;}function Dog(age) { this.age = age;}var dog = new Dog(2);要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)
Dog.prototype = new Animal("wangwang");这时,dog就将有两个属性,name和age。而如果对dog使用instanceof操作符
console.log(dog instanceof Animal); // trueconsole.log(dog instanceof Dog); // false这样就实现了继承,但是有个小问题
console.log(Dog.prototype.constructor === Animal); // trueconsole.log(Dog.prototype.constructor === Dog); // false可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们new出来的实例属于谁。因此,我们可以加一句话:
Dog.prototype.constructor = Dog;再来看一下:
console.log(dog instanceof Animal); // falseconsole.log(dog instanceof Dog); // truedone。这种方法是属于原型链的维护中的一环,下文将详细阐述。
<pre name="code" class="javascript">function Animal(name) { this.name = name;}Animal.prototype.setName = function(name) { this.name = name;}function Dog(age) { this.age = age;}Dog.prototype = Animal.prototype;这样就实现了prototype的拷贝。这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即constructor指向了父对象,还只能复制父对象用prototype声明的属性和方法。也即是说,上述代码中,Animal对象的name属性得不到复制,但能复制setName方法。最最致命的是,对子对象的prototype的任何修改,都会影响父对象的prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。
function Animal(name) { this.name = name;}function Dog(age) { this.age = age;}var animal = new Animal("wangwang");Dog.prototype = animal;var dog = new Dog(2);提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:
我们可以看到,子对象的prototype指向父对象的实例,构成了构造器原型链。子实例的内部proto对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于
function getAttrFromObj(attr, obj) { if(typeof(obj) === "object") { var proto = obj; while(proto) { if(proto.hasOwnProperty(attr)) { return proto[attr]; } proto = proto.__proto__; } } return undefined;}
在这个例子中,我们如果在dog中查找name属性,它将在dog中的成员列表中寻找,当然,会找不到,因为现在dog的成员列表只有age这一项。接着它会顺着原型链,即.proto指向的实例继续寻找,即animal中,找到了name属性,并将之返回。假如寻找的是一个不存在的属性,在animal中寻找不到时,它会继续顺着.proto寻找,找到了空的对象,找不到之后继续顺着.proto寻找,而空的对象的.proto指向null,寻找退出。
(new obj()).prototype.constructor === obj;然后,当我们重写了原型属性之后,子对象产生的实例的constructor不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:
Dog.prototype = new Animal("wangwang");Dog.prototype.constructor = Dog;看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的.proto属性是无法访问的。于是,SpiderMonkey提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。但是,这样又两个问题:
function Dog(age) { this.constructor = arguments.callee; this.age = age;}Dog.prototype = new Animal("wangwang");这样,所有子对象的实例的constructor都正确的指向该对象,而原型的constructor则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写constructor属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用Object.getPrototypeOf() 来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:
function getAttrFromObj(attr, obj) { if(typeof(obj) === "object") { do { var proto = Object.getPrototypeOf(dog); if(proto[attr]) { return proto[attr]; } } while(proto); } return undefined;}当然,这种方法只能在支持ES5的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。
新闻热点
疑难解答