一 概览
这篇文章是我通过阅读《JavaScript 高级程序设计》和《你不知道的JavaScript》中关于 继承 模块的一点心得。
二 面向对象回顾
面向类编程
你是否还记得大学里面刚学C++的时候关于面向对象的介绍呢,让我们一块来回顾一下吧。
类的 定义:在面向对象编程中,类是一种 代码组织结构形式,一种从真实世界到软件设计的建模方法。
类的 组织形式:面向对象或者面向类编程强调 数据 和 操作数据的行为 应该 封装 在一起,在正式计算机科学中我们称为 数据结构。
类与23种高级设计模式
类是面向对象的 底层设计模式,它是面向对象23种高级设计模式的 底层机制。
你或许还听说过 过程化编程,一种不借助高级抽象,仅仅由 过程(函数)调用 来组织代码的编程方式。程序语言中,Java只支持面向类编程,C/C++/Php既支持过程化编程,也支持面向类编程。
类的机制
在类的设计模式中,它为我们提供了 实例化、 继承、多态 3种机制。
构造器:类的实例由类的一种特殊方法构建,这个方法的名称通常与类名相同,称为 “构造器(constructor)”。这个方法的明确的工作,就是初始化实例所需的所有信息(状态)。
实例化:借助构造函数,由通用类到具体对象的过程。
继承:子类通过 拷贝(请一定要记住这个词)父类的属性和方法,从而使自己也能拥有这些属性与方法的过程。
多态:由继承产生的,子类重写从父类中继承的属性和方法,从而子类更加具体。
类的继承
(1)相对多态:任何方法都可以引用位于继承层级上更高一层的其他方法(同名或不同名)。我们说“相对”,因为我们不绝对定义我们想访问继承的哪一层(也就是类),而实质上在说“向上一层”来相对地引用。
(2)超类:在许多语言中,使用 super 关键字来引用 父类或祖先类。
(3)如果子类覆盖父类的某个方法,原版的方法和覆盖后的方法都是可以存在的,允许访问。
(4) 不要让多态搞糊涂,子类并不是链接到父类上,子类只是父类的一个副本,类继承的实质是拷贝行为。
(5)多重继承:子类的父类不止一个,JavaScript不支持多重继承。
混入
原理: 子构造函数混入父构造函数的属性和方法。
JavaScript的复合类型以 引用 的方式传递,不支持拷贝行为。混入(Mixin) 以 手动拷贝 的方式模拟继承的拷贝行为。
明确混入:
(1)定义:显示的把一个对象的属性混入另一个对象。
(2)实现如下:
// 另一种mixin,对覆盖不太“安全”
// 大幅简化的`mixin(..)`示例:
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 仅拷贝非既存内容
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log( "Turning on my engine." );
},
drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
}
} );
(3)显示假想多态:Vehicle.drive.call(this)。因为ES6之前,JavaScript无法实现相对多态(inherit:drive()),所以我们明确地用名称指出Vehicle对象,然后在它上面调用drive()函数。
(4)问题:
A.技术上讲,函数没有被复制,只是复制了函数的引用;
B.在每一个需要建立 假想多态 引用的函数中都需要建立手动链接(Vehicle.drive.call(this)),维护成本高。可以尝试通过它实现 多重继承。
(5)结论:明确混入复杂、难懂、维护成本高,不推荐使用。
寄生继承:
(1)明确的mixin模式的一个变种,在某种意义上是明确的而在某种意义上是隐含的。
(2)实现如下:在子构造函数中new一个如果找函数的实例对象,在这个对象上扩展属性、方法,最后将这个对象返回。
// “传统的JS类” `Vehicle`
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log( "Steering and moving forward!" );
};
// “寄生类” `Car`
function Car() {
// 首先, `car`是一个`Vehicle`
var car = new Vehicle();
// 现在, 我们修改`car`使它特化
car.wheels = 4;
// 保存一个`Vehicle::drive()`的引用
var vehDrive = car.drive;
// 覆盖 `Vehicle::drive()`
car.drive = function() {
vehDrive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
};
return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
(3)问题:子函数的初始化创建对象丢失,改变了this绑定,不过不用new去直接创建。
隐式混入
(1)定义:父、子构造函数在原有构造函数与属性、方法之间,添加一层函数,子构造函数中间函数的this绑定到父构造函数中间函数
(2)实现原理:利用了this的二次绑定。
(3) 实现如下:
var Something = {
cool: function() {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
}
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function() {
// 隐式地将`Something`混入`Another`
Something.cool.call( this );
}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (不会和`Something`共享状态)
(4) 问题:单纯的利用this的二次绑定,不能实现相对应用。
(5) 结论:谨慎使用。
三 原型
prototype
prototype 定义:JavaScript中每个对象都拥有一个prototype属性,它只是一个 其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个 非null 值。
类
“类”函数
代码如下:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
结论: 当通过调用new Foo()创建实例对象时,实例对象会被链接到Foo.prototype指向的对象。
拷贝与链接
代码如下:
function Foo() {
}
Foo.prototype.fruit = ['apple'];
// foo1的[prototype]链接到了 Foo.prototype
var foo1 = new Foo();
foo1.fruit.push('banana');
// foo2的[prototype]也被链接到了 Foo.prototype
var foo2 = new Foo();
foo2.fruit // [apple, banana]
在面向类的语言中,可以创造一个类的多个拷贝。在JavaScript中,我们不能创造一个类的多个实例,可以创建多个对象,它们的[prototype]链接指向一个共同对象。但默认地,没有拷贝发生,如此这些对象彼此间最终不会完全分离和切断关系,而是 链接在一起。
“继承”意味着 拷贝 操作,而JavaScript不拷贝对象属性(原生上,默认地)。相反,JS在两个对象间建立链接,一个对象实质上可以将对属性/函数的访问 委托 到另一个对象上。对于描述JavaScript对象链接机制来说,“委托”是一个准确得多的术语。
new调用和普通调用本质相同
JavaScript中,new在某种意义上劫持了普通函数,并将它以另一种函数调用:构建一个对象,外加调用这个函数所做的任何事。
实例对象没有constructor属性
function Foo() { }
var foo1 = new Foo();
foo1.constructor === Foo // true
// 修改Foo.prototype指向的对象
Foo.prototype = {
//
}
var foo2 = new Foo();
foo2.constructor === Foo // false
a.constructor === Foo为true意味着a上实际拥有一个.constructor属性,指向Foo?不对。
实际上,.constructor引用也 委托 到了Foo.prototype,它 恰好 有一个指向Foo的默认属性。
3 “原型继承”
原型继承分析
代码如下:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
// 构造函数内部相对多态
Foo.call( this, name );
this.label = label;
}
// 这里,我们创建一个新的`Bar.prototype`链接链到`Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );
// 注意!现在`Bar.prototype.constructor`不存在了,
// 如果你有依赖这个属性的习惯的话,可以被手动“修复”。
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
核心代码分析:
代码1:
function Bar(para1, para2) {
Foo.call(this, para1);
//...
}
代码1分析:构造函数内部初始化,利用this绑定,根据父构造函数初始化子构造函数内部。
代码2:
Bar.prototype = Object.create(Foo.prototype)
代码2分析:原型初始化,将子构造函数的[prototype]链接到父构造函数的[prototype]链接的对象。
误区:
Bar.prototype = Foo.prototype
这种方法是错误的,子构造函数会污染到父构造函数
ES6 新方法:
Object.setPrototypeOf(Bar.prototype, Foo.prototype)
“自身”
面向类语言中,根据实例对象查找创建它的类模板,称为自省(或反射)。JavaScript中,如何根据实例对象,查找它的委托链接呢?
1 instanceOf:
代码如下:
function Foo() {
//...
}
var a = new Foo();
a instanceOf Foo // true
代码分析:
a: instanceOf 机制,在实例对象(a)的原型链中,是否有Foo.prototype;
b: 需要用于可检测的构造函数(Foo);
c: 无法判断实例对象间(比如a,b)是否通过[prototype]链相互关联。
2 isPrototypeOf [[prototype]]反射:
代码如下:
function Foo() {
// ...
}
var a = new Foo();
Foo.prototype.isPrototypeOf(a); // true
// 对象b是否在a的[[prototype]]链出现过
b.isPrototypeOf(a);
代码分析:
a:在实例对象(a)的原型链中,是否有Foo.prototype;
b:需要用于可检测的构造函数(Foo);
c:可以判断对象间是否通过[prototype]链相互关联。
3 getPrototypeOf 获取原型链:
代码如下:
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf(a) // 查看constructor属性
4 proto:
代码如下:
function Foo() {
// ...
}
var a = new Foo();
a.__proto__ === Foo.prototype // true
代码分析:
a:proto属性在ES6被标准化;
b:proto属性跟 constructor属性类似,它不存在实例对象中。constructor属性存在于 原型链中,proto存在于Object.prototype中。
c:proto看起来像一个属性,但实际上将它看做是一个getter/setter更合适。
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// setPrototypeOf(..) as of ES6
Object.setPrototypeOf( this, o );
return o;
}
} );
对象关联
创建关联
代码如下:
var foo = {
printFoo: function() {
console.log('foo');
}
}
var a = Object.create(foo);
a.printFoo(); // 'foo'
代码分析:
a、Object.create()会创建一个对象(a),并把它链接到指定对象(foo);
b、相比new 调用,Object.create()不会产生 prototype引用和 constructor引用。
关联是否备用
代码如下:
var anotherObject = {
cool: function() {
console.log('cool');
}
}
var bar = Object.create(anotherObject);
bar.cool(); // 'cool'
代码分析:
a、单纯的在bar无法处理属性或方法时,建立备用链接(anotherObject),代码会变得很难理解和维护,这种模式应该慎重使用;
b、ES6提供“代理”功能,它实现的就是“方法无法找到”时的行为。