继承机制实例
要用ECMAScript实现继承机制,首先从基类入手。所有开发者定义的类都可作为基类。
出于安全原因,本地类和宿主类不能作为基类,这样可以防止公用访问编译过的浏览器级的代码,因为这些代码可以被用于恶意攻击。
选定基类后,就可以创建它的子类了。是否使用基类完全由你决定。
有时,你可能想创建一个不能直接使用的基类,它只是用于给子类提供通用的函数。在这种情况下,基类被看作抽象类。
尽管ECMAScript并没有像其他语言那样严格地定义抽象类,但有时它的确会创建一些不允许使用的类。通常,我们称这种类为抽象类。
创建的子类将继承超类的所有属性和方法,包括构造函数及方法的实现。
记住,所有属性和方法都是公用的,因此子类可直接访问这些方法。
子类还可添加超类中没有的新属性和方法,
也可以覆盖超类中的属性和方法。
继承的方式
和其他功能一样,ECMAScript中实现继承的方式不止一种。
这是因为JavaScript中的继承机制并不是明确规定的,而是通过模仿实现的。
这意味着所有的继承细节并非由解释程序处理。
作为开发者,你有权决定最适用的继承方式。
1. 对象冒充
构想原始的ECMAScript时,根本没打算设计对象冒充(object masquerading)。
它是在开发者开始理解函数的工作方式,尤其是如何在函数环境中使用this关键字后才发展出来的。
其原理如下:构造函数使用this关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。
因为构造函数只是一个函数,所以可使ClassA的构造函数成为ClassB的方法,然后调用它。
ClassB就会收到ClassA的构造函数中定义的属性和方法。例如,用下面的方式定义ClassA和ClassB:
function ClassA(sColor){
this.color = sColor;
this.sayColor = function(){
alert(this.color);
};
}
function ClassB(sColor){
}
还记得吗?关键字this引用的是构造函数当前创建的对象。不过在这个方法中,this指向的是所属的对象。
这个原理是把ClassA作为常规函数来建立继承机制,而不是作为构造函数。如下使用构造函数ClassB可以实现继承机制:
function ClassB(sColor){
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
}
在这段代码中,为ClassA赋予了方法newMethod(记住,函数名只是指向它的指针)。
然后调用该方法,传递给它的是ClassB构造函数的参数sColor。最后一行代码删除了对ClassA的引用,这样以后就不能再调用它。
所有的新属性和新方法都必须在删除了新方法的代码行后定义。否则,可能会覆盖超类的相关属性和方法:
为证明前面的代码有效,可以运行下面的例子:
function ClassB(sColor, sName){
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
this.name = sName;
this.sayName = function(){
alert(this.name);
}
}
var objA = new ClassA("red");
var objB = new ClassB("blue", "Nicholas");
objA.sayColor();
objB.sayColor();
objB.sayName();
有趣的是,对象冒充可以支持多重继承。也就是说,一个类可以继承多个超类。
例如,如果存在两个类ClassX和ClassY,ClassZ想继承这两个类,可以使用下面的代码:
function ClassZ(){
this.newMethod = ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod = ClassY;
this.newMethod();
delete this.newMethod;
}
这里存在一个弊端,如果ClassX和ClassY具有同名的属性或方法,ClassY具有高优先级,因为继承的是最后的类。除这点小问题之外,用对象冒充实现多继承机制轻而易举。
由于这种继承方法的流行,ECMAScript的第三版为Function对象加入了两个新方法,即call()和apply()。
2. call()方法
call()方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作this的对象。其他参数都直接传递给函数自身
function sayColor(sPrefix, sSuffix){
alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "red";
sayColor.call(obj, "the color is", ",a very nice color indeed. ");
在这个例子中,函数sayColor()在对象外定义,即使它不属于任何对象,也可以引用关键字this。
对象obj的color属性等于"red"。调用call()方法时,
第一个参数是obj,说明应该赋予sayColor()函数中的this关键字值是obj。
第二个和第三个参数是字符串。它们与sayColor()函数中的参数prefix和suffix匹配,
最后生成的消息"The color is red, a very nice color indeed"将被显示出来。
要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:
function ClassB(sColor, sName){
//this.newMethod = ClassA;
//this.newMethod(sColor);
//delete this.newMethod;
ClassA.call(this, sColor);
this.name = sName;
this.sayName = function(){
alert(this.name);
}
}
这里,想让ClassA中的关键字this等于新创建的ClassB对象,因此this是第一个参数。第二个参数sColor对两个类来说都是唯一的参数。
3. apply()方法
apply()方法有两个参数,用作this的对象和要传递给函数的参数的数组。例如:
function sayColor(sPrefix, sSuffix){
alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "red";
sayColor.call(obj, new Array("the color is", ",a very nice color indeed. "));
这个例子与前面的例子相同,只是现在调用的是apply()方法。
调用apply()方法时,第一个参数仍是obj,说明应该赋予sayColor()中的this关键字值是obj。第二个参数是由两个字符串构成的数组,与sayColor()的参数prefix和suffix匹配。
生成的消息仍是"The color is red, a very nice color indeed",将被显示出来。
该方法也用于替换前三行的赋值、调用和删除新方法的代码:
function ClassB(sColor, sName){
//this.newMethod = ClassA;
//this.newMethod(sColor);
//delete this.newMethod;
ClassA.apply(this, new Array(sColor));
this.name = sName;
this.sayName = function(){
alert(this.name);
}
}
同样的,第一个参数仍是this。第二个参数是只有一个值color的数组。
可以把ClassB的整个arguments对象作为第二个参数传递给apply()方法function ClassB(sColor, sName){
//this.newMethod = ClassA;
//this.newMethod(sColor);
//delete this.newMethod;
ClassA.call(this, arguments));
this.name = sName;
this.sayName = function(){
alert(this.name);
}
}
当然,只有超类中的参数顺序与子类中的参数顺序完全一致时才可以传递参数对象。如果不是,就必须创建一个单独的数组,按照正确的顺序放置参数。此外,还可使用call()方法。
4. 原型链
继承这种形式在ECMAScript中原本是用于原型链的。上一章介绍了定义类的原型方式。
原型链扩展了这种方式,以一种有趣的方式实现继承机制。
在上一章中学过,prototype对象是个模板,要实例化的对象都以这个模板为基础。
总而言之,prototype对象的任何属性和方法都被传递给那个类的所有实例。原型链利用这种功能来实现继承机制。
如果用原型方式重定义前面例子中的类,它们将变为下列形式:
function ClassA(){
}
ClassA.prototype.color = "red";
ClassA.prototype.sayColor = function(){
alert(this.color);
};
function ClassB(){
}
ClassB.prototype = new ClassA();
原型链的神奇之处在于突出显示的代码行。这里,把ClassB的prototype属性设置成ClassA的实例。
这很有意义,因为想要ClassA的所有属性和方法,但又不想逐个将它们赋予ClassB的prototype属性。
还有比把ClassA的实例赋予prototype属性更好的方法吗?
注意,调用ClassA的构造函数时,没有给它传递参数。这在原型链中是标准做法。要确保构造函数没有任何参数。
与对象冒充相似,子类的所有属性和方法都必须出现在prototype属性被赋值后,因为在它之前赋值的所有方法都会被删除。为什么?因为prototype属性被替换成了新对象,添加了新方法的原始对象将被销毁。所以,为ClassB类添加name属性和sayName()方法的代码如下:
ClassB.prototype.name = "";
ClassB.prototype.sayName = function(){
alert(this.name);
};
可通过运行下面的例子测试这段代码:
var objA = new ClassA();
var objB = new ClassB();
objA.color = "red";
objB.color = "blue";
objB.name = "nicholas";
objA.sayColor();
objB.sayColor();
objB.sayName();
此外,在原型链中,instanceof运算符的运行方式也很独特。对ClassB的所有实例,instanceof为ClassA和ClassB都返回true。例如:
var objB = new ClassB();
alert(objB instanceof ClassA);//true
alert(objB instanceof ClassB);//true
在ECMAScript的弱类型世界中,这是极其有用的工具,不过使用对象冒充时不能使用它。
原型链的弊端是不支持多重继承。记住,原型链会用另一类型的对象重写类的prototype属性。
5. 混合方式
这种继承方式使用构造函数定义类,并未使用任何原型。
对象冒充的主要问题是必须使用构造函数方式(如上一章中学到的),这不是最好的选择。
不过如果使用原型链,就无法使用带参构造函数了。
开发者该如何选择呢?答案很简单,两者都用。
在前一章,你学过创建类的最好方式是用构造函数方式定义属性,用原型方式定义方法。
这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承prototype对象的方法。
用这两种方式重写前面的例子,代码如下:
function ClassA(sColor){
this.color = sColor;
}
ClassA.prototype.sayColor = function(){
alert(this.color);
};
function ClassB(sColor, sName){
ClassA.call(this, sColor);
this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function(){
alert(this.name);
};
在此例子中,继承机制由两行突出显示的代码实现。
在第一行突出显示的代码中,在ClassB构造函数中,用对象冒充继承ClassA类的sColor属性。
在第二行突出显示的代码中,用原型链继承ClassA类的方法。
由于这种混合方式使用了原型链,所以instanceof运算符仍能正确运行。
分享到:
相关推荐
《JavaScript高级程序设计》是JavaScript开发者的经典教材,它深入探讨了这门语言的核心概念和技术。这篇读书笔记主要聚焦在第三部分,这部分通常涵盖了更高级的主题,如对象、原型、闭包以及模块化等。结合提供的...
### HTML5高级程序设计学习笔记 #### 一、HTML5新增结构标签 在HTML5中,为了更好地组织页面内容并增强语义性,引入了一系列新的结构标签,这些标签不仅能够帮助开发者更清晰地定义页面的不同部分,同时也为搜索...
在《JavaScript高级程序设计》中,对ECMAScript中引用类型的相关知识点进行了详细阐述。引用类型在JavaScript中是非常重要的概念,它们相较于原始类型而言,在内存中占有较大的比重,并且其行为也更为复杂。以下是对...
Window对象 窗口操作 Window对象对操作浏览器窗口非常有用,开发者可以移动或调整浏览器窗口的大小。可用四种方法实现这些操作: moveBy(dx,dy):把浏览器窗口相对当前位置水平移动dx个像素,垂直移动dy个像素。...
JavaScript中的继承机制是其面向对象编程的关键特性,允许一个对象或类从另一个对象或类获取并复用属性和方法。本文将深入探讨JavaScript中的五种主要继承实现方式:对象冒充、call()、apply()、原型链以及混合方式...
JavaScript 中定义类或对象的方式 JavaScript 中定义类或对象有多种方式,包括工厂方式、构造函数方式、原型方式、混合的构造函数/原型方式和动态原型方法。 工厂方式是通过创建并返回特定类型的对象的函数来定义...
JavaScript中的Function类是一种特殊的数据类型,它可以用来动态创建或表示任何开发者定义的函数。通过`new Function()`构造函数,我们可以传递参数列表和函数体来创建一个新的函数。例如,`var sayHi = new ...
JavaScript,作为一种广泛应用于Web开发的脚本语言,其核心规则和规范由ECMAScript标准定义。ECMA-262是这个标准的正式名称,它详细规定了JavaScript的语法、类型、关键字、保留字、操作符以及对象。ECMAScript为...
### JavaScript高级程序设计读书笔记之九:本地对象Array #### 创建Array对象 在JavaScript中,`Array`是一种非常重要的本地对象,主要用于存储一系列有序的数据。创建`Array`对象的方法有多种,具体包括: 1. **...
### JavaScript高级程序设计阅读笔记(七):ECMAScript中的语句 #### 一、引言 本篇文章基于《JavaScript高级程序设计》一书第七章的内容进行总结与扩展,主要探讨了ECMAScript中的各种语句类型及其用法。通过...
在JavaScript中,内置对象是指由ECMAScript标准实现并提供的一类特殊对象,它们独立于宿主环境(例如浏览器或Node.js环境)存在,并在程序执行时自动创建。这些对象提供了许多基本功能和常用方法,使得开发者可以...
JavaScript高级程序设计中的第十七章主要探讨了JS事件的相关概念和处理机制。事件流、事件处理函数以及事件对象是这一章的核心内容。 **事件流**是指事件在DOM(文档对象模型)中传播的方式。在IE中,事件流采用的...
### JavaScript中的Date对象详解 #### 一、概述 在JavaScript编程语言中,`Date`对象是一种内置对象,用于处理日期和时间数据。它提供了一系列的方法和属性,可以帮助开发者轻松地进行日期时间的操作,如获取当前...