关于语法中的代码复用,有两大方向,一种为宏,一种为继承,就js而言,选择了后者
代码复用 - 继承(对象系统)
继承是面向对象中的概念,表示子类具有父类的属性与方法
对象系统的继承特性,又有三种实现方案
- 基于类 - class-bassed
- 基于元类 - metaclass-based
- 基于原型 - prototype-based
js使用的是基于原型的实现方案
js对象系统 - 构造器(无类)
js是一个标准的无类语言,其实现抽取类中构造器(constructor)来实现继承的功能。类似于类,构造器是用于描述对象的组织结构的语法(正如面向对象中类与实例的关系,构造器与对象也是这样一个关系)
js对象系统 - 原型
js最终通过[构造器].prototype 来描述对象的组织结构
针对对象系统,可知对象并没有原型,只有构造器有原型
js对象系统 - 复用实现
对象来自于原型,构造器与原型最终实现了语义(语法)上的代码实现,针对最终代码的实现,又有三种策略可以选择
- 原型复制
即深度copy
实现简单,但会占用过多的内存,尤其是相同的函数都会开辟新的空间,不可能作为底层实现
- 写时复制(prototype)
类似于dll,在访问时,访问的都是同一指向,但是在第一次写入时,从新分配空间,copy prototype
针对大量读写,会与1一样
- 写时复制(属性)
与上条类似,但在写入时,重新开辟的空间为属性,故可以节约更多的内存,但此处写只
1.原型污染
同样的,针对指向没有改变的修改,会直接修改原型,引起全局污染
function User(){}
User.prototype.baseinfo = {
job:'',
name:'',
age:1
}
let u1 = new User();
let u2 = new User();
//读时
console.log(User.prototype.baseinfo === u1.baseinfo)
u1.baseinfo.age = 18;
console.log(u2.baseinfo.age)
就如上的表现,可以用Proxy去理解,即js针对属性set进行拦截,如果set修改,则对当前对象的属性就行修正,但不同于Proxy,prototype可以动态添加
2.原型动态添加
即原型链可动态更新,此为不同于第一类(原型复制)的特性
function User(){}
let u1 = new User();
console.log(u1.baseinfo)
User.prototype.baseinfo = {
job:'',
name:'',
age:1
}
console.log(u1.baseinfo)
js对象系统 - 构建过程
对于关键字function
来讲,其有两层意义
- 声明函数
尽管拥有prototype,但他没有任何意义,也不应该存在 - 声明构造器
construcor
可以有如下理解
{
prototype:{
get(){
if(!this.__proto__){
this.__proto__=new object();
this.__proto__.construcor=this;
}
}
}
即prototype
只有在被使用后才进行创建,此时constructor
属性默认会指向函数本身
做为一个快速实现的语言,js选择了无节操的复用形式,即原型用于创建实例,但原型又是一种实例这种衔尾蛇的形式,以至于生出各种奇巧淫技
js对象系统 - 构造器的维护
由单个原型构造过程可知,构造器指向创建函数本身
多个原型连接在一起成为原型链
实现如下,他可以准确的实现继承的关系(复用行为)
function Parent(){}
function Son(){}
//构建原型链
Son.prototype = new Parent();
let u1 = new Parent();
let u2 = new Son();
//true
console.log(u1.constructor == u2.constructor)
但有一个简单的逻辑问题,即u1与u2来自不同的构造器,但其内部构造器描述却指向了相同的构造器
原因只是因为new
创建后,默认构造器来自于其本身,而在创建原型链时,并不需要这一特性,简单的修正即可
Son.prototype.constructor = Son;
但此时已然有问题
function Parent(){}
function Son(){}
//构建原型链
Son.prototype = new Parent();
Son.prototype.constructor = Son
var u1 = new Parent();
var u2 = new Son();
//u2的构造器 == (来自)Son
console.log( u2.constructor == Son)
//u2的构造器(即Son)的原型 == parent的实例
console.log( u2.constructor.prototype instanceof Parent)
console.log( Son.prototype instanceof Parent)
//u2的构造器的原型的构造器 应该为parent实例的构造器 即Parent
console.log( u2.constructor.prototype.constructor == Parent)
console.log( Son.prototype.constructor == Parent) //与上列相同,显然这里被修正了
console.log( new Parent().constructor == Parent) //原型链中应有的表现
我们希望原型链如下进行显示
即我们手动修复Son.prototype.constructor = Son
后,在自动?
处也受到了影响,此时原型链中断
这里又有两个问题
1.为什么能成功
构造器修复可以说是"官方"提供的一种实现继承的方式
2.如何解决
这与我们理解的原型链不一样
js对象系统 - 内部原型链(proto)
实例拥有构造器的指向 【实例】.constructor
构造器拥有原型的指向 【构造器】.prototype
即实例若想寻找原型,需要通过构造器进行查询,但其内部还有一个属性__proto__即内部原型链
实际上,当如下代码生效时,继承关系就已经实现,具体原因则是对js来讲,真正实现原型链的属性为__proto__
function Parent(){}
function Son(){}
//构建原型链
Son.prototype = new Parent();
var u2 = new Son();
console.log(u2.__proto__ == Son.prototype);
console.log(Son.prototype.__proto__==Parent.prototype);
console.log(Parent.prototype.__proto__==Object.prototype);
内部原型链是js原型继承机制实现的底层实现
通过constructor与prototype所维护的构造器原型链,则是用户代码要回溯时才需要的
如果不需要代码回溯,不进行维护也是可以的
__proto__最早为火狐提供的属性,目的是为解决,js无法真正实现继承的属性。
js无法使用构造器原型链的另一个原因是,js内部本身对构造器原型链的维护异常
实际上,一个构造器function
的真正指向如下所示(盗图,画的比我好)
可以明显看到,没有 proto,原型链就会中断, null是一切对象的基础,这一真理也将不复存在
吐槽1
我们的目的是为了代码复用顺便进行优化(内存复用),原型继承通过两条原型链进行实现
- 构造器原型链
辅助开发人员回溯 - 内部原型链
真正的实现
没有需求的话,让构造器原型链见鬼去吧,否则,instanceof/typeof都是框架级继承实现需要考虑的重灾区
吐槽2
es6早都普及了,原型链也该下岗了吧