JavaScript原型链和继承

 

1.概念javascript

  JavaScript并不提供一个class的实现,在ES6中提供class关键字,可是这个只是一个语法糖,JavaScript仍然是基于原型的。JavaScript只有一种结构:对象。每一个对象都有一个私有属性:_proto_,这个属性指向它构造函数的原型对象(Prototype)。它的原型对象也有一个属于本身的原型对象,这样层层向上只至这个原型对象的属性为null。根据定义null没有本身的原型对象,它是这个原型链中的最后一个环节。css

  几乎全部的JavaScript中的对象都是位于原型链顶端的Object的实例。html

2.基于原型链的继承java

  JavaScript对象是动态的属性“包”(指其本身的属性)。JavaScript对象有一个指向原型对象的链。当访问一个对象的属性时,它不只仅在对象上搜寻,还会试图搜寻对象的原型,以及该对象原型的原型,依次层层向上搜索,直至找到一个名字匹配的属性或者到达原型链的顶端为止。es6

  在ECMA标准中,someObject.[[Prototype]]符号是表示指向someObject的原型。从ES6开始,[[Prototype]]能够经过Object.getPrototypeOf()和Object.setPrototype()访问器来访问。这个是JavaScript的非标准api,可是不少浏览器都实现了__proto__,两者做用等同。注意浏览器没有实现对象的object.Prototype这样的属性,即没有实现对象实例的Prototype属性,只有构造函数.prototype属性。ajax

  可是[[Prototype]]和构造函数func的prototype属性不一样,不要弄混。构造函数建立的实例对象的[[prototype]]指向func的prototype属性。Object.prototype属性表示Object的原型对象。api

  这里咱们举一个例子,假设咱们有一个对象o,它有本身的属性a, b,o 的原型 o.__proto__有属性 b 和 c, 最后, o.__proto__.__proto__ 是 null,JavaScript代码以下:
数组

    var o = {a: 1, b: 2};
    o.__proto__ = {b: 3, c: 4};
    console.log(Object.getPrototypeOf(o));
    console.log(o.__proto__);
    console.log(Object.getPrototypeOf(Object.getPrototypeOf(o)));
    console.log(o.__proto__.__proto__);
    console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(o))));
    console.log(o.__proto__.__proto__.__proto__);

输出结果以下:浏览器

第一句:定义一个对象o,对象有属性a,b缓存

第二句:设置对象o的原型为一个新的对象{b: 3, c: 4}

第三句:使用ES6方法Object.getPrototypeOf获取对象o的原型,输出{b: 3, c: 4}

第四句:使用浏览器实现的原型属性__proto__获取对象o的原型,输出{b: 3, c: 4}

第五句:使用ES6的方法Object.getPrototypeOf获取对象o的原型的原型,是原型链顶端Object的实例

第六句:使用浏览器实现的原型属性__proto__获取对象o的原型的原型,是原型链顶端Object的实例

第七句:使用ES6的方法Object.getPrototypeOf获取对象o的原型的原型的原型,是null

第八句:使用浏览器实现的原型属性__proto__获取对象o的原型的原型的原型,null

 

3.继承方法
JavaScript没有其余基于类的语言中定义的“方法”。在JavaScript里,任何函数均可以添加到对象上做为对象的属性。函数的继承与其余的属性继承没有任何区别,包括“属性遮蔽”(这至关于其余语言的方法重写)。
当继承的函数被调用时,this指向的当前继承的对象,而不是继承的函数所在的原型对象。看下面的例子:

    var o = {
        a: 2,
        m: function () {
            return this.a + 1;
        }
    };
    // 当调用o.m()的时候,this指向了o
    console.log(o.m());

    // 建立一个对象p,p.__proto__是o,p是一个继承自o的对象
    var p = Object.create(o);
    // 下面两句和上面的效果同样
    //    var p = {};
    //    p.__proto__ = o;

    // 建立p自身的属性a
    p.a = 4;
    // 调用p.m()函数时this指向了p,p继承o的m函数此时this.a,即p.a指向p自身的属性a,最后获得5
    console.log(p.m());

上面代码中,调用p对象的m()方法时,m()方法中this.a指向p对象的a属性,而不是它的父对象o的属性a,有点相似英语语法中的“就近原则”,即先从自身属性开始找,而不是它的原型对象。

4.__proto__和prototype的关系

上面提到“JavaScript中只有一种结构,就是对象”,在JavaScript任何数据结构归根结底都是对象类型,他们都有对象的共同特色,即都有私有属性__proto__,基本上全部的浏览器都实现了这个属性,可是不建议在代码中使用这个属性,因此它使用了一个比较怪异的名字__proto__,表示只能在内部使用,也叫隐式属性,意思是一个隐藏起来的属性。__proto__属性指向当前对象的构造函数的原型,它保证了对象实例可以访问在构造函数原型中定义的全部属性和方法。

JavaScript中的方法除了和其余对象同样有隐式属性__proto__以外,还有本身特有的属性prototype,这个属性是一个指针,prototype指向原型对象,这个对象包含全部实例共享的属性和方法,咱们把prototype属性叫作原型属性prototype指向的原型对象又有一个属性constructor,这个属性也是一个指针,指回原构造函数,即这个方法。

下面咱们来看一张图:


1.构造函数Foo()的原型属性Foo.prototype指向了原型对象,在原型对象中有共有的方法,构造函数声明的实例f1,f2都共享这个方法。

