JavaScript原型链和继承

Posted Tyler‘s Blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript原型链和继承相关的知识,希望对你有一定的参考价值。

1.概念

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

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

2.基于原型链的继承

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

  在ECMA标准中,someObject.[[Prototype]]符号是表示指向someObject的原型。从ES6开始,[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototype()访问器来访问。除此之外__proto__这个是JavaScript的非标准api,但是很多浏览器都实现了__proto__,二者作用等同。注意浏览器没有实现对象的object.Prototype这样的属性,即没有实现对象实例的Prototype属性,只有构造函数.prototype属性。

  但是[[Prototype]]和构造函数func的prototype属性不同,不要弄混。构造函数创建的实例对象的[[prototype]]指向function的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__,指向构造函数的原型对象Foo.prototype,这样就可以访问原型对象的所有方法。

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

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

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

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. 定义子类对象instance1instance2,它们继承了父类的属性,方法以及父类原型对象上的属性,方法
5. 输出子类instance1的属性books,在子类对象的内部没有,在在父类上有这个属性输出["javascript", "html"]
6. 在子类对象instance2上找books属性,它来自继承的父类内部,并且是一个引用属性,修改这个属性,添加一个元素“java”
7. 输出子类对象instance1book属性,她来自继承的父类内部,已经被修改,输出["javascript", "html", "java"]
8. 输出子类对象instance2book属性,她来自继承的父类内部,已经被修改,输出["javascript", "html", "java"]
9. 在父类函数SuperClass上访问它内部的属性books,找不到这个属性,输出undefined
10. 定义父类对象sup1,和sup2,他们调用父类函数,初始化共有属性books,通过new命令创建,是两个完全不同的对象
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. 申明两个子类实例instance1instance2,并分别传参给父类10,11
5. 修改子类实例instance1books属性,这是一个引用属性,给数组添加一个元素“java”
6. 输出子类实例instance1books属性,“java”已经被添加上去了
7. 输出子类实例instance1的id属性是10
8. 输出子类实例instance2books属性,这里是没有“java”元素的,因为它是在调用call方法的时候直接复制的一份,和instance1的是两个完全不同的数组对象
9. 输出子类实例instance2books属性是11
10. 调用子类实例instance1showBooks()方法,在子类中找不到,子类的原型对象中找不到,在子类继承的父类中找不到(这里不会在子类继承的父类的原型对象中找这个方法),因此报错
11. 调用子类实例instance2showBooks()方法,在子类中找不到,子类的原型对象中找不到,在子类继承的父类中找不到(这里不会在子类继承的父类的原型对象中找这个方法)因此报错
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. 访问子类对象实例instance2books属性,在子类方法中找不到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. 访问子类对象newBookname属性,赋值为“ajax book”,子类对象的原型对象中有这个属性,并且是一个值类属性
5. 访问子类对象newBookalikeBook属性,添加元素“xml book”,子类对象的原型对象中有这个属性,并且是一个引用属性
6. 定义子类对象otherBook,调用原型式继承方法,继承book对象中的属性
7. 访问子类对象otherBookname属性,赋值为“ajax book”,子类对象的原型对象中有这个属性,并且是一个引用类型变量
8. 访问子类对象otherBookalikeBook属性,添加元素“as book”,子类对象的原型对象中有这个属性,并且是一个引用类型变量
9. 输出newBookname属性,值是“ajax book”
10. 输出newBookbooks属性,它是从父类原型对象上继承来的,是同一个变量,这个数组内被添加了 “as book”,输出[\'css book\', \'html book\', \'xml book\', \'as book\']
11. 输出othername属性,值是“flash book”
12. 输出oterBookbooks属性,它是从父类原型对象上继承来的,是同一个变量,这个数组内被添加了 “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() {}
        JavaScript高级原型和继承相关:原型对象函数原型原型链和继承继承的优化对象判断相关方法

JavaScript高级原型和继承相关:原型对象函数原型原型链和继承继承的优化对象判断相关方法

对Javascript的原型,原型链和继承的个人理解

JavaScript原型链和继承

JavaScript中的原型链和继承

JavaScript之继承(原型链)