一篇就够-JS继承的多种方式和实现

原型链继承

方法:子构造函数的prototype指向为父构造函数的实例,因为原型链是proto的链表,父构造函数的实例的proto指向父构造函数实例的原型。

function Parent(){
    this.name = 'johe'
}

Parent.prototype.getName = function(){
    console.log(this.name)
}

function Child(){

}

//原型必须是对象,所以为Parent的实例
Child.prototype = new Parent()

var child1 = new Child()

child1.getName()

问题:

  1. 引用类型的属性被所有实例共享
  2. 创建Child的实例时,不能向parent传参
function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]

这是因为Parent在实例化之后,成为了Child的原型,原型上的属性和方法是共享的。

借用构造函数(经典继承)

调用父构造函数

function Parent(){
    this.names = ['kevin','daisy'];
}

function Child(){
    Parent.call(this);
}

var Child1 = new Child()

Child1.names.push('johe')

var Child2 = new Child()

//['kevin','daisy']
Child2.names

优点:

  1. 避免了引用类型的属性被所有实例共享
  2. 可以在Child中向Parent传参
    缺点:
  3. 方法都在构造函数中定义,每次创建实例都会创建一遍方法
  4. instanceof检验为false
function Parent(name){
    this.name = name;
}

function Child(name){
    Parent.call(this,name)
}

var Child1 = new Child('johe')

//johe 
Child1.name

组合继承(原型链继承和经典继承)

优点:借用构造函数继承解决了传参问题和实例属性被共享的问题,原型链继承能够满足共享方法不被重复创建。
缺点:调用了两次父构造函数

function Parent(name){
    this.name = name
    this.names = ['johe']
}

Parent.prototype.getName = function(){
    return this.name
}

function Child(name,age){
    //调用父级的构造方法,实现实例属性
    Parent.call(this,name)
    this.age = age
}

//这里不用参数是因为子构造函数调用父构造函数时已实现实例属性,有实例属性的情况下不会从原型链中查找
Child.prototype = new Parent()

var child1 = new Child('johe',18)
child1.names.push("johe2")
console.log(child1.name);//johe
console.log(child1.age);18
//["johe","johe2"]
console.log(child1.names);

var child2 = new Child('child2',19)
console.log(child2.name);//child2
console.log(child2.age);19
//["johe"]
console.log(child1.names);

原型式继承(Object.create)

Object.create的模拟实现

function createObj(o){
    function F();
    F.protoype = o;
    return new F()
}

缺点:
包含引用类型的属性值始终都会共享响应的值,这点跟原型链继承一样。

寄生组合式继承(组合继承优化)

组合继承的最大缺点就是会调用两次父构造函数
一次是设置子类型实例的原型:

Child.protoype = new Parent()

一次是创建子类型实例的时候:

function Child(name,age){
    Parent.call(this,name)
}

var child1 = new Child('johe',18)

这个时候Child.prototype这个对象内的属性其实是没用的,因为子类型实例已经调用了父构造函数进行了属性实例化。

所以就用到了寄生组合式继承,让Child.prototype间接的访问到Parent.prototype

function Parent(name){
    this.name = name
    this.names = ['johe']
}
Parent.prototype.getName = function(){return this.name}

function Child(name,age){
    Parent.call(this,name)
    this.age = age
}

function F(){}

F.prototype = Parent.prototype

Child.prototype = new F()

var child1 = new Child('johe',18)

封装继承方法:

function Parent(name){
    this.name = name
    this.names = ['johe']
}
Parent.prototype.getName = function(){return this.name}

function Child(name,age){
    Parent.call(this,name)
    this.age = age
}

function createObject(o){
    function F(){}
    F.prototype = o
    return new F();
}

function setPrototype(child,parent){
    var prototype = createObject(parent)
    prototype.constructor = Child
    Child.protoype = prototype
}

setPrototype(Child,Parent)

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式

这里为什么不直接使用Child.prototype=Parent.prototype,是因为Parent.constructor应该指向Parent,并且如果我们需要给Child实例的原型设置方法和属性时,会影响到Parent的实例,这明显是不合理的。

ES6的继承

ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。