2.原型对象Foo.prototype保存着实例的共享的方法,它又有一个指针constructor,指回到构造函数,即函数Foo()。

3.f1,f2是Foo这个构造函数的两个实例,这两个对象的属性__proto__,指向构造函数的原型对象,这样就能够访问原型对象的全部方法。

4.构造函数Foo()是方法,也是对象,它的__proto__属性指向它的构造函数的原型对象Function.prototype,这个对象中有共有属性call(),bind()等。

5.Foo()的原型对象Function.prototype是对象,它的__proto__属性指向它的构造函数的原型对象,即Object.prototype,这个对象中共有共有属性length,is()等。

6.Function.prototype的prototype属性指向原型对象function Function(),该原型对象的constructor属性指向Function.prototype自己。

7.Function.prototype的__proto__属性指向它构造函数的原型对象Object.prototype。

8.function object()的__proto__属性指向构造函数的原型对象Function.prototype,这个对象包含object实例共享的属性和方法。

9.function ojbect()的prototype属性指向原型对象Object.prototype。

9.最后Object.prototype对象的__proto__指向null。

10.对象有属性__proto__,指向该对象的构造函数的原型对象

11.方法除了有属性__proto__,还有属性prototype,指向该方法的原型对象

 

5. 使用不一样的方法来建立对象和生成原型链

5.1 语法结构建立的对象

var o = { a: 1 };这是一个定义对象的语法,这个语句使对象o继承了Object.prototype上全部的属性,o自己没有名为hasOwenProperty的属性,hasOwnProperty是Object.property的属性,所以对象o继承了Object.prototype的hasOwnProperty属性方法。Object.property的原型为null,原型链以下:o -> Object.prototype -> null,截图以下:

 

var a = ["yo", "whadup", "?"]; 这是一个定义数组的语法,数组都继承于Array.prototype,Array.prototype中包含indexOf,forEach等方法,原型链以下:a -> Array.prototype -> Object.prototype -> null,截图以下:

 

function f() = { return 2; } 这是一个定义函数的语法,函数都继承于Function.prototype,Function.prototype中包含call,bind等方法,原型链以下:f -> Function.prototype -> Object.prototype -> null,使用console.log方法输出f,console.log(f)只能把函数的内容输出,并不能看到函数的原型,函数的原型的原型,只能看到这个方法体,目前本人尚未搞清楚这个问题。截图以下:

 

5.2 使用构造器建立的对象

在JavaScript中,构造器(构造方法)其实就是一个普通的函数。当使用new操做符来做用这个函数时,它就能够被称为成为构造方法或者构造函数。看下面的代码:

    function Graph() {
        this.vertices = []
        this.edges = []
    }

    Graph.prototype = {
        addVertice: function (v) {
            this.vertices.push(v);
        }
    }
    var g = new Graph();
    console.log(g);

输出以下:

g是使用构造方法new Graph()生成的对象,它有本身的属性‘vertices’和‘edges’,还有从本身的原型对象中继承的addVertice方法,在g被实例化时,g.[[Prototype]]指向了Graph.prototype

5.3 Object.create建立的对象

ECMAScript5中引入了一个新的方法:Object.create()。能够调用这个方法来建立一个新对象。新对象的原型就是调用create方法时传入的第一个参数。咱们来看下面的例子:输出结果以下: 

    var a = {a: 1};
    var b = Object.create(a);
    console.log(b.a);

    var c = Object.create(b);
    console.log(c);
    console.log(c.a);

    var d = Object.create(null);
    console.log(d.hasOwnProperty);

输出结果以下:

第一句:定义对象a,它有属性a

第二句:使用Object.Create(a)建立对象b,b的原型是a

第三句:输出b.a,如今对象b上查找属性a,没有,而后在b的原型上找,值是1,输出1

第四句:使用Object.Create(b)建立对象c,c的原型是b

第五句:输出对象c,它的原型的原型上有一个属性c,值为1

第六句:输出c.a,如今对象c的属性中查找a,没有,在c的原型b上查找属性a,没有,在b的原型a上查找属性a,有,值为1,输出1

第七句:使用Object.Create(null)建立对象d,注意null没有原型

第八句:输出d.hasOwnProperty方法,在d的方法中找,没有,在d的原型null中找,也没有,最后输出undefined

5.4 class关键字建立对象

es6引入一套新的关键字来实现class。使用基于类的语言对这些结构会很熟悉,但它们是不一样的。JavaScript是基于原型的。这些新的关键字包括class,constructor,static,extends和super。来看下面的例子: 

    class Polygon {
        constructor(height, width) {
            this.width = width;
            this.height = height;
        }
    }

    class Square extends Polygon {
        constructor(sideLength) {
            super(sideLength, sideLength);
        }

        get area() {
            return this.height * this.width;
        }

        set sideLength(sideLength) {
            this.height = sideLength;
            this.width = sideLength;
        }
    }

    var square = new Square(2);
    writeStr(square.area);

输出结果以下:

在原型链上查找属性比较耗时,对性能有反作用,试图访问不存在的属性的时候会遍历整个原型链。遍历对象的属性时,原型链上每一个可枚举属性都会被枚举出来。要检查对象是否有一个本身定义的属性,而不是从原型链上继承的属性,可使用从Object.prototype上继承的hasOwnPrototype方法。hasOwnPrototype是JavaScript中处理属性但不会遍历原型链的方法之一,另外可使用Object.keys()方法。注意这个并不能解决一切问题,没有这个属性的时候hasOwnPrototype会返回undefined,可能该属性存在,可是它的值就是undefined。

