资讯专栏INFORMATION COLUMN

JavaScript 的继承方式及优缺点

nanchen2251 / 2931人阅读

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


阅读原文


前言

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.stringifyJSON.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

在上面可以看出复制后的新对象 childa 属性和 b 的引用是独立的,与 parentab 毫无关系,实现了深复制,但是 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 构造函数,并借用 callapply 在内部执行了父类 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 丢失和子类原型上多余共有属性的问题还是没有解决,在这基础上又产生了新的问题。

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


原型式继承

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

// 原型式继承
// 父类
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" }

原型式继承其实是借助了一个中间的构造函数,将中间构造函数 Fprototype 替换成了父类的原型,并创建了一个 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 可以实现对父类属性的继承,父类的原型方法和静态方法直接会被子类继承,在子类的原型方法中使用父类的原型方法只需使用 thissupper 调用即可,此时 this 指向子类的实例,如果在子类的静态方法中使用 thissupper 调用父类的静态方法,此时 this 指向子类本身。


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

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

相关文章

  • 复习Javascript专题(三):面向对象(对象创建与继承,原型原型链)

    摘要:在创建子类实例时,不能向超类型的构造函数中传递参数。构造函数继承子类传进的值是基本思想是在子类构造函数的内部调用超类或父类型构造函数。继承保证构造函数指针指向如果想同时继承多个,还可使用添加属性的方式类继承, OOP:Object Oriented Programming 面向对象编程。 题外话:面向对象的范围实在太大,先把这些大的东西理解理解。 1.什么是对象? 根据高程和权威指南上...

    testHs 评论0 收藏0
  • 7个 Javascript 面试题回答策略

    摘要:使用异步编程,有一个事件循环。它作为面向对象编程的替代方案,其中应用状态通常与对象中的方法搭配并共享。在用面向对象编程时遇到不同的组件竞争相同的资源的时候,更是如此。 翻译:疯狂的技术宅原文:https://www.indeed.com/hire/i... 本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章 不管你是面试官还是求职者,里面...

    李义 评论0 收藏0
  • 深入理解JavaScript

    摘要:深入之继承的多种方式和优缺点深入系列第十五篇,讲解各种继承方式和优缺点。对于解释型语言例如来说,通过词法分析语法分析语法树,就可以开始解释执行了。 JavaScript深入之继承的多种方式和优缺点 JavaScript深入系列第十五篇,讲解JavaScript各种继承方式和优缺点。 写在前面 本文讲解JavaScript各种继承方式和优缺点。 但是注意: 这篇文章更像是笔记,哎,再让我...

    myeveryheart 评论0 收藏0
  • JavaScript之深入各种继承

    摘要:通常有这两种继承方式接口继承和实现继承。理解继承的工作是通过调用函数实现的,所以是寄生,将继承工作寄托给别人做,自己只是做增强工作。适用基于某个对象或某些信息来创建对象,而不考虑自定义类型和构造函数。 一、继承的概念 继承,是面向对象语言的一个重要概念。通常有这两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。 《JS高程》里提到:由于函数没有签名,...

    tomlingtm 评论0 收藏0
  • js中对数据类型总结判断数据类型各种方法缺点

    摘要:最常见的判断方法它的官方解释操作符返回一个字符串,表示未经计算的操作数的类型。另外,是判断对象是否属于某一类型,而不是获取的对象的类型。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。 js中的数据类型 js中只有六种原始数据类型和一个Object: Boolean Null Undefined Number String Symbol ...

    voyagelab 评论0 收藏0

发表评论

0条评论

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