JavaScript系列-3-继承的实现

本文作者:钟昕灵,叩丁狼高级讲师。原创文章,转载请注明出处。

混入式继承的实现

在讲继承的实现之前,我们首先来回顾一下什么是继承?继承的作用是什么?

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

通过继承创建的新类称为“子类”或“派生类”。

被继承的类称为“基类”、“父类”或“超类”。

在清楚继承的作用之后,下面我们来探讨一下JS中的几种继承实现的方式:

//混入式继承(拷贝)
//obj2继承到obj1中的成员,可以直接将obj1中的成员拷贝到obj2中即可
var obj1 = {name:"zs",age:10};
var obj2 = {};
// 将obj1中的成员拷贝到obj2中
for (var key in obj1) {
    obj2[key] = obj1[key];
}
console.log(obj1);
console.log(obj2);

最终得到的obj2中的成员和obj1中的成员完全一致,当然,我们需要清除的是,此时的obj1和obj2是两个不同的对象。

混入式继承方式看似很简单,但是存在共享数据安全的问题。

var obj1 = {name:"zs",age:10,car:{name:"mini"}};
var obj2 = {};
// 将obj1中的成员拷贝到obj2中
for (var key in obj1) {
    obj2[key] = obj1[key];
}
//修改obj1对象中的car属性
obj1.car.name = "bus";

console.log(obj1);
console.log(obj2);

当我们需要修改某些对象中的引用类型对象的时候,会造成其他相关的对象也被修改,这是我们不希望看到的。

原型式继承的实现

回想一下,当我们在访问一个对象中的成员的时候,首先是在当前对象中查找,如果找不到,就往上,在原型链中依次查找,如果在整个原型链中也不存在该成员,那么就返回undefined。

所以,我们想要在A对象中访问到B对象中的成员,除了将B中的成员添加到A中,如:混入式,我们也可以考虑将B中成员添加到A的原型链中,实现对象成员的共享。

function Animal() {
}
Animal.prototype.name="animal";
function Person() {
}
//修改Person的原型对象
Person.prototype= Animal.prototype;

Person.prototype.useTool = function () {
    console.log("use fire");
}
var p = new Person();
console.log(p);
var ani = new Animal();
console.log(ani);

画图分析:

  1. 最初,Animal和Person的两个对象没有任何关系,所以各自只能访问各自的成员
叩丁狼教育.png
  1. 现在,Person对象如果想要继承Animal对象,只需要将Person的原型对象修改为Animal的原型对象即可
叩丁狼教育.png

这种方式实现的继承称之为原型继承,实现也是比较方便的,当时和混入式一样,存在数据共享的问题。

原型链继承的实现

原型式继承

在前面的课程中,我们讲了原型式的继承,这种继承方式是修改子类对象原型指向父类对象的原型,如前面的MyArray执行的是Array的原型对象。

这种方式存在问题是,只能继承父类对象原型上的成员,但无法继承父类对象中的成员。

function Animal() {
    this.color="red";
}
Animal.prototype.weight=100;
function Person() {
}
Person.prototype = Animal.prototype;
var p = new Person();
console.log(p);

Person.prototype = Animal.prototype;原型式的继承

此时创建出来的Person对象p可以访问Animal中的weight属性,但是无法访问color属性。看图理解

叩丁狼教育.png

那么,如果我们既要继承原型对象中的成员,也要继承实例对象中的成员,就应该使用这一节中的原型链继承

function Animal() {
    this.color="red";
}
Animal.prototype.weight=100;
function Person() {
}
Person.prototype = new Animal();
var p = new Person();
console.log(p.color);//red
叩丁狼教育.png

使用原型链继承方式能够继承到更多的成员。但是依然存在问题:

  1. 共享的问题
  2. 无法向构造器传参

复杂的原型链继承示例---

Object-Animal-Person-Boy

Object.create方法的基本使用

在Object的构造函数上存在一个create方法,该方法的作用是用来创建对象的。

该方法可以接收的参数有一下两种

  1. null

    创建一个空对象,这个空对象中连最基本的原型对象都没有的

  2. 对象

    创建传递进来的对象,并设置该对象的原型对象为当前的参数

叩丁狼教育.png

该方法的使用率比较低,要求大家知道存在这样的一种创建对象的方式即可。

call方法和apply方法的基本使用

call和apply方法作用:

1. 方法借用
2. 设置方法中this的指向
var obj1 = {
    name:"Neld",
    age:10,
    showInfo:function () {
        console.log("name:"+this.name,"age:"+this.age);
    }
}
var obj2 = {
    name:"Lily",
    age:12
}
obj1.showInfo();//name:Neld age:10
obj2.showInfo();//obj2.showInfo is not a function

obj1对象中有showInfo方法,而obj2对象中没有,所以如果直接使用obj2调用showInfo方法的时候抛出错误信息。

如果我们临时需要使用obj2调用showInfo方法来打印出name和age属性的值,此时可以使用这里的call或者apply方法来实现。

obj1.showInfo.call(obj2);//name:Lily age:12
obj1.showInfo.apply(obj2);//name:Lily age:12