常用的一个错误作法是扩展Object.prototype或其余内置原型,这种技术会破坏封装,尽管一些流行的框架例如Prototype.js在使用该技术,可是仍然没有足够好的理由使用附加的非标准方法来混入内置原型。扩展内置原型惟一的理由是支持JavaScript引擎的新特性,例如Array.forEach,固然在es6中这个特性已经存在。

6. JavaScript中的继承

6.1 先看看如何封装

上面咱们讲到建立对象的方式,有了对象以后就会有封装,在JavaScript中封装一个类很容易。经过构造器建立对象时,在构造函数(类)的内部经过对this(函数内部自带的变量,用于指向当前这个对象)添加属性或者方法来实现添加属性或方法。代码以下: 

    // 类的封装
    function Book1 (id, bookname, price) {
        this.id = id;
        this.bookname = bookname
        this.price = price
    }
    var Book2 = function (id, bookname, price) {
        this.id = id;
        this.bookname = bookname
        this.price = price
    }

 也能够经过在构造函数类(对象)的原型对象上添加属性和方法。有两种方式,一种是为原型对象赋值,另外一种是将一个对象赋值给类的原型对象。以下:

    // 方式一
    Book.prototype.display = function () {

    }
    // 方式二
    Book.prototype = {
        display: function () {

        }
    }

须要访问类的属性和方法时不能直接使用Book类,例如Book.name,Book.display(),而要用new关键字来建立新的对象,而后经过点语法来访问。

经过this添加的属性,方法是在当前函数对象上添加的,JavaScript是一种基于原型prototype的语言,因此每次经过一个构造函数建立对象的时候,这个对象都有一个原型prototype指向其继承的属性,方法。因此经过prototype继承来的属性和方法不是对象自身的,可是在使用这些属性和方法的时候须要经过prototype一级一级向上查找。

经过this定义的属性或方法是该函数对象自身拥有的,每次经过这个函数建立新对象的时候this指向的属性和方法都会相应的建立,而经过prototype继承的属性或者方法是经过prototype访问到的,每次经过函数建立新对象时这些属性和方法不会再次建立,也就是说只有单独的一份。

面向对象概念中“私有属性”,“私有方法”,“公有属性”,“公有方法”,“保护方法”在JavaScript中又是怎么实现的呢?

私有属性,私有方法:因为JavaScript函数级做用域,声明在函数内部的变量和方法在外界是访问不到的,经过这个特性能够建立类的私有变量以及私有方法。

公有属性,公有方法:在函数内部经过this建立的属性和方法,在类建立对象时,没有对象自身都拥有一份而且能够在外部访问到,所以经过this建立的属性,方法能够看作对象公有属性和对象公有方法。类经过prototype建立的属性或方法在实例的对象中经过点语法访问到,因此能够将prototype对象中的属性和方法也称为类的公有属性,类的公有方法。

特权方法:经过this建立的方法,不但能够访问这些对象的共有属性,方法,并且能够访问到类或者对象自身的私有属性和私有方法,权利比较大,因此能够看作是特权方法。

类构造器:在对象建立时能够经过特权方法实例化对象的一些属性,所以这些在建立对象时调用的特权方法能够看作类的构造器。

静态共有属性,静态共有方法:经过new关键字和方法名来建立新对象时,因为函数外面经过点语法函数名.xxx)添加的属性和方法没有执行到,因此新建立的对象中没法使用它们,可是能够经过类名来使用。所以在类外面经过点语法来建立的属性,方法能够被称为类的静态共有属性和类的静态共有方法。

参考下面的代码:

    var Book = function (id, name, price) {
        // 私有属性
        var num = 1;
        // 私有方法
        function checkId() {
        };
        // 特权方法
        this.getName = function () {
        };
        this.getPrice = function () {
        };
        this.setName = function () {
        };
        this.setPrice = function () {
        };
        // 对象公有属性
        this.id = id;
        // 对象公有方法
        this.copy = function () {
        };
        // 构造器
        this.setName(name);
        this.setPrice(price);
    }
    // 类静态公有属性(对象不能访问)
    Book.isChinese = true;
    // 类静态公有方法(对象不能访问)
    Book.resetTime = function () {
        console.log('new Time');
    };
    Book.prototype = {
        // 公有属性
        isJSBook: false,
        //公有方法
        display: function () {
        }
    };

经过new关键字建立对象的本质是对新对象的this不断的赋值,并将prototype指向类的prototype所指向的对象,而在类的构造函数外面经过点语法定义的属性,方法不会添加在新的对象上。所以要想在新建立的对象上访问isChinese就得经过Book类而不能经过this,如Book.isChinese,类的原型上定义的属性在新对象里能够直接使用,这是由于新对象的prototype和类(Boo()方法)的prototype指向同一个对象。

类的私有属性num以及静态公有属性isChiese在新建立的对象里是访问不到的,而类的公有属性isJSBook在对象中能够经过点语法访问到。看下面实例代码,注意这段代码是在上面的实例代码基础上写的:

    var b = new Book(11, 'Javascript', 50);
    console.log(b.num); // undefined
    console.log(b.isJSBook); // false
    console.log(b.id); // 11
    console.log(b.isChinese); // undefined
    console.log(Book.isChinese); // true
    Book.resetTime(); // new Time

