07-继承

继承

Javascript中继承都基于两种方式:
1.通过原型链继承,通过修改子类原型的指向,使得子类实例通过原型链查找,实现继承父类的属性和方法。这种方式可以让所有实例共享原型链上的方法和属性。但当某个实例修改引用类型的属性时,其他实例也会受影响(引用地址相同)。
2.借用构造函数,通过apply、call方法在子类中调用父类构造函数,子类实例继承了父类构造函数的实例属性和方法。这个方式继承的属性和方法相互独立,但实例方法重复创建,没有共享。

  • 原型链继承
    原理:将父类的实例对象作为子类的原型对象。
    原型链继承的实质是重写子类的原型对象,将父类的实例作为子类的新原型,于是子类新原型就继承了父类所有的属性和方法。
    function Parent() {}; // 父类
    function Child() {}; // 子类
    var c1 = new Child(); // 继承前子类的实例
    Child.prototype = new Parent();//将子类的原型重写为父类的实例
    var p1 = new Child(); // 继承后子类的实例
    // 没有修改Child.constructor时
    console.log(c1.__proto__.constructor.prototype === p1.__proto__); // true
    console.log(p1.__proto__.__proto__.constructor === Parent); // true
原型链继承.png

将父类的实例对象作为子类的原型对象,也就是将原来存在父类实例中的属性和方法,继承给了子类的原型对象。
当读取某个子类实例对象的属性时:1.首先在子类实例中查找;2.如果没找到,就到子类的原型(也就是父类的实例)中查找;3.如果没找到,就在父类的原型中查找;4.如果没找到,就到Object.prototype中查找。

    function Parent () {
        // 父类的实例属性
        this.name = 'Parent'; //基本类型的实例属性,实例之间相互独立
        this.age = [30, 35];// 引用类型的实例属性,所有实例共享
    }
    // 父类的原型方法
    Parent.prototype.getName = function () {
        console.log(this.name);
    }
    function Child () {}
    // 将父类的实例对象作为子类的原型对象
    Child.prototype = new Parent();
    // 子类原型的继承了父类实例的所有属性和方法
    var p1 = new Child();
    var p2 = new Child();
    p1.getName(); // Parent, 子类继承了父类的原型方法
    console.log(p1.age); // [30, 35],子类继承了父类的实例属性
    // 修改子类实例的属性
    p1.name = 'Tom'; // 修改基本类型的属性
    p1.age.push(40); // 修改引用类型的属性
    // 基本类型属性值相互独立
    p1.getName(); // Tom
    p2.getName(); // Parent
    // 引用类型的属性被所有实例共享,修改某一实例的值会影响其他实例
    console.log(p1.age); // [30, 35, 40]
    console.log(p2.age); // [30, 35, 40]

子类覆盖父类中某个方法或添加父类中不存在的某个方法时,需要写在替换原型语句的后面。
并且不能使用字面量(相当于Object的实例)的方法创建子类原型方法,否则会覆盖之前的原型。
原型链继承的缺点:
1.使用原型链继承时,父类的实例成为子类的原型,父类的实例属性就变成了原型属性。而引用类型值的原型属性会被所有实例共享。
2.创建子类实例时,不能向父类构造函数传递参数。

  • 借用构造函数继承
    原理:在子类构造函数中,调用父类构造函数。(没有用到原型)
    使用call()或apply()方法可以在创建子类实例时,调用执行父类构造函数,这样子类实例就会执行父类构造函数定义的初始化代码。
    创建子类实例时,可以向父类传递参数。并且子类实例不会共享父类引用类型属性,因为继承的都是父类的实例属性和方法,不在原型链上。
    function Parent(name) {
        this.name = name;
        this.age = [30, 35];
    }
    function Child(name) {
        // 调用父类的构造函数
        // Parent.call(this, name);
        Parent.apply(this, arguments);
    }
    var p1 = new Child('Leo');
    var p2 = new Child('Tom');
    console.log(p1.name); // Leo
    console.log(p1.age); // [30, 35]
    p1.age.push(40);
    console.log(p1.age); // [30, 35, 40]
    console.log(p2.age); // [30, 35]不会共享父类引用类型属性

借用构造函数继承的缺点:
1.只能继承父类的实例属性和方法,不能继承原型属性和方法(没有使用原型)。
2.方法都在定义在构造函数中,每次创建实例都会创建一遍方法,无法实现函数复用,影响性能。

  • 组合继承
    原理:通过原型链实现原型属性和方法的继承,通过借用构造函数来实现实例属性和方法的继承。
    一般在构造函数上定义实例属性,在原型上定义方法,通过组合继承既满足方法的复用,又保证实例有自己的属性。
    function Parent(name) {
        this.name = name;
        this.colors = ['blue', 'red'];
    }
    Parent.prototype.getName = function () {
        console.log(this.name);
    }
    function Child(name, age) {
        // 调用父类的构造函数
        Parent.call(this, name); // 第二调用Parent()
        this.age = age;
    }
    // 原型链继承
    Child.prototype = new Parent();// 第一调用Parent()
    Child.prototype.constructor = Child;
    var p1 = new Child('Leo', 26);
    var p2 = new Child('Tom', 20)
    p1.colors.push('green');
    p1.getName(); // Leo
    console.log(p1.age); // 26
    console.log(p1.colors); // ["blue", "red", "green"]
    console.log(p2.colors); // ["blue", "red"]

