原型与原型链

320 阅读7分钟

什么是原型?

原型:每个JS对象(null除外)在创建的时候都会有一个与之关联的对象,这个对象就是我们所说的原型,每个对象都会从原型继承属性,我们通过实例对象的__proto__来查找它的原型。
要理解什么是原型,首先要理解,原型的作用是为了共享多个对象之间的一些共有特性(属性或方法)
例如:我们创建两个不同的数组,不同数组有不同的内容和长度,这是每个数组实例私有的属性,但是不同的数组却都可以使用push等方法,这些公共方法就是从原型中就继承的。我们可以在控制台打印不同数组的push方法,发现它们是严格相等的,因此可以确定,这个方法是从同一个原型(同一个内存地址)中获取得到的。
举个栗子:

function Test(){}
let test=new Test()
console.log(test)
console.log(test.__proto__)
console.log(Test.prototype)
console.log(Test.prototype===test.__proto__)

控制台输出:

image.png

我们根据创造函数创造出一个实例对象,实例对象的__proto__属性指向的就是它的原型对象,同时我们输出构造函数的prototype属性,可以发现Test.prototypetest.__proto__是严格的,根据以上的关系,可以先得出以下的思维关系图:
image.png

根据控制台输出可以发现,在原型对象Test.prototype中存在constructor属性,我们称之为构造器。这个属性用于构建函数对象,该属性返回对创建此对象的函数对象的引用。同时考虑到实例test也是有函数Test构建的对象,因此我们在控制台输出:

console.log(Test.prototype.constructor)//f Test()
console.log(test.constructor)//f Test()

补全思维关系图:

image.png

以上我们完成了一个简单原型的概念闭环。

什么是原型链?

我们再回顾一遍原型的概念:
每个JS对象(null除外)在创建的时候都会有一个与之关联的对象,这个对象就是我们所说的原型,每个对象都会从原型继承属性。

对照以上思维图,我们发现,既然根据Tset函数创建的实例有原型,那Test函数实际上也是某个函数创建的实例对象,是不是也应该有原型呢?而test实例的原型Test.prototype本质上是一个对象,它是不是也应该有原型呢?

先来讲Test.prototype这边

毫无疑问,Test.prototype是有原型的,我们通过Test.prototype.__proto__可以获取到其原型,同时我们可以看到该原型的constructor是JS内建对象Object
此处先补充JS内建对象的一些概念:JS有5种基础的内建对象:ObjectFunctionErrorSymbolBoolean,而Object/Function尤为特殊,是定义其他内建对象或者普通对象和方法基础。

image.png

根据这个再补充思维关系图:
image.png

然后根据这一步接着深入,那么test实例的原型的原型存不存在原型呢?我们再回顾下之前关于内建对象的定义:JS中的其他内建对象或者普通对象和方法都是由内建对象Object定义的,如果Object.prototype存在原型,那么这个原型是由什么创建的呢?只能是Object,这就失去了原型的意义,因此Object.propotype.__proto__为null(个人理解,后续考虑从内建对象原理来进行分析),补充思维导图:
image.png

我们结合以上思维导图和原型的概念,可以得出以下原型链的概念:
原型链:当我们查找对象的属性或方法时,如果在当前对象中找不到定义,会继续在当前对象的原型对象中查找,如果在原型对象中依然没有找到,则会继续在原型对象的原型中进行查找,直到找到为止,如果查到最顶层的原型对象中依然没有找到,则结束查找,返回undefined,每个对象都有一个到它自身原型对象的链接,这些链接组件的整个链条就是原型链。

构造函数Test这边

我们首先输出Test函数的原型和原型的构造函数:

console.log(Test.__proto__)
console.log(Test.__proto__.constructor)

输出:

image.png

可以看到,构造函数的原型是一个内置函数,而这个原型的构造函数是JS内建对象Function,补全思维图:
image.png

我们继续获取这个内置函数的原型:

console.log(Test.__proto__.__proto__)
console.log(Test.__proto__.__proto__.constructor)

image.png

根据输出,补全思维图:
image.png

这样,我们就把整个流程图实现闭环。

补全思维流程图

我们继续看上述的流程图,它真的完整了吗?
实例test是否有prototype?
内建对象Function的原型是什么?
内建对象Object的原型是什么?
内置函数f()的prototype是什么?
Test,Object,Functionconstructor是什么?
我们在控制台中对这些内容进行打印:

