“继承”是面向对象中的一个概念,说开去无非就是代码重用的一种方式罢了。
虽然JavaScript并不是一门真正的面向对象语言,甚至连类的概念都没有。但得益于构造器的存在,在JavaScript中是可以完全模拟出 类-对象 行为的。如:
1
| var person = new Person();
|
看上去除了变量声明时不是强类型之外,完全与面向对象如出一辙。所以谈及继承时,大家首推的也是一种叫“类式继承”的手法了。
类式继承之基于原型链
1 2 3 4 5 6 7 8 9 10 11
| var Parent = function (name) { this.name = name || 'heroic'; }; Parent.prototype.printName = function () { console.log(this.name); }; var Child = function (name) {};
inherit(Child, Parent);
Child.inherit(Parent);
|
上面的伪代码是类式继承的理想状态,但inherit
方法并不存在,需要由自己实现。
1 2 3 4 5 6 7
| var inherit = function (subClass, superClass) { subClass.prototype = new superClass(); };
Function.prototype.inherit = function (superClass) { this.prototype = new superClass(); };
|
和计划中的一样,子对象不仅继承了父对象的属性,也继承了父对象原型上的方法。
1 2 3
| var child = new Child(); child.name = 'test'; child.printName();
|
但是这种方式却存在一些问题:
- 对象属性的写操作是直接发生的,即对象如果不存在这个属性,则为该对象创建这个属性,并为其赋值;如果存在,则直接为其赋新值。而对于对象属性的读操作,则完全不一样了:首先查找对象本身是否有该属性,有则返回,没有则查找其原型链,直到找到该属性为止,如果到了原型链的最顶层(Object)都没找到,则返回
undefined
。由于存在这种读写的不对等性,我们都不会采取从父对象继承属性,而是直接为子对象添加属性即可,而需要继承的方法则放到原型上。
- 且上面的实现中,我们无法完成这样的初始化:
var child = new Child('test')
利用原型链实现的类式继承先放一边,为了解决在初始化就能传入参数的问题,便产生了一种叫“借用构造函数”方式的继承。
类式继承之借用构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var Parent = function (name) { this.name = name || 'heroic'; this.printName = function () { console.log(this.name); }; }
var Child = function (name) { Parent.call(this, name); }
var child = new Child('test'); child.printName();
var parent = new Parent(); parent.printName();
|
首先来谈谈这种机制相对于第一种的优点,Talk is cheap, Show me the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var Parent = function () { this.tags = ['.NETer']; } var parent = new Parent();
var ChildA = function () {}; ChildA.prototype = parent;
var ChildB = function () { Parent.apply(this, arguments); };
var child_a = new ChildA(); child_a.tags.push('Javaer');
var child_b = new ChildB(); child_b.tags.push('Pythoner');
console.log(child_a.tags.join(', ')); console.log(child_b.tags.join(', '));
console.log(parent.tags.join(', '));
|
显而易见,借用构造函数方式在继承时是采取一份单独的拷贝,而原型链方式则是指向同一个引用。(但是由此可见,原型链上的属性或方法不会在每个实例中都创建一次。)
接下来则是谈谈缺陷了。
1 2 3 4 5 6 7 8 9 10 11 12
| var Parent = function () {}; Parent.prototype.papapa = function () { console.log('pa pa pa...'); };
var Child = function () { Parent.apply(this, arguments); };
var child = new Child(); child.papapa();
|
借用构造函数其实是在构造时,通过改写方法调用上下文来实现属性的拷贝,所以并未涉及到prototype
,所以就没有办法继承原型了。
由于原型链上的属性或方法不会在每个实例中都创建一次,所以是我们放置需要重用的属性和方法的理想地方;而借用构造函数则可以使子对象拥有自己一份独立的拷贝,不存在意外改写父对象属性的风险。所以两者互补产生了第三种比较完美的继承方式。
类式继承之组合模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var Parent = function (name) { this.name = name || 'heroic'; this.tags = ['coder']; }; Parent.prototype.print = function () { console.log('name: ', this.name, ', tags: ', this.tags.join(', ')); };
var parent = new Parent();
var Child = function (name) { Parent.apply(this, arguments); }; Child.prototype = parent;
var child = new Child('test'); child.tags.push('player');
child.print(); parent.print();
|
近乎完美的实现,子对象继承了父对象的成员,但拥有自己的一份拷贝,不会担心修改自己而影响到父对象;子对象也复用了父对象原型中的方法;且子对象也可以传递任意参数给父对象的构造函数。可谓是面向对象中“类式继承”的准确诠释。