Javascript中,一切可变的键控集合(keyed collections),称为对象。(好拗口,实际上就是指引用数据类型)
在一个纯粹的原型模式中,应当抛弃类的概念,拥抱于对象。由于没有了constructor的概念,继承会显得更为简单——子对象可以继承父对象的属性/方法。
方法一:通过创建一个中介空构造函数去继承父对象,然后子对象通过中介构造函数生成的实例来继承父对象
/**
* @param {object} o - 需要继承的父对象
*/
function extendsObject(o) {
var F = function() {};
F.prototype = o;
return new F;
}
// 父对象
var parent_1 = { // 由于已经不在通过构造函数的方式,所以这里的父对象首字母不再以大写形式出现
name: 'parent_1',
sayName: function () {
return this.name;
}
};
// 子对象
var c1 = extendsObject(parent_1);
c1.uber = parent_1; // 访问父对象的通道
// 进而再对子对象进行扩展,所以,这种继承方法,也叫做差异化继承(differential inheritance)
// 即通过创建一个新的子对象,然后指明它与其继承的父对象之间的区别
c1.name = 'child_1';
// 访问子对象方法
console.log(c1.sayName()); // child_1
// 还可以方便地访问父对象的方法
console.log(c1.uber.sayName()); // parent_1
方法二:子对象直接浅拷贝父对象上的属性
/**
* @param {object} o - 需要继承的父对象
*/
function shallowCopyObject(o) {
var c = {};
for(var k in o) {
c[k] = o[k];
}
c.uber = o;
return c;
}
var parent_2 = {
name: 'parent_2',
children: ['foo', 'bar'], // 本意是给父对象创建一个“私有属性”,但是这里却会被继承的子对象所篡改,等会会说到如何解决这个问题(转方法四)
sayName: function() {
return this.name;
}
};
var c2 = shallowCopyObject(parent_2); // 自此,c2继承了parent_2上的所有属性
// 不幸的是,由于父对象中的children属性是一个数组对象,是引用数据类型,
// 故parent_2.children只是存储了该数组对象在堆中的地址,也即是说,对子对象上children的任何改动,
// 都会影响到父对象,非常糟糕,例如:
c2.children[0] = 'ugly_foo'; // 父对象的“私有属性”被非法篡改了
console.log(parent_2.children); // ['ugly_foo', 'bar']
方法三:子对象直接深拷贝父对象上的属性
function deepCopyObject(o) {
var c = {}, t;
for (var k in o) {
if (typeof o[k] === 'object') {
c[k] = (o[k].constructor === Array) ? [] : {};
c[k] = deepCopyObject(o[k]);
} else {
c[k] = o[k];
}
}
c.uber = o;
return c;
}
以上代码有一个问题,就是父对象中的每一个引用类型的属性,都会产生一个uber属性,并指向该引用类型的属性(好拗口> <)。
下面改进一下:
function deepCopyObject() {
// 使用闭包(closure),在闭包中创建root变量作为根父对象的判断标识
var root = true;
return function _deepCopyObject(o) {
var c = {};
if (root) { // 如此,子对象中便只有第一层会产生uber属性,并指向父对象
c.uber = o;
root = false;
}
for (var k in o) {
if (typeof o[k] === 'object') {
i += 1;
c[k] = (o[k].constructor === Array) ? [] : {};
c[k] = _deepCopyObject(o[k]);
} else {
c[k] = o[k];
}
}
return c;
}
}
var parent_3 = {
name: 'parent_3',
children: ['foo', 'bar'],
sayName: function () {
return this.name;
}
};
var c3 = deepCopyObject()(parent_3);
c3.name = 'child_3';
c3.children[0] = 'ugly_foo';
// 我们可以看到,对于子对象中引用类型的改动,没有影响到父对象
// 即是说,子对象中的引用类型属性,已经完全地存储到了不同的内存地址中,这就是深拷贝的作用
console.log(parent_3.children); // ['foo', 'bar']
方法四:模块化继承
到此为止,我们所看到的继承模式,无论是父还是子,都没有私有属性和方法,所有的属性和方法都是对外可见的,且能够被篡改。为此我们引进另一种好的方法——模块模式,如下:
// 父对象
var parent_4 = function(o) {
var that = {};
// 其他的私有属性
var children = ['foo', 'bar'];
// ...
var sayName = function() {
return 'parent sayName: ' + o.name;
};
var sayChildren = function () {
return children;
};
// 对外接口,暴露出可被继承的方法
// 且这里将函数的定义与暴露给that分两步开写的好处是:
// 1. 如果其他方法想要调用sayName,可以直接调用sayName()而不是that.sayName()
// 2. 如果该对象实例被篡改,比如that.sayName已经被替换掉,sayName将同样起作用,
// 因为sayName方法是私有的,重写that.sayName只会重新赋值,不会破坏到私有方法
that.sayName = sayName;
that.sayChildren = sayChildren;
// 最后返回包含了可被继承的属性和方法的对象
return that;
};
// 子对象
var child_4 = function(o) {
var that = parent_4(o); // 先继承父对象
// 同时我们还可以在子对象中调用父对象的方法,尽管上下文环境已经变化成子对象了(即this的指向)
var uberSayName = that.sayName;
// 或者也可以创建整个锚来指向父对象(浪费内存的做法)
that.uber = parent_4(o);
// 子对象自身的属性/方法
var sayName = function() {
return o.name;
};
var sayHi = function() {
return o.saying;
};
// 对外暴露子对象的方法/属性
that.sayName = sayName;
that.uberSayName = uberSayName;
that.sayHi = sayHi;
return that;
};
// 创建子对象实例
var c4 = child_4({
// name和saying属性现在完全是私有属性了,除非调用对外接口sayName和sayHi,否则无法对其进行访问,
// 这样,我们拥有了真正意义上的私有属性,而不是那些有着稀奇古怪名称的“伪私有属性”
name: 'child_4',
saying: 'hello world!'
});
console.log(c4.sayName()); // 'child_4'
console.log(c4.uberSayName()); // 'parent sayName: child_4'
console.log(c4.uber.sayName()); // 'parent sayName: child_4'
console.log(c4.sayHi()); // 'hello world!'
结论:使用模块化继承的好处很多,其中最重要的就是对私有属性的保护(对象封装),以及对外暴露接口(对象间通信),以及访问父对象方法的能力
欢迎交流,完。兄弟篇——继承:构造函数式