当前位置: 首页 > news >正文

JS专题之继承

前言

众所周知,JavaScript 中,没有 JAVA 等主流语言“类”的概念,更没有“父子类继承”的概念,而是通过原型对象和原型链的方式实现继承。

于是,我们这一篇讲一讲 JS 中的继承(委托)。

一、为什么要有继承?

JavaScript 是面向对象编程的语言,里面全是对象,而如果不通过继承的机制将对象联系起来,势必会造成程序代码的冗余,不方便书写。

二、为什么又是原型链继承?

好,既然是 OO 语言,那么就加继承属性吧。但是 JS 创造者并不打算引用 class,不然 JS 就是一个完整的 OOP 语言了,而创造者 JS 更容易让新手开发。

后来,JS 创造者就将 new 关键字创建对象后面不接 class,改成构造函数,又考虑到继承,于是在构造函数上加一个原型对象,最后让所有通过 new 构造函数 创建出来的对象,就继承构造函函数的原型对象的属性。

function Person() {
    // 构造函数
    this.name = "jay";
}

Person.prototype = {
    sex: "male"
}

var person1 = new Person();
console.log(person1.name);  // jay
console.log(person1.sex);  // male

所以,就有了 JavaScript 畸形的继承方式:原型链继承~

三、原型链继承

function Parent() {
    this.names = ["aa", "bb", "cc"];
    this.age = 18;
}

function Child() {
    // ...
}

Child.prototype = new Parent();  // 改变构造函数的原型对象

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
console.log(child1.age);   // 18
child1.names.push("dd");
child1.age = 20;
var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age);  // 18

以上例子中,暴露出原型链继承的两个问题:

  1. 包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。
  2. 在创建子类型实例时,无法向父类型的构造函数中传递参数。

四、call 或 apply 继承

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}
function Child() {
    Parent.call(this, 18);
}

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age);  // 18

call 或 apply 的原理是在子类型的构造函数中,“借调”父类型的构造函数,最终实现子类型中拥有父类型中属性的副本了。

call 或 apply 这种继承方式在《JavaScript 高级程序设计》中叫作“借用构造函数(constructor stealing)”,解决了原型链继承中,引用数据类型被所有子实例共享的问题,也能够实现传递参数到构造函数中,但唯一的问题在于业务代码也写在了构造函数中,函数得不到复用。

五、组合继承

组合继承(combination inheritance)也叫作伪经典继承,指的是,前面两种方法:原型链继承和 call 或 apply 继承 组合起来,保证了实例都有自己的属性,同时也能够实现函数复用:

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}

Parent.prototype.sayName = function () {
    console.log(this.names);
}

function Child() {
    Parent.call(this, 18);  // 第一次调用
}

Child.prototype = new Parent();  // 第二次调用:通过原型链继承 sayName 方法
Child.prototype.constructor = Child;  // 改变 constructor 为子类型构造函数

var child1 = new Child();
child1.sayName();   // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age); 
child2.sayName();  // ["aa", "bb", "cc"]

组合继承将继承分为两步,一次是创建子类型关联父类型原型对象的时候,另一次是在子类型构造函数的内部。是 JS 最常用的继承方式。

六、原型式继承

原型式继承说白了,就是将父类型作为一个对象,直接变成子类型的原型对象。

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
 
var parent = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 

var child1 = object(parent);

// 继承了 names 属性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = object(parent);
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18

原型式继承其实就是对原型链继承的一种封装,它要求你有一个已有的对象作为基础,但是原型式继承也有共享父类引用属性,无法传递参数的缺点。

这个方法后来有了正式的 API: Object.create({...})

所以当有一个对象,想让子实例继承的时候,可以直接用 Object.create() 方法。

七、寄生式继承

寄生式继承是把原型式 + 工厂模式结合起来,目的是为了封装创建的过程。

function createAnother(original){ 
    var clone= object(original);    //通过调用函数创建一个新对象
    clone.sayHi = function(){      //以某种方式来增强这个对象
        console.log("hi");
    };
    return clone;                  //返回这个对象
}
 
