要用 ECMAScript 实现继承机制,您能够从要继承的基类入手。全部开发者定义的类均可做为基类。出于安全缘由,本地类和宿主类不能做为基类,这样能够防止公用访问编译过的浏览器级的代码,由于这些代码能够被用于恶意攻击。数组
选定基类后,就能够建立它的子类了。是否使用基类彻底由你决定。有时,你可能想建立一个不能直接使用的基类,它只是用于给子类提供通用的函数。在这种状况下,基类被看做抽象类。浏览器
尽管 ECMAScript 并无像其余语言那样严格地定义抽象类,但有时它的确会建立一些不容许使用的类。一般,咱们称这种类为抽象类。安全
建立的子类将继承超类的全部属性和方法,包括构造函数及方法的实现。记住,全部属性和方法都是公用的,所以子类可直接访问这些方法。子类还可添加超类中没有的新属性和方法,也能够覆盖超类的属性和方法。app
和其余功能同样,ECMAScript 实现继承的方式不止一种。这是由于 JavaScript 中的继承机制并非明确规定的,而是经过模仿实现的。这意味着全部的继承细节并不是彻底由解释程序处理。函数
构想原始的 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 能够实现继承机制:this
function ClassB(sColor) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; }
在这段代码中,为 ClassA 赋予了方法 newMethod(请记住,函数名只是指向它的指针)。而后调用该方法,传递给它的是 ClassB 构造函数的参数 sColor。最后一行代码删除了对 ClassA 的引用,这样之后就不能再调用它。prototype
全部新属性和新方法都必须在删除了新方法的代码行后定义。不然,可能会覆盖超类的相关属性和方法:设计
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("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //输出 "blue" objB.sayColor(); //输出 "red" objB.sayName(); //输出 "John"
有趣的是,对象冒充能够支持多重继承。也就是说,一个类能够继承多个超类。用 UML 表示的多重继承机制以下图所示:
例如,若是存在两个类 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()。
call() 方法是与经典的对象冒充方法最类似的方法。它的第一个参数用做 this 的对象。其余参数都直接传递给函数自身。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.call(obj, "The color is ", "a very nice color indeed.");
在这个例子中,函数 sayColor() 在对象外定义,即便它不属于任何对象,也能够引用关键字 this。对象 obj 的 color 属性等于 blue。调用 call() 方法时,第一个参数是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个和第三个参数是字符串。它们与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息 "The color is blue, a very nice color indeed." 将被显示出来。
要与继承机制的对象冒充方法一块儿使用该方法,只需将前三行的赋值、调用和删除代码替换便可:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.call(this, sColor); this.name = sName; this.sayName = function () { alert(this.name); }; }
这里,咱们须要让 ClassA 中的关键字 this 等于新建立的 ClassB 对象,所以 this 是第一个参数。第二个参数 sColor 对两个类来讲都是惟一的参数。
apply() 方法有两个参数,用做 this 的对象和要传递给函数的参数的数组。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));
这个例子与前面的例子相同,只是如今调用的是 apply() 方法。调用 apply() 方法时,第一个参数还是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个参数是由两个字符串构成的数组,与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息还是 "The color is blue, a very nice color indeed.",将被显示出来。
该方法也用于替换前三行的赋值、调用和删除新方法的代码:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //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(color); //delete this.newMethod; ClassA.apply(this, arguments); this.name = sName; this.sayName = function () { alert(this.name); }; }
固然,只有超类中的参数顺序与子类中的参数顺序彻底一致时才能够传递参数对象。若是不是,就必须建立一个单独的数组,按照正确的顺序放置参数。此外,还可以使用 call() 方法。
继承这种形式在 ECMAScript 中本来是用于原型链的。上一章介绍了定义类的原型方式。原型链扩展了这种方式,以一种有趣的方式实现继承机制。
在上一章学过,prototype 对象是个模板,要实例化的对象都以这个模板为基础。总而言之,prototype 对象的任何属性和方法都被传递给那个类的全部实例。原型链利用这种功能来实现继承机制。
若是用原型方式重定义前面例子中的类,它们将变为下列形式:
function ClassA() { } ClassA.prototype.color = "blue"; 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() 方法的代码以下:
function ClassB() { } ClassB.prototype = new ClassA(); ClassB.prototype.name = ""; ClassB.prototype.sayName = function () { alert(this.name); };
可经过运行下面的例子测试这段代码:
var objA = new ClassA(); var objB = new ClassB(); objA.color = "blue"; objB.color = "red"; objB.name = "John"; 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 属性。
这种继承方式使用构造函数定义类,并不是使用任何原型。对象冒充的主要问题是必须使用构造函数方式,这不是最好的选择。不过若是使用原型链,就没法使用带参数的构造函数了。开发者如何选择呢?答案很简单,二者都用。
在前一章,咱们曾经讲解过建立类的最好方式是用构造函数定义属性,用原型定义方法。这种方式一样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象的方法。用这两种方式重写前面的例子,代码以下:
function ClassA(sColor) { this.color = sColor; } ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB(sColor, sName) { this.name = sName; } ClassB.prototype.sayName = function () { alert(this.name); }; ClassA.call(this, sColor);ClassB.prototype = new ClassA();
在此例子中,继承机制由两行突出显示的蓝色代码实现。在第一行突出显示的代码中,在 ClassB 构造函数中,用对象冒充继承 ClassA 类的 sColor 属性。在第二行突出显示的代码中,用原型链继承 ClassA 类的方法。因为这种混合方式使用了原型链,因此 instanceof 运算符仍能正确运行。
下面的例子测试了这段代码:
var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //输出 "blue" objB.sayColor(); //输出 "red" objB.sayName(); //输出 "John"