JavaScript 的继承方式及优缺点

参考https://www.pandashen.com/2017/07/10/20170710162724/

前言

JavaScript 原本不是纯粹的 “OOP” 语言,因为在 ES5 规范中没有类的概念,在 ES6 中才正式加入了 class 的编程方式,在 ES6 之前,也都是使用面向对象的编程方式,当然是 JavaScript 独有的面向对象编程,而且这种编程方式是建立在 JavaScript 独特的原型链的基础之上的,我们本篇就将对原型链以及面向对象编程最常用到的继承进行刨析。

继承简介

在 JavaScript 的中的面向对象编程,继承是给构造函数之间建立关系非常重要的方式,根据 JavaScript 原型链的特点,其实继承就是更改原本默认的原型链,形成新的原型链的过程。

复制的方式进行继承

复制的方式进行继承指定是对象与对象间的浅复制和深复制,这种方式到底算不算继承的一种备受争议,我们也把它放在我们的内容中,当作一个 “不正经” 的继承。

1、浅复制

创建一个浅复制的函数,第一个参数为复制的源对象,第二个参数为目标对象。

// 浅复制方法
function extend(p, c = {}) {
    for (let k in p) {
        c[k] = p[k];
    }
    return c;
}

// 源对象
let parent = {
    a: 1,
    b: function() {
        console.log(1);
    }
};

// 目标对象
let child = {
    c: 2
};

// 执行
extend(parent, child);
console.log(child); // { c: 2, a: 1, b: ƒ }

上面的 extend 方法在 ES6 标准中可以直接使用 Object.assign 方法所替代。

2、深复制

可以组合使用 JSON.stringify 和 JSON.parse 来实现,但是有局限性,不能处理函数和正则类型,所以我们自己实现一个方法,参数与浅复制相同。

// 深复制方法
function extendDeeply(p, c = {}) {
    for (let k in p) {
        if (typeof p[k] === "object" && typeof p[k] !== null) {
            c[k] = p[k] instanceof Array ? [] : {};
            extendDeeply(p[k], c[k]);
        } else {
            c[k] = p[k];
        }
    }
    return c;
}

// 源对象
let parent = {
    a: {
        b: 1
    },
    b: [1, 2, 3],
    c: 1,
    d: function() {
        console.log(1);
    }
};

// 执行
let child = extendDeeply(parent);

console.log(child); // { a: {b: 1}, b: [1, 2, 3], c: 1, d: ƒ }
console.log(child.a === parent.a); // false
console.log(child.b === parent.b); // false
console.log(child.d === parent.d); // true

在上面可以看出复制后的新对象 child 的 a 属性和 b 的引用是独立的,与 parent 的 a 和 b 毫无关系,实现了深复制,但是 extendDeeply 函数并没有对函数类型做处理,因为函数内部执行相同的逻辑指向不同引用是浪费内存的。

原型替换

原型替换是继承当中最简单也是最直接的方式,即直接让父类和子类共用同一个原型对象,一般有两种实现方式。

// 原型替换
// 父类
function Parent() {}

// 子类
function Child() {}

// 简单粗暴的写法
Child.prototype = Parent.prototype;

// 另一种种实现方式
Object.setPrototypeOf(Child.prototype, Parent.prototype);

上面这种方式 Child 的原型被替换掉,Child 的实例可以直接调用 Parent 原型上的方法,实现了对父类原型方法的继承。

上面第二种方式使用了 Object.setPrototypeOf 方法,该方法是将传入第一个参数对象的原型设置为第二个参数传入的对象,所以我们第一个参数传入的是 Child 的原型,将 Child 原型的原型设置成了 Parent 的原型,使父、子类原型链产生关联,Child 的实例继承了 Parent 原型上的方法,在 NodeJS 中的内置模块 util 中用来实现继承的方法 inherits,底层就是使用这种方式实现的。

缺点:父类的实例也同样可以调用子类的原型方法,我们希望继承是单向的,否则无法区分父、子类关系,这种方式一般是不可取的。

原型链继承

原型链继承的思路是子类的原型的原型是父类的原型,形成了一条原型链,建立子类与父类原型的关系。

// 原型链继承
// 父类
function Parent(name) {
    this.name = name;
    this.hobby = ["basketball", "football"];
}

// 子类
function Child() {}

// 继承
Child.prototype = new Parent();

上面用 Parent 的实例替换了 Child 自己的原型,由于父类的实例原型直接指向 Parent.prototype,所以也使父、子类原型链产生关联,子类实例继承了父类原型的方法。

缺点 1:只能继承父类原型上的方法,却无法继承父类上的属性。
缺点 2:由于原型对象被替换,原本原型的 constructor 属性丢失。
缺点 3:如果父类的构造函数中有属性,则创建的父类的实例也会有这个属性,用这个实例的作为子类的原型,这个属性就变成了所有子类实例所共有的,这个属性可能是多余的,并不是我们想要的,也可能我们希望它不是共有的,而是每个实例自己的。

构造函数继承

构造函数继承又被国内的开发者叫做 “经典继承”。

// 构造函数继承
// 父类
function Parent(name) {
    this.name = name;
}

// 子类
function Child() {
    Parent.apply(this, arguments);
}

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }

构造函数继承的原理就是在创建 Child 实例的时候执行了 Child 构造函数,并借用 call 或 apply 在内部执行了父类 Parent,并把父类的属性创建给了 this,即子类的实例,解决了原型链继承不能继承父类属性的缺点。