组合继承的缺点:
调用两次父类构造函数。第一次是在重写子类原型的时,将子类的原型指向父类实例,此时子类原型会继承父类所有的属性和方法(包括实例属性和方法及原型属性和方法)。第二次是在子类调用构造函数创建实例时,在子类构造函数中调用父类构造函数,此时新创建的子类实例对象的实例属性和方法(直接继承来自父类构造函数的实例属性和方法)会覆盖子类原型中的同名实例属性和方法。

  • 原型式继承
    原理:使用Object.create()方法,将传入的对象作为空构造函数的原型,并返回这个空构造函数的实例。
    这种方法不需要创建构造函数,只需传入一个对象,就能从传入的对象衍生出新的对象。本质上还是通过原型链,实例的原型指向传入的对象。
    Object.create()方法接受一个对象为参数,必须传一个对象否则报错,返回一个新的对象,传入的对象作为新对象的原型。
    该方法还可接受第二个参数,该参数是属性描述对象,它所描述的对象属性,会添加到新对象。
    Object.create()方法生成的对象,继承了它原型对象的构造函数。
    // 模拟Object.create()
    function create(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
    }

使用Object.create()实现继承。

    function Parent(name) {
        this.name = name;
        this.colors = ['blue', 'red'];
        this.getName = function () {
            console.log(this.name);
        }
    }
    Parent.prototype.getColor = function () {
        console.log(this.colors);
    }
    var p1 = new Parent('Leo');
// 使用Object.create()方法,需传入一个对象
    var c1 = Object.create(p1);
    var c2 = Object.create(p1);
    c1.colors.push('green');
    c1.getName();// Leo
    c1.getColor(); // ["blue", "red", "green"]
    c2.getColor(); // ["blue", "red", "green"]

原型式继承缺点:
传入的对象(原型)的引用类型属性,会被所有创建出来的实例共享。

  • 寄生式继承
    原理:创建一个仅用于封装继承过程的函数,在该函数内部创建一个新对象,再增强对象,最后返回这个新对象。
    寄生继承的思路跟工厂模式差不多,就是调用一个仅用于封装继承过程的函数。
    寄生继承不用实例化父类,直接实例化一个临时副本实现了原型链继承。
    在该函数内部创建新对象的方法很多:可以使用Object.create(),new或者使用字面量创建。
    function createObj(obj) {
        // 创建新对象()
        var clone = Object.create(obj);
        // 增强这个对象
        clone.sayHi = function() {
            console.log('Hi,'+name);
        }
        // 返回这个对象
        return clone;
    }

寄生式继承缺点:
无法实现函数复用(封装在一个函数内,没有用到原型,和构造函数模式一样)。

  • 寄生组合式继承
    原理:组合继承的改进版,还是使用原型链来继承原型属性和方法,借用构造函数来继承实例属性和方法。但是,不将子类原型指向父类实例,而是创建一个空构造函数,将父类的原型指向这个空构造函数的原型,将子类的原型指向这个空构造函数的实例。从而实现子类原型间接继承父类原型。
   // 1.模拟Object.create()
    function object(o) {
       function F() {}; // 创建空构造函数
       F.prototype = o; // 将空构造函数的原型指向传入的对象
       return new F();  // 返回构造函数的实例对象,该实例对象继承了传入对象的属性和方法
    }
    // 封装继承函数
   function extend(Child, Parent) {
       // 传入父类的原型,返回的实例obj继承了父类的原型属性和方法
       var obj = object(Parent.prototype);
       // 子类的原型指向obj,子类的原型间接继承了父类的原型属性和方法
       Child.prototype = obj;
       // 修正子类新原型(即obj对象)的constructor,使其指向子类构造函数
       obj.constructor = Child;
   }

   // 2.封装成一个函数
   function extend(Child, Parent) {
       var F = function () {};
       // 空构造函数的原型指向父类的原型,相当于复制了一份父类原型
       F.prototype = Parent.prototype;
       // 子类的原型指向空构造函数的实例,相当于子类原型间接继承了父类原型
       Child.prototype = new F();
       // 修正子类原型constructor指向
       Child.prototype.constructor = Child;
   }

   // 3.直接使用Object.create()方法
   function extend(Child, Parent) {
       var obj = Object.create(Parent.prototype);
       Child.prototype = obj;
       obj.constructor = Child;
   }
寄生组合继承.png
    function Parent (name) {
        this.name = name;
        this.colors = ['blue', 'red'];
    }
    Parent.prototype.getName = function () {
        console.log(this.name);
    }
    function Child (name, age) {
      // 通过借用构造函数继承父类的实例属性和方法
      Parent.call(this, name);
      this.school = age; // 添加新的子类属性
    }
    // 声明继承函数
    function extend(Child, Parent) {
        var F = function () {};
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
    }
    // 寄生式继承父类原型属性和方法
    extend(Child, Parent);
    var p1 = new Child('Leo', 26);
    var p2 = new Child('Tom', 20);
    p1.colors.push('green');
    p1.getName(); // Leo
    console.log(p1.colors); //["blue", "red", "green"]
    console.log(p2.colors); //["blue", "red"]

参考资料:
《JavaScript高级程序设计》
《JavaScript 标准参考教程》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 博客内容:什么是面向对象为什么要面向对象面向对象编程的特性和原则理解对象属性创建对象继承 什么是面向对象 面向对象...
    _Dot912阅读 1,391评论 3 12
  • class的基本用法 概述 JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子: ...
    呼呼哥阅读 4,052评论 3 11
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而EC...
    lovelydong阅读 367评论 0 2
  • 1.浏览器发出请求:http://localhost:8080/ContextTest/stest 2.解析请求协...
    叶桐学阅读 353评论 0 0