彻底理解原型链

1,312 阅读8分钟

彻底理解原型链

引言:

终于搞懂了JS中的原型,原型链。

现在来总结其中的规律。

看懂图就看懂了原型链

02.png

这张图在我不理解原型链,_ proto _,prototype之前是根本看不懂的,但也是弄懂了这张图我才充分理解了原型链到底是怎么回事。

可以说,看懂这张图,你就大致理解了原型链。

但在看懂这张图之前需要一些前置知识

JS的引用数据类型

JS是7种基础数据类型和1种引用类型,这个引用数据类型就是Object,其中又含有(Function,Array,Map,Set,Math....)等多种分类,但这些都可以说是Object对象,一句话:引用类型皆对象

数据的储存

变量值都存在栈空间,但基础数据类型和引用数据类型有些不同:

基础数据类型的变量值直接存在栈中,但是引用数据类型的变量值是引用,如

let number1 = 1;
let func1 = function(){console.log('func1')} 

number1变量值为1,直接存在栈空间中,而func1接收了这个匿名函数的地址(func1变量的值就是这个函数的地址)

所有的函数都可以是构造函数

本文提到的所有构造函数只是与new相挂钩的一个概念,JS中并没有什么规定的构造函数,只是把new 后面的函数我们一般称为构造函数。

_proto _ 和prototype

先声明:JS没有实质上的类Class,es6之后的Class语法也只是语法糖而已,本质还是函数还是对象。继承在生活中很常见,JS也想实现这种继承关系,于是有了_proto _(原型链)

tip:这里插入一条与主线无关的知识:_proto _在之前ECMA规范是不希望JS使用者来操作的一条属性,所以有下划线,在之后由于某些原因承认了这条属性并创建新的获取原型对象的一个方法 Object.getPrototypeOf(),在这里由于习惯我统一使用了proto属性,但更推荐使用这个方法。

_proto _表示原型链,表示这个对象继承的对象指向。现在有了proto来表示继承的关系,那这个prototype又是什么鬼?

先来看一段代码:

function ClassA(value){
	this.Avar = value;
	this.FuncXX = ()=>{
	console.log('FuncXX')
	}
}
a1 = new ClassA('a1');
a2 = new ClassA('a2');
console.log(a1._proto_ === ClassA.prototype)

我们先不看这个打印里的关系和new内部操作,做个大胆的假设: ClassA是一个构造函数,这个对象中存放了这个类的构造操作,那new出来的对象是指向ClassA这个对象? 显然不是,这里a1和a2都各自开辟了一个新的对象空间。

那a1和a2这两个实例的_proto _都指向ClassA?

假设是这样,通过new ClassA出来的两个对象都有了ClassA的两个属性,一个是Avar,一个是FuncXX。如果了解过执行上下文的同学会知道,每次调用ClassA这个函数都会创建一个执行上下文,在ClassA这个函数中每次都会开辟这个箭头函数的内存并把引用传给FuncXX。

这样一来,a1和a2两个对象同时存放了这两个属性的值,对于基础数据类型没什么问题,不同的实例可以拥有自己的属性进行修改独有,对于函数而言,大部分情况下实例的操作方法都应该是一样的(尤其是实现某个功能下)。但是上段提到,每创建一个实例就会开辟一个方法内存,这样是很浪费空间的,于是有了prototype——包含构造函数想要共享属性的一个对象

我们设置一个引用指向这个拥有共享属性的对象(ClassA.prototype ),而构造函数这个对象中一般就仅仅存放构造实例的操作。构造函数这个对象和这个prototype对象从声明函数后就默认绑在了一起,而且一般是双向绑定,除非你自己修改指向。

03.png

ClassA只是个构造函数而已,所以实例的proto_自然要指向这个原型对象。

下面是理解开头那张图和原型链最重要的三个对象的结构!!

04.png

可以看到就是上图基础上多加了两个_proto _指向的两个实例。这三个对象分别是构造函数对象,构造函数的原型对象,实例对象。那么接下来再讲解new到底做了什么。

new到底做了什么

上代码:

function ClassA(){
	this.val = 'value';
}
a = new ClassA();

//下面是模拟new操作的函数
function new_func(constructor,...args){
    let obj = {};
    let result = constructor.call(obj,args);
    obj._proto_ = constructor.prototype;
    if (typeof result === 'object'){
        return result;
    }
    return obj;
}

