Javascript 面向对象的缺陷,父类能调用被子类重写后的方法

news2024/11/24 0:15:16

问题背景

前些天做项目练手时,遇到一个需要写类的场景,各个类之间的交互我打算用事件的方式进行,就自然地在父类继承了EventEmitter类。然后在父类对一个具体事件注册了一个默认监听,子类通过注册自己专有的监听细化逻辑。代码逻辑如下:

import EventEmitter from "events";

class People extends EventEmitter {
    constructor() {
        super();
        this.on("say", this.say);
    }

    public say() {
        console.log("I am People Class");
    }
}

class Man extends People {
    constructor() {
        super();
        this.on("say", this.say);
    }

    public say() {
        console.log("I am Man Class");
    }
}

let man = new Man();
man.emit("say");

 
但是这时遇到一个百思不得其解的问题,当其他部件对子类发出事件时,子类注册监听响应了,但却响应了两次,而父类的监听"消失了"!运行上面的代码得到如下结果:

I am Man Class
I am Man Class

 
为了找出问题,尝试修改Man类的代码,在say方法中显示调用super.say

class Man extends People {
    constructor() {
        super();
        this.on("say", this.say);
    }

    public say() {
        super.say(); // 显示调用父类方法
        console.log("I am Man Class");
    }
}

 
重新运行代码,得到结果如下:

I am People Class
I am Man Class
I am People Class
I am Man Class

 
发现父类的方法还是能调用的,但是无论怎么样都是子类方法被调用了两次,而触发函数的地方只有父类和子类注册的两个对say事件的监听。于是我当时猜是注册时调用的this.say中的this关键字引发的问题,使得父类监听调用了被子类重写的方法。

求解过程

从传统面向对象的角度来说这非常让人疑惑,网上找了一圈没发现关于这个问题的相关讨论,就自己一点点的去研究这个问题。从前端角度出发,我们知道 JS 的面向对象是通过原型链模拟的,首先重新回顾一下 JS 面向对象技术发展过程中的重点。

资料收集

查阅《Javascript高级程序设计(第4版)》,得到知识点如下:

  1. 任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。

  2. 使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
  1. 每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。

  2. 在创建一个构造函数(创建一个函数,只在用 new 调用一个函数时,这个函数才是一个构造函数)时,原型对象默认只会获得 constructor 属性,指回与之关联的构造函数,其他的所有方法都继承自 Object。每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。

  3. 构造函数和构造函数的原型对象之间循环引用。

  4. 可以通过浏览器(具体实现)暴露在实例上的__proto__属性访问一个实例内部的[[Prototype]]。

  5. 同一个构造函数创建的两个实例,共享同一个原型对象。

  6. 原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。

  7. 在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。

原书中用一张图片总结了上述关系:

请添加图片描述

上面提到的几个知识点,会在以下问题的讨论过程中反复体现。

思考原因

将我们的 ES6 类代码转为 ES5 的代码(使用 tsc):

tsconfig.json
{
  "compilerOptions": {
    ...
    "target": "es5",
    ...
  },
  "files": ["test.ts"]
} 
"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var events_1 = __importDefault(require("events"));
var People = /** @class */ (function (_super) {
    __extends(People, _super);
    function People() {
        var _this = _super.call(this) || this;
        _this.on("say", _this.say);
        return _this;
    }
    People.prototype.say = function () {
        console.log("I am People Class");
    };
    return People;
}(events_1.default));
var Man = /** @class */ (function (_super) {
    __extends(Man, _super);
    function Man() {
        var _this = _super.call(this) || this;
        _this.on("say", _this.say);
        return _this;
    }
    Man.prototype.say = function () {
        _super.prototype.say.call(this);
        console.log("I am Man Class");
    };
    return Man;
}(People));
var man = new Man();
man.emit("say");

 
其实如果对 JS 原型链和继承的实现十分了解的话,上面的代码已经把答案写清楚了。ES6 extends 关键字转换成的__extends函数使用了类似寄生式组合继承方式去继承指定父类的公共方法,而我们又在类构造过程中通过this关键字去注册监听函数,两者中存在的问题交织引起了我们开篇提到的问题。

问题探索

 
下面逐步解析继承过程,一起来观察问题是怎么出现的。

 
首先从 Man 子类入手,该类定义(ES5)如下:

var Man = /** @class */ (function (_super) {
    __extends(Man, _super);
    function Man() {
        var _this = _super.call(this) || this;
        _this.on("say", _this.say);
        return _this;
    }
    Man.prototype.say = function () {
        _super.prototype.say.call(this);
        console.log("I am Man Class");
    };
    return Man;
}(People));

我们知道 js 里面会进行函数声明提升,所以第一行作用的代码是:

__extends(Man, _super);

_super就是传入的People,也就是首先执行的代码为

__extends(Man, People);

解析 __extends() 实现的经典继承

__extends方法的源码在前面已经贴出,这里直接点明该函数的继承效果。

 
当调用__extends(Man, People)时,__extends创建了一个匿名构造函数,并构造出一个匿名实例,
并显式地将该实例的[[Prototype]]属性赋值为People.prototypeconstructor属性赋值为Man

