JavaScript 的继承方式及优缺点

2018-10-11 admin

在这里插入图片描述

前言

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://segmentfault.com/a/1190000016653670

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-42444.html

文章标题:JavaScript 的继承方式及优缺点

相关文章
JavaScript编辑器推荐
主流编辑器有SublimeText,Notepad++,webstorm等,是使用最广泛的编辑器,但也有一些JavaScript编辑器提供有着各自的特性和功能,适应不同人的需求,以下是几款优秀的编辑器,相信你一定能找到自己喜欢的。 1. W...
2015-11-12
js性能优化 如何更快速加载你的JavaScript页面
确保代码尽量简洁 不要什么都依赖JavaScript。不要编写重复性的脚本。要把JavaScript当作糖果工具,只是起到美化作用。别给你的网站添加大量的JavaScript代码。只有必要的时候用一下。只有确实能改善用户体验的时候用一下。 ...
2015-11-12
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
2015年JavaScript或“亲库而远框架”
2014年过去了,作为一个JavaScript开发者很难满怀信心的去“挽回”一个特定的库或技术,即便是强大的Angular,似乎也因为最近的一些事情而动摇。 2014年10月的ng-europe会议上,Angular开发者团队透露了一个关于...
2015-11-12
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
JavaScript常用特效chm下载
下载地址:JavaScript常用特效chm下载 对了,如果打开空白,在手册上右键属性解除锁定即可。 ...
2015-11-12
css布局的各种FC简单介绍:BFC,IFC,GFC,FFC
什么是FC? Formatting Context,格式化上下文,指页面中一个渲染区域,拥有一套渲染规则,它决定了其子元素如何定位,以及与其他元素的相互关系和作用。 BFC 什么是BFC Block Formatting Context,块...
2018-05-17
从2014年的发展来展望JS的未来将会如何
<font face="寰�杞�闆呴粦, Arial, sans-serif ">2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
回到顶部