第一句,使用new关键字建立对象b,对Book函数对象内的this指定的属性赋值,而且将b的原型指向Book.prototype
第二句,输出b.num,由于num是类的私有属性,对象访问不到,在Book.prototype上也找不到,因此输出undefined
第三句,输出b.isJSBook,在构造函数内没有这个属性,在Book.prototype上有,因此输出false
第四句,输出b.id,在构造函数中有这个属性,它是共有属性,值为11
第五句,输出b.isChinese,这个是类的静态属性,在类的对象上是找不到的,输出undefined
第六句,输出Book.isChinese,这个是类的静态属性,使用类名直接访问,输出true
第七句,调用Book类的resetTime()方法,这个是类的静态属性,输出new time

new关键字的做用能够看作对当前对象的this不停地赋值,若是没有指定new关键字则this默认指向当前全局变量,通常是window

6.2 子类的原型对象继承—类式继承 

    // 类式继承
    // 申明父类
    function SuperClass() {
        this.superValue = true
    }
    //为父类添加共有方法
    SuperClass.prototype.getSuperValue = function () {
        return this.superValue;
    }

    // 申明子类
    function SubClass() {
        this.subValue = false;
    }
    // 继承父类
    SubClass.prototype= new SuperClass()
    // 为子类添加共有方法
    SubClass.prototype.getSubValue = function () {
        return this.subValue;
    }
    let sup = new SuperClass();
    let sub = new SubClass();
    console.log(sup.getSuperValue());       //true
    console.log(sup.getSubValue());         //Uncaught TypeError: sup.getSubValue is not a function
    console.log(sub.getSubValue());         // false
    console.log(sub.getSuperValue());       // true
    console.log(sub instanceof SubClass);   // true
    console.log(sub instanceof SuperClass); // true
    console.log(sup instanceof SubClass);   // false
    console.log(sup instanceof SuperClass); // true
    console.log(SubClass instanceof SuperClass); // false
    console.log(SubClass.prototype instanceof SuperClass); // true
    console.log(SubClass.prototype instanceof SuperClass.prototype); // Uncaught TypeError: Right-hand side of 'instanceof' is not callable
    console.log(sub.prototype instanceof SuperClass); // false

1. 申明父类(函数)SuperClass()
2. 在SuperClass的原型对象上设置共有方法getSuperValue
3. 申明子类(函数)SubClass()
4. 设置子类SubClass的原型对象是父类SuperClass的一个实例,子类继承了父类 的属性和方法,以及父类的原型对象上的属性和方法。
5. 在子类SubClass的原型对象设置共有方法getSubValue
6. 定义父类对象sup
7. 定义子类对象sub
8. 调用父类对象sup的getSuperValue()方法获得true
9. 调用父类对象sup的方法getSubValue(),它没有这个方法,报错了
10. 调用子类对象sub的getSubValue()方法,在它的内部找不到,在它的原型对象上找,有这个方法,返回this.subValue,返回false
11. 调用子类对象的getSuperValue()方法,在它的内部找不到,原型对象上找不到,继承的父类的原型对象上有,返回this.superValue,值为true
12. 子类对象sub是子类SubClass的一个实例
13. 子类对象sub是父类SuperClass的一个实例
14. 父类对象sup不是子类SubClass的一个实例
15. 父类对象sup是父类SuperClass的一个实例
16. 子类SubClass不是父类SuperClass的实例
17. 子类的原型对象SubClass.property是父类SuperClass的一个实例
18. 子类的原型对象SubClass.property不是父类原型对象SuperClass.property的实例,由于父类的原型对象不是一个类,而是一个对象
19. 子类的原型对象sub.property不是父类SuperClass的一个实例,而是指向一个父类对象

类的原型对象用来为类添加共有方法,可是不能直接添加,访问这些属性和方法,必须经过原型prototype来访问。新建立的对象复制了父类构造函数的属性和方法,并将原型__proto__指向父类的原型对象,这样就拥有了父类的原型对象上的属性和方法,这个新建立的对象能够直接访问到父类原型对象上的属性和方法。

这种继承方式有2个缺点,其一,子类经过其原型对父类实例化,继承了父类。若是父类中的共有属性是引用类型的话,全部子类的实例会公用这个共有属性,任何一个子类实例修改了父类属性(引用类型),会直接影响到全部子类和这个父类。看下面代码:

    function SuperClass() {
        this.books = ['javascript', 'html'];
    }
    function SubClass() {}
    SubClass.prototype = new SuperClass();
    var instance1 = new SubClass();
    var instance2 = new SubClass();
    console.log(instance1.books); //["javascript", "html"]
    instance2.books.push('java');
    console.log(instance1.books); //["javascript", "html", "java"]
    console.log(instance2.books); //["javascript", "html", "java"]
    console.log(SuperClass.books);//undefined
    var sup1 = new SuperClass();
    var sup2 = new SuperClass();
    sup2.books.push('css');
    console.log(sup1.books); // ["javascript", "html"]
    console.log(sup2.books); // ["javascript", "html", "css"]