四步走:

  1. 创建空对象obj
  2. 构造函数的call调用(this赋为obj)
  3. 设置原型链,实例指向构造函数的原型对象
  4. 分情况返回:如果构造函数返回对象则直接返回这个对象,否则返回obj

这样就可以清楚的理解为什么实例的proto指向了原型对象了。

来看图

02.png

图中只有两个地方不遵循那个三个对象的结构,下文会一一讲到。

从上往下看:

第一个出现的是f1,f2,Foo()函数的实例对象,由上段可以知道它的proto指向Foo构造函数的原型对象,看到这个原型对象马上可以对应那个三个对象的联系结构构造函数——原型对象——实例对象

接下来我们发现多出两个箭头:Foo的原型对象多出引用和Foo构造函数多出了引用。

  1. 我们先来看原型对象的引用,指向了Object构造函数的原型对象,上段讲到三个对象的联系中,实例proto会指向构造函数的原型对象,这里意味着Foo的原型对象就是Object的实例。再想想Foo的原型对象就是对象是不是就很容易相通了。这里又是Object构造函数——原型对象——实例的三个对象结构。除开这些箭头,在Object.prototype上出现了一个第一个很特殊的引用:null,这里是原型链的尽头。

  2. 我们再回到Foo构造函数的引用,可以对比上段:实例对象都有总的对象原型,构造函数也应该有一个总的构造函数才对。没错,这里的箭头就是指向所有的构造函数的原型对象Function.prototype。这里出现了第二个特殊的引用:Function._proto _ === Function.prototype ,按道理来说,实例才会指向原型对象啊,怎么这里是构造函数呢?这里我的解释是:因为这是总构造函数,总构造函数构造了自己的构造函数,所以Function既是构造函数也是实例对象。接下来还有一个很重要的点:Function.prototype也是个对象,所以proto指向Object的原型对象

讲到这里,其实这张图就已经讲解大半了,因为另一条线从o1,o2(Object构造函数的实例)也是一样的。

这里说下大致,o1的实例还是指向Object的原型对象,Object构造函数指向总构造函数的原型对象。

图已经全部讲解完了。

总结一下:原型链就是通过实例通过proto这个属性把与之相关的所有原型对象链接起来的一条链

实践:面试题

在看题目前还需要知道一个知识点,对象的属性会怎么查找

对象的属性是怎么查找的

规则:查找对象的属性会先看本身对象是否有这个,没有就会通过proto引用来继续,还是没有继续proto引用查找,一直到在原型对象中找到并返回或者找到了Object.prototype.proto(null)返回undefined。

//手写模拟 
function SearchProp(obj,varString){
    	if (obj == null)
            return undefined;
    	if (obj.hasOwnProperty(varString)){
            return obj.varString;
        }
    return SearchProp(obj._proto_);
}
看题

var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);

我们一行行来解释发生了什么:

  1. 把声明的匿名函数赋值给了A,A此时就是这个函数的引用。即A.prototype 与A构造函数构成双向连接。

  2. A的原型对象中添加n属性 n=1

  3. 创建实例b,此时 b 与 A与 A的原型对象构成三对象结构

此时的关系如下:

05.png

  1. A的prototype属性指向另一个对象,对象里有n和m的值

  2. 创建实例c,此时的c的proto指向A新的原型对象

最终关系如下:

06.png

对象属性查找规则:从自身到原型链尽头一层层找下去。

来看四行打印:

b.n :从自身到A的原先原型对象找到了n 打印1

b.m: 从自身到A的原先原型对象再到Object.ptototype再到null 打印undefied

c.n: 从自身找到了A的新的原型对象找到了n 打印2

c.m: 从自身找到了A的新的原型对象找到了m 打印3

var F = function() {};
Object.prototype.a = function() {
  console.log('a');
};
Function.prototype.b = function() {
  console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b();

这里直接放图

07.png

过程我就不分析了,这里直接看打印语句

f.a(): 从自身到F原型对象到Object原型对象找到a函数执行 打印a

f.b(): 从自身到F原型对象到Object原型对象到null 报错

F.a(): 从自身到Function原型对象到Object的原型对象找到a函数执行 打印a

F.b(): 从自身到Function原型对象找到b函数执行 打印b

关于原型链的题,个人觉得把关系像这样画出来就很清晰了,本文就只展示这两道题,其他题举一反三。

本文由自己总结,有写的不对的地方请各位大佬进行指正!

参考文章:

2019 面试准备 - JS 原型与原型链 - 掘金 (juejin.cn)

深入JavaScript系列(六):原型与原型链 - 掘金 (juejin.cn)