您的位置:首页 > Web前端 > JavaScript

【javascript高级程序设计】读书摘录3 第六章、面向对象

2015-04-08 23:45 691 查看

第六章、面向对象的程序设计

这一章应该是Javascript中最抽象的一章,其中原型原型链构造函数等多次出现,几乎贯穿了整个章节。而对于创建对象和继承,也都是基于原型和构造函数而来的。因此这一部分的内容需要细细琢磨。尤其是对于原型、原型链,应该多画图,加深理解。

1、面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此它的对象业余基于类的语言中的对象有所不同

2、创建对象:

(1)、最简单的方式

创建一个Object实例,然后为它添加属性和方法:

var person = new Object();

person.name = "Nico";
person.age = 29;
person.job = "software engineer";

person.sayName = function(){
   console.log( this.name );
}

person.sayName();

这种模式有一个缺点,使用同一个接口创建很多对象时,会产生大量的重复代码

(2)、工厂模式

这种模式抽象了穿件具体对象的过程:

function createPerson( name, age, job ){
	var o = new Object();
	o.name = name;
	o.job  = job;
	o.age  = age;

	o.sayName = function(){
		console.log(this.name);
	};
	return o;
}

var p1 = createPerson("Nico", 29,"soft eg");
var p2 = createPerson("Greg", 25,"doctor");

p1.sayName();
p2.sayName();
console.log(p1.constructor);
console.log(p2.constructor);

工厂模式虽然解决了创建多个相似对象的代码冗余问题,却没有解决对象识别的问题(无法区分对象的类型)

(3)构造函数模式

function Person( name, age, job ){
	this.name = name;
	this.age  = age;
	this.job  = job;
 
	this.sayName = function(){
       console.log( this.name );
    }
}

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Greg", 25,"doctor");

p1.sayName();
p2.sayName();
 
console.log(p1.constructor);
console.log(p2.constructor);
console.log(p1 instanceof Person);
console.log(p1 instanceof Object);

对象的constructor属性最初是用来标识对象属性的,但是,提到检测对象类型,还是instanceof更加可靠一点

虽然构造函数非常好用,但是并非没有缺点,使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,上述例子中,p1和p2的sayName被创建了两遍,这两个函数并不相等:

console.log(p1.sayName == p2.sayName);//false

创建两个相同的函数并没有必要,可以把sayName函数移到构造函数外部来解决这个问题:

function Person( name, age, job ){
	this.name = name;
	this.age  = age;
	this.job  = job;

	this.sayName = sayName;
}

function sayName(){
   console.log( this.name );
}

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Greg", 25,"doctor");
 
p1.sayName();
p2.sayName();

这样又带来了新的问题:全局变量中定义的函数实际上只能被某个对象调用,这让全局作用域名不副实。如果对象需要定义很多方法,那么就需要定义很多全局函数,于是这个自定义的引用类型就毫无封装性可言了。可以用原型模式来解决这些问题。

(4)、原型模式

每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由任何特定类型的所有实例共享的属性和方法。使用原型对象的好处是让所有对象实例共享它所包含的属性和方法。因此,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中:

function Person(){
}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
   console.log( this.name );
}
var p1 = new Person();
p1.sayName();

var p2 = new Person();
p2.sayName();

console.log(p1.sayName == p2.sayName);//true

上述例子中,创建的新对象具有相同的属性和方法,新对象的属性和方法是由所有实例共享的。p1和p2访问的都是同一组属性和同一个sayName函数。

理解原型对象

无论何时,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获取一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针:

function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";

这个例子中,Person.prototype.constructor指向Person。Person的原型对象默认只会取得constructor属性,至于其他方法,则都是从Object中继承而来的。调用函数创建一个实例后,实例的内部将包含一个指针,指向构造函数的原型对象。在很多实现中,这个内部属性的名字是__proto__,而且通过脚本可以访问到(FireFox,Safari,Chrome和Flash的ActionScript中,都可以通过脚本访问__proto__),例如在firefox中:

function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
var p = new Person();
console.log(p);

打印出的结果:

这个连接存在于实例与构造函数的原型对象之间,而不是实例与构造函数之间。下图展示了各个对象之间的关系(分别是Person的构造函数,Person的原型对象和Person的两个实例):

在此,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象除了包含constructor属性外,还包括后来添加的其他属性。Person的每个实例,person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype,它们与构造函数没有直接的关系。值得注意的是,虽然这两个实例都不包含属性和方法,但是却可以调用person1.sayName,这是通过查找对象属性的过程来实现的。可以通过isPrototypeOf()确定对象之间的关系。

对象属性的查找过程:每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,那么返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找属性,如果在原型对象中找到了这个属性,则返回该属性的值。

虽然可以通过对象实例访问保存在原型,但却不能通过对象实例重写原型中的值。如果在实例中添加了一个属性,(如果原型对象中存在该属性)那么该属性就会覆盖原型对象中的属性:

function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
console.log( this.name );
}

