再看原型继承

关于语法中的代码复用,有两大方向,一种为宏,一种为继承,就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) //原型链中应有的表现

我们希望原型链如下进行显示


再看原型继承_第1张图片
1.png

即我们手动修复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);
再看原型继承_第2张图片
2.png

内部原型链是js原型继承机制实现的底层实现
通过constructor与prototype所维护的构造器原型链,则是用户代码要回溯时才需要的
如果不需要代码回溯,不进行维护也是可以的

__proto__最早为火狐提供的属性,目的是为解决,js无法真正实现继承的属性。
js无法使用构造器原型链的另一个原因是,js内部本身对构造器原型链的维护异常

实际上,一个构造器function的真正指向如下所示(盗图,画的比我好)

再看原型继承_第3张图片
3.png

可以明显看到,没有 proto,原型链就会中断, null是一切对象的基础,这一真理也将不复存在

吐槽1
我们的目的是为了代码复用顺便进行优化(内存复用),原型继承通过两条原型链进行实现

  • 构造器原型链
    辅助开发人员回溯
  • 内部原型链
    真正的实现

没有需求的话,让构造器原型链见鬼去吧,否则,instanceof/typeof都是框架级继承实现需要考虑的重灾区

吐槽2
es6早都普及了,原型链也该下岗了吧

你可能感兴趣的:(再看原型继承)