class Fruits{
    constructor(type,size){
        this.type = type;
        this.size = size;
    }
    static sayHello(){
        console.log("hello");
    }
    sayType(){
        console.log('my type is '+this.type);
        return this.type;
    }
}

class Banana extends Fruits{
    constructor(type,size,color){
        super(type,size);
        this.color = color;
    }
    sayColor(){
        console.log('my color is '+ this.color);
        return this.color;
    }
}
let fruit = new Fruits("fruit","small");
let banana = new Banana("banana","small","yellow");
 //parent: Fruits {type:'fruit',size:'small'}
console.log('parent:',fruit);
//child: Banana {type:'banana',size:'small',color:'yellow' }
console.log('child:',banana);

//hello
console.log(Fruits.sayHello());
console.log(Banana.sayHello());

//true
console.log(Fruits.hasOwnProperty("sayHello"));
//false
console.log(Banana.hasOwnProperty("sayHello"));

//false
console.log(fruit.hasOwnProperty("sayType"))
//true
console.log(Fruits.prototype.hasOwnProperty("sayType"));
//my type is banana
console.log(banana.sayType());
//false
console.log(banana.hasOwnProperty("sayType"));
//Fruits {}
console.log(banana.__proto__);

首先查看class语法糖做了什么:

  • 将构造函数内的this属性实例化
  • 将构造函数外的非static函数给设置到构造函数的原型对象中,共享属性
  • static函数设置为构造函数的属性

用ES5实现就是组合方式来创建对象:

function Fruits(type,size){
    this.type = type;
    this.size = size;
}
Fruits.sayHello = function(){
    console.log('Hello');
}
Fruits.prototype = {
    constructor:Fruits,
    sayType:function(){
        console.log('my type is' +this.type);
        return this.type;
    }
}

再看看extends做了什么:

  • 继承父类的实例属性(调用父类的构造函数)
  • 继承父类的共享属性(原型链上有父类的原型对象)
  • 子类构造函数的proto指向父类构造函数(两个都是对象)(继承静态函数)
//true
console.log(Banana.__proto === Fruits);
//true
console.log(banana instance of Fruits);

es6继承的es5实现

知道extends做了什么之后,我们可以知道其转化成es5就是寄生组合式继承(组合=原型链继承+借用构造函数),并且设置子类构造函数的proto为父类构造函数.


function Fruits(type,size){
    this.type = type;
    this.size = size;
}
Fruits.sayHello = function(){
    console.log('Hello');
}
Fruits.prototype = {
    constructor:Fruits,
    sayType:function(){
        console.log('my type is' +this.type);
        return this.type;
    }
}

function Banana(type,size,color){
    Fruits.call(this,type,size);
    this.color = color;
}

function createObject(Parent){
    function Empty(){};
    Empty.prototype = Parent.prototype;
    return new Empty();
}

Banana.prototype = createObject(Fruits);
Banana.prototype.constructor = Banana;
Banana.prototype.sayColor = function(){
    console.log(this.color);
}

Banana.__proto__ = Fruits;

通过babeljs转码成ES5来查看,更严谨的实现。

//es6版本
class Parent{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log('hello');
    }
    sayName(){
        console.log('my name is ' + this.name);
        return this.name;
    }
}
class Child extends Parent{
    constructor(name, age){
        super(name);
        this.age = age;
    }
    sayAge(){
        console.log('my age is ' + this.age);
        return this.age;
    }
}
// 对转换后的代码进行了简要的注释
"use strict";
// 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
function _typeof(obj) {
    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }
    return _typeof(obj);
}
// _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}
// 如何 self 是void 0 (undefined) 则报错
function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}
// 获取__proto__
function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
}
// 寄生组合式继承的核心
function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function");
    }
    // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 
    // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置__proto__
function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
// instanceof操作符包含对Symbol的处理
function _instanceof(left, right) {
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
        return right[Symbol.hasInstance](left);
    } else {
        return left instanceof right;
    }
}

function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 设置共享属性和静态属性到不同的对象上
function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}
//将共享属性设置到原型对象上,静态属性设置到构造函数上
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