这就是把obj1中的showInfo方法借用给obj2使用。

同时我们观察到,在showInfo方法中使用到了this关键字,在obj2借用该方法的时候,其中的this已经指向了obj2对象,这就要达到修改方法中this关键字的指向的目的。

call和apply方法的作用是完全一样的,那么他们的区别是什么呢?继续往下分析。

var obj1 = {
    name:"Neld",
    age:10,
    add : function (a, b) {
        return a + b;
    }
}
var obj2 = {
    name:"Lily",
    age:12
}
console.log(obj1.add.call(obj2, 1, 2));//3
//console.log(obj1.add.apply(obj2, 1, 2));//CreateListFromArrayLike called on non-object
console.log(obj1.add.apply(obj2, [2, 2]));//4

在obj1中定义一个带有两个参数的方法,obj2中没有,问题一样,obj2也要使用到obj1中的add方法,此时使用call或者apply借用即可。

此时新的问题是,在调用add方法的时候参数应该如何传递?

call方法:

​ 将this指向的对象作为第一个参数,其他参数依次传递即可

apply方法:

​ 将this指向的对象作为第一个参数,其他参数封装到数组中传递

​ 如果没有使用数组,程序报错。

以上就是call和apply的基本使用,这两个方法在后续的课程中会大量的使用到,所以必须引起重视。

借用构造函数继承说明

所谓借用构造函数,就是在子构造函数中调用父构造函数,达到继承并向父构造函数传参的目的。

function SuperClass(name,age) {
    this.name = name;
    this.age = age;
}
SuperClass.prototype.fun1 = function () {
    console.log("name:",this.name,"age:",this.age);
}
function SubClass(color) {
    this.color = color;
}
SubClass.prototype = new SuperClass("xxx",10);//继承父构造函数并设置name和age的值
SubClass.prototype.fun2 = function () {
    console.log("color:",this.color);
}
var sub = new SubClass("red", "Neld", 10);
var sub2 = new SubClass("red", "Lily", 12);
console.log(sub);
console.log(sub2);

上面是原型链的继承,这种方式存在一个问题是,在创建不同对象的时候,无法为其继承过来的成员赋值。

这里的sub和sub2两个对象的name和age属性值都是“xxx”和10,很明显是不满足我们需求的。

那么我们来看看借用构造函数是否能解决这个问题呢?

function SuperClass(name,age) {
    this.name = name;
    this.age = age;
}
SuperClass.prototype.fun1 = function () {
    console.log("name:",this.name,"age:",this.age);
}
function SubClass(color, name, age) {
    this.newMethod = SuperClass;//①
    this.newMethod(name, age);//②
    this.color = color;
    delete this.newMethod;//③
}
//SubClass.prototype = new SuperClass();
SubClass.prototype.fun2 = function () {
    console.log("color:",this.color);
}
var sub = new SubClass("red", "Neld", 10);
var sub2 = new SubClass("red", "Lily", 12);
console.log(sub);
console.log(sub2);

①、②、③处代码是实现借用构造函数的关键。下面一一作出解释:

①:将父对象的构造函数设置为子对象的成员

②:调用这个方法,想过类似于将父构造函数的代码放在子构造函数中来执行

function SubClass(color, name, age) {
    this.newMethod = SuperClass;
    this.name = name;//父构造函数中的代码
    this.age = age;//父构造函数中的代码
    this.color = color;
    delete this.newMethod;
}

这样看应该更直观一点,执行之后就是在为当前创建出来的对象封装name和age属性。

③:在子构造函数中,newMethod仅仅为了在②调用父构造函数使用,用完之后也就没了价值,所以,直接删除该方法即可

叩丁狼教育.png

可以看到,借用构造函数继承方式解决了原型链及继承的问题。

下面再看看另外一种借用构造函数的实现方式(使用call或apply):

function SubClass(color, name, age) {
    //SuperClass.call(this,name,age);
    SuperClass.apply(this,[name,age]);
    this.color = color;
}

我们可以使用call或apply更简单快捷的实现和上面一样的效果。

以上就是借用构造函数继承(也要对象冒充)的两种实现方式。当然,这种继承方式都存在下面两个问题:

  1. 如果父子构造函数存在相同的成员,那么子构造函数会覆盖父构造函数中的成员
  2. 不能继承原型链中的成员

组合继承的基本结构

基于原型链继承和借用构造函数继承两种方式存在的问题,我们提出一个解决方案---组合继承,就是两种一起使用。

function SubClass(color, name, age) {
   //SuperClass.call(this,name,age);
   SuperClass.apply(this,[name,age]);//继承构造函数中的成员
    this.color = color;
}

SubClass.prototype = new SuperClass();//继承原型链上的成员

总结: ECMAScript 实现继承的方式不止一种。这是因为 JavaScript 中的继承机制并不是明确规定的,而是通过模仿实现的。这意味着所有的继承细节并非完全由解释程序处理。作为开发者,你有权决定最适用的继承方式。

绘制完整的原型链结构图

这一节重点探讨函数对象的原型链结构。完整的结构图如下:

叩丁狼教育.png

想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

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

推荐阅读更多精彩内容