起源:为什么使用原型链
使用原型链是为了实现继承,那js的继承为什么选择了原型链呢?我们来看看网络的解释
https://www.jianshu.com/p/a97863b59ef7
https://www.zhihu.com/search?type=content&q=js%E5%8E%9F%E5%9E%8B%E9%93%BE
网络上的解释基本很一致啊,我觉得这个讲的比较有道理,简单的讲解了为什么使用原型链
但是我感觉还是没有说清楚,所以我大胆假设,小心分析,思考出了以下内容。
首先,我们去看ECMAScript的历史,网景公司是它的发明者,那时候网页多简单啊,一个单纯的页面ok了,js可能就是随便写点数据修改云云就ok,而且那时候C语言应该还很活跃吧,所以这么简单的需求,面向过程啊,js对于数据的处理直接生硬的怼上去就好了呀。所以,依照预料,js或者说ECMAScript一开始就是面向过程的。(当年网页有多简单如下,这tm的丑爆了好吧,图源阮一峰)
我不知道为什么后来有了C++,JAVA(么得研究过)这么diao的OOP语言,但是对于ECMAScript而言,他一直坚挺着,在web界保持着自己的地位,所以时代更迭OOP逐渐登上流行舞台,但是JS还保持着自己在web界的地位,却发现很多内容如果用单纯的面向过程不太OK了,那就意味着JS得进步呀,他也得去做OOP。
所以这时候我就想到了类比C语言,为什么?因为C语言也是面向过程,JS使用的是function来表示一个类,C语言则是使用了struct(这里的类不具有OO含义)。JS有一个很有趣的点就叫做模拟,因为WEB只有JS,不能继承不能多态只能模拟(但是C语言这种,对吧,不能继承不能多态,换C++啊=w=)。那么JS选择了原型模式,原型链去模拟这种继承。
tips:原型链的由来
类模式(这是我自己的叫法,包括生成器,工厂,抽象工厂模式)与原型模式
大致可以这样理解,
抽象工厂模式: 一个类由一个构造函数初始化
生成器模式: 一个类由一个专门的私有函数决定其初始状态
工厂模式: 一个类初始化了一些基本的内容(可以称之为基类),具体内容可以由子类进行丰富完整
原型模式: 当实现一个类式的实例消耗很大的时候,使用的模式
分析:继承与原型链
刚才简单介绍了JS选择原型链继承的由来,但是感觉还不够清楚,以下继续分析。
我专门百度了C语言的模拟继承:https://blog.csdn.net/snow_5288/article/details/70197366
我们可以看到,继承的核心无非就是两个类,其中一个要复用另外一个之中的方法,如果要用C语言去模拟,那就是在其中的一个struct之中将另一个struct声明出来,然后在这个struct中就包含了另外一个struct的属性和方法。
当然这是不太够的吧,如果我去调用:
typedef void (*FUN)()
struct _A //父类
{
FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
int _a;
};
struct _B //子类
{
_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承
int _b;
};
void Test()
{
//测试C++中的继承与多态
A a; //定义一个父类对象a
B b; //定义一个子类对象b
A* p1 = &a; //定义一个父类指针指向父类的对象
p1->fun(); //调用父类的同名函数
p1 = &b; //让父类指针指向子类的对象
p1->fun(); //调用子类的同名函数
//C语言模拟继承与多态的测试
_A _a; //定义一个父类对象_a
_B _b; //定义一个子类对象_b
_a._fun = _fA; //父类的对象调用父类的同名函数
_b._a_._fun = _fB; //子类的对象调用子类的同名函数
/* 这堆可以不用看。。。。
_A* p2 = &_a; //定义一个父类指针指向父类的对象
p2->_fun(); //调用父类的同名函数
p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转
p2->_fun(); //调用子类的同名函数
*/
}
不太够在哪里呢?我调用方法得b.a.func(),好蠢啊不是么?
所以原型登场了,JS用原型去解决了这个问题。它对于每个实例,都初始化了一个proto属性,这个属性指向的是实例的基类的公共方法,当用户用实例去调用方法的时候,如果是继承的内容(如何判断后面会讲,其实我觉得大家也都清楚,要不然面试都过不了),就会到这里去遍历查找,查找到了就可以使用基类的方法,所以有:
let a = new String()
a.hasOwnProperty() // ->可以运行
let b = null
b.hasOwnProperty() // ->无法运行
对于a来说,a本身是个string类型的实例,在string上并不具有hasOwnProperty这样的方法,但是String本身是继承自Object的,这些方法写在Object类的原型之中,所以a.hasOwnProperty() === a.proto.proto.hasOwnProperty
而JS基本类型还有俩神经病,null和undefined,这俩是完全独立的(啊,虽然有typeof null = object的bug,但是这是bug,别在意这些神经病细节)。他们不具有proto,所以他俩没有办法直接调用相应的方法(似乎这是很显然的事情,不过这里为了作对比,强行讲一下)
PS.typeof null = object 大致是因为,object的proto实际上是指向了null。
proto与prototype
好的,终于到了大家都懂的部分了,有点不太想写这种东西,网上一大堆,自己找好了,我觉得写的很nice的几个:
https://zhuanlan.zhihu.com/p/34766836
https://zhuanlan.zhihu.com/p/23026595
https://zhuanlan.zhihu.com/p/22787302
https://zhuanlan.zhihu.com/p/40708626
我画的图:
https://online.visual-paradigm.com/tw/w/zbrkocui/diagrams.jsp#diagram:proj=0&id=1
简单来说,我觉得这就是一个new的问题,为什么这么说呢?首先我们要知道,proto是一个自带的属性,不需要我们去编写(当然你非要搞事情改里面内容也没人拦着),而prototype呢是程序员自己去写的一个内容。虽然这样说不太准确,因为类似Object,String等基本类型的prototype也没让程序员自己写啊(那是因为JS开发的程序员替你写了啊喂),看如下代码:
// 需要注意的是,几大基本类型的prototype,是需要考量的。
// 对于几大基本类型,string,number,boolean应该是类似的,
// function, array等混合类型需要特别讨论,null和undefined没有原型链
let a = '' // a 为string
let b = 0
let c = function () { }
let d = false
// Object 混合类型,也是一种基本混合类型,但是他完全可以规划到new类型当中
let O = new Object()
// 不过前四种也是可以转换一下的
let aa = new String()
let bb = new Number()
let cc = new Function()
let dd = new Boolean()
// 所以归根结底,是讨论一个new的原型链传递
console.log(a) //string
console.log(a.__proto__) //object.prototype
console.log(aa) // string
console.log(aa.__proto__) //object.prototype
console.log(c) // function
console.log(c.__proto__) // f() {}
console.log(O)
console.log(cc)
console.log(O.__proto__)
console.log(cc.__proto__)
我们可以看到,每一个实例实际上都是用new去初始化的,即使像我一开始写的那四种方式,实际上也可以理解为var a = new ...的语法糖
而prototype代表的也是一个基类的属性,实际上他的实现也是一个指针,它指向的即是子类的proto(这样说不太准确,更准确的是这两个可以说是严格相等的,两者都是指针,指向的都是一片包含着基类公共函数的公共区域),例如我写Object.prototype里面就包含了前面举例用的hasOwnProperty函数,也包含了一系列会被子类继承的原型函数,而如果你去写一条Object.prototype.fuckYou = 'oh my gosh' , 那么你再次打印,就可以看到Object的prototype下面多了一个fuckYou,而String,number等继承自Object的子类的 proto也多了一个 fuckYou属性。
当然这一点对于Function类型和Array类型在console出来的时候似乎是不适用的,但是实际上,他俩也是符合这条规则的,我们打印Function.proto的时候发现是一个ƒ () { [native code] }(同理,打印Array的proto时也是一样),而我们如果使用Function.hasOwnProperty发现是可以调的到的,不出意外这个函数应该是搜索原型链从Object上搜索出来的(我不相信,JS的编写者把这个函数分别在Function和Array上又实现了一遍),为什么会打印出来与基本类型不同的东西,大概这就是混合类型区别基本类型的地方吧,这一点我还没有来得及细细研究,欢迎大家来回答一下。
所以本质上,我们可以看到,这就是一个new的问题,对于对象,new了一个实例之后,或者被一个函数继承了之后,这个实例或者子类的proto就指向了基类或者父类的prototype。
关于这堆乱七八糟我也经常混淆,这篇知乎讲的尤为详细,必要时可参考:
https://zhuanlan.zhihu.com/p/23026595
最后还应该讨论一下这个“链的问题”,他之所以是链,是因为他是一个逐层向上的搜索过程,类似于dom冒泡的那种感觉,如果子类调用了一个方法,那么对于这个方法将从子类的本身构造开始,到子类的proto也就是父类的prototype,再到父类的父类的原型,一直到object再到null(应该这两个是终极了吧),成为一种链式的状态,称之为原型链。
原型链与继承方法
红宝书里讲了一堆啊,什么构造函数方式,工厂模式,寄生构造函数,组合继承,寄生组合继承等等等(我这里不太想把这些展开,因为感觉一讲就要飞到设计模式,莫哥说了别这样)。这些的继承方式其实是异曲同工,从根本上来讲,他们都是从
function foo() {
this.a = 1
}
foo.prototype = {
protoFoo: ()=> {
return 6666
}
}
let ex = new foo()
ex.protoFoo() // ->6666
这样的类型脱胎而来的,只不过,前人用各种设计模式的方法,基本上模拟出来了类的extend,public,private,protected等等js中本来不存在的关键字的功能
具体的可以看这篇文章,还是蛮详细:
https://zhuanlan.zhihu.com/p/24964910
from JS to TS
ok,我们现在在写TS,小伙伴们怕是好久没有接触到原型链这个东西了吧,感觉很爽因为再也不用像我一样在写这篇文章的示例的时候一样纠结啦~
但是实际上,很多东西,都只是语法糖,你之所以不用纠结,是因为,微软的工程师们替你纠结了。
如下是我写的一段示例TS
class A {
protected test1!: number
protected test2: string = ''
private x: number = 1
constructor() {
this.test1 = Infinity
}
public foo1() {
if (this.x > 0) {
this.x - this.x * (1 / 1000)
this.foo1()
} else {
const result = (1 + this.x ^ (1 / 2) - 1) * 2 / this.x
return result
}
}
}
class B extends A {
constructor() {
super()
this.test1 = 1
}
}
let a = new A()
let b = new B()
b.foo1()
而下面是编译之后的JS
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var A = /** @class */ (function () {
function A() {
this.test2 = '';
this.x = 1;
this.test1 = Infinity;
}
A.prototype.foo1 = function () {
if (this.x > 0) {
this.x - this.x * (1 / 1000);
this.foo1();
}
else {
var result = (1 + this.x ^ (1 / 2) - 1) * 2 / this.x;
return result;
}
};
return A;
}());
var B = /** @class */ (function (_super) {
__extends(B, _super);
function B() {
var _this = _super.call(this) || this;
_this.test1 = 1;
return _this;
}
return B;
}(A));
var a = new A();
var b = new B();
b.foo1(); // ->可以运行
稍微阅读一下代码,可以看出, ts的编写者使用了寄生式组合继承的方式实现了一个extends函数,将其作为了一个js本来原型继承的一个语法糖。然后在你写到extend的时候,大概是进行了正则匹配句法分析词法分析的编译原理工作,检测到了你的继承,然后就会调用这个全局__extend函数,并且将从java而来的super方式引入,将其作为一个回调函数的默认参数传入,让其获取到父类的上下文,并在子类的构造函数中执行一遍,这样符合了我上面所说的继承的本质 ---- 将父类的东西让子类可以用。而可以看到这样实现之后,子类是可以调到父类的函数的,不用使用b.a.foo的方式,也阐释了我们上面对于原型方式模拟类式继承的理论。
可以看到,在编译之后的TS广泛的使用了proto和prototype,并在父类 A进行了一系列的类型判断和属性set,而这仅仅是实现了一个继承而已,如果要实现TS中向强类型语言学习的泛型,多态,写出来的代码可谓成倍量的提升,而且TS是经过检验的,可以说考虑到了很多边际情况。如果我们自己写这样的js,或者真的可以写出来,但是如果遇到了我上文所说的一些神经病边际情况(诸如typeof null = object)那么等待着你的很可能是’咋肥四鸭‘