JavaScript原型链和继承

简介: 1.概念   JavaScript并不提供一个class的实现,在ES6中提供class关键字,但是这个只是一个语法糖,JavaScript仍然是基于原型的。JavaScript只有一种结构:对象。

 

1.概念

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

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

2.基于原型链的继承

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

  在ECMA标准中,someObject.[[Prototype]]符号是表示指向someObject的原型。从ES6开始[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototype()访问器来访问。这个是JavaScript的非标准api,但是很多浏览器都实现了__proto__。注意浏览器没有实现对象的object.Property这样的属性,即没有实现对象实例的Prototype属性。

  但是[[Prototype]]和构造函数func的prototype属性不同。构造函数创建的实例对象的[[prototype]]指向func的prototype属性。Object.prototype属性表示Object的原型对象。

  这里我们举一个例子,假设我们有一个对象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。

5.Foo()的原型对象也是对象,它的__proto__属性指向它的构造函数的原型对象,即Object.prototype。

6.最后Object.prototype的__proto__指向null。

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

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

4. 使用不同的方法来创建对象和生成原型链

4.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)只能把函数的内容输出,并不能看到函数的原型,函数的原型的原型,只能看到这个方法体,目前本人还没有搞清楚这个问题。截图如下:

 

4.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是生成的对象,它有自己的属性‘vertices’和‘edges’,还有从自己的原型对象中继承的addVertice方法,在g被实例化时,g.[[Prototype]]指向了Graph.prototype

4.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

4.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中这个特性已经存在。

5. JavaScript中的继承

5.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关键字和方法名来创建新对象时,由于外面通过点语法添加的属性和方法没有执行到,所以新创建的对象中无法使用它们,但是可以通过类名来使用。因此在类外面通过点语法来创建的属性,方法可以被称为类的静态共有属性和类的静态共有方法。

参考下面的代码:

    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。

5.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的一个实例,子类继承了父类 的属性和方法,以及父类的原型对象上的属性和方法。SubClass的原型对象本来应该是
5. 在子类SubClass的原型对象设置共有方法getSubValue
6. 定义父类对象sup
7. 定义子类对象sub
8. 调用父类对象sup的getSuperValue()方法得到true
9. 调用父类对象sup的方法getSuperValue(),它没有这个方法,报错了
10. 调用子类对象的getSubValue()方法,在它的内部找不到,在它的原型对象上找,有这个方法,返回this.subValue,返回false
11. 调用子类对象的getSuperValue()方法,在它的内部找不到,原型对象上找不到,继承的父类内部找不到,继承的父类的原型对象上有,返回this.superValue,值为true
12. 子类对象是子类的一个实例
13. 子类对象是父类的一个实例
14. 父类对象不是子类的一个实例
15. 父类对象是父类的一个实例
16. 子类不是父类的实例
17. 子类的原型对象是父类的一个实例
18. 子类的原型对象不是父类原型对象的实例,因为父类的原型对象不是一个类
19. 子类的原型对象不是父类的一个实例,而是指向一个父类对象

类的原型对象用来为类添加共有方法,但是不能直接添加,访问这些属性和方法,必须通过原型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();)创建父类的时候是无法向父类传递参数的,因此在实例化父类的时候无法调用父类的构造函数进而对父类构造函数内部的属性初始化。 

5.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. 申明父类方法,方法内部有共有属性
2. 给父类的原型对象上申明共有方法
3. 申明子类方法,在子类方法中使用call调用父类方法,在当前子类中执行父类方法给this赋值,这样子类就继承了父类内部的方法和属性,但是子类不会继承父类的原型对象中的属性和方法
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绑定属性的,因此子类就继承了父类的共有属性。由于这种类型的继承没有涉及原型,所以父类的原型中的方法和属性不会被子类继承,要想被子类继承就必须放在构造函数中,这样创建的实例会单独拥有一份父类的属性和方法,而不是共用,这样违背了代码复用的原则。

5.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. 申明父类方法,方法内部有共有属性
2. 在父类方法的原型对象上定义共有方法getname(),输出当前属性name
3. 申明子类方法,在子类方法中使用call调用父类方法,在当前子类中执行父类方法给this赋值,这样子类就继承了父类内部的方法和属性,但是子类不会继承父类的原型对象中的属性和方法。子类方法中有共有属性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

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

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

5.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属性,它是从父类原型对象上继承来的,是同一个变量,这个变量内被添加了“xml book”, “as book”两个变量
11. 输出other的name属性,值是“flash book”
12. 输出oterBook的books属性,它是从父类原型对象上继承来的,是同一个变量,这个变量内被添加了“xml book”, “as book”两个变量
13. 输出父类对象的name属性,值是“js book”
14. 输出父类对象的alikeBook属性,这个属性被修改过了,添加了“xml book”, “as book”两个变量

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

5.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. 声明原型式继承方法
2. 定义父类对象book
3. 声明创建Book对象的方法createBook,方法内部使用new表达式创建一个继承自传递参数的对象,在这个对象上扩展属性,最后返回创建的对象
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"]

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

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

5.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. 定义原型式继承方法,通过过渡对象返回一个通过原型对象继承自传入自参数的实例
2. 定义寄生式继承方法,传入父类和子类。复制一份父类原型的副本保存在变量中,修正因为重写子类原型而导致子类的constructor属性问题,设置子类的原型为这个对象。
3. 定义父类方法,内部有自己的属性
4. 访问父类的原型对象,添加getName方法,输出当前属性name
5. 定义子类方法,在子类中调用call方法,在子类中执行父类的构造方法,给子类的this赋值。定义子类自己的属性time
6. 调用寄生式继承方法,先拷贝父类原型对象赋值给变量p,修改它的constructor属性,让它指向子类构造函数subClass,设置子类的原型对象为这个新的对象p
7. 访问子类的原型对象,设置共有方法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. 子类不是父类的实例
16. 子类原型对象是父类的实例
17. 子类原型对象不是父类原型对象的实例
18. instance2是子类的实例
19. instance2是父类的实例

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

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

 

作者:Tyler Ning
出处:http://www.cnblogs.com/tylerdonet/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,如有问题,可以通过以下邮箱地址williamningdong@gmail.com  联系我,非常感谢。

目录
相关文章
|
2天前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
28天前
|
JavaScript 前端开发 开发者
理解JavaScript中的原型链:基础与实践
【10月更文挑战第8天】理解JavaScript中的原型链:基础与实践
|
3天前
|
JavaScript 前端开发
JavaScript 原型链的实现原理是什么?
JavaScript 原型链的实现原理是通过构造函数的`prototype`属性、对象的`__proto__`属性以及属性查找机制等相互配合,构建了一个从对象到`Object.prototype`的链式结构,实现了对象之间的继承、属性共享和动态扩展等功能,为 JavaScript 的面向对象编程提供了强大的支持。
|
11天前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
3天前
|
JavaScript 前端开发
原型链在 JavaScript 中的作用是什么?
原型链是 JavaScript 中实现面向对象编程的重要机制之一,它为代码的组织、复用、扩展和多态性提供了强大的支持,使得 JavaScript 能够以简洁而灵活的方式构建复杂的应用程序。深入理解和熟练运用原型链,对于提升 JavaScript 编程能力和开发高质量的应用具有重要意义。
|
5天前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
5天前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
18 1
|
15天前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
25 1
|
2月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
23天前
|
JavaScript 前端开发 开发者
深入理解JavaScript原型链:从基础到进阶
【10月更文挑战第13天】深入理解JavaScript原型链:从基础到进阶
19 0