Man继承Perso
图1.1 Man 继承 Person 后

通过这种方式实现的继承,既可以通过Man.prototype访问到匿名函数实例,从而注册公共方法。又能借助原型链访问到Person类的原型对象,使得Man的实例可以调用Person的公共方法。并且这里的继承实现过程中也使用了“盗用构造函数”的技术,可以让一份实例同时具有ManPerson的实例属性。

 
而后Person类对EventEmitter类的继承也是同样过程,只是要把Person.prototype更改为另一个匿名实例,继承后的情况如下:

Person继承EventEmitter
图1.2 Person 继承 EventEmitter 后

 
再多的继承也是按照这个基本思路解析,那么基于这个继承逻辑,我们创建一个Man的实例的情况如下:

创建一个 Man 实例
图1.3 创建一个 Man 实例

这里使用了“盗用构造函数”技术,Person构造函数中的实例属性也是注册在同一份Man实例上。

解析构造函数执行过程

重申一下本文探讨的问题:为什么Person类接收到say事件时,触发的是子类Man注册的回调函数?

同时强调一个知识点:

在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。

进入探讨之前,注意Man构造函数中的语句执行顺序为:

function Man() { ... };
__extends(Man, People);
Man.prototype.say = function() { ... };
return Man;

带着这些,我们来解析一下Man的构造函数:

var Man = /** @class */ (function (_super) {
    __extends(Man, _super);                     // 让内部函数 Man 继承 Person
    function Man() {                            // 声明内部函数 Man
        var _this = _super.call(this) || this;  // 将创建的实例传给 Person,盗用 Person 的实例属性
        _this.on("say", _this.say);             // 在含有 Person 和 EventEmitter 实例属性和原型方法的实例上
                                                // 调用 on 方法,将此刻实例中的 say 函数注册为 "say" 事件的监听
        return _this;                           // 返回创建的实例
    }
    Man.prototype.say = function () {           // 在 Man 的原型对象上注册自己的 "say" 方法
        _super.prototype.say.call(this);        
        console.log("I am Man Class");
    };
    return Man;                                 // 返回内部函数 Man
}(People));

单从注释还不能直观地看出问题,我们基于前面绘制的原型图来继续讨论。

  • 当外部调用了new Man(),此时让我们的执行过程停在:
function Man() {
    var _this = _super.call(this) || this;  // 将创建的实例传给 Person,盗用 Person 的实例属性
    ...
}

此时的原型链情况和图 1.3 大致相同,注意Mansay方法注册在自己的原型对象上:

刚创建 Man 实例时
图1.4 刚创建 Man 实例时

这时问题来了,由于需要盗用Person的构造函数来注册实例属性,当将this传给Person.call时,Person内部执行了如下代码:

function People() {
    var _this = _super.call(this) || this;
    _this.on("say", _this.say);
    return _this;
}

我们知道此时的_this就是刚创建的Man 实例,那么Person此时将_this.say注册为监听函数,而此刻_this上并没有say属性或者方法,那么顺着原型链,_this.say找到了Man.prototype.say,也就是图中第一个匿名函数实例上的say方法:

查找 _this 实例上的 say 方法
图1.5 查找 _this 实例上的 say 方法

结论

这就是答案了,为了盗用构造函数,需要让同一份实例在所有父类的构造函数中“游走”,导致在当前实例上不存在say之前,就通过_this.say去访问它,从而启动了原型链查找机制,使得Person构造函数中注册的监听是Man原型对象上的say

最终ManPersonsay事件注册的监听都为同一个函数,这样就造成了父类调用被子类重写后的方法的结果。

尾声

起初遇到这个问题,和组里的老大讨论后,只能模糊的知道是原型继承的问题,但是没有深入地去剖析它。后来我在网上发帖求解,也很少有人和我讨论。无奈之下就只能自己去看书籍找材料来解答。最终能把这篇博文完成我也是很开心的,整个问题的解析过程让我收益良多,希望也能为阅读博文的各位带来帮助。

感谢大家看到这里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/91467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

win7、win10关闭驱动签名,进入驱动测试模式,以及常见初级问题的解决

win7关闭驱动签名,进入驱动测试模式win7、win10关闭驱动签名、进入驱动测试模式DebugView工具运行提示"Dbgv.sys: 拒绝访问"驱动项目配置属性常用设置驱动中KdPrint打印UNICODE_STRING字符串常用方法没使用的变量在编译时报警告:未引用的形参错…

文字转语音真人发声软件哪个好?这些实用软件快来收好

平时大家结束了一天的工作,会不会在空闲时间好好放松一下呢?如果是坐公交车或者地铁的小伙伴,想要在下班途中看一些电子书籍或者新闻,却因为下班高峰期人潮拥挤,导致无法腾出手来阅读,这时候你们会怎么解决…

红队基础知识