缺点:子类的实例只能继承父类的属性,却不能继承父类的原型的方法。

构造函数原型链组合继承

为了使子类既能继承父类原型的方法,又能继承父类的属性到自己的实例上,就有了这种组合使用的方式。

// 构造函数原型链组合继承
// 父类
function Parent(name) {
    this.name = name;
}

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

// 子类
function Child() {
    Parent.apply(this, arguments);
}

// 继承
Child.prototype = new Parent();

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }
c.sayName(); // Panda

这种继承看似完美,但是之前 constructor 丢失和子类原型上多余共有属性的问题还是没有解决,在这基础上又产生了新的问题。

缺点:父类被执行了两次,在使用 call 或 apply 继承属性时执行一次,在创建实例替换子类原型时又被执行了一次。

原型式继承

原型式继承主要用来解决用父类的实例替换子类的原型时共有属性的问题,以及父类构造函数执行两次的问题,也就是说通过原型式继承能保证子类的原型是 “干净的”,而保证只在继承父类的属性时执行一次父类。

// 原型式继承
// 父类
function Parent(name) {
    this.name = name;
}

// 子类
function Child() {
    Parent.apply(this, arguments);
}

// 继承函数
function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}

// 继承
Child.prototype = create(Parent.prototype);

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }

原型式继承其实是借助了一个中间的构造函数,将中间构造函数 F 的 prototype 替换成了父类的原型,并创建了一个 F 的实例返回,这个实例是不具备任何属性的(干净的),用这个实例替换子类的原型,因为这个实例的原型指向 F 的原型,F 的原型同时又是父类的原型对象,所以子类实例继承了父类原型的方法,父类只在创建子类实例的时候执行了一次,省去了创建父类实例的过程。

原型式继承在 ES5 标准中被封装成了一个专门的方法 Object.create,该方法的第一个参数与上面 create 函数的参数相同,即要作为原型的对象,第二个参数则可以传递一个对象,会把对象上的属性添加到这个原型上,一般第二个参数用来弥补 constructor 的丢失问题,这个方法不兼容 IE 低版本浏览器。

寄生式继承

寄生式继承就是用来解决子统一为原型式继承中返回的对象统一添加方法的问题,只是在原型式继承的基础上做了小小的修改。

// 寄生式继承
// 父类
function Parent(name) {
    this.name = name;
}

// 子类
function Child() {
    Parent.apply(this, arguments);
}

// 继承函数
function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}

// 将子类方法私有化函数
function creatFunction(obj) {
    // 调用继承函数
    let clone = create(obj);
    // 子类原型方法(多个)
    clone.sayName = function() {};
    clone.sayHello = function() {};

    return clone;
}

// 继承
Child.prototype = creatFunction(Parent.prototype);

缺点:因为寄生式继承最后返回的是一个对象,如果用一个变量直接来接收它,那相当于添加的所有方法都变成这个对象自身的了,如果创建了多个这样的对象,无法实现相同方法的复用。

寄生组合式继承

// 寄生组合式继承
// 父类
function P(name, age) {
    this.name = name;
    this.age = age;
}

P.prototype.headCount = 1;
P.prototype.eat = function() {
    console.log("eating...");
};

// 子类
function C(name, age) {
    P.apply(this, arguments);
}

// 寄生组合式继承方法
function myCreate(Child, Parent) {
    function F() {}
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    // 让 Child 子类的静态属性 super 和 base 指向父类的原型
    Child.super = Child.base = Parent.prototype;
}

// 调用方法实现继承
myCreate(C, P);

// 向子类原型添加属性方法,因为子类构造函数的原型被替换,所以属性方法仍然在替换之后
C.prototype.language = "javascript";
C.prototype.work = function() {
    console.log("writing code use " + this.language);
};
C.work = function() {
    this.super.eat();
};

// 验证继承是否成功
let f = new C("nihao", 16);
f.work();
C.work();

// writing code use javascript
// eating...

寄生组合式继承基本规避了其他继承的大部分缺点,应该比较强大了,也是平时使用最多的一种继承,其中 Child.super 方法的作用是为了在调用子类静态属性的时候可以调用父类的原型方法。

缺点:子类没有继承父类的静态方法。

class...extends... 继承

在 ES6 规范中有了类的概念,使继承变得容易,在规避上面缺点的完成继承的同时,又在继承时继承了父类的静态属性。

// class...extends... 继承
// 父类
class P {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayName() {
        console.log(this.name);
    }
    static sayHi() {
        console.log("Hello");
    }
}

// 子类继承父类
class C extends P {
    constructor(name, age) {
        supper(name, age); // 继承父类的属性
    }
    sayHello() {
        P.sayHi();
    }
    static sayHello() {
        super.sayHi();
    }
}

let c = new C("jack", 18);

c.sayName(); // jack
c.sayHello(); // Hello
C.sayHi(); // Hello
C.sayHello(); // Hello

在子类的 constructor 中调用 supper 可以实现对父类属性的继承,父类的原型方法和静态方法直接会被子类继承,在子类的原型方法中使用父类的原型方法只需使用 this 或 supper 调用即可,此时 this 指向子类的实例,如果在子类的静态方法中使用 this 或 supper 调用父类的静态方法,此时 this 指向子类本身。

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

推荐阅读更多精彩内容