JS中的构造函数,原型,原型链,继承

一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends

2018/08/02 · JavaScript
· 继承

原文出处:
这是你的玩具车吗   

说实在话,以前我只需要知道“寄生组合继承”是最好的,有个祖传代码模版用就行。最近因为一些事情,几个星期以来一直心心念念想整理出来。本文以《JavaScript高级程序设计》上的内容为骨架,补充了ES6
Class的相关内容,从我认为更容易理解的角度将继承这件事叙述出来,希望大家能有所收获。

JavaScript继承基础讲解(原型链、借用构造函数、混合模式、原型式继承、寄生式继承、寄生组合式继承),javascript构造函数

说好的讲解JavaScript继承,可是迟迟到现在讲解。废话不多说,直接进入正题。

  既然你想了解继承,证明你对JavaScript面向对象已经有一定的了解,如还有什么不理解的可以参考《面向对象JS基础讲解,工厂模式、构造函数模式、原型模式、混合模式、动态原型模式》,接下来讲一般通过那些方法完成JavaScript的继承。

  原型链

  JavaScript中实现继承最简单的方式就是使用原型链,将子类型的原型指向父类型的实例即可,即“子类型.prototype
= new 父类型();”,实现方法如下:

// 为父类型创建构造函数
function SuperType() {
  this.name = ['wuyuchang', 'Jack', 'Tim'];
  this.property = true;
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType() {
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

// 实现继承的关键步骤,子类型的原型指向父类型的实例
SubType.prototype = new SuperType();

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType();
instance1.name.push('wyc');
instance1.test.push('h5');
alert(instance1.getSuerperValue());    // true
alert(instance1.getSubValue());      // false
alert(instance1.name);          // wuyuchang,Jack,Tim,wyc
alert(instance1.test);          // h1,h2,h3,h4,h5


var instance2 = new SubType();
alert(instance2.name);          // wuyuchang,Jack,Tim,wyc
alert(instance2.test);          // h1,h2,h3,h4

可以看到如上的代码就是通过原型链实现的一个简单的继承,但看到测试代码示例中还是存在些问题。相信看了我的博文《面向对象JS基础讲解,工厂模式、构造函数模式、原型模式、混合模式、动态原型模式》的童鞋一定知道原型链代码存在的第一个问题是由于子类型的原型是父类型的实例,也就是子类型的原型中包含的父类型的属性,从而导致引用类型值的原型属性会被所有实例所共享。以上代码的instance1.name.push(‘wyc’);就可以证明此问题的存在。而原型链的第二个问题就是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。因此我们在实际开发中,很少单独使用原型链。 

   借用构造函数

  为了解决原型链中存在的两个问题,开发人员开始使用一种叫做借用构造函数的技术来解决原型链中存在的问题。这种技术的实现思路也挺简单,只需要在子类型的构造函数内调用父类型的构造函数即可。别忘了,函数只不过是在特定环境中执行代码的对象,因此可以通过apply()或call()方法执行构造函数。代码如下:

// 为父类型创建构造函数
function SuperType(name) {
  this.name = name;
  this.color = ['pink', 'yellow'];
  this.property = true;

  this.testFun = function() {
    alert('http://tools.jb51.net/');
  }
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType(name) {
  SuperType.call(this, name);
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun();            // http://tools.jb51.net/
alert(instance1.name);            // wuyuchang,Jack,Nick,hello
// alert(instance1.getSuerperValue());    // error 报错
alert(instance1.test);            // h1,h2,h3,h4,h5    
alert(instance1.getSubValue());        // false    
alert(instance1.color);            // pink,yellow,blue

var instance2 = new SubType('wyc');
instance2.testFun();            // http://tools.jb51.net/
alert(instance2.name);            // wyc    
// alert(instance2.getSuerperValue());    // error 报错
alert(instance2.test);            // h1,h2,h3,h4
alert(instance2.getSubValue());        // false
alert(instance2.color);            // pink,yellow

可以看到以上代码中子类型SubType的构造函数内通过调用父类型”SuperType.call(this,
name);”,从而实现了属性的继承,也可以在子类型创建实例的时候为父类型传递参数了,但新的问题又来了。可以看到我在父类型的构造函数中定义了一个方法:testFun,在父类型的原型中定义了一个方法:getSuperValue。可是在实例化子类型后仍然是无法调用父类型的原型中定义的方法getSuperValue,只能调用父类型中构造函数的方法:testFun。这就同创建对象中只使用构造函数模式一样,使得函数没有复用性可言。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承(原型链+借用构造函数)

  顾名思义,组合继承就是结合使用原型链与借用构造函数的优点,组合而成的一个模式。实现也很简单,既然是结合,那当然结合了两方的优点,即原型链继承方法,而在构造函数继承属性。具体代码实现如下:

// 为父类型创建构造函数
function SuperType(name) {
  this.name = name;
  this.color = ['pink', 'yellow'];
  this.property = true;

  this.testFun = function() {
    alert('http://tools.jb51.net/');
  }
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType(name) {
  SuperType.call(this, name);
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

SubType.prototype = new SuperType();

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun();            // http://tools.jb51.net/
alert(instance1.name);            // wuyuchang,Jack,Nick,hello
alert(instance1.getSuerperValue());      // true
alert(instance1.test);            // h1,h2,h3,h4,h5    
alert(instance1.getSubValue());        // false    
alert(instance1.color);            // pink,yellow,blue

var instance2 = new SubType('wyc');
instance2.testFun();            // http://tools.jb51.net/
alert(instance2.name);            // wyc    
alert(instance2.getSuerperValue());      // true
alert(instance2.test);            // h1,h2,h3,h4
alert(instance2.getSubValue());        // false
alert(instance2.color);            // pink,yellow

以上代码通过SuperType.call(this,
name);继承父类型的属性,通过SubType.prototype = new
SuperType();继承父类型的方法。以上代码很方便的解决了原型链与借用构造函数所遇到的问题,成为了JavaScript中最为常用的实例继承的方法。但混合模式也并非没有缺点,可以看到在以上代码中在继承方法的时候实际已经继承了父类型的属性,只不过此时对于引用类型属于共享的,因此在子类型的构造函数内在次调用父类型的构造函数从而继承了父类型的属性而去覆盖了原型中所继承的属性,这样调用两次构造函数显然没有必要,但有什么方法可以解决呢?在解决此问题时先看以下两个模式。

原型式继承

  原型式继承的的实现方法与普通继承的实现方法不同,原型式继承并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。具体代码如下:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

奥门威尼斯网址,代码示例:

/* 原型式继承 */
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var person = {
  name : 'wuyuchang',
  friends : ['wyc', 'Nicholas', 'Tim']
}

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Bob');

var anotherPerson2 = object(person);
anotherPerson2.name = 'Jack';
anotherPerson2.friends.push('Rose');

alert(person.friends);  // wyc,Nicholas,Tim,Bob,Rose

寄生式继承

/* 寄生式继承 */
function createAnother(original) {
  var clone = object(original);
  clone.sayHi = function() {
    alert('hi');
  }
  return clone;
}

使用示例:

/* 原型式继承 */
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

/* 寄生式继承 */
function createAnother(original) {
  var clone = object(original);
  clone.sayHi = function() {
    alert('hi');
  }
  return clone;
}

var person = {
  name : 'wuyuchang',
  friends : ['wyc', 'Nicholas', 'Rose']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi();

寄生组合式继承

  前面说过了JavaScrip中组合模式实现继承的缺点,现在我们就来解决它的缺点,实现思路是,对于构造函数继承属性,而原型链的混成形式继承方法,即不用在继承方法的时候实例化父类型的构造函数。代码如下:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

/* 寄生组合式继承 */
function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

而在使用时只需要将组合模式中的“SubType.prototype = new
SuperType();”这行代码替换成inheritPrototype(subType,
superType);即可。寄生组合式继承的高效率体现在它只调用了一次父类型构造函数,避免了创建不必要的或多余的属性。与此同时,原型链还能保持不变,因此,还能够正常使用instanceof和isPrototypeof()。这也是目前来说最理想的继承方式了,目前也在向这种模式转型。(YUI也使用了这种模式。)

此博文参考《JavaScript高级程序设计第3版》,代码为经过改写,更具体,并加了注释使大家更易懂。如对JS继承方面有独到见解的童鞋不别吝啬,回复您的见解供大家参考!

JavaScript继承的6种方法

如果你不是张无忌,有九阳神功护体,那么乾坤大挪移的确不是一炷香的时间就能练到九重的。

说好的讲解JavaScript继承,可是迟迟到现在讲解。废话不多说,直接进入正题。

1. 继承分类

先来个整体印象。如图所示,JS中继承可以按照是否使用object函数(在下文中会提到),将继承分成两部分(Object.create是ES5新增的方法,用来规范化这个函数)。

其中,原型链继承和原型式继承有一样的优缺点,构造函数继承与寄生式继承也相互对应。寄生组合继承基于Object.create,
同时优化了组合继承,成为了完美的继承方式。ES6 Class
Extends的结果与寄生组合继承基本一致,但是实现方案又略有不同。

下面马上进入正题。

奥门威尼斯网址 1

在JavaScript的原型链继承方式中,为何子类在调用父类的构造函数时不可以传参数?

以前我在看书时也遇到过这样的问题,找了很多资料都没有明确的解释。
我觉得,并不是语法上不能实现对构造函数的参数传递,而是这样做不符合面向对象编程的规则:对象(实例)才是属性的拥有者。
如果在子类定义时就将属性赋了值,对象实例就不能再更改自己的属性了。这样就变成了类拥有属性,而不是对象拥有属性了。
举个例子,子类 Children 继承父类 Parents,Parents 构造函数:
function Parents(name){ this.name=name; }
使用原型链并给父类构造函数传参数:
Children.prototype=new Parents(“Hello”);
那么此时,Children 类就拥有了 name=“Hello” 属性,而 Children
类的实例对象 c1、c2、c3 等等只能被迫接受这个 name 属性。Children 是
“Hello” 的拥有者而 c1、 c2、c3不是!
如此写完全失去了面向对象编程的意义,所以在原型链继承方式中规定不能对父类构造函数传递参数。也因为这个原因,原型链继承方式并不实用。
 

1,原型链继承

2,借用构造函数继承

3,组合继承(原型+借用构造)

4,原型式继承

5,寄生式继承

6,寄生组合式继承

记得几个月以前还是看不懂高程上的原型一章的,不是没仔细看,相反,我反反复复看了好几遍,每看一次都感觉自信心受到了打击。。。。今天莫名打通任督二脉=

  既然你想了解继承,证明你对JavaScript面向对象已经有一定的了解,如还有什么不理解的可以参考《面向对象JS基础讲解,工厂模式、构造函数模式、原型模式、混合模式、动态原型模式》,接下来讲一般通过那些方法完成JavaScript的继承。

2. 继承方式

上图上半区的原型链继承,构造函数继承,组合继承,网上内容比较多,本文不作详细描述,只指出重点。这里给出了我认为最容易理解的一篇《JS中的继承(上)》。如果对上半区的内容不熟悉,可以先看这篇文章,再回来继续阅读;如果已经比较熟悉,这部分可以快速略过。另,上半区大量借用了yq前端的一篇继承文章[1]。

JS 类继承与原型继承不同

类式继承就像java的继承一样,思想也比较简单:在子类型构造函数的内部调用超类型构造函数。

原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链

而你的 下面这段代码不是严格意义上的类式继承,按照Nicholas
C.Zakas的说法,这个应该叫做组合式继承。它调用了两次parent2()。第一次是
child2.prototype=new parent2(‘param’);
child2就会得到两个属性param,getParam(),他们都是parent2的属性,但是他们在child2的原型中。第二次是parent2.call(this,cparam);
这次又在新对象上创建了实例属性param,getParam()。于是,这两个属性就屏蔽了原型中的两个同名属性。这有什么好处呢,就是你在构建一个child3时也继承parent2()的属性,还可以定义自己的属性。与此同时他长的就和他兄弟不同了,但又有一样的“血统(使用父类的方法)”。

纯手打,欢迎继续讨论
 

说好的讲解Java…

1.原型链继承.

写在前头

首先思考一个问题,我们知道通过构造函数和new操作符可以创建一个新的对象,并且同一个构造函数可以创建出相同属性和方法的对象,这正是prototype所做的(使用原型对象的好处是可以让所有实例共享它所包含的属性和方法)。并且,在其他构造函数中通过call和apply,调用其他构造函数不就可以实现继承了吗,那我们还要prototype做什么?


原因是,其实new操作符生成的对象并不能共享属性和方法,每次new一个新的对象时,都要为这个对象开辟一个新的空间来存放它的属性和方法,而且,在构造函数中,每次想要修改某个属性和方法时,就要重新生成所有的实例,对资源造成了极大的浪费!!!
ok,因为不想浪费,所以作者引入了原型

  原型链

2.1 原型式继承

核心:将父类的实例作为子类的原型

SubType.prototype = new SuperType() //
所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;

1
2
3
SubType.prototype = new SuperType()
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;

优点:父类方法可以复用

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数
<script type="text/javascript">
    function Person(name,sex)
    {
        this.name=name;
        this.sex=sex;
        this.friends=['李四'];
        this.getName=function(){
            alert(this.name);
        }
    }
   Person.prototype.id=1;

    function Boy(age)
    {
        this.age=age;
        this.getAge=function(){
            alert(this.age);
        }

    }
    //继承
    Boy.prototype=new Person("张三","男");


    var boy=new Boy(16);

    alert(boy.name);              //张三
    boy.getAge();               //16
    alert(boy.id);                 //1


    //属性共享问题
    console.log(boy.friends);            //李四

    var boy2=new Boy(17);
    boy2.friends.push('王五');  //修改boy2的friends属性的同时也影响了boy的属性

    console.log(boy.friends);            //李四 王五

    //验证能否使用instanceof 和 isPrototypeof
    console.log(boy instanceof Person);                 //true
    console.log(Person.prototype.isPrototypeOf(boy));   //true
</script>
注:部分例子摘自JS高程

  JavaScript中实现继承最简单的方式就是使用原型链,将子类型的原型指向父类型的实例即可,即“子类型.prototype
= new 父类型();”,实现方法如下:

2.2 构造函数继承

核心:将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype的继承。

SuperType.call(SubType);

1
SuperType.call(SubType);

优点:和原型链继承完全反过来。

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

 

理解原型对象

遵循以下几个规则
1,
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个{…}(对象,也就是原型对象)包含了所有实例共享的属性和方法。
嗯,函数拥有prototype指针,指向原型对象
2,
原型对象除了1所说包含所有实例的共享的属性和方法,还有一个constructor指针指向构造函数,对的,构造函数和原型对象互指,通过prototype和constructor
3,
实例:当构造函数为空,仅在构造函数的原型对象上有属性和函数时,此构造函数所创建的实例本质上只有一个名为proto的指针,所有实例的这个指针统统指向原型对象,因此他们才有了共享的属性和方法,而且没有独自开辟自己的空间,节省了资源
看个例子,理解这三条

function animal(){
}
animal.prototype.name = "Tom"
animal.prototype.say = function(){
      alert('喵喵喵?')
}
var cat = new animal()
console.log(cat.name) //"Tom"
console.log(cat.say) //"喵喵喵?"

ok,解释一下。上面代码中
1,
构造函数为aniaml,当我们创建了这个构造函数时,他已经包含了一个原型属性
prototype
2, 该构造函数的原型对象
console.log(animal.prototype) //返回一个对象,即原型对象,该对象包含一个name属性,一个say函数
3, 该实例
cat.__prpto__ == animal.prototype //true
都指向原型对象


在这个例子中,构造函数自身为空,在构造函数的原型对象上创建了一个属性和方法,他们被所有实例所共享,因为实例中有proto指针指向了该原型对象。但是如果使用这种方法,我们在构造函数内写的属性和方法不是都可以转到prototype上吗,那还要构造函数干什么?
因为,此处是指针式的获取到了原型上的数据,当这个数据为引用型数据(也是指针式)(如数组时),我们在实例上改变这个数组,会影响原型对象上的这个数组属性,从而影响所有实例。因此,构造函数也有它的必要用途,函数原型上一般只定义函数和非引用型数据。

// 为父类型创建构造函数
function SuperType() {
  this.name = ['wuyuchang', 'Jack', 'Tim'];
  this.property = true;
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType() {
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

// 实现继承的关键步骤,子类型的原型指向父类型的实例
SubType.prototype = new SuperType();

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType();
instance1.name.push('wyc');
instance1.test.push('h5');
alert(instance1.getSuerperValue());    // true
alert(instance1.getSubValue());      // false
alert(instance1.name);          // wuyuchang,Jack,Tim,wyc
alert(instance1.test);          // h1,h2,h3,h4,h5


var instance2 = new SubType();
alert(instance2.name);          // wuyuchang,Jack,Tim,wyc
alert(instance2.test);          // h1,h2,h3,h4

2.3 组合继承

核心:原型式继承和构造函数继承的组合,兼具了二者的优点。

function SuperType() { this.name = ‘parent’; this.arr = [1, 2, 3]; }
SuperType.prototype.say = function() { console.log(‘this is parent’) }
function SubType() { SuperType.call(this) // 第二次调用SuperType }
SubType.prototype = new SuperType() // 第一次调用SuperType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
    this.name = ‘parent’;
    this.arr = [1, 2, 3];
}
 
SuperType.prototype.say = function() {
    console.log(‘this is parent’)
}
 
function SubType() {
    SuperType.call(this) // 第二次调用SuperType
}
 
SubType.prototype = new SuperType() // 第一次调用SuperType

优点:

  • 父类的方法可以被复用
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:

调用了两次父类的构造函数,第一次给子类的原型添加了父类的name,
arr属性,第二次又给子类的构造函数添加了父类的name,
arr属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。

特点:既继承了父类的模板,又继承了父类的原型对象。

组合使用构造函数和原型模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby","Court"];
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person("Nicholas",29,"Software");
var person2 = new Person("Greg",27,"Doctor");

perons1.frineds.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //“Shelby,Count”

在这个例子中,构造函数中定义实例属性,原型对象上定义方法和共享属性。
每次调用构造函数,给新的对象开辟一个新的空间,此时的实例上的friends属性是全新的,非proto指向的,拥有独立空间的,所以每个空间的friends数组是单独互不影响的,因此在person1对象中添加一个friends后,person2的friends属性并未改变,而person1和person2都可以共享sayName函数,还有name,age,job等属性。

可以看到如上的代码就是通过原型链实现的一个简单的继承,但看到测试代码示例中还是存在些问题。相信看了我的博文《面向对象JS基础讲解,工厂模式、构造函数模式、原型模式、混合模式、动态原型模式》的童鞋一定知道原型链代码存在的第一个问题是由于子类型的原型是父类型的实例,也就是子类型的原型中包含的父类型的属性,从而导致引用类型值的原型属性会被所有实例所共享。以上代码的instance1.name.push(‘wyc’);就可以证明此问题的存在。而原型链的第二个问题就是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。因此我们在实际开发中,很少单独使用原型链。 

2.4 原型式继承

核心:原型式继承的object方法本质上是对参数对象的一个浅复制。

优点:父类方法可以复用

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

function object(o){ function F(){} F.prototype = o; return new F(); }
var person = { name: “Nicholas”, friends: [“Shelby”, “Court”, “Van”]
}; var anotherPerson = object(person); anotherPerson.name = “Greg”;
anotherPerson.friends.push(“Rob”); var yetAnotherPerson =
object(person); yetAnotherPerson.name = “Linda”;
yetAnotherPerson.friends.push(“Barbie”); alert(person.friends);
//”Shelby,Court,Van,Rob,Barbie”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
 
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
 
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
 
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"
 

ECMAScript 5 通过新增
Object.create()方法规范化了原型式继承。这个方法接收两个参数:一
个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,
Object.create()与 object()方法的行为相同。——《JAVASCript高级编程》

所以上文中代码可以转变为

var yetAnotherPerson = object(person); => var yetAnotherPerson =
Object.create(person);

1
var yetAnotherPerson = object(person); => var yetAnotherPerson = Object.create(person);

缺点:只能在父类设置一些参数,子类不能灵活传参,不符合面向对象的思想,包含引用类型值的属性始终都会共享相应的值。

这种构造函数与原型模式混成的模式,是目前在ECMAScript中使用最广泛的、认同度最高的一种创建自定义类型的方法。这是用来定义引用类型的一种默认模式。

   借用构造函数

2.5 寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

优缺点:仅提供一种思路,没什么优点

function createAnother(original){ var clone=object(original);
//通过调用函数创建一个新对象 clone.sayHi = function(){
//以某种方式来增强这个对象 alert(“hi”); }; return clone; //返回这个对象
} var person = { name: “Nicholas”, friends: [“Shelby”, “Court”, “Van”]
}; var anotherPerson = createAnother(person); anotherPerson.sayHi();
//”hi”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(original){
    var clone=object(original);    //通过调用函数创建一个新对象
    clone.sayHi = function(){      //以某种方式来增强这个对象
        alert("hi");
    };
    return clone;                  //返回这个对象
}
 
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

 奥门威尼斯网址 2

原型链继承

在java等oop语言中继承由class和extend关键字来实现,而在ES5中没有这些关键字(
ES6有
),因此只能通过使子函数的原型对象等于父函数的实例来实现链式继承。看例子

function SuperType(){
    this.color = ['red', 'blue' , 'yellow'];
}

function SubType(){}

SubType.prototype = new SuperType(); //子函数的原型对象等于父函数的实例

var instance1 = new SubType();
instance1.color.push ('white');
alert(instance1.color); // red,blue,yellow,white

var instance2 = new SubType();
alert(instance2.color); // red,blue,yellow,white

子函数的原型对象等于父函数的实例,此时子函数SubType的原型被改写,成为了SuperType的实例,而上面讲了实例拥有一个proto属性(指针)指向原型对象,因此这个SubType的原型对象上的proto属性指向了SuperType的原型对象。然后当我们创建了SubType的实例instance时,instance中也有一个proto指针,而它指向SubType.prototype,SubType.prototype上的proto指针,指向SuperType.prototype,其实再往上,SuperType.prototype也是对象的一种,因此它也是Object的一个实例,SuperType.prototype的proto指针指向Object.prototype。(顶层)(原型链的核心就是proto指针)

  为了解决原型链中存在的两个问题,开发人员开始使用一种叫做借用构造函数的技术来解决原型链中存在的问题。这种技术的实现思路也挺简单,只需要在子类型的构造函数内调用父类型的构造函数即可。别忘了,函数只不过是在特定环境中执行代码的对象,因此可以通过apply()或call()方法执行构造函数。代码如下:

2.6 寄生组合继承

刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。

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”, “green”]; }
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); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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", "green"];
}
 
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);
}

优缺点:这是一种完美的继承方式。

2.借用构造函数继承

这种实例与原型的链条,就叫做原型链
// 为父类型创建构造函数
function SuperType(name) {
  this.name = name;
  this.color = ['pink', 'yellow'];
  this.property = true;

  this.testFun = function() {
    alert('http://tools.jb51.net/');
  }
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType(name) {
  SuperType.call(this, name);
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun();            // http://tools.jb51.net/
alert(instance1.name);            // wuyuchang,Jack,Nick,hello
// alert(instance1.getSuerperValue());    // error 报错
alert(instance1.test);            // h1,h2,h3,h4,h5    
alert(instance1.getSubValue());        // false    
alert(instance1.color);            // pink,yellow,blue

var instance2 = new SubType('wyc');
instance2.testFun();            // http://tools.jb51.net/
alert(instance2.name);            // wyc    
// alert(instance2.getSuerperValue());    // error 报错
alert(instance2.test);            // h1,h2,h3,h4
alert(instance2.getSubValue());        // false
alert(instance2.color);            // pink,yellow

2.7 ES6 Class extends

核心:
ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class A {} class B extends A { constructor() { super(); } }

1
2
3
4
5
6
7
class A {}
 
class B extends A {
  constructor() {
    super();
  }
}

ES6实现继承的具体原理:

class A { } class B { } Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto; return obj; } // B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
}
 
class B {
}
 
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
 
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
 
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
 

ES6继承与ES5继承的异同:

相同点:本质上ES6继承是ES5继承的语法糖

不同点:

  • ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
  • ES6子类实例的构建,基于父类实例,ES5中不是。
<script type="text/javascript">
      //父类
    function Person(name,sex)
    {
        this.name=name;
        this.sex=sex;
        this.friends=['李四'];
    }
    Person.prototype.id=1;
    //子类
    function Boy(name,sex,age)
    {

        //借用
        Person.call(this,name,age);

        this.age=age;
        this.getAge=function(){
            alert(this.age);
        }

    }


    var boy=new Boy("张三","男",16);
    alert(boy.name);  //张三

    boy.getAge();        //16

    alert(boy.id);  //undefined  没有继承父类的原型对象


    //属性共享问题 ————不会有共享


    //验证能否使用instanceof 和 isPrototypeof
    console.log(boy instanceof Person);                 //false
    console.log(Person.prototype.isPrototypeOf(boy));   //false
</script>

组合继承

上述的SubType.prototype = new SuperType(); //子函数的原型对象等于父函数的实例的确实现了继承,但是当SuperType构造函数中有引用型数据时,即此时SubType.prototype上也有了引用类型,现在又出现了上面提过的问题,在sub的实例中改变引用类型的值会反映到所有实例中。
第二点,在创建子类型的实例时无法给超类型的构造函数传递参数。

可以看到以上代码中子类型SubType的构造函数内通过调用父类型”SuperType.call(this,
name);”,从而实现了属性的继承,也可以在子类型创建实例的时候为父类型传递参数了,但新的问题又来了。可以看到我在父类型的构造函数中定义了一个方法:testFun,在父类型的原型中定义了一个方法:getSuperValue。可是在实例化子类型后仍然是无法调用父类型的原型中定义的方法getSuperValue,只能调用父类型中构造函数的方法:testFun。这就同创建对象中只使用构造函数模式一样,使得函数没有复用性可言。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

3. 总结

  • ES6 Class extends是ES5继承的语法糖
  • JS的继承除了构造函数继承之外都基于原型链构建的
  • 可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别

 

单独使用借用构造函数

解决上述问题有一种借用构造函数的方法,在子构造函数中使用call或apply,如SuperType.call(this,arguments)改变sub构造函数中this的指向,而且可以通过这种方法向超类型传递参数,但是单单使用借用构造函数,并不能达到函数复用的效果,依然要为每个实例上的函数开辟空间(因为此时函数没有定义在原型上),为了融合借用构造函数和原型链继承的优点,出现了组合继承

组合继承(原型链+借用构造函数)

参考文章:

[1]《js继承、构造函数继承、原型链继承、组合继承、组合继承优化、寄生组合继承》

[2]《JavaScript高级编程》

1 赞 收藏
评论

奥门威尼斯网址 3

 

组合

function SuperType(name){
    this.name = name;
    this.color = ['red', 'blue' , 'yellow'];
}

SuperType.prototype.sayName = function(){
    alert(this.name);
}

function SubType(name, age){
    //继承属性 超类
    //获得name和color属性
    SuperType.call(this, name);
    this.age= age;
} 

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    alert(this.age)
}

var instance1 = new SubType("Nicholas", 29);
instance1.color.push("black");
console.log(instance1.color); //["red", "blue", "yellow", "black"]
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("Greg", 27);
console.log(instance2.color); //["red", "blue", "yellow"]
instance2.sayName();
instance2.sayAge();

此处截取了一张图片进行分析

奥门威尼斯网址 4

image.png

在这个例子中,超类型构造函数定义了name和colors两个属性,超类型原型对象上定义了一个sayName函数可复用,子类型中使用借用构造函数的方法,使所有的子类实例都继承了父类的colors和name属性,还设立了一个自己的age属性,之后使子类型的原型对象等于超类型的一个实例,继承链生效,又在子类型的原型对象上增加了一个sayAge函数(此处顺序不能反,否则子类原型对象被覆盖,新函数无效),再之后新建了两个子类型的实例,在instance1改变colors数组并未影响instance2,且子类型,超类型函数皆可调用。

此时的逻辑关系从所截的图上来看,实例之所以引用类型互不影响,因为此时实例中的colors属性是同过借用构造函数,新建实例时,向超类型传递参数,并获得了name和colors属性,每个新的实例都是新的空间,且在原型链的最下面(原型链向上查找是否拥有某属性),所以对数组的操作并不互相影响。
这里子类型的实例其实若本身没有colors属性,指向原型的color属性还是会互相影响的,所以是借用构造函数解决了这个问题,让子类型实例先获得了这个属性,当在实例上访问到时,便不会再往下寻找
*子类型的实例上的proto依然指向子类型的原型对象,即SubType.prototype,其上还有一个定义的sayAge函数。
此原型对象因为等于超类型的实例,所以也有一个
proto指向超类型的原型对象,即SuperType.prototype。
其上还有一个sayName函数,SuperType.prototype上还有一个
proto,再往下就是原生的Object.prototype了。*


继承方式多种多样,但组合继承无疑是js中最常用的继承模式,读懂原理,其他模式也就不难理解了,千里之行,始于足下。

陈小源2017/9/4

现在ES6的继承通过class…extends…来实现,子类型没有自己的this对象,子类型的构造函数中要显示地调用super()方法,使用父类的构造函数,并获取this对象,再对this对象进行改造,从而实现继承。

  顾名思义,组合继承就是结合使用原型链与借用构造函数的优点,组合而成的一个模式。实现也很简单,既然是结合,那当然结合了两方的优点,即原型链继承方法,而在构造函数继承属性。具体代码实现如下:

特点:只继承了父类的模板,不继承父类的原型对象。

// 为父类型创建构造函数
function SuperType(name) {
  this.name = name;
  this.color = ['pink', 'yellow'];
  this.property = true;

  this.testFun = function() {
    alert('http://tools.jb51.net/');
  }
}

// 为父类型添加方法
SuperType.prototype.getSuerperValue = function() {
  return this.property;
}

// 为子类型创建构造函数
function SubType(name) {
  SuperType.call(this, name);
  this.test = ['h1', 'h2', 'h3', 'h4'];
  this.subproperty = false;
}

SubType.prototype = new SuperType();

// 在此处给子类型添加方法,一定要在实现继承之后,否则会在将指针指向父类型的实例,则方法为空
SubType.prototype.getSubValue = function() {
  return this.subproperty;
}


/* 以下为测试代码示例 */
var instance1 = new SubType(['wuyuchang', 'Jack', 'Nick']);
instance1.name.push('hello');
instance1.test.push('h5');
instance1.color.push('blue');
instance1.testFun();            // http://tools.jb51.net/
alert(instance1.name);            // wuyuchang,Jack,Nick,hello
alert(instance1.getSuerperValue());      // true
alert(instance1.test);            // h1,h2,h3,h4,h5    
alert(instance1.getSubValue());        // false    
alert(instance1.color);            // pink,yellow,blue

var instance2 = new SubType('wyc');
instance2.testFun();            // http://tools.jb51.net/
alert(instance2.name);            // wyc    
alert(instance2.getSuerperValue());      // true
alert(instance2.test);            // h1,h2,h3,h4
alert(instance2.getSubValue());        // false
alert(instance2.color);            // pink,yellow

缺点:方法都在构造函数中定义,不能做到函数复用。

以上代码通过SuperType.call(this,
name);继承父类型的属性,通过SubType.prototype = new
SuperType();继承父类型的方法。以上代码很方便的解决了原型链与借用构造函数所遇到的问题,成为了JavaScript中最为常用的实例继承的方法。但混合模式也并非没有缺点,可以看到在以上代码中在继承方法的时候实际已经继承了父类型的属性,只不过此时对于引用类型属于共享的,因此在子类型的构造函数内在次调用父类型的构造函数从而继承了父类型的属性而去覆盖了原型中所继承的属性,这样调用两次构造函数显然没有必要,但有什么方法可以解决呢?在解决此问题时先看以下两个模式。

奥门威尼斯网址 5

原型式继承

3.组合继承(原型+借用构造)最常用的继承模式

  原型式继承的的实现方法与普通继承的实现方法不同,原型式继承并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。具体代码如下:

<script type="text/javascript">
     //父类
    function Person(name,sex)
    {
        this.name=name;
        this.sex=sex;

    }
    Person.prototype.id=1;
    //子类
    function Boy(name,sex,age)
    {

        //借用构造函数  继承父类的模板
        Person.call(this,name,age);

        this.age=age;
        this.getAge=function(){
            alert(this.age);
        }

    }

    //不传递参数,继承父类的模板,继承父类的原型对象 id 
    Boy.prototype=new Person();

    var boy=new Boy("张三","男",16);
    alert(boy.name);  //张三

    boy.getAge();        //16

    alert(boy.id);  //1


    //属性共享问题 ————除了父类的原型对象,不会有共享


    //验证能否使用instanceof 和 isPrototypeof
    console.log(boy instanceof Person);                 //true
    console.log(Person.prototype.isPrototypeOf(boy));   //true
</script>
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

 

代码示例:

特点:既继承了父类的模板,又继承了父类的原型对象。

/* 原型式继承 */
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var person = {
  name : 'wuyuchang',
  friends : ['wyc', 'Nicholas', 'Tim']
}

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Bob');

var anotherPerson2 = object(person);
anotherPerson2.name = 'Jack';
anotherPerson2.friends.push('Rose');

alert(person.friends);  // wyc,Nicholas,Tim,Bob,Rose

缺点:做了3件事,继承了父类两次模板,继承了一次原型对象

寄生式继承

原型式继承解决了这个问题

/* 寄生式继承 */
function createAnother(original) {
  var clone = object(original);
  clone.sayHi = function() {
    alert('hi');
  }
  return clone;
}

奥门威尼斯网址 6

使用示例:

4.原型式继承

/* 原型式继承 */
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

/* 寄生式继承 */
function createAnother(original) {
  var clone = object(original);
  clone.sayHi = function() {
    alert('hi');
  }
  return clone;
}

var person = {
  name : 'wuyuchang',
  friends : ['wyc', 'Nicholas', 'Rose']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi();
<script type="text/javascript">
   var person={
    name:'张三',
    sex:'男',
    friends:['李四']
    }

    function object(obj)
    {
        function F(){}   //创建一个空的构造函数
        F.prototype=obj;  //将传入的对象作为这个构造函数的原型
        return new F();   //返回一个新实例  对传入的对象进行了一次浅复制
    }

    var boy=object(person);
    //ECMAScript 5 新增了Object.create()方法 与本例 object 方法行为相同
    //可改为 Object.create(person);

    alert(boy.name);  //张三

    //但是原型式同样存在属性共享的问题
    //例如:

    var girl=object(person);
    girl.friends.push('王五');

    alert(boy.friends); //李四 王五
    alert(girl.friends);//李四 王五

    //修改girl中的friends属性  boy 也会受到影响


       //无法使用 instanceof 和 isPrototypeof

</script>

寄生组合式继承

 

  前面说过了JavaScrip中组合模式实现继承的缺点,现在我们就来解决它的缺点,实现思路是,对于构造函数继承属性,而原型链的混成形式继承方法,即不用在继承方法的时候实例化父类型的构造函数。代码如下:

创造的目的是基于已有的对象创建新的对象,同时还不必因此创建自定义类型。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

/* 寄生组合式继承 */
function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

而在使用时只需要将组合模式中的“SubType.prototype = new
SuperType();”这行代码替换成inheritPrototype(subType,
superType);即可。寄生组合式继承的高效率体现在它只调用了一次父类型构造函数,避免了创建不必要的或多余的属性。与此同时,原型链还能保持不变,因此,还能够正常使用instanceof和isPrototypeof()。这也是目前来说最理想的继承方式了,目前也在向这种模式转型。(YUI也使用了这种模式。)

 奥门威尼斯网址 7

此博文参考《JavaScript高级程序设计第3版》,代码为经过改写,更具体,并加了注释使大家更易懂。如对JS继承方面有独到见解的童鞋不别吝啬,回复您的见解供大家参考!

5.寄生式继承

您可能感兴趣的文章:

  • JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)
  • js面向对象之常见创建对象的几种方式(工厂模式、构造函数模式、原型模式)
  • 详解JavaScript中的构造器Constructor模式
  • 构造函数+原型模式构造js自定义对象(最通用)
  • javascript组合使用构造函数模式和原型模式实例
  • javascript设计模式之对象工厂函数与构造函数详解
  • JavaScript构造函数详解
  • JS特殊函数(Function()构造函数、函数直接量)区别介绍
  • 深入理解javascript构造函数和原型对象
  • JavaScript中的普通函数与构造函数比较
  • JS常见构造模式实例对比分析
<script type="text/javascript">
    function object(obj)
    {
        function F(){}   //创建一个空的构造函数
        F.prototype=obj;  //将传入的对象作为这个构造函数的原型
        return new F();   //返回一个新实例  对传入的对象进行了一次浅复制
    }

    function Person(person)
    {
        var clone=object(person);
        clone.getSex=function(){
            alert(this.sex);
        }
        return clone;
    }
    var person={
        name:'张三',
        sex:'男',
        friends:['李四']
    }
    var boy=Person(person);
    boy.getSex();         //男



    //属性共享问题————因为传入同一个实例,所以存在共享问题
      var boy2=Person(person);
      boy2.friends.push('王五');

    alert(boy.friends);     //李四  王五


    //验证能否使用instanceof 和 isPrototypeof
    console.log(boy instanceof Person);                 //false
    console.log(Person.prototype.isPrototypeOf(boy));   //false
</script>

 

与构造函数模式类似,不能做到函数复用会降低效率。

6.寄生组合式继承

<script type="text/javascript">

    function object(o)
    {
        function F(){}
        console.log(o);
        F.prototype=o;
        return new F();

    }
    function extend(subType,superType)
    {
        var prototype=object(superType.prototype);  //创建父类的一个副本
        prototype.constructor=subType;                //为创建的副本添加失去默认的构造函数
        subType.prototype=prototype;                //将新创建的对象赋值给子类的原型
    }
    function Person(name)
    {
        this.name=name;
        this.friends=['李四'];
    }
    function Boy(name,sex)
    {
        Person.call(this,name);             //借用构造函数  继承父类的模板
        this.sex=sex;
    }

    extend(Boy,Person);  //继承

    var boy=new Boy('张三','男');

    alert(boy.name);     //张三



    //属性共享问题————不会存在共享问题
      var boy2=new Boy('张三','男');
      boy2.friends.push('王五');

    alert(boy.friends);     //李四


    //验证能否使用instanceof 和 isPrototypeof
    console.log(boy instanceof Person);                 //false
    console.log(Person.prototype.isPrototypeOf(boy));   //false
</script>

 

寄生式组合继承解决了组合继承会调用两次父类构造函数,子类最终会包含父类的全部实例属性,父类的属性不是必须的,子类的属性会覆盖父类的属性。

寄生式组合继承只调用一次父类构造函数,原型链能保持不变,因此还能够正常使用instanceof和isPrototypeOf(),YUI的extend方法就使用的是寄生组合继承,是实现基于类型继承的最有效的方式。

奥门威尼斯网址 8

发表评论

电子邮件地址不会被公开。 必填项已用*标注