Javascript面向对象剖析

        读完《Javascript权威指南》和《Javascript高级程序设计》有关面向对象对应章节,对Javascript面向对象和实现继承的原理还是有些模糊,无法真正的理解,有可能对C++面向对象先入为主,希望本章通过描述,能够督促自己理解Javascript面向对象的本质亦是一桩收获。

        如果你想深入理解Javascript,需要对文章中的每一个示例进行测试,并反复思考和总结。如果想快速写出正确的继承代码,直接跳至终极继承方案、拷贝方法两节进行查看。



Javascript面向对象基础

创建对象

      Javascript中除去基本类型,如数值型,布尔型,字符串,其他皆为对象。不管是函数、数值都是对象。对象的创建很简单,归结下来如下几种方式。

1) 直接量创建对象
var obj1 = {};       //创建空对象
obj1.name = "Word";  //添加属性

var obj2 = { name:"Hello", age:18};  //创建对象时构造属性

2) 构造函数创建对象
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();

dog,cat两个实例对象的行为不同,但是可以使用统一的方法调用,Javascript是弱类型的,不用C++实现多态时必须使用父类指针才能产生多态效果,可以将dog,cat两个对象放到数组里,然后迭代调用,Javascript的弱类型更方便多态性的使用。


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)

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();

当定义一个Animal构造函数,创建一个Animal实例后,它们在内存中的模型如下图:
OO1

上图由两个构造函数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动态增加属性和方法,严格意义来说是一种伪继承。

该方案内存模型:
OO2
图上可以看到出来,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实例对象。


该方案内存模型:
OO3
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属性。这样的继承实际上有很大的隐患。
我们实际上的想法是通过构造函数调用继承父类的实例属性,通过原型对象实现对父类原型属性的继承,并不希望原型对象也拥有父类的实例属性。

该方案内存模型:
OO4


过渡方案 

将父类的原型对象赋值给子类原型对象
代码如下:
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之后,这两个类的原型对象执行同一地址,因此对子类原型对象的修改都将反应到父类的原型对象上。

该方案内存模型:
OO5
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

通过终极方案的描述,解决了上述所有方案存在的问题,集合所有方案的有点,终极方案当之无愧。

该方案内存模型:
OO6



拷贝方法继承

至此,上述所有继承都是基于构造函数实现继承,如果没有构造函数,如何实现两个对象间的继承呢?这就是本节所要阐述的。

浅拷贝
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

拷贝方法的原理就是通过属性动态增加的特性将源对象所有可枚举的属性进行拷贝。浅拷贝对于引用类型的拷贝做得不够好,因为只是将引用类型进行复制仅仅对引用的对象多了一次引用,比如Animal的type数组是一个引用类型,我们通过下面测试代码看看是什么效果。

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

cloned和ani两个对象的type属性实际指向同一地址。下面介绍下深拷贝。



深拷贝
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

深拷贝实现方法是判断出引用类型属性,使用递归的方法层层拷贝。

该方案内存模型:
OO7
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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值