1. 申明父类(函数)SuperClass,内部有共有引用属性books
2. 申明子类(函数)SubClass,函数内部没有内容
3. 子类的原型对象设置为父类的一个对象,子类继承了父类的属性,方法和父类原型对象上的属性,方法
4. 定义子类对象instance1,instance2,它们继承了父类的属性,方法以及父类原型对象上的属性,方法
5. 输出子类instance1的属性books,在子类对象的内部没有,在在父类上有这个属性输出["javascript", "html"]
6. 在子类对象instance2上找books属性,它来自继承的父类内部,而且是一个引用属性,修改这个属性,添加一个元素“java”
7. 输出子类对象instance1的book属性,她来自继承的父类内部,已经被修改,输出["javascript", "html", "java"]
8. 输出子类对象instance2的book属性,她来自继承的父类内部,已经被修改,输出["javascript", "html", "java"]
9. 在父类函数SuperClass上访问它内部的属性books,找不到这个属性,输出undefined
10. 定义父类对象sup1,和sup2,他们调用父类函数,初始化共有属性books
11. 给父类对象sup2的引用属性books添加一个元素“css”
12. 输出父类对象sup1的属性books,输出["javascript", "html"],这个books属性和sup2的books属性是没有关系的
13. 输出父类对象sup2的属性books,输出["javascript", "html", "css"]

上面例子中instance2修改了父类的books属性,添加了一个“java”,结果instance1的books属性也有了个新的元素“java”。注意SubClass.prototype = new SuperClass();这一句中new操做符会复制一份父类的属性和方法,var sup = new SuperClass();也会复制一份父类的属性和方法,可是他们是不一样的,相互后者不会影响。而且只有前者才会出现这种引用类型被无心修改的状况,前者是经过设置SubClass的原型对象添加的属性和方法。

其二,因为子类实现继承是靠其原型prototype对父类的实例化实现的,所以在(实例化子类时会建立父类,就是这一句:let sub = new SubClass();)建立父类的时候是没法向父类传递参数的,所以在实例化父类的时候没法调用父类的构造函数进而对父类构造函数内部的属性初始化。 

6.3 构造函数继承—call方法建立继承

    // 构造函数继承
    // 申明父类
    function SuperClass(id) {
        // 引用类型共有属性
        this.books = ['javascript', 'html', 'css'];
        // 值型共有属性
        this.id = id;
    }

    // 父类申明原型方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books);
    }

    // 申明子类
    function subClass(id) {
        // 继承父类
        SuperClass.call(this, id);
    }
    // 建立两个实例
    var instance1 = new subClass(10);
    var instance2 = new subClass(11);
    instance1.books.push('java');

    console.log(instance1.books); // ["javascript", "html", "css", "java"]
    console.log(instance1.id);    // 10
    console.log(instance2.books); // ["javascript", "html", "css"]
    console.log(instance2.id);    // 11
    instance1.showBooks();        // Uncaught TypeError: instance1.showBooks is not a function
    instance2.showBooks();        // Uncaught TypeError: instance1.showBooks is not a function

    // 申明父类实例
    var instance3 = new SuperClass(12);
    instance3.showBooks();      // ["javascript", "html", "css"]

1. 申明父类方法SuperClass,方法内部有共有属性books,id
2. 给父类的原型对象上申明共有方法showBooks()
3. 申明子类方法SubClass(),在子类方法中使用call调用父类SuperClass方法,在当前子类中执行父类方法,给this赋值,这样子类就继承了父类内部的方法和属性(id,books),可是子类不会继承父类的原型对象中的属性和方法(showBooks())
4. 申明两个子类实例instance1,instance2,并分别传参给父类10,11
5. 修改子类实例instance1的books属性,这是一个引用属性,给数组添加一个元素“java”
6. 输出子类实例instance1的books属性,“java”已经被添加上去了
7. 输出子类实例instance1的id属性是10
8. 输出子类实例instance2的books属性,这里是没有“java”元素的,由于它是在调用call方法的时候直接复制的一份,和instance1的是两个彻底不一样的数组对象
9. 输出子类实例instance2的books属性是11
10. 调用子类实例instance1的showBooks()方法,在子类中找不到,子类的原型对象中找不到,在子类继承的父类中找不到(这里不会在子类继承的父类的原型对象中找这个方法),所以报错
11. 调用子类实例instance2的showBooks()方法,在子类中找不到,子类的原型对象中找不到,在子类继承的父类中找不到(这里不会在子类继承的父类的原型对象中找这个方法)所以报错
12. 申明父类实例instance3,传入参数12
13. 调用父类实例instance3的showBooks()方法,在父类内部找不到这个方法,在父类的原型对象中有这个方法,输出books对象,注意这个对象并无被子类实例instance1修改,全部子类实例都有一份本身单独的属性和方法

注意SuperClass.call(this, id);这句是构造函数式继承的关键。call方法能够改变函数的做用环境,在子类中对SuperClass调用这个方法就是将子类中的变量在父类中执行一遍,因为父类是给this绑定属性的,所以子类就继承了父类的共有属性。因为这种类型的继承没有涉及原型,因此父类的原型中的方法和属性不会被子类继承,要想被子类继承就必须放在构造函数中,这样建立的实例会单独拥有一份父类的属性和方法,而不是共用,这样违背了代码复用的原则

6.4 组合继承

