详解 原型、原型链、继承

302 阅读5分钟

1. 原型

原型的出现,就是为了解决构造函数的缺点。也就是给我们提供了一个给对象添加函数的方法,不然构造函数就只能给对象添加属性,不能合理的添加函数就太没有意义了。

1.1 prototype
  1. 每一个函数天生自带一个成员,叫做 prototype,是一个对象空间。
  2. 即然每一个函数都有,构造函数也是函数,构造函数也有这个对象空间。
  3. 这个 prototype 对象空间可以由函数名来访问。
function PersonInfo(){}
console.log(PersonInfo.prototype);//一个对象

所以既然是一个对象,那么我们就可以向里面放入一些东西

function PersonInfo(){}

PersonInfo.prototype.name = '原型';
PersonInfo.prototype.fn = function(){}
  1. 此时我们发现一个叫做prototype的空间是和函数有关联的。并且可以向里面存储一些东西
  2. 重点: 在函数的 prototype 里面存储的内容,不是给函数使用的,是给函数的每一个实例化对象使用的。

哪实例化对象该怎么使用呢 ?

1.2 proto

每一个对象都天生自带一个成员,叫做 __proto__,是一个对象空间;即然每一个对象都有,实例化对象也是对象,那么每一个实例化对象也有这个成员。这个 __proto__ 对象空间是给每一个对象使用的。

当你访问一个对象中的成员的时候。

  1. 如果这个对象自己本身有这个成员,那么就会直接给你结果。
  2. 如果没有,就会去 __proto__ 这个对象空间里面找,里面有的话就给你结果。

那么这个 __proto__ 又指向哪里呢 ?

  1. 这个对象是由哪个构造函数 new 出来的。
  2. 那么这个对象的 __proto__就指向这个构造函数的 prototype
function Person() {}

var p1 = new Person();

console.log(p1.__proto__ === Person.prototype); // true

我们发现实例化对象的 __proto__ 和所属的构造函数的 prototype 是一个对象空间。我们可以通过构造函数名称来向 prototype 中添加成员;对象在访问的时候自己没有,可以自动去自己的 __proto__ 中查找。

那么,我们之前构造函数的缺点就可以解决了。

  1. 我们可以把函数放在构造函数的prototype中。
  2. 实例化对象访问的时候,自己没有,就会自动去 __proto__ 中找,那么就可以使用了。
function PersonInfo() {}

Person.prototype.fn = function () {
  console.log('hello !');
}

var p1 = new PersonInfo();
p1.fn();

p1 自己没有 fn 方法,就会去自己的 __proto__ 中查找p1.__proto__就是 Person.prototype我们又向 Person.prototype 中添加了 sayHi方法所以 p1.fn就可以执行了。

  1. 到这里,当我们实例化多个对象的时候,每个对象里面都没有方法。

都是去所属的构造函数的 protottype 中查找;那么每一个对象使用的函数,其实都是同一个函数;那么就解决了我们构造函数的缺点。

p1Person 的一个实例;p2 Person 的一个实例;也就是说 p1.__proto__ p2.__proto__ 指向的都是 Person.prototype。当 p1 去调用 fn 方法的时候是去 Person.prototype 中找;当 p2 去调用 fn 方法的时候是去 Person.prototype 中找。那么两个实例化对象就是找到的一个方法,也是执行的一个方法。

  1. 结论

当我们写构造函数的时候

属性我们直接写在构造函数体内

方法我们写在原型上

2. 原型链

我们刚才聊过构造函数了,也聊了原型;那么问题出现了,我们说构造函数的 prototype 是一个对象

又说了每一个对象都天生自带一个__proto__属性。那么 构造函数的 prototype里面的 __proto__属性又指向哪里呢 ?

2.1 一个对象所属的构造函数
  1. 每一个对象都有一个自己所属的构造函数。比如: 数组。
// 数组本身也是一个对象
var arr = [];
var arr2 = new Array();

以上两种方式都是创造一个数组,我们就说数组所属的构造函数就是 Array

  1. 比如: 函数。
// 函数本身也是一个对象
var fn = function () {}
var fun = new Function();

以上两种方式都是创造一个函数;我们就说函数所属的构造函数就是 Function

2.2 constructor

对象的 __proto__ 里面也有一个成员叫做 constructor;这个属性就是指向当前这个对象所属的构造函数。

2.3 链状结构

当一个对象我们不知道准确的是谁构造的时候,我们呢就把它看成 Object 的实例化对象;也就是说,我们的 构造函数 的 prototype __proto__ 指向的是 Object.prototype; 那么 Object.prototype也是个对象,那么它的 __proto__ 又指向谁呢?

因为 Object js 中的顶级构造函数,我们有一句话叫 万物皆对象, 所以 Object.prototype 就到顶了,Object.prototype__proto__就是 null

2.4 原型链的访问原则

我们之前说过,访问一个对象的成员的时候,自己没有就会去 __proto__ 中找;接下来就是,如果 __proto__里面没有就再去 __proto__ 里面找; 一直找到 Object.prototype 里面都没有,那么就会返回 undefiend

2.5 对象的赋值

到这里,我们就会觉得,如果是赋值的话,那么也会按照原型链的规则来。但是: 并不是! 。赋值的时候,就是直接给对象自己本身赋值。如果原先有就是修改、原先没有就是添加。不会和 __proto__有关系。

3. 继承

继承:原型继承 => 实例继承于创建该实例对象的构造函数身上的方法或者属性

原型链继承 => 沿着原型链一层一层往下找,直到找到Object.prototype的时候为止

原型链终点:

1. null

2. Object.prototype(个人更推荐这种说法)

因为原型链的目的就是为了让实例化对象能够顺着这种链式查找规则一步一步往下找,而Object作为整个最大的构造函数

如果它的原型身上都没有,则继续往下找肯定也是找不到的