本文已参加「新人创造礼」活动,一同敞开创造之路。
问题布景
前些天做项目练手时,遇到一个需求写类的场景,各个类之间的交互我打算用事情的方式进行,就自然地在父类承继了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版)》,得到知识点如下:
-
任何函数只要运用
new
操作符调用便是结构函数,而不运用new
操作符调用的函数便是一般函数。 -
运用
new
操作符,以这种方式调用结构函数会履行如下操作:
- 在内存中创立一个新目标
- 这个新目标内部的[[Prototype]]特性被赋值为结构函数的 prototype 特点
- 结构函数内部的 this 被赋值为这个新目标(即 this 指向新目标)
- 履行结构函数内部的代码(给新目标添加特点)
- 假如结构函数回来非空目标,则回来该目标;不然,回来刚创立的新目标
-
每个函数都会创立一个 prototype 特点,这个特点是一个目标,包括应该由特定引用类型的实例共享的特点和办法。
-
在创立一个结构函数(创立一个函数,只在用
new
调用一个函数时,这个函数才是一个结构函数)时,原型目标默许只会取得 constructor 特点,指回与之相关的结构函数,其他的所有办法都承继自 Object。每次调用结构函数创立一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为结构函数的原型目标。 -
结构函数和结构函数的原型目标之间循环引用。
-
能够经过浏览器(详细完成)暴露在实例上的__proto__特点访问一个实例内部的[[Prototype]]。
-
同一个结构函数创立的两个实例,共享同一个原型目标。
-
原型上查找值的进程是动态的,所以即使实例在修正原型之前已经存在,任何时候对原型目标所做的修正也会在实例上反映出来。
-
在读取实例上的特点时,首要会在实例上查找这个特点。假如没找到,则会承继查找实例的原型。
原书顶用一张图片总结了上述关系:
上面提到的几个知识点,会在以下问题的评论进程中反复体现。
考虑原因
将咱们的 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.prototype
,constructor
特点赋值为Man
。
经过这种方式完成的承继,既能够经过Man.prototype
访问到匿名函数实例,从而注册公共办法。又能借助原型链访问到Person
类的原型目标,使得Man
的实例能够调用Person
的公共办法。并且这儿的承继完成进程中也运用了“盗用结构函数”的技术,能够让一份实例一起具有Man
和Person
的实例特点。
而后Person
类对EventEmitter
类的承继也是同样进程,仅仅要把Person.prototype
更改为另一个匿名实例,承继后的状况如下:
再多的承继也是依照这个基本思路解析,那么根据这个承继逻辑,咱们创立一个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 大致相同,留意Man
将say
办法注册在自己的原型目标上:
这时问题来了,由于需求盗用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
办法:
定论
这便是答案了,为了盗用结构函数,需求让同一份实例在所有父类的结构函数中“游走”,导致在当前实例上不存在say
之前,就经过_this.say
去访问它,从而启动了原型链查找机制,使得Person
结构函数中注册的监听是Man
原型目标上的say
。
终究Man
和Person
对say
事情注册的监听都为同一个函数,这样就造成了父类调用被子类重写后的办法的成果。
尾声
起先遇到这个问题,和组里的老大评论后,只能模糊的知道是原型承继的问题,可是没有深化地去剖析它。后来我在网上发帖求解,也很少有人和我评论。无奈之下就只能自己去看书籍找材料来回答。终究能把这篇博文完成我也是很开心的,整个问题的解析进程让我收益良多,希望也能为阅览博文的各位带来协助。
感谢我们看到这儿。