JavaScript高级程序设计---学习笔记(二)
面向对象程序设计
1、属性类型、定义多属性、读取属性特性
对象的属性在创建时都带有一些特征值,JavaScript通过这些特征值来定义它们的行为。这些特性是为了实现JavaScript引擎用的,因此不能直接访问它们。
ECMAScript中有两种属性:数据属性和访问器属性。
1)数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特征。
1.[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
2.[[Enumerable]]:表示能否通过for-in循环返回属性。
3.[[Writable]]:表示能否修改属性的值。
4.[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读,写入属性的时候,把新值保存在这个位置。这个特性的默认值为undefined。
对于直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]、[[Writable]]特性的默认值都为true。而[[Value]]特性被设置为指定的值。例如:
var person = { name: "Kalus" };
这里创建了一个名为name的属性,为它指定的值是"Kalus",也就是说[[Value]]特性将被设置为"Kalus",而这个值的任何修改都将反映在这个位置。
要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。
其中描述符对象的属性必须是:configurable、enumerable、writable、value。设置其中的一个或多个值,可以修改对应的特征值。例如:
var person = {}; Object.defineProperty(person,"name",{ writable: false, value: "Kalus" }); alert(person.name);//Kalus person.name = "Demon"; alert(person.name);//Kalus
这个例子创建了一个名为name的属性,它的值"Kalus"是只读的,这个属性的值是不可修改的,如果尝试为它指定新值,则在非严格模式下赋值操作会被忽略,严格模式下会报错。
类似的规则也适用于不可配置的属性,例如:
var person = {}; Object.defineProperty(person,"name",{ configurable: false, value: "Kalus" }); alert(person.name);//Kalus delete person.name; alert(person.name);//Kalus
这个例子中把configurable设置为false,表示不能从对象中删除属性,如果对这个属性调用delete,则非严格模式下什么也不会发生,严格模式下会报错。
而且,一旦把属性定义为不可配置的,就不能再把它变回可配置的了。此时,再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误,如:
var person = {}; Object.defineProperty(person,"name",{ configurable: false, value: "Kalus" }); //抛出错误 Object.defineProperty(person,"name",{ configurable: true, value: "Kalus" });
也就是说,可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable特性设置为false之后就会有限制了。
在调用Object.defineProperty()方法时,如果不指定,configurable、enumerable、writable特性的默认值都是false。
2)访问器属性
访问器属性不包含数据值,它们包含一对getter和setter函数(两个函数都不是必需的)。
在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值。在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。
访问器属性有下列4个特性:
1.[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。直接在对象上定义的属性该特性的默认值为true。
2.[[Enumerable]]:表示能否通过for-in循环返回属性。直接在对象上定义的属性该特性的默认值为true。
3.[[Get]]:在读取属性时调用的函数,默认值为undefined。
4.[[Set]]:在写入属性时调用的函数,默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义,例:
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book,"year",{ get: function(){ return this._year; }, set: function(newValue){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition);//2
这个例子中创建了一个book对象并给她定义了两个默认的属性:_year和edition。_year前面的下划线是一种常用的标记,用于表示只能通过对象方法访问的属性。
而访问器属性year则包含一个getter和setter函数。getter函数返回_year的值,setter函数通过计算来确定正确的版本。因此,把year属性修改为2005会导致_year变成2005,
而edition变为2.这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。
3)定义多个属性
由于为对象定义多个属性的可能性很大,ECMAScript中定义了一个Object.defineProperties()方法,利用这个方法可以通过描述符一次定义多个属性。
这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应,例如:
var book = {}; Object.defineProperties(book,{ _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } } } });
例子中在book对象上定义了两个数据属性(_year和edition)和一个访问器属性(year)。最终的对象与上个例子中定义的对象相同。区别是这里的属性是在同一时间创建的。
4)读取属性的特性
使用Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符。该方法接收两个参数:属性所在对象和要读取其描述符的属性名称。其返回值是一个对象。
如果是访问器属性,这个对象的属性有:configurable、enumerable、get和set。如果是数据属性,这个对象的属性有:configurable、enumerable、writable和value。
例:
var book = {}; Object.defineProperties(book,{ _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function(){ return this._year; }, set: function(newValue){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } } } }); var descriptor = Object.getOwnPropertyDescriptor(book,"_year"); alert(descriptor.value);//2004 alert(descriptor.configurable);//false alert(typeof descriptor.get);//undefined var descriptor = Object.getOwnPropertyDescriptor(book,"year"); alert(descriptor.value);//undefined alert(descriptor.enumerable);//false alert(typeof descriptor.get);//function 是一个指向getter函数的指针
在JavaScript中,可以针对任何对象,包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。
2、创建对象
1)工厂模式:抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。工厂模式无法解决对象识别的问题(即怎样知道一个对象的类型)
例:
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var person1 = createPerson("Kalus",28,"Engineer"); var person2 = createPerson("Demon",29,"Doctor");
2)构造函数模式
可以创建自定义构造函数,从而定义自定义对象类型的属性和方法,例如,使用构造函数模式将上面的例子重写:
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("Kalus",28,"Engineer"); var person2 = new Person("Demon",29,"Doctor");
构造函数Person()和上面的createPerson()区别:
1.没有显示地创建对象
2.直接将属性和方法赋给了this对象
3.没有return语句
注意按照惯例构造函数的第一个字母应是大写,非构造函数以小写开头。
用构造函数方法想要创建Person的新实例,必须使用new操作符,以这种方法调用构造函数实际上会经历4个步骤:
1.创建一个新对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3.执行构造函数中的代码(为这个新对象添加属性)
4.返回新对象
构造函数与其他函数的唯一区别就是调用它们的方式不同,任何函数通过new操作符来调用就可以作为构造函数,不通过new操作符调用就和普通函数一样
例,上面的Person()函数可以通过下列三种方式来调用:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } //当作构造函数使用 var person = new Person("Kalus",28,"Engineer"); person.sayName(); //作为普通函数调用 Person("Demon",29,"Doctor");//添加到window,因为当在全局作用域中调用一个函数时,this对象指向window对象 window.sayName(); //在另一个对象的作用域中调用 var o = new Object(); Person.call(o,"Greg",30,"Nurse"); o.sayName();
3、原型模式
构造函数的缺点是每个方法都要在每个实例上重新创建一遍,上面的person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function实例,
因为函数是对象,因此每定义一个函数都实例化了一个对象,所以以这种方法创建函数会导致不同的作用域链和标识符解析,但创建Function新实例的机制是相同的,
因此,不同实例上的同名函数是不相等的:
alert(person1.sayName == person2.sayName);//false
这个问题可以通过原型模式来解决。
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
prototype就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,
即,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,例:
function Person(){ } Person.prototype.name = "Kalus"; Person.prototype.age = 28; Person.prototype.job = "Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Kalus" var person2 = new Person(); person2.sayName();//"Kalus" alert(person1.sayName == person2.sayName);//true,这样person1和person2访问的都是同一组属性和同一个sayName()函数,因为新对象的这些属性和方法是所有实例共享的
4、原型对象
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
默认情况下所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性指向prototype属性所在函数的指针,
如上面的例子,Person.prototype.constructor指向Person。通过这个构造函数,可以继续为原型对象添加其他属性和方法。
当调用构造函数创建一个新实例后,该实例内部将包含一个指针([[Prototype]]),指向构造函数的原型对象,
这个属性是不可见的,这个连接存在于实例与构造函数的原型对象之间。
虽然无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系,
如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true,如:
function Person(){
}
Person.prototype.name = "Kalus";
var person1 = new Person();
var person2 = new Person();
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true,因为创建新实例时内部都有一个指向Person.prototype的指针
Object.getPrototypeOf()方法可以返回[[Prototype]]的值,使用Object.getPrototypeOf()方法可以方便的取得一个对象的原型,如:
function Person(){ } Person.prototype.name = "Kalus"; var person1 = new Person(); alert(Object.getPrototypeOf(person1) == Person.prototype);//true,Object.getPrototypeOf()返回的对象实际就是这个对象的原型 alert(Object.getPrototypeOf(person1).name);//Kalus,取得了原型对象中name的值
虽然可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值,如果在对象实例中添加了一个属性,而该属性与实例原型中的一个属性同名,
那么就会在实例中创建该属性并会屏蔽掉原型中的那个属性,使用delete操作符可以完全删除实例中的属性,如:
function Person(){ } Person.prototype.name = "Kalus"; var person1 = new Person(); var person2 = new Person(); person1.name = "Demon"; alert(person1.name);//"Demon"---来自实例 alert(person2.name);//"Kalus"----来自原型
delete person1.name;
alert(person1.name);//"Kalus"----来自原型
原理:每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始,
如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中在查找具有给定名字的属性,
如果在原型对象中找到了这个属性,就返回该属性的值。
使用hasOwnProperty()方法可以检测一个属性是存在一个实例中,还是存在于原型中。(该方法继承自Object)
只有在给定属性存在于对象实例中时,才会返回true
5、原型与in操作符
1)在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在实例中还是原型中,如:
function Person(){ } Person.prototype.name = "Kalus"; var person1 = new Person(); var person2 = new Person(); person1.name = "Demon"; alert(person1.name);//"Demon"---来自实例 alert("name" in person1);//true alert(person2.name);//"Kalus"----来自原型 alert("name" in person2);//true
2)同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在与对象中,还是存在于原型中,如:
function Person(){ } Person.prototype.name = "Kalus"; var person1 = new Person(); var person2 = new Person(); person1.name = "Demon"; alert(person1.name);//"Demon"---来自实例 alert(person2.name);//"Kalus"----来自原型 function hasPrototypeProperty(object,name){ return object.hasOwnProperty(name) && (name in object); } alert(hasPrototypeProperty(person1,"name"));//true alert(hasPrototypeProperty(person2,"name"));//false
3)在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,既包括实例中的也包括原型中的。所有开发人员定义的属性都是可枚举的。
要取得对象上所有可枚举的属性,可以使用Object.keys()方法,该方法接受一个参数,返回一个包含所有可枚举属性的字符串数组,如:
function Person(){ } Person.prototype.name = "Kalus"; Person.prototype.age = 28; Person.prototype.job = "Doctor"; Person.prototype.sayName = function(){ alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys);//name,age,jib,sayName var person1 = new Person(); person1.name = "Demon"; person1.age = 30; var p1keys = Object.keys(person1); alert(p1keys);//name,age
4)如果想得到所有的属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames()方法,结果会包含不可枚举的constructor属性,如:
var keys = Object.getOwnPropertyNames(Person.prototype); alert(keys);//constructor,name,age,jib,sayName
Object.keys()方法和Object.getOwnPropertyNames()方法都可以用来替代for-in循环。
6、更简单的原型语法及原型的动态性
为了减少不必要的输入,也为了视觉上更好的封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如:
function Person(){ } Person.prototype = { name: "Kalus", age: 28, job: "Engineer", sayName: function(){ alert(this.name); } };
这里的Person.prototype设置为等于一个以对象字面量形式创建的新对象,注意constructor属性不再指向Person,
因为这里重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数)。
可以用下面的方法特意将constructor属性设回适当的值:
function Person(){ } Person.prototype = { constrcutor:Person, name: "Kalus", age: 28, job: "Engineer", sayName: function(){ alert(this.name); } };
原型的动态性:
由于在原型中查找值的过程是一次搜索因此对原型对象所做的任何修改都能立即从实例中反映出来,即使是先创建了实例后修改原型,如:
function Person(){ } var person1 = new Person(); Person.prototype.sayHi = function(){ alert("Hi"); } person1.sayHi();//Hi
但是如果是重写整个原型对象时,情况就会不一样,因为调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,
而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
注意:实例中的指针仅指向原型,而不指向构造函数。如:
function Person(){ } var person1 = new Person(); Person.prototype = { constructor: Person, name : "Kalus", age: 30, sayName: function(){ alert(this.name); } }; person1.sayName();//Uncaught TypeError: person1.sayName is not a function
上面的例子先创建了Person的一个实例,然后又重写了其原型对象,再调用person1.sayName()就会发生错误,因为person1指向的原型中不包含以该名字命名的属性。
所以重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型。
7、原型对象的问题
原型中所有属性是被很多实例共享的,对于包含引用类型值的属性来说会产生问题,如:
function Person(){ } Person.prototype = { constructor: Person, name: "Kalus", friends: ["Shellby","Court"] } var person1 = new Person(); var person2 = new Person(); person1.friends.push("Demon"); alert(person1.friends);//Shellby,Court,Demon alert(person2.friends);//Shellby,Court,Demon alert(person1.friends === person2.friends);//true
上面的例子修改了person1.friends引用的数组,向数组中添加了一个字符,由于friends数组存在于Person.prototype而非person1中,所以所做的修改
也会通过person2.friends(与person1.friends指向同一个数组)反映出来。
这样的话实例就不能拥有属于自己的属性了,所以很少单独使用原型模式。
8、组合使用构造函数和原型模式
创建自定义类型最常见的方式就是组合使用构造函数模式与原型模式。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,最大限度地节省了内存,这种混成模式还支持向构造函数传递参数。如:
function Person(name,age,job){ this.name = name; this.gae = age; this.job = job; this.friends = ["Kalus","Demon"]; } Person.prototype = { constructor: Person, sayName: function(){ alert(this.name); } } var person1 = new Person("Nicholas",29,"Enigneer"); var person2 = new Person("Greg",30,"Doctor"); person1.friends.push("Van"); alert(person1.friends);//Kalus,Demon,Van alert(person2.friends);//Kalus,Demon alert(person1.friends === person2.friends);//false alert(person1.sayName === person2.sayName);//true
9、动态原型模式
动态原型模式是把所有信息都封装在了构造函数中,通过在构造函数中初始化原型,保持了同时使用构造函数和原型的优点,
即可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,如:
function Person(name,age,job){ //属性 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); } } } var person1 = new Person("Kalus",30,"Engineer"); person1.sayName();
这里只在sayName()方法不存在的情况下才会将它添加到原型中,if语句中的代码只会在初次调用构造函数中执行,
此后原型已经完成初始化,不需再做修改。注意这里所做的修改能够立即在所有实例中得到反映。
10、寄生构造函数模式
在前面的几种模式都不适用的情况下,可以使用寄生构造函数模式,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); }; return o; } var person1 = new Person("Kalus",28,"Enigneer"); person1.sayName();//Kalus
这个例子中,Person函数创建了一个新对象,并以相应的属性和方法初始化了该对象,然后又返回了这个对象。
构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数返回的值。
这种模式可以在特殊的情况下用来为对象创建构造函数,比如想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这种模式。
例:
function SpecialArray(){ //创建数组 var values = new Array(); //添加值 values.push.apply(values,arguments); //添加方法 values.toSipedString = function(){ return this.join("|"); } //返回数组 return values; } var colors = new SpecialArray("red","blue","orange"); alert(colors.toSipedString());//red|blue|orange
这个例子中,创建了一个SpecialArray的构造函数,在这个函数内部首先创建了一个数组,然后push()方法(用构造函数接收到的所有参数)初始化了数组的值,
然后又给数组实例添加了一个toPipedString()方法,最后将数组以函数值的形式返回。之后调用了SpecialArray构造函数,向其中传入了用于初始化数组的值。
注意:关于寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说构造函数返回的对象与在构造函数外部创建的对象没有什么不同。
所以不能依赖instanceof操作符来确定对象类型,所以在可以使用其他模式的情况下不要使用这种模式。
11、继承
由于函数没有签名,所以在ECMAScript中无法实现接口继承,只支持实现继承,而且其实现继承主要依赖于原型链来实现。
1)原型链
原型链是作为继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
如果让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针,
假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。
实现原型链有一种基本模式,代码如下:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了SuperType SubType.prototype = new SuperType();//让原型对象等于另一个类型的实例 SubType.prototype.getSubValue = function(){ return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue());//true
1.上面的例子中定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法,它们的区别是SubType继承了SuperType,
而继承是通过创建SuperType的实例,并将实例赋给SubType.prototype实现的,实现的本质是重写原型对象,代之以新类型的实例。
所以原来存在于SuperType的实例中的所有属性和方法也存在于SubType.protype中了。
在确立了继承关系之后,给SubType.prototype添加了一个方法,这样就在继承了SuperType的属性和方法的基础上添加了一个新方法。
2.上面代码中,没有使用SubType默认提供的原型,而是给它换了一个新原型,这个新原型就是SuperType的实例。
于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。
最终结果就是:instance指向SubType的原型,SubType的原型又指向SuperType的原型。
getSuperValue()方法仍然还在SuperType.prototype中,但property则位于SubType.prototype中,这是因为property是一个属性,而getSuperValue()则是一个原型方法。
既然SubType.prototype现在是SuperType的实例,那么property当然就位于该实例中了。
3.注意:instance.constructor现在指向的是SuperType,这是因为原来的SubType.prototype中的constructor被重写了的缘故。
(实际上,不是SubType的原型的constructor属性被重写了,而是SubType的原型指向了另一个对象---SuperType的原型,而这个原型对象的constructor属性指向的是SuperType)
4.例子中的instance.getSuperValue()会经历三个搜索步骤:1)搜索实例 2)所有SubType.prototype 3)搜索SuperType.prototype,最后一步才找到该方法。
在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
5.实际上,上面的例子中展示的原型链还少一环,还应该包括另外一个继承层次,因为所有类型默认都继承了Object,而这个继承也是通过原型链实现的。
注意:所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,
这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。
所以,SubType继承了SuperType,而SuperType继承了Object。当调用instance.toString()时,实际上调用的是保存在Object.prototypr中的那个方法。
6.可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceOf操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
alert(instance instanceof Object);//true alert(instance instanceof SuperType);//true alert(instance instanceof SubType);//true
由于原型链的关系,我们可以说instance是Object、SuperType、或SubType中任何一个类型的实例,所以测试这三个构造函数的结果都返回true。
第二种方法是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true。
alert(Object.prototype.isPrototypeOf(instance));//true alert(SuperType.prototype.isPrototypeOf(instance));//true alert(SubType.prototype.isPrototypeOf(instance));//true
7.子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
8.通过原型链实现继承时,不能使用对象字面量创建原型方法,否则就会重写原型链。如:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了SuperType SubType.prototype = new SuperType(); //如果使用字面量添加新方法,会导致上一行代码无效 SubType.prototype = { getSubValue : function(){ return this.subproperty; }, someOtherMethod : function(){ return false; } }; var instance = new SubType(); alert(instance.getSuperValue());//error
这个例子中刚刚把SuperType的实例赋值给原型,紧接着又将一个原型替换成一个对象字面量,所以现在的原型包含的是一个Object的实例,而非SuperType的实例,
因此设想中的原型链已经被切断,SubType和SuperType之间就没有联系了。
9.原型链继承存在的问题:
最主要的问题来自包含引用类型值的原型。因为包含引用类型值的原型属性会被所有实例共享,所以在通过原型来实现继承时,原型实际上会变成另一个类型的实例,
于是原先的实例属性也会变成现在的原型属性。例:
function SuperType(){ this.colors = ["red","blue","orange"]; } function SubType(){ } //继承了SuperType SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors);//red,blue,orange,black var instance2 = new SubType(); alert(instance2.colors);//red,blue,orange,black
这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有各自包含自己数组的colors属性,
当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的colors属性,就和专门创建了
一个SubType.prototype.colors属性一样,但造成的结果就是所有的实例都会共享这一个colors属性,所以对instance1.colors的修改就会通过insatnce2.colors反映出来。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数,实际上是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
由于这两个问题,实践中很少会单独使用原型链。
2)借用构造函数
为了解决原型中包含引用类型值所带来的问题,可以使用一种叫做借用构造函数的方式(也叫做伪造对象或经典继承)。
其基本思想就是在子类型构造函数的内部调用超类型构造函数。
因为函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法可以在(将来)新创建的对象上执行构造函数。
例:
function SuperType(){ this.colors = ["red","blue","green"]; } function SubType(){ //继承了SuperType SuperType.call(this);//“借调”了超类型的构造函数。通过使用call()方法(或apply()方法),实际上是在(将要)新创建的SubType实例的环境下调用了SuperType构造函数。 // 这样就会在SubType对象上执行SuperType()函数中定义的所有对象初始化代码 } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors);//red,blue,green,black var instance2 = new SubType(); alert(instance2.colors);//red,blue,green
1.传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数,如:
function SuperType(name){ this.name = name; } function SubType(){ //继承了SuperType,同时还传递了参数 SuperType.call(this,"Kalus"); //实例属性 this.age = 29; } var instance = new SubType(); alert(instance.name);//Kalus alert(instance.age);//29
这个例子中的SuperType只接受一个参数name,该参数会直接赋给一个属性。在SubType构造函数内部调用SuperType构造函数时,
实际上是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后再添加应该在子类型中定义的属性。
2.借用构造函数的问题
借用构造函数中的方法都在构造函数中定义,所以就不能函数复用。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式,
由于这些问题,借用构造函数的技术也是很少单独使用。
3)组合继承
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块从而发挥二者之长的一种继承模式。
其背后的思想是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
这样就可以既通过在原型上定义方法实现函数复用,又能够保证每个实例都有它自己的属性。
例:
function SuperType(name){ this.name = name; this.colors = ["red","blue","green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name); //继承属性 this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("Kalus",29); instance1.colors.push("black"); alert(instance1.colors);//red,blue,green,black instance1.sayAge();//29 instance1.sayName();//Kalus var instance2 = new SubType("Demon",30); alert(instance2.colors);//red,blue,green instance2.sayAge();//30 instance2.sayName();//Demon
例子中,SuperType构造函数定义了两个属性:name和colors。SuperType原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name属性
紧接着又定义了自己的属性age。然后将SuperType的实例赋给SubType的原型,又在该新原型上定义了方法sayAge()。
这样就可以让两个不同的SubType实例既可以分别拥有自己的属性(包括colors属性),也可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,是最常用的继承模式。而且instanceOf和isPrototypeOf()也能够用于识别基于组合继承创建额的对象。
4)原型式继承
原型式继承要求必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改。
例:
var person = { name: "Kalus", friends: ["Shelby","Court","Van"] }; var anotherPerson = Object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = new Object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends);//Shelby、Court、Van、Rob、Barbie
这个例子中,可以作为另一个对象基础的是person对象,把它传入到了object()函数中,然后该函数就会返回一个新对象。
这个新对象将person作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。
这就意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际相当于又创建了person对象的两个副本。
ECMAScript5新增了Object.create()方法规范化了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象、一个为新对象定义额外属性的对象(可选的)。
在传入一个参数的情况下,Object.create()与Object()方法的行为相同。例:
var person = { name: "Kalus", friends: ["Shelby","Court","Van"] }; var anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson =Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends);//Shelby、Court、Van、Rob、Barbie
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。
以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例:
var person = { name: "Kalus", friends: ["Shelby","Court","Van"] }; var anotherPerson = Object.create(person,{ name:{ value: "Greg" } }); alert(anotherPerson.name);//Greg
5)寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式相似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象。
例:
function createAnother(original){ var clone = Object(original);//通过调用函数创建一个新对象 clone.sayHi = function(){//以某种方式来增强这个对象 alert("hi"); }; return clone;//返回这个对象 } var person = { name: "Kalus", friends: ["Shelby","Court","Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi();//hi
这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象,然后把这个对象(original)传递给Object()函数,将返回的结果赋给clone,
再为clone对象添加一个新方法sayHi(),最后返回clone对象。后面调用了createAnother(original)函数,基于person返回了一个新对象----anotherPerson,
新对象不仅具有person的所有属性和方法,而且还有自己的sayHi方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式,使用的Object函数不是必需的,任何能够返回新对象的函数都适用此模式。
使用寄生式继承和构造函数模式有相似的问题:会由于不能做到函数复用而降低效率。
6)寄生组合式继承
组合继承虽然是js中最常用的继承模式,但它存在最大的问题是:无论什么情况下都会调用两次超类型构造函数:一次是在创建子类型原型的时候,一次是在子类型构造函数内部。
子类型最终会包含超类型对象的全部实例属性,但不得不在调用子类型构造函数时重写这些属性,如下例子:
function SuperType(name){ this.name = name; this.colors = ["red","blue"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name);//第二次调用SuperType() this.age = age; } SubType.prototype = new SuperType();//第一次调用SuperType SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); };
例子中,在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors,它们都是SuperType的实例属性,只不过现在位于SubType的原型中,
当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
这两组name和colors属性:一组在实例上,一组在SubType原型中,这就是调用两次SuperType构造函数的结果,解决这个问题就可以使用寄生组合式继承。
寄生组合式继承就是借用构造函数来继承属性,通过原型链的混成形式来继承方法。
其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的只是超类型的一个副本,所以本质上就是使用寄生式继承来继承超类型的原型,
然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下:
function inheritPrototype(sunType,superType){ var prototype = Object(superType.prototype);//创建对象 prototype.constructor = subType;//增强对象 subType.prototype = prototype;//指定对象 }
这个inheritPrototype()函数实现了寄生组合式继承的最简单形式。函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本,
第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性,最后一步将新创建的对象(即副本)赋值给子类型的原型。
这样就可以调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。例:
function inheritPrototype(subType,superType){ var prototype = Object(superType.prototype);//创建对象 prototype.constructor = subType;//增强对象 subType.prototype = prototype;//指定对象 } function SuperType(name){ this.name = name; this.colors = ["red","blue","orange"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name); this.age = age; } inheritPrototype(subType,SuperType); SubType.prototype.sayAge = function(){ alert(this.age); }
这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。同时原型链还能保持不变,
因此还能正常使用instanceOf和isPrototypeOf()。寄生组合式继承是引用类型最理想的继承范式。
总结:
ECMAScript支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。
在没有类的情况下,可以采用下列模式来创建对象:
1.工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回对象,该模式后来被构造函数模式所取代。
2.构造函数模式:可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。缺点是它的每个成员都无法得到复用,包括函数。
由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
3.原型模式:使用构造函数的prototype属性来指定那些应该共享的属性和方法。
组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样子类型就能够访问超类型的所有属性和方法。
原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。
这样就可以做到每个实例都有自己的属性,同时还能保证只使用构造函数模式来定义类型。
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
此外,还存在下列可供选择的继承模式:
1.原型式继承:可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制,而复制得到的副本还可以得到进一步改造。
2.寄生式继承:与原型继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。
为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
3.寄生组合式继承:集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。