组合继承又叫“伪经典继承”,是指将原型链和构造函数技术组合在一块儿的一种继承方式,下面看一个例子:

    // 申明父类
    function SuperClasss(name) {
        // 值类型共有属性
        this.name = name;
        // 引用类型共有属性
        this.books = ['html', 'css', 'Javascript'];
    }
    // 父类原型共有方法
    SuperClasss.prototype.getName = function () {
        console.log(this.name);
    }
    // 申明子类
    function SubClass(name, time) {
        // 构造函数式继承父类name属性
        SuperClasss.call(this, name);
        // 子类的共有属性
        this.time = time;
    }
    // 类式继承,子类原型继承父类
    SubClass.prototype = new SuperClasss();
    // 子类原型方法
    SubClass.prototype.getTime = function () {
        console.log(this.time);
    }
    var instance1 = new SubClass('js book', 2014);
    instance1.books.push('java');
    console.log(instance1.books); // ['html', 'css', 'Javascript', 'java']
    instance1.getName(); // 'js book'
    instance1.getTime(); // 2014

    var instance2 = new SubClass('css book', 2013);
    console.log(instance2.books); // ['html', 'css', 'Javascript']
    instance2.getName(); // 'css book'
    instance2.getTime(); // 2013

1. 申明父类方法SuperClass(),方法内部有共有属性
2. 在父类方法的原型对象上定义共有方法getname(),输出当前属性name
3. 申明子类方法SubClass(),在子类方法中使用call调用父类方法,在当前子类中执行父类方法给this赋值,这样子类就继承了父类内部的方法和属性(name,books),可是子类不会继承父类的原型对象中的属性和方法。子类方法中有共有属性time
4. 子类的原型对象设置为父类的一个实例对象,子类继承了父类的属性,方法和父类原型对象上的属性,方法。
5. 在子类的原型对象上定义共有方法getTime(),输出当前对象的属性time
6. 定义子类对象实例instance1,分别向父类构造函数“js book”,子类构造函数传递参数2014
7. 访问子类对象实例instance1的books属性,在子类方法中找不到books属性,在子类对象实例的原型对象的构造函数内有这个属性,给这个引用属性添加一个元素“java”
8. 访问子类对象实例instance1的getName()方法,在子类方法构造函数中找不到,在子类原型对象中有这个方法,输出“js book”
9. 访问子类对象实例instance1的getTIme()方法,在子类方法构造函数中找不到,在子类原型对象中有这个方法,输出2014
10. 定义子类对象实例instance2,分别向父类构造函数“css book”,子类构造函数传递参数2013
11. 访问子类对象实例instance2的books属性,在子类方法中找不到books属性,这里是构造函数继承,在子类对象实例的原型对象的构造函数内有这个属性,这个属性是从父类构造函数中拷贝的一份,它和instance1的books属性是不一样的,相互没有影响
12. 访问子类对象实例instance2的getName()方法,子类构造函数中找不到,父类构造函数中找不到,父类原型对象上有这个方法,输出当前对象的name属性,所以输出“css book”
13. 访问子类对象实例instance2的getTime()方法,子类构造函数中找不到,子类原型对象中有这个方法,输出当前对象的time属性,所以输出2013

注意这里经过call方式继承父类后,访问方法的前后顺序是:
1. 子类方法中的共有方法SubClass.this.getName,
2. 父类方法中的共有方法SuperClasss.this.getName,
3. 子类原型对象中的共有方法SubClass.prototype.getName,
4. 父类原型对象中的共有方法SuperClasss.prototype.getName

访问属性books的时候也是这个顺序,因此优先考虑经过call方法给当前this赋值获得的books,而不是经过原型对象继承的books。

在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式。经过this将引用属性books定义在父类的共有属性中,每次实例化子类都会单独拷贝一份,所以在子类的实例中更改父类继承下来的引用类型属性books不会影响到其余实例,而且子类实例化过程当中又能将参数传递到父类的构造函数中。

这种方式也有缺点,在使用构造函数继承时执行了一次父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类的构造函数,父类的构造函数调用了两次。

6.5 简洁的继承—原型式继承

原型式继承的思想是借助prototype根据已有的对象建立一个新的对象,同时没必要建立新的自定义对象类型。代码以下:

    // 原型式继承
    function inheritObject(o) {
        // 申明一个过渡函数对象
        function F() {}
        // 过渡对象的原型继承父对象
        F.prototype = o;
        // 返回过渡对象的一个实例,该实例的原型继承了父对象
        return new F();
    }
    var book = {
        name: 'js book',
        alikeBook: ['css book', 'html book']
    }
    var newBook = inheritObject(book);
    newBook.name = 'ajax book';
    newBook.alikeBook.push('xml book');

    var otherBook = inheritObject(book)
    otherBook.name = 'flash book';
    otherBook.alikeBook.push('as book');

    console.log(newBook.name); // ajax book
    console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    console.log(otherBook.name); // flash book
    console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    console.log(book.name); // js book
    console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]