var p1 = new Person();
p1.sayName();

var p2 = new Person();
p2.sayName();

p1.name = "new name";
console.log(p1.name);// new name 来自实例p1
console.log(p2.name);// Nico 来自原型对象


为对象添加一个属性时,这个属性就会屏蔽原型对象中的同名属性,换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使把这个属性设置为null, 也不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而可以从新访问原型中的属性:

var p1 = new Person();
var p2 = new Person();

p1.name = "new name";

delete p1.name;
delete p2.name;

console.log(p1.name);
console.log(p2.name);

使用hasOwnProperty()方法可以检测一个属性时存在于实例中,还是存在于原型中,这个方法只有在给定属性存在于对象实例中时,才会返回true:

var p1 = new Person();
var p2 = new Person();

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
p1.name = "new name";

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));
delete p1.name;
delete p2.name;

console.log(p1.hasOwnProperty("name" ));
console.log(p2.hasOwnProperty("name" ));

in操作符可以检测通过对象能否访问特定的属性,无论这个属性存在于原型中还是存在于实例中。

for-in循环可以返回所有能够通过对象访问的,可枚举的属性,其中既包含了存在实例中的属性,也包含了存在于原型中的属性:

function Person(){

}

Person.prototype.name = "Nico";
Person.prototype.age  = 29;
Person.prototype.job  = "soft eg";
Person.prototype.sayName = function(){
console.log( this.name );
}

var p1 = new Person();
p1.aprop = 'a property';
p1.aprop2 = 'another property';

for( var pro in p1 ){
console.log(pro + ":" + p1[pro]);
}


IE中存在一个bug,即屏蔽不可枚举的实例属性不会出现在for-in属性中。

为了从视觉上更好地封装原型的功能,更常用的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Person(){
}

Person.prototype = {
         name: "Nico",
         age  : 29,
         job  : "soft eg",
         sayName:function(){
                   console.log(this.name );
         }
};


这种方法创建的对象,其constructor属性不再指向Person了,尽管通过instanceOf还能返回正确的结果,但是constructor已经无法确定对象的类型了:

function Person(){

}

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   sayName: function(){
       console.log( this.name );
    }
};

var p1 = new Person();
console.log(p1.constructor);
console.log(p1 instanceof Person);

如果constructor值特别重要,可以将它设置为适当的值:

Person.prototype = {
   constructor:Person,
   name : "Nico",
   age  : 29,
   job  : "soft eg",

   sayName: function(){
       console.log( this.name );
    }
};

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有的对象实例中反应出来,但如果是重写整个原型对象,那么情况就不一样了:调用构造函数会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另一个对象就等于切断了构造函数与最初的原型对象之间的联系:实例中的指针仅仅指向原型,而不指向构造函数:

function Person(){

}

var p1 = new Person();

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   sayName: function(){
       console.log( this.name );
    }
};

console.log(p1.__proto__);
var p2 = new Person();
console.log(p2.__proto__);

原型模式的重要性不仅体现在自定义类型方面,就连所有的引用类型,都是按照这种方式创建的,通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法:

String.prototype.startWith =function(text){
         returnthis.indexOf(text) == 0;
}

var msg = "hello world";
console.log(msg.startWith("hello"));


原型对象的问题:

原型模式的缺点在于:它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认的情况下都取得相同的属性值。原型模式的最大问题是由其共享的本性导致的:

function Person(){

}

Person.prototype = {
   name : "Nico",
   age  : 29,
   job  : "soft eg",
   friends: ["f1", "f2"],
   sayName: function(){

       console.log( this.name );
    }

}

var p1 = new Person();
var p2 = new Person();

p1.friends.push("f3");

console.log(p1.friends);
console.log(p2.friends);

基于以上原因,很少单独使用原型模式。比较常用的方式是组合使用构造函数模式和原型模式:

function Person( name, age, job ){
   this.name = name;
   this.job = job;
   this.age = age;
   this.friends = [ ];
}

Person.prototype = {
    constructor: Person,
     sayName : function(){
         console.log( this.name );
     }
}

var p1 = new Person("Nico", 29,"soft eg");
var p2 = new Person("Gego", 25,"doctor");

p1.friends.push("f1","f2");
p2.friends.push("f2","f3","f4");

console.log(p1.friends);
console.log(p2.friends);

这个实例中,所有的实例属性都是在构造函数中定义的,而所有实例共享的属性则是在原型中定义的。这种构造函数与原型混合而成的模式,是目前ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方式。

动态原型模式

可以通过判断某个应该存在的方法是否有效,来决定是否需要初始化原型:

function Person( name, age, job ){
   this.name = name;
   this.job = job;
   this.age = age;

   if( typeof this.sayName != "function" ){
       Person.prototype.sayName = function(){
           console.log( this.name );
       }

    }
}

var p1 = new Person("Nico", 29,"soft eg");
p1.sayName();

寄生构造函数模式

这种模式的基本思想与构造函数相似,不同的是创建对象的方式:

function Person( name, age, job ){
   var o = new Object();
   o.name = name;
   o.job = job;
   o.age = age;

   o.sayName = function(){
       console.log( this.name );
   };

   return o;

}

var p1 = new Person("Nico", 29,"soft eg");

p1.sayName();

寄生构造函数模式返回的对象与构造函数或者构造函数的原型属性之间没有关系,也就是说,寄生构造函数返回的对象与构造函数外部创建的对象没什么不同,不能依赖于instanceof来确定对象类型。

稳妥构造函数模式

稳妥对象,指的是没有公共属性,而且方法也不引用this的对象,稳妥对象使用于一些安全的环境中。稳妥对象与寄生构造函数有两点不同:一是创建对象的实例方法不引用this,二是不适用new操作符调用构造函数:

function Person( name, age, job ){
   var o = new Object();
   o.sayName = function(){
       console.log( name );
   };
   return o;
}

var p1 = Person("Nico", 29,"soft");
var p2 = Person("cc",   22, "soft");
p1.sayName();
p2.sayName();

继承

许多OO语言都支持两种继承方式,接口继承和实现继承,接口继承只继承方法签名,实现继承则继承实际的方法。由于ECMA中函数没有签名,因此ECMAScript中无法实现接口继承,只支持实现继承,这是通过原型链来实现的。

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针(constructor),而实例则包含一个指向原型对象的内部指针。因此,如果让原型对象等于另一个类型的实例,结果是此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含了指向另一个构造函数的指针。如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。

实现原型链的基本模式:

function SuperType(){
   this.name = "super";
}

SuperType.prototype.getSuperValue =function(){
   console.log( this.name) ;
};

function SubType(){
   this.subName = "sub";
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
   console.log( this.subName );
}

var instance = new SubType();
instance.getSuperValue();
instance.getSubValue();

console.log(instance instanceof Object);
console.log(instance instanceof SuperType);
console.log(instance instanceof SubType);

实现的本质是重写原型对象,代之以一个新类型的实例。原来存在于SuperType实例中的所有属性和方法,现在也存在于SubType.prototype中了。这个例子中的实例以及构造函数和原型之间的关系如下:

通过实现原型链,本质上扩展了前面接扫的原型搜索机制。当读取一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,就会继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就会沿着原型链继续向上。上面的过程:搜索实例、搜索SubType.prototype, 搜索SuperType.prototype.。

事实上,上面介绍的原型链还缺少一环,所有的引用类型都继承自Object,而这个继承也是通过原型链实现的。因此,所有的函数的默认原型都是Object的实例,因此默认原型都包含一个内部指针,指向Object.prototype,这也是所有的自定义类型默认都会继承toString, valueOf()等默认方法的根本原因.一句话:SubType继承了SuperType,而SuperType继承了Object,当调用instance.toString时,实际上调用的是保存在Object.prototype中的那个方法。

确定原型和实例的关系:instanceof操作符和isPrototypeOf函数。

子类型有时需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后(即SubType.protoType = new SuperType()之后)。而且通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做为重写原型链。

原型链的两个问题:(1)引用类型属性的共享问题。(2)不能向超类型的构造函数中传递参数。由于上述两个原因,实践中很少单独使用原型链。

借用构造函数

这种技术的基本思想非常简单,子子类型的构造函数内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此可以通过apply和call方法在新创建的对象上执行构造函数:

function Super(){
   this.colors = ["red", "green", "blue"];
}

function Sub(){
   Super.call(this);
}

var p1 = new Sub();
p1.colors.push("yellow");
console.log(p1.colors);

var p2 = new Sub();
console.log(p2.colors);

组合继承:组合继承也叫伪经典继承,试讲原型链和构造函数的技术组合在一起,发挥两者之长。基本思路是使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。这样,即通过原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性:

function Super( name ){
   this.name = name;
   this.colors = ["red", "blue"];
}

Super.prototype.sayName = function(){
   console.log( this.name );
}

function Sub(name , age){
   Super.call(this, name);
   this.age = age;
}

Sub.prototype = new Super();
Sub.prototype.sayAge = function(){
   console.log( this.age );
}

var p1 = new Sub("nico", 29);
p1.colors.push("green");
p1.sayName();
p1.sayAge();

组合继承是javascript中最常用的继承模式。

寄生组合式继承时引用类型最理想的继承范式:

function obj( o ){
   function F(){}
   F.prototype = o;
   return new F();
}

function inherit(Sub, Super){
   var proto = obj( Super.prototype );
   proto.constructor = Sub;
   Sub.prototype = proto;
}

function Super(){
   this.name = name;
   this.colors = ["red", "blue" ];
}

Super.prototype.sayName = function(){
   console.log( this.name );
}

function Sub(name, age){
   Super.call( this, name );

   this.age = age;
}

inherit(Sub, Super);

Sub.prototype.sayAge = function(){
   console.log( this.age );
}


TODO: YUI
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