我们知道继承是 oo 语言中不可缺少的一部分,对于 JavaScript 也是如此。一般的继承有两种方式:其一,接口继承,只继承方法的签名;其二,实现继承,继承实际的方法。JavaScript 不支持签名,因此只有实现继承。其中实现继承主要是依赖于原型链的。下面我将以原型链为重点说说继承的几种主要的方式:
A
要说原型链继承,不得不首先介绍一下原型链的概念。
想象一下,如果使原型对象等于另一个对象的实例,则此时原型对象将包含一个指向另一个原型的指针。相应地,另一个原型也将包含指向另一个构造函数的指针。假设另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条(注意:这里的实例和原型都是相对的),这便是原型链的基本概念。
- function SuperType() {
- this.property = true;
- }
- SuperType.prototype.getSuperValue = function() {
- return this.property;
- };
- function SubType() {
- this.subproperty = false;
- }
- SubType.prototype = new SuperType();
- SubType.prototype.getSubvalue = function() {
- return this.subproperty;
- }
- var instance = new SubType();
- console.log(instance.getSuperValue()); //true
在上述代码中,我们可以看出 subType 的原型是 SuperType 的实例,因此,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。且我们没有使用 SubType 默认提供的原型对象,而是给它换了一个新原型对象(即 SuperType 的实例)。因此,新原型对象不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了 SuperType 的原型。即:instance 指向 SubType 的原型,SubType 的原型指向了 SuperType 的原型。值得注意的是:property 现在位于 SubType.protoType 中(因为 SuperType 构造函数中的 this 指向的是创建的对象实例)。
当以读取模式访问一个实例属性时,搜索过程会沿着原型链向上进行搜索。比如,调用 instance.getSuperValue() 会经历三个搜索步骤:(1). 搜索实例中是否存在该方法,结果:无。(2). 沿着原型链向上,搜索 SubType.prototype 中是否存在该方法,结果:无。(3). 继续沿着原型链,搜索 SuperType.prototype 中是否存在该方法,结果:存在。于是停止搜索。也就是说:在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
注意:instance.constructor 现在指向的是 SuperType,这是因为 SubType 的原型指向了另一个对象 --SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。我们可以用以下代码做出验证:
- console.log(instance.constructor);
最终返回的是 SuperType 这个构造函数。
重要:别忘记默认的原型。我们知道,所有的引用类型都继承了 Object,而这个继承也是通过原型链实现的,即所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也是所有引用类型都会继承 toString()、valueOf() 方法的根本原因。我们可以使用下面代码做出验证:
- console.log(Object.prototype.isPrototypeOf(instance)); //true
- console.log(SuperType.prototype.isPrototypeOf(instance)); //true
- console.log(SubType.prototype.isPrototypeOf(instance)); //true
也就是说 instace 实例对象的原型对象分别是 Object.prototype、SuperType.prototype、SubType.prototype。另外我们还可以使用 instanceof 操作符判断,实例 instance 与构造函数之间的关系,如下所示:
- console.log(instance instanceof Object); //true
- console.log(instance instanceof SuperType); //true
- console.log(instance instanceof SubType); //true
即 instance 是 Object SuperType SubType 的实例。下面我们使用一张图表表示他们之间的关系。
这里,我们可以认为加粗的线条就是原型链(实例与原型的链条)。
从这张图表中,我们可以看到 SubType Prototype 是没有 constructer 属性的,更没有指向 SubType 构造函数,这是因为创建 SubType 构造函数同时创建的原型对象和这个原型对象不是同一个,这个原型对象是 SuperType 的实例。注意到,后两个原型对象都有一个 [[prototype]] 属性,因为这时他们是被当作实例来处理的。
B
谨慎地定义方法
当子类型有时候需要覆盖(与原型中覆盖属性是同样的道理,见)超类型的某个方法,或者需要添加超类型中不存在的某个方法。这时,应当注意:给原型添加方法的代码一定要放在(用超类型的对象实例作为子类型的原型来)替换原型的语句之后。看以下代码:
- function SuperType() {
- this.property = true;
- }
- SuperType.prototype.getSuperValue = function() {
- return this.property;
- };
- function SubType() {
- this.subproperty = false;
- }
- SubType.prototype = new SuperType(); //这一句代码即为替换的原型的语句
- SubType.prototype.getSubValue = function() {
- return this.subproperty; //这时在子类型中新添加的方法
- }
- SubType.prototype.getSuperValue = function() {
- return false; //这时在子类型添加的超类型的同名方法,用于覆盖超类型中的方法,因此,最后反悔了false
- }
- var instance = new SubType();
- console.log(instance.getSuperValue()); //false
如果顺序颠倒,那么这两个新添加的方法就是无效的了,最终 instance.getSuperValue()得到的结果仍然是从超类型中搜索到的,返回 false。这时因为如果颠倒,那么后面添加的方法给了 SubType 最开始的原型,后面替换原型之后,就只能继承超类型的,而刚刚添加的方法不会被实例所共享,此时实例的 [[prototype]] 指向的是替换之后的原型对象而不在指向最初的添加了方法的原型对象。
还有一点需要注意的就是,在通过原型链实现继承时,不能使用对象字面量创建原型方法(这样就会再次创建一个原型对象,而不会刚刚的那个用超类型的实例替换的对象),因为这样会切断原型链,无法实现继承。
C
单独使用原型链的问题
问题 1:最主要的问题是当包含引用类型值的原型。首先,回顾以下原型模式创建对象的方法,对于包含引用类型值的原型属性会被所有的实例共享,这样改变其中一个实例,其他都会被改变,这不是我们想要的。这也正是之前关于原型的讲解中为什么要将引用类型的值定义在构造函数中而不是定义在原型对象中。对于原型链,也是同样的问题。
看以下的代码;
- function SuperType() {
- this.colors = ["red", "blue", "green"];
- }
- function SubType() {}
- SubType.prototype = new SuperType(); //这时,SuperType中的this对象指向的是SubType.prototype
- var instance1 = new SubType();
- instance1.colors.push("black");
- console.log(instance1.colors); //["red", "blue", "green", "black"]
- var instance2 = new SubType();
- console.log(instance2.colors); //["red", "blue", "green", "black"]
在 SuperType 构造函数中的 this 一定是指向由他创建的新对象的,而 SubType.prototype 正是这个新对象,因此 SubType 的原型对象便有了 colors 属性,由于这个属性值是数组(引用类型),因而尽管我们的本意是向 instance1 中添加一个 "black",但最终不可避免的影响到了 instance2。而 colors 放在构造函数中有问题,如果放在其他的原型对象中,依然会有问题。因此,这是原型链继承的一个问题。
问题二:
在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
正因为单单使用原型链来实现继承出现的以上两个问题,我们在实践中很少会单独使用原型链。
A
为解决以上问题,人们发明了借用构造函数(又称伪造对象或经典继承),这种方法的核心思想是:在子类型构造函数的内部调用超类型构造函数。由于函数只不过是在特定环境中执行代码的对象,因此通过使用 apply() 和 call() 方法也可以在(将来)新创建的对象上执行构造函数。注意:这种继承方式没有用到原型链的知识,与基于原型链的继承毫无关系。代码如下:
- function SuperType() {
- this.colors = ["red", "blue", "green"];
- }
- function SubType() {
- SuperType.call(this); //在子类型构造函数的内部调用超类型构造函数
- }
- var instance1 = new SubType();
- instance1.colors.push("black");
- console.log(instance1.colors); //["red", "blue", "green", "black"]
- var instance2 = new SubType();
- console.log(instance2.colors); //["red", "blue", "green"]
首先,我们可以看到此种继承方式既完成了继承任务,又达到了我们希望达到的效果:对一个实例的值为引用类型的属性的修改不影响另一个实例的引用类型的属性值。
值得注意的是:这种继承方式与原型链的继承方式是完全不同的。看以下代码:
- console.log(instance1 instanceof SubType); //true
- console.log(instance1 instanceof SuperType); //false
instance1 和 instance2 都不是 SuperType 的实例。这里的继承只是表面上的继承。我们可以分析一下这个继承的过程:首先声明了两个构造函数,然后执行 var instance1=new SubType(); 即通过 new 调用了构造函数 SubType,既然调用了 SubType 构造函数,此时便进入了 SubType 执行环境,该环境中又调用了 SuperType()函数(注意:这里未使用 new,故此时应当把 SuperType 函数当作一般函数来处理),又因为 SubType() 中 this 是指向 instance1(SubType 是构造函数啊!)的,所以,接下来就会在 instance1 对象上调用普通函数 SuperType, 因为这个普通函数在 instance1 上被调用,因此,SuperType 中的 this 又指向了 Instance1,这是,instance1 对象便添加了属性值为应用类型的 colors 属性,instance2 同理。
这解决了原型链继承中的第一个问题。
B
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。如下所示:
- function SuperType(name) {
- this.name = name;
- }
- function SubType() {
- SuperType.call(this, "zzw");
- this.age = 21;
- }
- var instance1 = new SubType();
- console.log(instance1.name); //zzw
- console.log(instance1.age); //21
其中 SuperType.call(this,"zzw"); 又可以写做 SuperType.apply(this,["zzw"]);(关于这一部分知识点可以看《》第三部分)。
言归正传,让我们先分析函数时如何执行的:首先声明了两个构造函数,然后通过 new 操作符调用了 Subtype 构造函数,随即进入 Subtype 构造函数的执行环境,执行语句 SuperType.call(this.zzw);,随即进入了普通函数(同样地,只要没有使用 new 操作符,它就是一般函数)的执行环境并传递了参数,且使用了 call 方法,说明在 instance1 对象上调用普通函数 SuperType,因为在对象上调用的,所以 SuperType 函数中的 this 指向 instance1,并最终获得了 name 属性。SuperType 函数执行环境中的代码执行完毕之后,执行环境又回到了 SubType 构造函数,这时,instance 对象又获得了属性值为 21 的 age 属性。
ok!借用构造函数继承又解决了原型链继承的第二个问题。
然而,借用构造函数就没有缺点吗?答案是有!因为仅仅使用借用构造函数,就无法避免构造函数模式的问题 -- 方法在构造函数中定义(而导致浪费)。而且,我们说这种方式与原型链不同,因此在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
考虑到上述问题,借用构造函数的技术也是很少单独使用的。
与创建对象时,我们将自定义构造函数模式和原型模式组合一样,这种继承方式即将原型链和借用构造函数的技术组合到一起,从而发挥两者之长。主要思想是:使用原型链实现对原型属性(即希望让各个实例共享的属性)和方法(对于借用构造函数,继承方法显然是不合适的)的继承,而通过借用构造函数来实现对实例属性(即不希望共享的属性,之前方法是通过实例属性覆盖原型属性)的继承。这样,既通过在原型上定义方法实现了函数复用(即只创建一次方法,被多次使用,如果将函数定义在构造函数中,创建一个实例,就会同时创建一个相同的方法,无法复用,影响性能),又能够保证每个实例都有自己的属性(因为借用构造函数可以传递参数啊!把实例属性通过借用构造函数实现,就不用去覆盖了)。
下面来看这样一个例子:
- function SuperType(name, age) {
- this.name = name; //实例属性使用借用构造函数模式
- this.age = age; //实例属性使用借用构造函数模式
- this.colors = ["red", "blue", "green"]; //这个数组虽然会同时被原型链和借用构造函数添加使用,但最后根据原型链的搜索机制,是按照借用构造函数模式实现的。
- }
- SuperType.prototype.sayName = function() {
- console.log(this.name); //实现同样效果的方法使用原型链模式
- };
- function SubType(name, age) {
- SuperType.call(this, name, age); //借用构造函数模式的有点就是可以向子类型构造函数中的超类型构造函数传递参数,这里this的用法很重要
- };
- SubType.prototype = new SuperType(); //使用SuperType的实例来替换为SubType的原型对象
- SubType.prototype.constructor = SubType; // 这句代码即将SubType的原型对象的constructor属性指向SubType,但这一句去掉也不会影响结果。
- SubType.prototype.sayAge = function() {
- console.log(this.age); //在原型对象中定义方法,可以使得该方法实现复用,增强性能
- };
- var instance1 = new SubType("zzw", 21);
- instance1.colors.push("black");
- console.log(instance1.colors); //["red", "blue", "green", "black"]
- instance1.sayName(); //zzw
- instance1.sayAge(); //21
- var instance2 = new SubType("ht", 18);
- console.log(instance2.colors); //["red", "blue", "green"]
- instance2.sayName(); //ht
- instance2.sayAge(); //18
关键点:在 SuperType 构造函数中代码 this.colors=["red","blue","green"]; 实际上也会向单独的原型链继承那样,将 colors 数组添加到 SubType 的原型对象中去,但是借用构造函数在执行时会将 colors 数组直接添加给实例,所以,访问 colors 数组时,根据原型链的搜索机制,在实例中的 colors 数组一旦被搜索到,就不会继续沿着原型链向上搜索了(屏蔽作用)。因此最终 instance1 的 colors 的改变并不会影响到 instance2 的 colors 数组的改变(两者的 colors 数组都来自实例本身而不是原型对象)。
只会幻想而不行动的人,永远也体会不到收获果实时的喜悦。 Just do it!
来源: http://www.cnblogs.com/zhuzhenwei918/p/6040523.html