1. 定义原型式继承方法,在方法内部申明过渡类,设置类的原型对象为传入的参数,访问这个对象实例,这个实例继承了父类对象
2. 定义book对象,对象内有name属性和alikeBook属性
3. 定义子类对象newBook,调用原型式继承方法,继承book对象中的属性
4. 访问子类对象newBook的name属性,赋值为“ajax book”,子类对象的原型对象中有这个属性,而且是一个值类属性
5. 访问子类对象newBook的alikeBook属性,添加元素“xml book”,子类对象的原型对象中有这个属性,而且是一个引用属性
6. 定义子类对象otherBook,调用原型式继承方法,继承book对象中的属性
7. 访问子类对象otherBook的name属性,赋值为“ajax book”,子类对象的原型对象中有这个属性,而且是一个引用类型变量
8. 访问子类对象otherBook的alikeBook属性,添加元素“as book”,子类对象的原型对象中有这个属性,而且是一个引用类型变量
9. 输出newBook的name属性,值是“ajax book”
10. 输出newBook的books属性,它是从父类原型对象上继承来的,是同一个变量,这个数组内被添加了 “as book”,输出['css book', 'html book', 'xml book', 'as book']
11. 输出other的name属性,值是“flash book”
12. 输出oterBook的books属性,它是从父类原型对象上继承来的,是同一个变量,这个数组内被添加了 “as book”,输出['css book', 'html book', 'xml book', 'as book']
13. 输出父类对象的name属性,值是“js book”
14. 输出父类对象book的的alikeBook属性,它是从父类原型对象上继承来的,是同一个变量,这个数组被修改过了,添加了“as book”,输出['css book', 'html book', 'xml book', 'as book']

和类式继承同样,父类对象book中的值类型被复制,引用类型属性被共用,它也有类式继承的缺点,即修改修改子类中从父类继承来的引用类型属性,会影响到其余子类中的同名属性,他们是同一个属性。这种方法的优势是F()函数内部没有什么内容,开销比较小,还能够将F过渡类缓存起来。也可使用新的语法Object.create()来代替这一句。不过建立子类实例的时候是能够向父类构造函数传参的,这里再也不展开介绍。

6.6 寄生式继承—加强版的原型式继承

    // 原型式继承
    function inheritObject(o) {
        // 申明一个过渡函数对象
        function F() {}
        // 过渡对象的原型继承父对象
        F.prototype = o;
        // 返回过渡对象的一个实例,该实例的原型继承了父对象
        return new F();
    }
    var book = {
        name: 'js book',
        alikeBook: ['css book', 'html book']
    }
    function createBook(obj) {
        // 经过原型继承方式建立对象
        var o = new inheritObject(obj);
        // 拓展对象
        o.getName = function () {
            console.log(obj.name);
        }
        // 返回拓展后的新对象
        return o;
    }
    var newBook = createBook(book);
    newBook.name = 'ajax book';
    newBook.alikeBook.push('xml book');

    var otherBook = createBook(book);
    otherBook.name = 'flash book';
    otherBook.alikeBook.push('as book');

    console.log(newBook.name); // ajax book
    newBook.getName(); // js book
    console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"]

    console.log(otherBook.name); // flash book
    console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    otherBook.getName(); // js book

    console.log(book.name); // js book
    console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]

1. 声明原型式继承方法inheritObject,实现原型式继承
2. 定义父类对象book
3. 声明建立Book对象的方法createBook,方法内部使用new表达式建立一个继承自传递参数的对象o,在这个对象上扩展属性,最后返回建立的对象
4. 使用createBook方法建立对象newBook,传递参数是book对象
5. 访问对象newBook的name属性,在它的原型对象上有这个属性,从新赋值为“ajax book”
6. 访问对象newBook的alikeBook属性,在它的原型对象上有这个属性,添加元素“xml book”,这样会影响全部继承自这个对象的对象
7. 使用createBook方法建立对象otherBook,传递参数是book对象
8. 访问对象otherBook的name属性,在它的原型对象上有这个属性,从新赋值为“flash book”
9. 访问对象otherBook的alikeBook属性,在它的原型对象上有这个属性,添加元素“as book”,这样会影响全部继承自这个对象的对象
10. 访问newBook的属性name,虽然继承自它的原型对象的,可是这个属性是值类型,已经被修改为“ajax book”
11. 访问newBook的getName方法,这个方法是经过在原型对象上扩展的方法继承的,输出传入参数的name属性,值为“js book”
12. 访问newBook的alikeBook属性,这个属性是继承自原型对象的,而且是一个引用类型,已经被修改为["css book", "html book", "xml book", "as book"]
13. 访问otherBook的属性name,虽然继承自它的原型对象的,可是这个属性是值类型,已经被修改为“flash book”
14. 访问otherBook的alikeBook属性,这个属性是继承自原型对象的,而且是一个引用类型,已经被修改为["css book", "html book", "xml book", "as book"]
15. 访问otherBook的getName方法,这个方法是经过在原型对象上扩展的方法继承的,输出传入参数的name属性,值为“js book”
16. 访问父对象book的name属性,它仍然是“js book”
17. 访问父类对象book的alikeBook属性,这个属性已经被经过原型对象继承book对象的子类对象修改了,已经被修改为["css book", "html book", "xml book", "as book"]

寄生式继承是对原型继承的二次封装,并在二次封装过程当中对继承的对象进行了拓展,这样新建立的对象不只仅继承了父类中的属性和方法,并且还添加了新的属性和方法。之因此叫寄生式继承,是指能够像寄生虫同样寄托于某个对象的内部生长,寄生式继承这种加强新建立对象的继承方式是依托于原型继承模式。

从上面的测试代码能够看出,这种方式仍然会有全部子类共用一个引用实例的问题。

6.7 寄生组合式继承-改造组合继承

上面介绍的组合继承是把类式继承和构造函数继承组合使用,这种方式有一个问题,就是子类不是父类的实例,而子类的原型是父类的实例,因此才有了这里要说的寄生组合继承。寄生继承依赖于原型继承,原型继承又与类式继承很像,寄生继承有些特殊,它处理的不是对象,而是对象的原型。