// ES6
var Parent = function () {
    function Parent(name) {
        _classCallCheck(this, Parent);
        this.name = name;
    }
    _createClass(Parent, [{
        key: "sayName",
        value: function sayName() {
            console.log('my name is ' + this.name);
            return this.name;
        }
    }], [{
        key: "sayHello",
        value: function sayHello() {
            console.log('hello');
        }
    }]);
    return Parent;
}();

var Child = function (_Parent) {
    _inherits(Child, _Parent);
    function Child(name, age) {
        var _this;
        _classCallCheck(this, Child);
        // Child.__proto__ => Parent
        // 所以也就是相当于Parent.call(this, name); 是super(name)的一种转换
        // _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
        _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
        _this.age = age;
        return _this;
    }
    _createClass(Child, [{
        key: "sayAge",
        value: function sayAge() {
            console.log('my age is ' + this.age);
            return this.age;
        }
    }]);
    return Child;
}(Parent);

var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18


从babel转译我们可以认识到几点:

  • 尽量使用Object.create来创造父构造函数的实例
//创建一个superClass的实例,constructor属性指向subClass
subClass.prototype = Object.create(superClass&&superClass.prototype,{
    constructor:{
        value:subClass,
        writable:true,
        configurable:trues
    }
});
//替代方案
function Empty(){};
Empty.prototype = superClass.prototype;
subClass.protoype = new Empty();
subClass.protoype.constructor = subClass;
  • 尽量使用Object.setPrototypeOf而不是proto
Object.setPrototypeOf(subClass,superClass);
//替代方案,不推荐
subClass.__proto__ = superClass.__proto__

模拟一下转换实现:

class Fruit{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log("hello");
    }
    sayName(){
        console.log(this.name);
    }
}
class Banana extends Fruit{
    constructor(name,color){
        super(name);
        this.color = color;
    }
    sayColor(){
        console.log(this.color);
    }
}

//转换实现:
function createPropertiesByObject(target,props){
    for(var i =0;i<props.length;i++){
        let descriptor = props[i];
        //省略
        Object.defineProperties(target,descriptor.key,descriptor);
    }
}
function createProperties(target,protoProps,staticProps){
    createPropertiesByObject(target.prototype,protoProps);
    createPropertiesByObject(target,staticProps);
}

function createPolyFill(Parent){
    function Empty(){};
    Empty.protoype = Parent.prototype;
    return new Empty();
}

function inherit(Child,Parent){
    if(typeof Child!=='function'||typeof Parent!=='function'){
        throw new Error("");
    }
    Child.prototype = Object.create ? Object.create(Parent&&Parent.prototype,{
        constructor:{
            value:Child,
            writable:true,
            configurable:true
        }
    }) : createPolyFill(Parent);
    if(!Object.create){
        Child.prototype.constructor = Child;
    }
}

function setPrototypeOf(Child,Parent){
    setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf : function(C,P){
        C.__proto__ = P
    }
    setPrototypeOf(Child,Parent); 
}

var Fruit = function(){
    function Fruit(){
        this.name = name;
    }
    createProperties(Fruit,[{
        key:"sayName",
        value:function(){
            console.log(this.name)
        }
    }],[{
        key:"sayHello",
        value:function(){
            console.log("hello");
        }
    }])
    return Fruit;
}()

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

推荐阅读更多精彩内容

  • 继承6种套餐 参照红皮书,JS继承一共6种 1.原型链继承 核心思想:子类的原型指向父类的一个实例 Son.pro...
    灯不梨喵阅读 3,113评论 1 2
  • 继承 Javascript中继承都基于两种方式:1.通过原型链继承,通过修改子类原型的指向,使得子类实例通过原型链...
    LeoCong阅读 291评论 0 0
  • 原文链接:zhuanlan.zhihu.com (一) 原型链继承: function Parent(name) ...
    越努力越幸运_952c阅读 293评论 0 2
  • 知道你在,一直都在,就在这片熟悉的土地上。就如同水底静卧无数的石头,却不知要挪动哪一块,才能找到你藏身的地方? 城...
    布谷鸟_ee85阅读 317评论 2 8
  • 新的言雨的言语 我不想选择 我愿意判断 执行
    言雨的言语阅读 111评论 0 0