console.log(test.prototype)
console.log(Function.__proto__)
console.log(Object.__proto__)
console.log(Function.prototype.prototype)
console.log(Test.constructor)
console.log(Function.constructor)
console.log(Object.constructor)

image.png

由此可知:__proto__存在于所有实例对象(null除外)中,这个结论正好契合了原型的概念,而prototype存在于函数对象中
同时补全思维流程图:
image.png

学以致用:

根据以上的流程图,我们对原型和原型链应该有了一些基础的理解,尝试一下以下题目:

1.原型与原型链中的流程关系

参照思维导图即可完成

function Test() {}
let test = new Test();
console.log(test.__proto__)
//实例对象的__proto__指向的是它的原型,即它的构造函数的prototype:Test.prototype,
console.log(test.__proto__.__proto__)
//即Test.prototype.__proto__,Test.prototype本身是一个对象,因此输出为:Object.prototype
console.log(test.__proto__.__proto__.__proto__)
//即Object.prototype的__proto__,输出为:null
console.log(test.__proto__.__proto__.__proto__.__proto__)
//null并没有原型,因此报错
console.log(test.constructor)
//Test
console.log(test.prototype)
//undefined test是实例对象,不是函数对象,没有prototype属性
console.log(Test.constructor)
//Function
console.log(Test.prototype)
//Test.prototype这个对象里所有的方法和属性
console.log(Test.prototype.constructor)
//Test
console.log(Test.prototype.__proto__)
//Test.prototype是对象,所以输出:Object.prototype
console.log(Test.__proto__)
//Function.prototype
console.log(Function.prototype.__proto__)
//Object.prototype
console.log(Function.__proto__)
//Function.prototype
console.log(Object.__proto__)
//Function.prototype
console.log(Object.prototype.__proto__)
//null

2.原型链查找

根据原型链的概念:如果在当前对象中找不到定义,会继续在当前对象的原型对象中查找。那如果在当前的实例对象中存在这个属性或方法的定义,而在实例的原型中也存在,是否只会查找当前实例中的方法,找到之后就停止了查找?我们来看以下的例题:

function Fn() {
    this.x = 100
    this.y = 200
    this.getX = function() {
        console.log(this.x + 'inner')
    }
}
Fn.prototype.getX = function() {
    console.log(this.x)
}
Fn.prototype.getY = function() {
    console.log(this.y)
]}
let f1 = new Fn()
console.log(Fn)
console.log(Fn.prototype)
console.log(f1.getX)
console.log(f1.getY)

image.png

因此可以看到,如果实例对象内部有该属性,调用的时候会调用实例对象的属性,可以理解为既然已经找到该属性,那么对于该属性的后续查找自然也就不存在了。
我们对上述的例题进行拓展:

function Fn() {
	this.x = 100;
	this.y = 200;
	this.getX = function () {
		console.log(this.x);
	}
}
Fn.prototype.getX = function () {
	console.log(this.x);
};
Fn.prototype.getY = function () {
	console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
//fasle,因实例内部有该属性,因此getX是实例的私有属性,两个不同实例的私有方法不是严格相等
console.log(f1.getY === f2.getY);
//true,都是从相同的堆内存Fn.prototype中获得的方法
console.log(f1.__proto__.getY === Fn.prototype.getY);
//ture,f1.__proto__===Fn.prototype
console.log(f1.__proto__.getX === f2.getX);
//fasle,前者为undefined(this指向问题),后者为100
console.log(f1.getX === Fn.prototype.getX);
//false,和上述一致
console.log(f1.constructor);
//Fn
console.log(Fn.prototype.__proto__.constructor);
//Object
f1.getX();
//100
f1.__proto__.getX();
//undefined
f2.getY();
//200
Fn.prototype.getY();
//undefined

反向拓展:

image.png

我们再回过头来看这个思维图,我们从底层的实例test开始,通过原型链一步步晚上到顶层的Object,那我们能不能考虑从顶层的Object开始,一步步实现一个实例test呢?同时对于顶层的内建对象,我们是不是还有些一知半解的地方:
1.为什么Function,Object,构造函数Test的原型都是Function.prototype?
2.为什么Function.proto===Function.prototype?
3.为什么Function.prototype是一个函数,但是打印Function.prototype.prototype却是undefined?
带着这些问题,我们下一篇从JS内建对象开始对原型和原型链再进行一些扩展。

写在最后:

第一次尝试把笔记整理成文章的形式,写的东西比较浅显,希望各位大神不吝赐教,一起探讨。如果哪些概念有偏差或者错误的麻烦大家不吝指正哈!