源解 0 - 你怎么理解原型链?

275 阅读3分钟

很透彻的权威解释 MDN

背景

js 是一个面向对象的动态脚本语言,而设计者当初10日创造javaScript,参考了当时面向对象语言程序设计,又考虑到是为Web做脚本动态化服务的,因此需要尽可能轻量,就舍弃了面向对象语言中的 Class 概念,但是核心设计理念,比如封装 继承 多态等需要保留,而针对继承,就设计了js原型链来实现js 继承。

设计原理

抽象出某些类共有的属性 行为,封装为一个类(父类),而当某个类(子类)需要实现这些属性,行为,就可以直接继承这个类(父类),这就是继承最大的功能.

那么原型链如何实现的上述功能?

首先js万物皆为对象,包括Function也是一个对象,所以要实现原型继承,还得从对象下手 !
那么设计者就在每个对象里都加入了一个属性 从最顶端的Object 到你实例化的一个var a = [] 数组对象都有这个属性 _ proto _.
有什么用呢?

这个_proto_属性存储着一个引用,指向它的构造函数的原型对象 (prtotype)。

原型对象当然也有一个 _ proto _,引用着它的原型对象,就这样层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为原型链的结束.

也就是我们常见的

let a = [];
a._proto_ = Array.prototype // 所以a可以调用有数组方法

看不懂没关系,我们慢慢来解析上边这句话\

1.一个新名词原型对象

首先原型对象顾名思义就是一个对象,所以我们可以对这个对象的属性进行 修改 增加等操作

// 覆写之前的Array.find() 函数
Array.prototype.find = function(){

}
// 添加属性
Array.prototype.myAttr = 'test'

那么问题来了,这个原型对象在什么时候创建的?又在何处引用呢?
js 里除了每个对象有_proto_属性外,每个函数在创建时都附带创建一个原型对象(保存在堆中),而对此原型对象的引用就保存在函数的 prototype 属性中(别忘了函数也是对象)

2. 构造函数

js函数都可以作为构造函数,因此这里的构造函数,也可以理解为new了一个对象的函数

function myFun(num){
    const  a = 0;
    const b = {};
    this.c = num;
    
}
let myObj = new myFun(2);
myObj._proto_ = myFun.prototype

3.为什么要指向构造函数的原型对象呢

因为原型对象中,存储的就是可以复用与继承的属性与方法 !
这是不是类似于我们抽象出来的超类或者父类?\

所以通过_proto_能访问到其构造函数的原型对象,也就能获取到可以继承的属性与方法,比如 数组的length属性的get,数组的find() every() 方法等

 let arr = [1,2,3,4,6];
 console.log(arr.length);

当你访问arr.length,发现并没有这个length 属性,就通过_proto_找到arr的原型对象,也就是Array.prototype,再找一找length,找到了! 如果依旧没找到,继续通过_proto_找到原型对象... 大部分最终都会到Object.prototype._ proto _ = null ,也就结束了.

image.png 来自一篇文章,更详细的可以看看

补充

  1. 每个对象都有 __ proto __ 属性,但只有函数对象才有 prototype 属性

  2. constructor属性,只有 prototype对象 才有这个属性,这个属性很简单,就是从prototype对象指向构造函数,而构造函数的peototype属性是从构造函数指向prototype对象,相对应.而其它对象的constructor属性(比如你声明的arr 也有constructor属性)都是通过__proto__对象从prototype对象继承而来。

  3. 函数在创建时,JS会为该函数创建一个对应的prototype对象,而这个prototype对象的constructor属性又指向该函数,即Foo.prototype.constructor === Foo