组合继承中,经过构造函数继承的属性和方法是没有问题的,这里主要探讨经过寄生式继承从新继承父类的的原型。咱们须要继承的仅仅是父类的原型,再也不须要调用父类的构造函数,也就是在构造函数继承中咱们已经调用了父类的构造函数。所以咱们须要的就是父类的原型对象的一个副本,而这个副本咱们经过原型继承能够获得,可是这么直接赋值给子类会有问题的,由于对父类原型对象复制获得的复制对象p中的constructor指向的不是subClass子类对象,所以在寄生式继承中要对复制对象p作一次加强处理,修复它的constructor属性指向不正确的问题,最后获得的复制对象p赋值给子类的原型,这样子类的原型就继承了父类的原型而且没有执行父类的构造函数。测试代码以下:

    /**
     * 原型式继承
     * @param o 父类
     * */
    function inheritObject(o) {
        // 申明一个过渡函数对象
        function F() {}
        // 过渡对象的原型继承父对象
        F.prototype = o;
        // 返回过渡对象的一个实例,该实例的原型继承了父对象
        return new F();
    }

    /**
     * 寄生式继承,继承原型
     * @param subClass 子类
     * @param superClass 父类
     */
    function inheritPrototype(subClass, superClass) {
        // 复制一份父类的原型副本保存在变量中
        var p = inheritObject(superClass.prototype);
        // 修正由于重写子类原型而致使子类的constructor属性被修改
        p.constructor = subClass;
        // 设置子类的原型
        subClass.prototype = p;
    }
    // 定义父类
    function SuperClass(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    // 定义父类原型方法
    SuperClass.prototype.getName = function () {
        console.log(this.name);
    };
    // 定义子类
    function SubClass(name, time) {
        // 构造函数式继承
        SuperClass.call(this, name);
        // 子类新增属性
        this.time = time;
    }
    // 寄生式继承父类原型
    inheritPrototype(SubClass, SuperClass);
    // 子类新增原型方法
    SubClass.prototype.getTime = function () {
        console.log(this.time);
    }

    var instance1 = new SubClass('js book', 2014);
    var instance2 = new SubClass('css book', 2013);
    instance1.colors.push('black');
    console.log(instance1.colors); //["red", "blue", "green", "black"]
    console.log(instance2.colors); //["red", "blue", "green"]
    instance2.getName(); //css book
    instance2.getTime(); //2013
    console.log(SubClass instanceof SuperClass); // false
    console.log(SubClass.prototype instanceof SuperClass); // true
    console.log(SubClass.prototype instanceof SuperClass.prototype); // Right-hand side of 'instanceof' is not callable
    console.log(instance2 instanceof SubClass); // true
    console.log(instance2 instanceof SuperClass); // true

1. 定义原型式继承方法inheritObject,经过过渡对象返回一个经过原型对象继承自传入自参数的实例
2. 定义寄生式继承方法inheritPrototype,传入父类和子类。复制一份父类原型的副本保存在变量中,修正由于重写子类原型而致使子类的constructor属性问题,设置子类的原型为这个对象。
3. 定义父类方法SuperClass,内部有本身的属性
4. 访问父类的原型对象,添加getName方法,输出当前属性name
5. 定义子类方法SubClass,在子类中调用call方法,在子类中执行父类的构造方法,给子类的this赋值。定义子类本身的属性time
6. 调用寄生式继承方法inheritPrototype,先拷贝父类原型对象赋值给变量p,修改它的constructor属性,让它指向子类构造函数subClass,设置子类的原型对象为这个新的对象p
7. 访问子类SubClass的原型对象,设置共有方法getTime,输出当前对象time
8. 定义子类对象实例instance1,传入两个参数,第一个“js book”传递给父类方法,第二个2014用于对象本身的共有属性
9. 定义子类对象实例instance2,传入两个参数,第一个“css book”传递给父类方法,第二个2013用于对象本身的共有属性
10. 访问对象instance1的colors属性,它是经过call方法从父类构造函数中单独拷贝的,给colors属性新加一个元素“black”
11. 访问对象instance1的colors属性,输出的是新增“black”元素以后的数组
12. 访问对象instance2的colors属性,它是经过call方法从父类构造函数中单独拷贝的,这个没有被修改过
13. 访问对象instance2的getName方法,它是经过父类的原型对象继承来的,输出当前对象的name属性“css book”
14. 访问对象instance2的getTime方法,它是经过子类对象的原型对象继承来的,输出当前对象的time属性2013
15. SubClass子类不是父类SuperClass的实例
16. 子类原型对象SubClass.prototype是父类SuperClass的实例
17. 子类原型对象SubClass.prototype不是父类原型对象SuperClass.prototype的实例
18.子类对象instance2是子类SubClass的实例
19. 子类对象instance2是父类SuperClass的实例

最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,所以这里有一点要注意的就是子类再想添加原型方法必须经过prototype对象,经过点语法的方式一个一个添加方法了,不然直接赋予对象就会覆盖掉从父类原型继承的对象。

从上面的例子来看,寄生组合继承还解决了子类共用父类中引用类型属性的问题,子类中继承的引用类型实例互不影响。还有子类也继承了父类原型中的属性和方法

相关文章
相关标签/搜索