var person = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

八、 寄生组合式继承

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

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); // 创建了父类原型的浅复制
    prototype.constructor = subType;             // 修正原型的构造函数
    subType.prototype = prototype;               // 将子类的原型替换为这个原型
}
 
function SuperType(age){
    this.age = age;
    this.names = ["aa", "bb", "cc"];
}
 
SuperType.prototype.sayName = function(){
    console.log(this.names);
};
 
function SubType(age){
    SuperType.call(this, age);
    this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var child1 = new SubType(22)
child1.sayAge()  // 22
child1.sayName()  // ["aa", "bb", "cc"]

九、ES6 class extends

class Parent {
    constructor(name) {
    this.name = name;
    }
    doSomething() {
            console.log('parent do something!');
    }
    sayName() {
        console.log('parent name:', this.name);
    }
}

class Child extends Parent {
    constructor(name, parentName) {
    super(parentName);
    this.name = name;
    }
    sayName() {
         console.log('child name:', this.name);
    }
}

const child = new Child('son', 'father');
child.sayName();            // child name: son
child.doSomething();        // parent do something!

const parent = new Parent('father');
parent.sayName();   // parent name: father

ES6 的 class extends 本质上是 ES5 的语法糖。
ES6实现继承的具体原理:

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

总结

javascript 由于历史发展原因,继承方式实际上是通过原型链属性查找的方式,但正规的叫法不叫继承而叫“委托”,ES6 的 class extends 关键字也不过是 ES5 的语法糖。所以,了解 JS 的原型和原型链非常重要,详情请翻看我之前的文章《JavaScript原型与原型链》

参考:
《JavaScript 高级程序设计》

2019/02/10 @Starbucks

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

相关文章:

  • 阿里云服务器怎么升级配置?升级有哪些限制?
  • UniDAC使用教程(五):数据加密
  • React-生命周期杂记
  • 关于VirtualDom的知识点
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • [CF703D]Mishka and Interesting sum/[BZOJ5476]位运算
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 生成、打包、部署和管理应用程序及类型(3):将模块合并成程序集
  • windows下使用nginx调试简介
  • Ajax 知识
  • 什么软件可以提取视频中的音频制作成手机铃声
  • TypeScript(ES6) 的一些使用的小技巧
  • git远程分支回退
  • 开源SQL-on-Hadoop系统一览
  • Terraform入门 - 3. 变更基础设施
  • 【译】JS基础算法脚本:字符串结尾
  • Java 最常见的 200+ 面试题:面试必备
  • k8s 面向应用开发者的基础命令
  • linux学习笔记
  • nodejs调试方法
  • Odoo domain写法及运用
  • Redis 中的布隆过滤器
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • TCP拥塞控制
  • Tornado学习笔记(1)
  • 阿里云应用高可用服务公测发布
  • 安卓应用性能调试和优化经验分享
  • 小程序01:wepy框架整合iview webapp UI
  • 协程
  • 学习ES6 变量的解构赋值
  • 一些基于React、Vue、Node.js、MongoDB技术栈的实践项目
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 原生Ajax
  • 云大使推广中的常见热门问题
  • 智能合约Solidity教程-事件和日志(一)
  • 06-01 点餐小程序前台界面搭建
  • AI算硅基生命吗,为什么?
  • Java性能优化之JVM GC(垃圾回收机制)
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • #14vue3生成表单并跳转到外部地址的方式
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • #每日一题合集#牛客JZ23-JZ33
  • (C语言)共用体union的用法举例
  • (每日持续更新)jdk api之FileFilter基础、应用、实战
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理 第13章 项目资源管理(七)
  • (转)linux下的时间函数使用
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .NET 材料检测系统崩溃分析
  • .netcore 获取appsettings
  • .Net调用Java编写的WebServices返回值为Null的解决方法(SoapUI工具测试有返回值)
  • .NET关于 跳过SSL中遇到的问题
  • /etc/sudoer文件配置简析