JavaScript_设计模式_中文版_第4章继承.doc_第1页
JavaScript_设计模式_中文版_第4章继承.doc_第2页
JavaScript_设计模式_中文版_第4章继承.doc_第3页
JavaScript_设计模式_中文版_第4章继承.doc_第4页
JavaScript_设计模式_中文版_第4章继承.doc_第5页
已阅读5页,还剩10页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

六月翻译QQ:13810701第4章 继承继承在JS中是一个非常复杂的课题,其困难程度远远超过了其他OO语言。不像在其他OO语言中用一个简单的关键字就可以继承一个类,JS需要一系列的步骤来使公共成员得以继承。使问题变得进一步复杂的是,JS是少数使用原型来继承(稍后会它的优势)的语言之一。当然,由于JS的灵活性,你可以选择使用标准的基于类的继承,或者稍微有些棘手的原型继承。在本章中,我们来看下JS中创建子类的方法,以及适用它们的不同情况。为什么要使用继承在我们涉及任何代码之前,我们需要知道使用继承有什么好处。一般来讲,你希望你设计的类可以减少大量重复的代码并且尽可能保持对象之间的松耦合。继承可以帮助你实现这两个概念中的第一个,并且可以让你在现有的类的基础上补充它的方法。他也允许你很容易的修改。如果你有几个类都需要一个toString方法用特定方法来做结构输出,你需要拷贝并且粘贴方法给每一个类。但当你每次修改这个方法时,你必需要为每个类修改。但如果你创建了一个ToStringProvider类,并且让这些类继承了它,那么其方法的代码仅需修改一次。让一个类继承另一个类有一种可能性,你使他们之间的耦合增强。就是说,一个类依靠内部实现了另外一个类。我们来看下一些避免这种情况的方法,包括使用掺元类(mixin classes)来世像其他类提供方法。标准继承JS可以像标准继承语言一样。通过使用函数声明类,并用new关键字来创建实例,对象的行为也和Java或C+中的类非常类似。这实在JS中一个非常基本的类声明。/* Class Person. */function Person(name) = name;Ptotype.getName = function() return ;首先创建构造体,按照惯例,类名称要使用了一个大写字母开头。在构造体中,使用this关键字来创建实例的属性。要创建方法,就把方法添加到类的prototype对象中,例如Ptotype.getName。要创建实例,只需要用new关键字调用构造体函数即可。var reader = new Person(John Smith);reader.getName();然后你就可以访问实例所有的属性和方法了,这是JS中非常简单的一类的例子原型链要创建一个类来继承Person就稍微有些复杂了:/* Class Author. */function Author(name, books) Person.call(this, name); / Call the superclasss constructor in the scope of this.this.books = books; / Add an attribute to Author.Atotype = new Person(); / Set up the prototype chain.Atotype.constructor = Author; / Set the constructor attribute to Author.Atotype.getBooks = function() / Add amethod to Author.return this.books;建立一个类去继承另外一个类需要多行代码(并不像大多数OO语言那样紧紧使用一个extend关键字就可以),首先,像前面的例子一样创建一个构造体函数。在这个构造体中,调用父类的构造体,并传递name参数给它。这一行应该多解释下。当你使用new操作符时,发生了相应的事情。首先一个空的对象被创建,然后构造体在在作用域链之前被这个空对象调用;this在每个构造体函数中指向空对象。所以要在Author调用父类的构造体,你必须要做类似的事情。Person.call(this,name)在将name作为一个参数时,通过空对象(本例中为this)在作用域链头部调用Person的构造体函数。接下来是创建原形链。尽管事实上实现它的代码很简单,但是它却是一个非常复杂的问题。在前面提过,JS没有extend关键字,但是,每个对象都有一个称之为prototype的属性,这个属性为另外一个对象或者为null。当对象的一个成员被访问时(例如reader.getName),如果当权对象中没有这个成员,JS就会寻找这个成员是否在对象的prototype对象中。如果在那也没有找到,它就顺着原型链,访问每一个对象的prototype直到这个成员被找到(或prototype为null)这意味着为了使一个类继承其他的类,你必须要让这个子类的prototype属性指向其父类的实例。最后一步是设置constructor属性为Author(当你设置prototype属性到Person的实例时,这个constructor属性被销毁了)。尽管创建一个继承需要三行额外的代码,但创建这个新子类的过程和Person是一样的。var author = ;author0 = new Author(Dustin Diaz, JavaScript Design Patterns);author1 = new Author(Ross Harmes, JavaScript Design Patterns);author1.getName();author1.getBooks();所有标准继承的复杂性都在类声名中,创建一个新的实例依然很简单。扩展函数为了使类的声明更加简单,你可以把整个子类化的过程放到一个称之为extend扩展函数中。完成跟其他语言相同的extend关键字所完成的事根据已有的结构创建一个新的对象。/* Extend function. */function extend(subClass, superClass) var F = function() ;F.prototype = superCtotype;subCtotype = new F();subCtotype.constructor = subClass;这个函数完成了一些你需要手工完成的事,它指定了protypype并且重新设置了正确的constroctor。注意,它增加了一个空类F在原型链中以避免一个新的(也可能是巨型的)子类实例被实例化。对避免子类构造体中副作用或者大量密集的运算有很多好处。由于被实例化后的对象对于原型来说只是一个被抛弃的实例,因此你不要觉得用这个空类F是没必要的。现在,前面的Person/Author例子就变成了:/* Class Person. */function Person(name) = name;Ptotype.getName = function() return ;/* Class Author. */function Author(name, books) Person.call(this, name);this.books = books;extend(Author, Person);Atotype.getBooks = function() return this.books;通过手工设置prototype和constructor属性被替换成为简单在在类声明后(在你为prototype添加任何方法之前)立即调用extend函数。唯一的问题是父类的名称(Person)在Author声明中被硬编码。这可以被改成更普遍一点的做法:function extend(subClass, superClass) var F = function() ;F.prototype = superCtotype;subCtotype = new F();subCtotype.constructor = subClass;subClass.superclass = superCtotype;if(superCtotype.constructor = Ototype.constructor) superCtotype.constructor = superClass;这个版本要更长一些,但却提供了superclass属性,这可以为Author和Person解耦,函数的头4行代码跟前面一样,最后三行代码确保了constructor属性正确的被指定到了superclass(甚至superclass就是Object类本身)。这点在你使用新的superclass调用父类的构造体时十分有用。/* Class Author. */function Author(name, books) Author.superclass.constructor.call(this, name);this.books = books;extend(Author, Person);Atotype.getBooks = function() return this.books;增加superclass属性后你可以直接调用父类的函数,如果你想覆盖一个父类方法但又必须访问父类的实现方法时,这是非常有用的。例如,要覆盖Person的getName函数,你可以首先调用Author.superclass.getName来获取原始的名字然后在后面追加内容:Atotype.getName = function() var name = Author.superclass.getName.call(this);return name + , Author of + this.getBooks().join(, );原型继承原型继承是又是另外一种讨厌。我们探讨它最好的方法就是忘记你所有关于类和实例的知识,并仅仅考虑对象。标准的创建对象方法是使用一个类声明定义对象的结构体,然后通过实例化来创建一个新的对象。通过这种方法创建的对象拥有所有实例属性的拷贝,加一个独立的对所有实例方法的链接。在原型继承中,不是通过类来指定对象构造体,而仅仅是简单的创建了一个对象。然后这个对象被新的对象重用,这要感谢原型链的使用。它被称之为prototype object(原型对象),因为它使得其他对象看起来很接近(为了避免和其他prototype对象混淆,我们用了斜体字)。这正是原型继承的名字由来。我们现在使用原型继承重新创建Person和Author类/* Person Prototype Object. */var Person = name: default name,getName: function() return ;类的结构并没有使用一个名为Person构造体函数来声明,而是用了一个对象直接量。它现在是其他所有你想创建的类似于Person对象的prototype object(原型对象)。定义所有你想让这些对象拥有的属性和方法,并给它们默认值。对于方法来说,默认值可能不会改变,但对于属性来说,它们则肯定会被修改。var reader = clone(Person);alert(reader.getName(); / This will output default = John Smith;alert(reader.getName(); / This will now output John Smith.为了创建一个类似于Person的新对象,使用了clone函数(我们再稍后的章节“clone函数中”将会研究更多的细节)。这提供了一个包含prototype属性的空对象,而prototype属性被设置prototype object(原型对象)。这意味着这个对象上的方法或者属性查找失败,那么查找就会在它的prototype object(原型对象)上继续。要创建Author,你不必为Person创建一个子类,而是创建它的一个克隆(clone);/* Author Prototype Object. */var Author = clone(Person);Author.books = ; / Default value.Author.getBooks = function() return this.books;这个克隆体的方法和属性可以被覆盖。你可以修改来自Person的默认值,或者你也可以为其添加新的属性和方法。同样这也创建了一个新的prototype object,这可以让你克隆一个新的类似于Author的对象。var author = ;author0 = clone(Author); = Dustin Diaz;author0.books = JavaScript Design Patterns;author1 = clone(Author); = Ross Harmes;author1.books = JavaScript Design Patterns;author1.getName();author1.getBooks();继承成员的读写不平衡我们前面提到的,为更高效的使用原型继承,你必须忘记你关于标准继承的所有知识。这有一个相关的例子。在标准继承中,每个Author的实例拥有自己的books数组。你必须用author1.books.push(New Book Title)这样的语法来添加。但是这个语法对你使用原型继承来创建的对象来说是不可能初始化的,因为有原型链的存在。一个克隆体不完全是其prototype object的拷贝;他是一个拥有指向原型对象的prototype属性的新建空对象。当它在创建的时候,确实指向原始的。这是因为被prototype的连接源对象具有读写的不对称继承。当你读取的值时,假如你还没有为author1设置name属性,你会得到连接到原型上的值,当你为写入新值时,你事实上是直接为author1定义了一个新的属性。这个例子描述了这个不对称性:var authorClone = clone(Author);alert(authorC); / Linked to the primative P, which is the / string default name.authorC = new name; / A new primative is created and added to the / authorClone object itself.alert(authorC); / Now linked to the primative authorC, which / is the string new name.authorClone.books.push(new book); / authorClone.books is linked to the array / Author.books. We just modified the / prototype objects default value, and all / other objects that link to it will now / have anew default value there.authorClone.books = ; / Anew array is created and added to the authorClone / object itself.authorClone.books.push(new book); / We are now modifying that new array.这同样也说明了为什么必须要为引用传递创建新的数据类型拷贝。在上面的例子中,为authorClone.books推送的新值事实上被推送到了Author.books上,这不是预期的,因为你仅仅想修改某个继承自Author的对象,而不是Author。你必须在修改数组和对象的成员之前为他们创建新的拷贝。这点很容易被忘记并导致prototype object的值直接被修改。应该避免这点,测试它们的类型费时费力。在这种情况下,你可以使用hasOwnProperty方法来分辨继承的成员和对象实际拥有的成员:有时,原型对象内部有自己的子对象,如果你想覆盖子对象的某个值,你必须重建整个对象。这可以通过将子对象创建为一个空的对象直接量之后重建来完成,但这意味着克隆的对象必须清楚每个子对象的确切结构和默认值。为了让所有的对象解耦,任何复杂的子对象应该这样创建:var CompoundObject = string1: default value,childObject: bool: true,num: 10var compoundObjectClone = clone(CompoundObject);/ Bad! Changes the value of CompoundObject.childOpoundObjectClone.childObject.num = 5;/ Better. Creates a new object, but compoundObject must know the structure/ of that object, and the defaults. This makes CompoundObject and / compoundObjectClone tightly poundObjectClone.childObject = bool: true,num: 5;在这个例子中,childObject被重建,而compoundObjectClone.childObject.num被修改,问题是compoundObjectClone必须要知道childObject有两个属性,值分别为true和10.一个更好的方案是用一个工厂方法来创建childObject:/ Best approach. Uses amethod to create anew object, with the same structure and/ defaults as the original.var CompoundObject = ;CompoundObject.string1 = default value,CompoundObject.createChildObject = function() return bool: true,num: 10;CompoundObject.childObject = CompoundObject.createChildObject();var compoundObjectClone = clone(CompoundObject);compoundObjectClone.childObject = CompoundObject.createChildObject();compoundObjectClone.childObject.num = 5;clone函数那么用来创建这些克隆对象的函数是如何的惊人呢:/* Clone function. */function clone(object) function F() F.prototype = object;return new F;首先clone函数创建了一个新的空函数F,然后将F的prototype属性赋值为prototype object,你可以看到原始的JS构造器。prototype属性的意思是指向原型对象,并且沿着原型链链接所有的继承成员。最后,这个函数通过在F上调用new操作符创建了一个新的对象。被返回的克隆对象完全为空,除了prototype属性,这个属性经由F对象指向了prototype object。标准继承和原型继承的比较两种创建新对象的模型是截然不同的,创建出来的对象也有很大区别,每种方案都有自己的优缺点,这可以让你根据情况来选择。标准继承容易被理解,同时在JS和程序员之间的对话。几乎所有用JS写成的OO代码都使用这种方案。如果你需要创建一个广泛使用的API,或者其他和你一起工作的程序员不熟悉原型继承,那么最好的方法就是用标准方法。JS是唯一广泛使用而又使用原型继承的语言,这样的差异是大多数人永远也不会使用的。它的混乱之处在于对象有一个指向它prototype object的连接。无法完全理解原型继承的程序员会把它当作某种反向继承,及父代继承子代。尽管事实并非如此,这也是一个很复杂的问题。但是由于标准继承是唯一模仿了真实的基于类的继承。高级的JS程序员需要理解原型继承在某系时候是如何工作的。一些人也许会认为这样做弊大于利。原型继承对提高内存使用效率有很大贡献。因为它是通过原型链来读取成员的,所有的克隆对象通过一个拷贝来分享所有的属性和方法,直到克隆对象通过直接写入值来覆盖。相对而言,标准继承创建的对象后为每个对象建立了一个所有属性(以及私有方法)的拷贝。它节省了大量内存。它看起来也非常的优雅,因为它仅需要一个clone函数,而不需要数行类似于SuperClass.call(this, arg) 和 SubCtotype = new SuperClass这样的艰涩语法来为每一个你所需要的类做扩展(可以被浓缩到extend函数中)。无需考虑这些仅仅因为原型继承简单有效。它的力量就在于它的简单。到底是使用标准继承还是使用原型继承取决于你的个人喜好。一些人看重了原型继承的简单,而另一些人则选择更为熟悉的标准继承。本书中的每个模式都可以用这两种方案。我们在后面的模式中则侧重于标准继承,因为它更容易理解,但是两种都适用与这本书中。继承和封装本章到这时还未曾提到封装是如何影响继承的。当你为已有类创建一个子类时,只有公共和特权成员能够被继承,这有些像其他的OO语言,例如在JAVA中,子类是无法访问父类的私有方法的,你必须显式的声明一个方法为protected才能将其传递给子类。因为这一点,完全暴露对象是子类化的最佳选择。所有的成员都是公共的并可以被传递给子类。如过某个成员需要被屏蔽,那么下划线语法也是不错的选择。如果一个拥有真正私有变量的类被子类化,那么特权方法就会被传递,因为它可被公共访问。这允许间接的访问私有变量。但是没有子类的实例可以直接访问这些私有属性。私有成员仅可以被已经创建好的特权方法访问;新的特权方法是不能被添加到子类中的。掺元类还有一种方法可以不使用继承而重用代码。如果你想有一个不止在一个类中使用的函数,你可以通过扩展来让多个类分享它。在实际应用中,它是这样的:你创建了一个包含通用方法的类,然后用它来扩展其他的类。这些拥有通用方法的类称之为掺元类。它们通常不被实例化或直接调用。它们仅仅是为了将他们的方法传递给其他类,我们最好用一个例子来说明:var Mixin = function() ;Mtotype = serialize: function() var output = ;for(key in this) output.push(key + : + thiskey);return output.join(, );Mixin类只有一个方法serialize。这个方法遍历this的每一个成员并将其输出为字符串。(这只是一个简单的示例,在JSONString方法中你可以发现一个更健壮的版本,这个方法是Douglas Crockford的JSON库中的一部分,参见/json.js)。这类方法可以被大量不同的类所使用,但是让这些类来继承mixin确实没有不要的。同样的,为每个类重复也是没有必要的,最好的方案是使用augment函数来把这个方法添加到需要的类中。augment(Author, Mixin);var author = new Author(Ross Harmes, JavaScript Design Patterns);var serializedString = author.serialize();这里我们用Mixin类中的所有方法来扩充了Author类。Author的实例就可以调用serialize函数了。这可以作为JS实现多亲继承的一种方法。类似C+和Python这样的语言允许子类继承不止一个父类;你在JS中无法做到这点,因为prototype属性只能指向一个对象,但是一个类可以用多个掺元类扩充,这在实际上提供了相同的功能;augment函数非常简单,使用了一个for in循环,遍历givingClass的原型的每一个成员并把它们添加到了receivingClass的原型中。如果这个成员已经存在,就跳过。接受类的成员不会被覆盖。/* Augment function. */function augment(receivingClass, givingClass) for(methodName in givingCtotype) if(!receivingCtotypemethodName) receivingCtotypemethodName = givingCtotypemethodName;我们还可以稍微的改善一下,比方说你有一个包含了多个方法的掺元类,但你仅想把其中的一个或两个方法复制给另外一个函数,使用上面的这个版本是不可能的。新的版本使用了可选参数,如果它存在,那就仅仅复制名称与参数中匹配的方法。function augment(receivingClass, givingClass) if(arguments2) / Only give certain methods.for(var i = 2, len = arguments.length; i len; i+) receivingCtotypeargumentsi = givingCtotypeargumentsi; else / Give all methods.for(methodName in givingCtotype) if(!receivingCtotypemethodName) receivingCtotypemethodName = givingCtotypemethodName;你现在可以写augment(Author, Mixin, serialize);来只用一个serialize方法来扩充Author类了。更多的方法名称都可以被添加,如果你想用多个方法来扩充。通常,使用一些方法来扩充一个类要比从一个类那里继承显得更为使用。这是一个避免代码重复的快捷方法。不幸的是它在许多情况下无法适用。只有方法的通用程度足以使用在非常不同的类时,这才是最佳候选。(如果类的差异不够大,继承通常是最佳选择)。例子:就地编辑我们把这个例子贯穿3遍,分别使用标准继承,原型继承和掺元类。在这个例子中,你要求完成这样一个任务:写一个模板,重用创建和管理就地编辑(edit-in-place,指一个普通页面中的文字块,当被点击时变为一个表单项和若干按钮,允许其中的内容被修改)字段的API。它允许你为对象赋以一个唯一的ID,给它一个默认值,并且指定你想要让它去的地方。它也允许你随时访问字段的当前值并有一些修改字段的类型可供选择(例如,一个多行文本框或者一个单行文本框)。使用经典继承首先我们利用经典继承来创建一个API/* EditInPlaceField class. */function EditInPlaceField(id, parent, value) this.id = id;this.value = value | default value;this.parentElement = parent;this.createElements(this.id);this.attachEvents();EditInPlaceFtotype = createElements: function(id) this.containerElement = document.createElement(div);this.parentElement.appendChild(this.containerElement);this.staticElement = document.createElement(span);this.containerElement.appendChild(this.staticElement);this.staticElement.innerHTML = this.value;this.fieldElement = document.createElement(input);this.fieldElement.type = text;this.fieldElement.value = this.value;this.containerElement.appendChild(this.fieldElement);this.saveButton = document.createElement(input);this.saveButton.type = button;this.saveButton.value = Save;this.containerElement.appendChild(this.saveButton); this.cancelButton = document.createElement(input);this.cancelButton.type = button;this.cancelButton.value = Cancel;this.containerElement.appendChild(this.cancelButton); this.convertToText();,attachEvents: function() var that = this;addEvent(this.staticElement, click, function() that.convertToEditable(); );addEvent(this.saveButton, click, function() that.save(); );addEvent(this.cancelButton, click, function() that.cancel(); );,convertToEditable: function() this.staticElement.style.display = none;this.fieldElement.style.display = inline;this.saveButton.style.display = inline;this.cancelButton.style.display = inline; this.setValue(this.value);,save: function() this.value = this.getValue();var that = this;var callback = success: function() that.convertToText(); ,failure: function() alert(Error saving value.); ;ajaxRequest(GET, save.php?id= + this.id + &value= + this.value, callback);,cancel: function() this.convertToText();,convertToText: function() this.fieldElement.style.display = none;this.saveButton.style.display = none;this.cancelButton.style.display = none; this.staticElement.style.display = inline;this.setValue(this.value);,setValue: function(value) this.fieldElement.value = value;this.staticElement.innerHTML = value;,getValue: function() return this.fieldElement.value;要创建一个字段,就要实例化这个类:var titleClassical = new EditInPlaceField(titleClassical, $(doc), Title Here);var currentTitleText = titleClassical.getValue();这里给出了一个EditInPlaceField类(等会要子类化)的实例,它有一个span标签显示内容和一个文本输入字段做修改。它还有一些配置方法(createElements, attachEvents),一些用来转换和保存的(convertToEditable, save, cancel, convertToText)内部方法,还有一个存取器方法((getValue, setvalue)。如果这些代码用于实际工作,那么最好给每个HTML元素指定class名称以便它可以由CSS来控制样式,为了简单起见,我们没有包含这些代码。记下来,创建一个使用文本框替代而不是文本输入字段的类。由于EditInPlaceField和EditInPlaceArea类的大部分都是一样的,所以为其中一个创建一个子类来避免代码重复。var EditInPlaceArea = function(id, parent, value)EditInPlaceArea.superclass.constructor.call(this, id, parent, value);extend(EditInPlaceArea, EditInPlaceField);EditInPlaceAtotype.createElements = function(id) this.containerElement = document.createElement(DIV);this.parentElement.appendChild(this.containerElement);this.staticElement = document.createElement(P);this.staticElement.innerHTML = this.value;this.containerElement.appendChild(this.staticElement);this.fieldElement = document.createElement(TEXTAREA);this.fieldElement.value = this.value;this.containerElement.appendChild(this.fieldElement);this.saveButton = document.createElement(INPUT);this.saveButton.type = button;this.saveButton.value = 保存;this.containerElement.appendChild(this.saveButton);this.cancelButton = document.createElement(INPUT);this.cancelButton.type = BUTTON;this.cancelButton.value = 取消;this.containerElement.appendChild(this.cancelButton);this.conventToText();EditInPlaceAtotype.conventToEditable = function()this.staticElement.style.display = none;this.fieldElement.style.display = block;this.fieldElement.focus();this.saveButton.style.display = inline;this.cancelButton.style.display = inline;this.setValue(this.value);EditInPlaceAtotype.conventToText = function()this.staticElement.style.display = block;this.fieldElement.style.display = none;this.saveButton.style.display = none;this.cancelButton.style.display = none;this.setValue(this.value);使用extend函数来创建子类,然后覆盖了一些函数来实现修改。这个新类用一个文本框替换了文本输入字段,用一个段落标记来替换了span.标准继承看起来非常适用这种情况。子类化EditInPlaceField非常普通,只需要修改几行代码。对类做的修改简单到只需覆盖或添加方法到原型中。我们可以通过其他类将这个field连接到另外一些输出并覆盖相关方法。因为类之间的修改非常小。直接继承比较理想。使用原型继承尽管标准继承和原型继承在实现机制上不同,使用原型继承重复这个例子可以显示两者之间的最终代码有多么类似:var EditInPlaceField = config : function(id, parent, value)this.id = id;this.parentElement = parent;this.value = value;this.createElements(this.id);this.attachEvents();,createElements : funct

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论