原型、原型链

93 阅读7分钟

原型、原型链

之前自己学习的一篇笔记,整理出来共同学习

一、继承机制设计思想

1、javascript初始只想作为一种简易的脚本语言,并不需要有“继承”的机制;但javascript里都是对象,必须有一种机制将对象联系起来。没有选择class,选择new命令,生成实例。

2、javascript中,new命令后跟的不是类,而是构造函数

function DOG(name){
  this.name = name;  //构造函数中的this关键字,它就代表了新创建的实例对象。
}
var dogA = new DOG('大毛');
alert(dogA.name); // 大毛

3、new命令的缺陷

不能共享属性与方法;每一个实例对象,都有自己的独立的属性和方法。

   function DOG(name){
    this.name = name;
    this.species = '犬科';
  }
  var dogA = new DOG('大毛');
  var dogB = new DOG('二毛');
  dogA.species = '猫科';
  alert(dogB.species); // 显示"犬科",不受dogA的影响

4、引入prototype

赋于构造函数一个prototype属性,该属性为一个Object类型(称之为prototype对象)

所有实例对象需要共享的属性和方法,都放在这个prototype对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

 function DOG(name){
    this.name = name;
  }
  DOG.prototype = { species : '犬科' };
  
  var dogA = new DOG('大毛');
  var dogB = new DOG('二毛');
  alert(dogA.species); // 犬科
  alert(dogB.species); // 犬科
  
  DOG.prototype.species = '猫科';
  alert(dogA.species); // 猫科
  alert(dogB.species); // 猫科

总结:由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。这就是Javascript继承机制的设计思想。

from: 阮一峰Javascript继承机制的设计思想

二、原型是什么?

从(一)中描述,粗暴来说,原型就是一个对象,构造函数的prototype属性。

ES2019规范中定义prototype为:object that provides shared properties for other objects

prototype本身也是对象,只是承担了某种特殊职能。(类似一个约定)如果这个对象失去了这个职能,它就不再是prototype。

说"prototype对象"就等于”xxx对象的prototype的对象“的简单描述,如果不同其他对象产生关系,也就无法构成这个prototype称谓。

因此,构造函数的原型:你能通过构造函数中名为prototype属性,访问到原型对象,prototype是函数独有的属性

image-20201116164243630.png

原型跟构造函数的关系,图解如下:

image-20201116171559556.png

有了构造函数,我们就能在原型上创建可以继承的属性,并且通过new命令创建实例:

image-20201116172428469.png

instance实例验证

image-20201116173500257.png

proto隐式引用

every object has an implicit reference(called the object's prototype)

__ proto __这是每一个JavaScript对象(除了null)都具有的一个隐式引用,被称为这个对象的prototype原型。

*注:__proto__最初是一个非标准属性,ES6已将其标准化,可以用标准方法Object.getPrototypeOf()替代

Object.getPrototypeOf(person);
person.__proto__

image-20201129104322424.png

obj被隐式地挂载了另一个对象的引用,放在__proto__属性中。隐式,意味着不是由开发者亲自操作的。

被隐式的挂载:操作上,偷偷做了挂载属性的动作。

隐式引用:这个属性不能直接被访问。

ECMAScript 规范描述 prototype 是一个隐式引用,但之前的一些浏览器,已经私自实现了__proto__这个属性,使得可以通过 obj.__proto__这个显式的属性访问,访问到被定义为隐式属性的 prototype。

1)通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象。

2)通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象。

3)部分浏览器提前开了__proto__的口子,使得可以通过obj.__proto__直接访问原型,通过 obj.__proto__ = anotherObj 直接设置原型。

4)ECMAScript 2015 规范只好向事实低头,将__proto__属性纳入了规范的一部分。

image-20201129110819439.png

从表面上看,对象中存在一个__proto__属性,其实只是为了开发者方便故意渲染的虚拟节点,虽然跟该对象的其他属性并列,但并不存在该对象中。

__proto__属性既不能被 for in 遍历出来,也不能被Object.keys(obj)查找出来。访问对象的obj.__proto__属性,默认走的是 Object.prototype 对象上__proto__属性的 get/set 方法。

因此,普通对象创建时,只需要将内部的隐式引用指向Object.prototype,就兼容了__proto__属性访问行为,不需要将原型隐式挂载到对象的__proto__属性。

image-20201116175442431.png

image-20201116175639509.png

三、原型的继承方式

显式继承

Object.setPrototypeOf(obj_b,obj_a)   // 将obj_a设置为obj_b 的原型
Object.create(obj.a)                 // 以obj_a为原型,生成一个对象

隐式原型继承

如果没有隐式原型继承,创建一个普通的js对象:

image-20201129132634687.png

让开发者无感知创建,继承,初始化的过程:

1)将某些函数称之为constructor,专门用于做属性的初始化

2) 约定 constructor有特殊的属性——prototype

3)通过new关键字,创建新对象

4)new 内部偷偷做关联原型,属性初始化

image-20201129133346864.png

constructor 函数,描述如何初始化对象的属性即可。除非需要新增方法,否则都不必操作 constructor 的 prototype 对象。

实例 —— 构造函数——原型:new干了什么

var obj  = {};                           // 1、创建一个新的空对象
obj.__proto__ = Base.prototype;          // 2、将空对象的隐式原型指向显示原型
Base.call(obj);                          // 3、将Base函数对象的this指针替换成obj,然后再调用Base函数

(1) 创建一个新对象; (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ; (3) 执行构造函数中的代码(为这个新对象添加属性) ; (4) 返回新对象。

constructor属性(!=Constructor构造函数)

普通函数创建时,自带prototype属性,该属性是一个对象,包含一个constructor字段,指向构造函数。

image-20201116181932910.png

原型链

粗暴来说:原型链就是原型继续向上寻找原型,直到找到为null为止

原型同样也可以通过 __proto__ 访问到原型的原型,比方说这里有个构造函数 Student 然后“继承”前者的构造函数 Person,然后 new Student 得到实例 student

当访问 student 中的一个非自有属性的时候,就会通过 __proto__ 作为桥梁连接起来的一系列原型(Student.prototype)、原型的原型(Person.prototype)、原型的原型的原型(Object.prototype)直到 null为止。

这个搜索的过程形成的链状关系就是原型链。

从数据结构角度看 , 原型链是一个隐式的单向链表,并且prototype屏蔽了继承数据的能力。

隐式属性访问让程序更不可靠

所有属性和方法,都是不可靠的,它们很容易通过原型继承后,加以篡改。

如果大家都往原型上挂载自己编写的方法,特别是挂载到 Object, Array, Number 等全局构造函数的原型上。所有代码都变得更不可靠,所有行为都更加难以预测,所有经验都更难复用。

每个开发者都难以控制,访问属性访问的是哪里的数据,调用方法调用的是谁编写的方法?

这种做法,相当于对全局变量和命名空间的滥用。如无必要,不要随意往原型上拓展方法,特别是全局构造函数里的原型。

四、ES6 class

语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法而已。

function Point(x, y) {
  this.x = x;
  this.y = y;
}
​
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
​
var p = new Point(1, 2);

改写

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
​
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
   // 公有方法
  foo (baz) {
    this._bar(baz);
  }
​
  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }
}
​
var point = new Point()

继承

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
​
  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

参考:重塑JS原型