资讯专栏INFORMATION COLUMN

JS专题之继承

rollback / 872人阅读

摘要:构造函数所以,就有了畸形的继承方式原型链继承三原型链继承改变构造函数的原型对象继承了属性以上例子中,暴露出原型链继承的两个问题包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。

前言

众所周知,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

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

包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。

在创建子类型实例时,无法向父类型的构造函数中传递参数。

四、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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101586.html

相关文章

  • [面试专题]js面向对象(OOP)

    摘要:之面向对象对象类型数据类型分六类简单类型五种复杂类型其中也属于基本类型。 js之面向对象(OOP) js对象类型(Object) js数据类型分六类,简单类型:Undefined,Null,Bollean,Number,String五种,复杂类型:Object.其中Undefined、Null、Boolean、Number也属于基本类型。Object、Array和Function则属...

    andycall 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    lemanli 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    xfee 评论0 收藏0
  • [面试专题]一线互联网大厂面试总结

    摘要:道阻且长啊前端面试总结前端面试笔试面试腾讯一面浏览器工作原理浏览器的主要组件包括用户界面包括地址栏后退前进按钮书签目录浏览器引擎用来查询及操作渲染引擎的接口渲染引擎渲染界面和是基于两种渲染引擎构建的,使用自主研发的渲染引擎,和都使用网络用来 道阻且长啊TAT(前端面试总结) 前端 面试 笔试 面试 腾讯一面 1.浏览器工作原理 浏览器的主要组件包括: 用户界面- 包括地址栏、后退/前...

    leap_frog 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<