文章目录红队前置准备网络攻击链工程和操作攻击性思维渗透测试漏洞研究软件开发基础设施网络和系统逆向工程社会工程学物理安全威胁情报安全事件的检测和响应技术写作培训与汇报总结红队前置准备 红队通常指在对抗情况下需要仿真、模拟或以其他方式扮演某个、某组入侵者或理论…

【Docker】(一)基本概念与安装使用

1.概述 最近学习了Docker的使用,想通过一个系列的笔记来记录学习的过程与收获,并为以后的生产工作提供指导。 我一直认为学习一门技术时,需要先了解这门技术的基本概念,了解它能解决的问题,这样才能定位明确的学习目标…

frp内网穿透https

在公网服务器搭建frps(service),在内网本地机子搭建frpc(client),流量通过访问公网ip,经过frps服务端转发到fprc客户端,fprc再转发到本地web应用。 官方下载地址​ https://github.com/fatedier/frp/releases 官方文档地址https…

智能门锁“激战正酣”

近年来,智能化已经成为了高频词,越来越多的行业都在朝着智能化方向发展,家居行业也不例外。受技术升级、居民收入水平提高等多重因素影响,整个智能家居行业呈现出了蓬勃发展态势。据亿欧智库预测,2025年中国智能家居市…

客运车票网站

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字): 题目:基于Wb的公路客运车票信息管理系统设计与实现 时了解客运站动态。 角色:乘客、管理员 (2)车…

[附源码]Node.js计算机毕业设计-高校人事管理系统Express

项目运行 环境配置: Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境:最好是Nodejs最新版,我…

C++设计模式的个人理解 繁琐的底层需要合理设计

一、为什么会有设计模式 由于每个人自身的能力、所在的层次、看问题的角度都不同,仅凭直觉“对现实建模”,很有可能会生成一些大小不均、职责不清、关系混乱的对象,最后搭建出一个虽然可以运行,但却难以理解、难以维护的系统。为此…

基于Java+Mysql实现酒店预订系统【100010045】

酒店预订系统 1.引言 1.1编制目的 1.详细完成对酒店预订系统的概要设计, 2.达到指导详细设计和开发的目的, 3.同时实现和测试人员及用户的沟通。 3.本报告面向开发人员,测试人员及最终用户的编写额,是了解系统的导航。 1.2…

Flink基本转换算子

文章目录1.映射(map)2.过滤(filter)3.扁平映射(flatMap)4.按键分区(keyBy)5. 简单聚合(sum,min,max等)6.归约聚合(reduce&…

m基于LDPC+QPSK通信链路误码率matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 LDPC ( Low-density Parity-check,低密度奇偶校验)码是由 Gallager 在1963 年提出的一类具有稀疏校验矩阵的线性分组码 (linear block codes),然而在接下来的 …

第五届“强网”拟态防御国际精英挑战赛在南京举行——开辟网络安全新赛道 引领网络弹性新优势

12月15日,第五届“强网”拟态防御国际精英挑战赛在南京紫金山实验室隆重开幕,来自国内外60支顶尖战队将通过云上和线下相结合的方式展开72小时的高强度对抗。大赛组委会主席、紫金山实验室首席科学家、中国工程院邬江兴院士指出,本届挑战赛瞄…

jvm内存模型+类加载机制+垃圾手机器

1、类加载器分类 1、引导类加载器,负责加载支撑Jre/lib目录下的核心类库 2、扩展类加载器:负责加载Jre/lib目录下的ext扩展类jar包 3、应用程序类加载器:负责加载classpath下的类包 4、自定义类加载器:负责加载用户自定义路径下的…

值得思索的:ArrayList和线性表,你确定错过这次机会

线性表: 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结 构,常见的线性表:顺序表、链表、栈、队列... 线性表在逻辑上是线性结构,也就说是连续的一条…

Go项目实战:01-聊天室+map竞争需要上锁

实现一个聊天室(群): 功能分析: 1、上线下线2、聊天:其他人和自己都可以看到聊天消息3、查询当前的聊天室用户所有人的名字4、可以修改自己的名字5、超时潜水踢出机制 技术点分析: 1、socket tcp编程2、…

Adam算法及python实现

文章目录算法介绍代码实现结果展示参考算法介绍 Adam算法的发展经历了:SGD->SGDM->SGDNA->AdaGrad->AdaDelta->Adam->Adamax的过程。它是神经网络优化中的常用算法,在收敛速度上比较快,比SGD对收敛速度的纠结上有了很大的…

单商户商城系统功能拆解46—应用中心—足迹气泡

单商户商城系统,也称为B2C自营电商模式单店商城系统。可以快速帮助个人、机构和企业搭建自己的私域交易线上商城。 单商户商城系统完美契合私域流量变现闭环交易使用。通常拥有丰富的营销玩法,例如拼团,秒杀,砍价,包邮…

基于微信小程序的课程分享平台-计算机毕业设计

项目介绍 随着社会的发展,社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景,运用软件工程原理和开发方法,它主要是采用java语言技术和mysql数据库来完成对系统的设计。整…

[附源码]Node.js计算机毕业设计高校就业管理信息系统Express

项目运行 环境配置: Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境:最好是Nodejs最新版,我…