读完《Javascript权威指南》和《Javascript高级程序设计》有关面向对象对应章节,对Javascript面向对象和实现继承的原理还是有些模糊,无法真正的理解,有可能对C++面向对象先入为主,希望本章通过描述,能够督促自己理解Javascript面向对象的本质亦是一桩收获。
如果你想深入理解Javascript,需要对文章中的每一个示例进行测试,并反复思考和总结。如果想快速写出正确的继承代码,直接跳至终极继承方案、拷贝方法两节进行查看。
Javascript面向对象基础
创建对象
Javascript中除去基本类型,如数值型,布尔型,字符串,其他皆为对象。不管是函数、数值都是对象。对象的创建很简单,归结下来如下几种方式。
1) 直接量创建对象
var obj1 = {}; //创建空对象
obj1.name = "Word"; //添加属性
var obj2 = { name:"Hello", age:18}; //创建对象时构造属性
function Person(name, age) {
this.name = name;
this.age = age;
}
3) 基于原型对象
function Animal() {
}
Animal.prototype.name = "hello kitty";
Animal.prototype.age = 5;
面向对象和基于对象
严格意义讲,Javascript不是面向对象的,而是基于对象。但是Javascript使开发者可以使用类似面向对象的特性。面向对象的三要素是封装、继承、多态。Javascript可以实现类似行为,但是底层机制是基于对象的,因此我们需要了解Javascript的面向对象机制。
封装
封装性是将内部实现隐藏起来,使用接口的形式向外提供,保证良好的扩展性、可读性、私有性。
Javascript使用闭包技术实现封装特性,示例如下:
function Encapsulation ()
{
var _name = "hello kitty";
this.setName = function(name) {
_name = name;
}
this.getName = function() {
return _name;
}
}
在函数体定义的变量_name无法被外界所访问,因此隐藏了内部变量,提供外部访问接口,从而实现面向对象的封装特性。
继承
继承是前提是抽象,抽象出公有特性,子类继承父类,承诺父类声明的义务(接口),或增强父类功能。 良好的面向对象继承设计准从如下原则。
原则一、尽可能将行为抽象出来,也就是Java语言里的抽象出接口,C++语言里的抽象类(纯虚接口)。
原则二、尽可能使用组合(is a),少用继承(has a)。除非明确是父子关系可以使用继承,因为继承的耦合度高。
Javascript天生使用基于对象来继承实现机制,也就是使用对象组合,并不是说Javascript有先见之明,应该有很多历史原因所造成。
关于继承的话题,在后面有更多介绍。
function Animal() {
}
Animal.prototype.name = "hello kitty";
Animal.prototype.age = 5;
function Dog() {
}
Dog.prototype = new Animal();
多态
多态和继承是天生一对,多态利用继承特性,子类能够覆盖父类定义的方法,调用者通过统一的接口调用,不同的对象产生不同的行为。多态在Java中使用Impl接口,在C++使用虚函数来实现。
Javascript中利用属性查找的优先顺序,可以模拟多态的效果,下面是Javascript中的示例。
function Animal() {
}
Animal.prototype.name = "hello kitty";
Animal.prototype.sayName = function() {
document.write("Name is " + this.name);
};
function Dog() {
}
Dog.prototype = new Animal();
Dog.prototype.sayName = function() {
alert("Name is " + this.name);
}
function Cat() {
}
Cat.prototype = new Animal();
var dog = new Dog();
var cat = new Cat();
dog.sayName();
cat.sayName();
Javascript的继承
Javascript语言基础
对象属性
1) 对象是由属性和方法所构成,属性和方式均可动态增加。
最直观的理解,可在对象上添加属性和访问方法。
2)对象的属性分为,可配置,可写,可枚举,值四个元属性。
可配置: 表示属性是否可删除,默认值true。
可写: 如果不可写,表示属性为只读属性,默认值true。
可枚举: 就是能否通过for-in枚举属性,默认值true。
值:属性值,默认值undefined。
3)对象皆为Object所派生,继承Object的原型。
所有对象的根都是Object。Java也有类似原理。
4)对象的隐藏属性。
prototype对象有称为原型对象,Javascript基于原型继承,每个实例都有一个原型对象属性。
原型对象既是一个对象,我们知道Javascript的使用的引用传递,数组类型等基本类型使用值传递。
prototype对象有一个constructor方法,指向对象的构造方法。
instanceof & isPrototypeOf
使用instanceof检查对象是否某个类的实例。语法 obj instanceof class
延用多态的代码示例,使用下面语法检查的结果是:
document.write("dog is Object:" + (dog instanceof Object));
document.write("dog is Animal:" + (dog instanceof Animal));
document.write("dog is Dog: " + (dog instanceof Dog));
document.write("dog is Cat: " + (dog instanceof Cat));
输出
dog is Object:true
dog is Animal:true
dog is Dog: true
dog is Cat: false
Class.prototype.isPrototypeOf(obj) 于 instanceof 用法类似。
dog instanceof Dog
转化为
Dog.prototype.isPrototypeOf(dog)
转化为
Dog.prototype.isPrototypeOf(dog)
hasOwnProperty
hasOwnProperty 用于检测是实例属性还是原型属性
true表示是一个实例属性,否则为原型属性
function Animal() {
this.age = 5;
}
Animal.prototype.name = "hello kitty";
var ani = new Animal();
document.write("name is OwnProperty = " + ani.hasOwnProperty("name");
document.write("age is OwnProperty = " + ani.hasOwnProperty("age"));
输出
name is OwnProperty = false
age is OwnProperty = true
对象的内存模型
function Animal(theName) {
this.name = theName;
}
Animal.prototype.sayName = function() {
document.write("Say Name : " + this.name + "</br>");
}
var ani = new Animal();
上图由两个构造函数Object,Animal; 两个原型对象Object Prototype, Animal Prototype, 一个实例对象instance。4根原型对象引用的线条(红色实线),3根构造函数引用的线条(绿色虚线)。
对象如何产生的?
*Object构造函数和Object Prototype是JS内置的,在任何Javascript环境中自然存在。
*当我们定义Animal构造函数时,内存中产生Animal构造函数,并且创建Animal的原型对象 Animal Prototype。
*当我new Animal();调用后,产生一个Animal的实例对象instance。
这些对象之间如何关联的?
*Javascript的原则一,任何对象都是Object对象的派生,继承Object。
*Javascipt的继承实质上是原型继承,是原型对象与原型对象间关联,从而实现继承。
*Animal Prototype是Animal的原型对象,它的Prototype属性指向Object Prototype(即Object的原型对象)。
*Animal构造函数和instance都有个一个prototype属性,并且都指向同一个原型对象。
*instance的constructor属性指向Animal构造函数。
*Animal Prototype的constructor属性指向Animal构造函数。
对象如何查找方法?
当在instance对象上使用toString()方法,会按照如下优先级进行查找属性。
1)instance的自身属性是否存在toString(),存在就结束,否则就执行2)。
2)通过访问instance的prototype属性访问原型对象,在原型对象上进行查找,存在就结束,否则就执行3)。
3)因为原型对象(Animal Prototype)也拥有prototype属性,通过prototype属性在父类的原型对象上进行查找,网上逐级查找,直到搜索到Object类。
Javascript继承实现
构造函数调用
function Animal(name) {
this.name = name;
this.sayName = function() {
document.write(this.name + "</br>");
}
}
function Dog(name) {
Animal.call(this, name);
}
var dog = new Dog("Wang Cai");
dog.sayName();
document.write("dog is Animal, " + (dog instanceof Animal) + "</br>");
document.write("dog is Dog, " + (dog instanceof Dog) );
输出:
Wang Cai
dog is Animal, false
dog is Dog, true
Dog的通过构造函数调用Animal,继承了Animal的name属性,但是dog跟Animal没有关系,因为instanceof返回值为false。
我们通过构造函数能够实现属性的继承和方法,但是Animal和Dog直接确没有发生联系,我的理解是通过调用Animal的构造函数,将this指针传递给Animal构造函数,实际上this执向Dog的实例,等同于为Dog动态增加属性和方法,严格意义来说是一种伪继承。
该方案内存模型:
图上可以看到出来,Dog和Animal没有任何联系,只是Dog和Animal实例拥有相同的name属性而已。
原型链继承
function Animal() {
this.name = "No Name";
document.write("Animal constructor called, name= " + this.name + "</br>");
}
Animal.prototype.sayName = function() {
document.write("Say Name : " + this.name + "</br>");
}
function Dog() {
document.write("Dog constructor called, name= " + this.name + "</br>");
}
Dog.prototype = new Animal();
var dog1 = new Dog();
var dog2 = new Dog();
dog1.sayName();
document.write( "Dog.prototype.constructor == Animal, " + (Dog.prototype.constructor == Animal) + "</br>");
document.write( "dog1.constructor == Animal, " + (dog1.constructor == Animal) + "</br>");
输出:
Animal constructor called, name= No Name
Dog constructor called, name= No Name
Dog constructor called, name= No Name
Say Name : No Name
Dog.prototype.constructor == Animal, true
dog1.constructor == Animal, true
1)Animal构造函数内定义name属性在Dog类并没有被真正继承。
2)Dog的实例对象constructor属性和Dog的原型对象prototype的Constructor属性都被改为Animal,实际上在没有继承前为Dog。
Animal constructor called 产生于Dog.prototype = new Animal();
Dog constructor called 两行产生于两次new Dog();
不同于C++面向对象的特点,子类构造时并没有调用父类的构造函数,因此也可以证明第1)个问题存在。
我们新增如下代码进行测试,让你更进一步了解Javascript的继承机制。
function Animal() {
this.name = "No Name";
this.type = ["Cat", "Dog"]; //新增
document.write("Animal constructor called, name= " + this.name + "</br>");
}
//新增
Animal.prototype.addType = function(newType) {
this.type.push(newType);
}
//新增
dog1.addType("Bird");
document.write("dog1 type is " + dog1.type + "</br>");
document.write("dog2 type is " + dog2.type + "</br>");
document.write("dog1.type == dog2.type, " + (dog1.type == dog2.type) + "</br>" );
新增输出:
dog1 type is Cat,Dog,Bird
dog2 type is Cat,Dog,Bird
dog1.type == dog2.type, true
dog1与dog2访问type属性是一个数组,是引用类型,并且指向同一个对象,这点和实例属性不同。因此再次也证明第1)问题存在。
如果说Dog没有继承Animal的实例属性,那么dog1和dog2对象访问type属性为什么会成功?原因是Javascript访问属性时如果在实例属性没有找到,会到原型对象(prototype)上继续寻找,而Dog的所有实例的原型对象指向同一个地址,也就是代码Dog.prototype = new Animal();通过new Animal()所产生的Animal实例对象。
该方案内存模型:
1)蓝色的虚线说明构造函数的指向有问题。
2)Dog Instance没有继承Animal的属性name与type。
组合继承
这种继承方式主要解决上一节的两个问题,具体代码如下:
function Animal(theName) {
this.name = theName;
this.type = ["Cat", "Dog"];
document.write("Animal constructor called, name= " + this.name + "</br>");
}
Animal.prototype.sayName = function() {
document.write("Say Name : " + this.name + "</br>");
}
Animal.prototype.addType = function(newType) {
this.type.push(newType);
}
function Dog(theName) {
Animal.call(this,theName); //新增
document.write("Dog constructor called, name= " + this.name + "</br>");
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; //新增
var dog1 = new Dog("Dog1");
var dog2 = new Dog("Dog2");
输出:
Animal constructor called, name= undefined
Animal constructor called, name= Dog1
Dog constructor called, name= Dog1
Animal constructor called, name= Dog2
Dog constructor called, name= Dog2
第一行打印依然由Dog.prototype = new Animal(); 中 new Animal()所产生。
我们通过上一节类似的测试代码,得到如下输出:
新增如下打印:
dog1 type is Cat,Dog,Bird
dog2 type is Cat,Dog
dog1.type == dog2.type, false
Dog.prototype.constructor == Animal, false
dog1.constructor == Animal, false
我们新增如下代码进行测试,并得出输出。
dog1.addType("Bird");
dog2.addType("Fish");
document.write("dog1 type is " + dog1.type + "</br>");
document.write("dog2 type is " + dog2.type + "</br>");
document.write("dog1.type == dog2.type, " + (dog1.type == dog2.type) + "</br>" );
delete dog1.type;
delete dog2.type;
document.write("delete dog1,dog2 type</br>");
document.write("dog1 type is " + dog1.type + "</br>");
document.write("dog2 type is " + dog2.type + "</br>");
document.write("dog1.type == dog2.type, " + (dog1.type == dog2.type) + "</br>" );
输出:
dog1 type is Cat,Dog,Bird
dog2 type is Cat,Dog,Fish
dog1.type == dog2.type, false
delete dog1,dog2 type
dog1 type is Cat,Dog
dog2 type is Cat,Dog
dog1.type == dog2.type, true
相信能够看的出来,由于dog1,dog2的原型对象prototype也拥有一个属性type,因此删除dog1,dog2的自身的type属性后,通过原型对象依然能够找到type属性。这样的继承实际上有很大的隐患。
我们实际上的想法是通过构造函数调用继承父类的实例属性,通过原型对象实现对父类原型属性的继承,并不希望原型对象也拥有父类的实例属性。
该方案内存模型:
过渡方案
将父类的原型对象赋值给子类原型对象
代码如下:
function Animal() {
}
function Dog() {
}
Dog.prototype = Animal.prototype;
Dog.prototype.constructor = Dog;
var ani = new Animal();
var dog = new Dog();
document.write(Animal.prototype.constructor == Dog.prototype.constructor);
document.write(ani.constructor == dog.constructor);
输出:
true true
该方案是解决Dog原型对象不再是Animal的实例对象,但是将Animal.prototype赋值给Dog.prototype之后,这两个类的原型对象执行同一地址,因此对子类原型对象的修改都将反应到父类的原型对象上。
该方案内存模型:
1) 蓝色虚线为错误的指向,Dog和Animal公用一个原型对象,操作Dog原型对象会影响Animal的原型对象。
虽然该无法完美解决上面的问题,但是已经为我们提供思路,请看下节终极继承方案。
终极继承
function Animal(theName) {
this.name = theName;
this.type = ["Cat", "Dog"];
document.write("Animal constructor called, name= " + this.name + "</br>");
}
Animal.prototype.sayName = function() {
document.write("Say Name : " + this.name + "</br>");
}
Animal.prototype.addType = function(newType) {
this.type.push(newType);
}
function Dog(theName) {
Animal.call(this,theName);
document.write("Dog constructor called, name= " + this.name + "</br>");
}
//Dog.prototype = new Animal();
//Dog.prototype.constructor = Dog;
//前面代码都一致,上面两行替换为:
function inherit(Child, Super) {
var F = function (){}; //新定义一个空构造函数
F.prototype = Super.prototype; //F对象的原型对象设置为父类原型对象。
Child.prototype = new F(); //子类的原型对象设置为F对象
Child.prototype.constructor = Child; //将子类构造函数更正
}
inherit(Dog, Animal);
var dog1 = new Dog("Dog1");
var dog2 = new Dog("Dog2");
输出:
Animal constructor called, name= Dog1
Dog constructor called, name= Dog1
Animal constructor called, name= Dog2
Dog constructor called, name= Dog2
看输出,相对于组合继承发现少了一行"Animal constructor call",原因在于没有创建一个Animal的对象作为Dog的原型对象。
其实终极方案原理很简单,利用了一个空对象F,将F的原型对象指向Animal的原型对象,然后将F实例对象作为Dog的原型对象,这样做一举两得。
1、F对象没有继承Animal对象的实例属性,因此将F对象作为原型对象的Dog类就没有继承Animal的实例属性。
2、利用F对象作为Dog和Animal之间的连接,使得Dog对原型对象的修改不影响Animal原型对象,从而避免了上一节“过渡方案”带来的问题。
新增如下代码测试:
dog1.addType("Bird");
dog2.addType("Fish");
document.write("dog1 type is " + dog1.type + "</br>");
document.write("dog2 type is " + dog2.type + "</br>");
document.write("dog1.type == dog2.type, " + (dog1.type == dog2.type) + "</br>" );
delete dog1.type;
delete dog2.type;
document.write("delete dog1,dog2 type</br>");
document.write("dog1 type is " + dog1.type + "</br>");
document.write("dog2 type is " + dog2.type + "</br>");
document.write("dog1.type == dog2.type, " + (dog1.type == dog2.type) + "</br>" );
输出:
Dog.prototype.constructor == Animal, false
dog1.constructor == Animal, false
dog1 type is Cat,Dog,Bird
dog2 type is Cat,Dog,Fish
dog1.type == dog2.type, false
delete dog1,dog2 type
dog1 type is undefined
dog2 type is undefined
dog1.type == dog2.type, true
该方案内存模型:
拷贝方法继承
至此,上述所有继承都是基于构造函数实现继承,如果没有构造函数,如何实现两个对象间的继承呢?这就是本节所要阐述的。
浅拷贝:
function Animal(theName) {
this.name = theName;
this.type = ["Cat", "Dog"];
document.write("Animal constructor called, name= " + this.name + "</br>");
}
Animal.prototype.sayName = function() {
document.write("Say Name : " + this.name + "</br>");
}
function shallowClone(theSrc) {
var clone = {};
for (var i in theSrc) {
clone[i] = theSrc[i];
}
return clone;
}
var ani = new Animal("Hello kitty");
var cloned = shallowClone(ani);
cloned.sayName();
document.write((cloned.constructor == Animal) + "</br>");
document.write((cloned.prototype == Animal.prototype) + "</br>");
输出:
Animal constructor called, name= Hello kitty
Say Name : Hello kitty
false
false
ani.type.push("Fish");
cloned.type.push("Bird");
document.write("cloned.type , " + cloned.type + "</br>");
document.write("ani.type , " + ani.type + "</br>");
输出:
cloned.type , Cat,Dog,Fish,Bird
ani.type , Cat,Dog,Fish,Bird
深拷贝:
function deepClone(theSrc, theDst) {
var theDst = theDst || {};
for ( var i in theSrc ) {
if ( typeof theSrc[i] === 'object') {
theDst[i] = (theSrc[i].constructor === Array) ? [] : {} ;
deepClone(theSrc[i], theDst[i]);
} else {
theDst[i] = theSrc[i];
};
}
return theDst;
}
var ani = new Animal("Hello kitty");
var cloned = {};
cloned = deepClone(ani, cloned);
通过上面的打印调用输出如下:
cloned.type , Cat,Dog,Bird
ani.type , Cat,Dog,Fish
深拷贝实现方法是判断出引用类型属性,使用递归的方法层层拷贝。
该方案内存模型:
1)cloned的对象于Animal也没有继承关系,但是将ani实例的属性和原型属性进行拷贝。
机制继承方式比较
是否基于构造函数继承: “拷贝方法”不基于构造函数继承, 其他都是基于构造函数继承
是否基于原型链继承: “构造函数调用”和“拷贝方法”不基于原型链,本质上没有实现对象的继承
几种原型链继承的比较: “原型链继承”、“组合继承”、“过度方案” 都存在缺陷,应该选择“终极继承方案”
JS推荐的继承方案有两种,“终极继承方案” 和 “拷贝方法”,JQuery库使用"拷贝方法“的继承方案,Node.js等框架推荐使用”终极继承方案“。
终极继承方案是完整的基于原型链继承方案,属于重量级继承,拷贝方法继承方案属于轻量级继承,由于JS的弱语言类型,只要对象拥有相同的属性,就可以实现多态。
小结
本章主要讨论了Javascript面向对象机制,对Javascript继承进行深入的剖析,对书上列举的几种继承方式进行进一步分析,画出各种情况下的内存状态图。通过本章的阐述,使我对Javascript面向对象的机制有了深刻的记忆。
对计算机语言掌握有五种层次,层次0:了解语法,记忆语法。 层次1:了解语法背后的机制,特别是内存模型。层次2:理解语言的实现机制。层次3:具备实现语言的能力。层次4:掌握语言本质,创造新的语言。本章可以看到出来是层次1的修炼,一般程序员可以修炼到层次2。如何才能修炼层次2呢,对应C++而言就是了解编译、链接原理,对于Javascript就是理解Javascript解析器的实现原理,希望后续有机会对Javascript层次2进行修炼。
参考
《Javascript权威指南》 David Flanagan
《Javascript高级程序设计》 Nicholas C.Zakas
修订
初稿 2014-11-19 Simon