深耕系列之原型和原型链

455 阅读3分钟

先看以下代码

function Test() {}
var test = new Test()
console.log(test.name) 	// undefined

Object.prototype.name = 'hello'
console.log(test.name)		// hello

在这个例子中,先定义Test函数,再创建一个test实例,然后打印test.name,意料之中的打印undefined,但在赋值给Object.prototype.name后,再次打印test.name,显示为hello。

这是为什么呢?要想知道其内部机制,我们要深入理解原型和原型链等知识,下面进入正题:

prototype原型

函数可以有属性,每个函数都有一个特殊的属性叫做原型(prototype),如下所示

function Test() {}
console.log(Test.prototype)

放在控制台打印,会出现以下内容

image.png

可以看出prototype有两个属性:contructor和__proto__ . 这两个属性后续会继续说,那我们尝试给prototype加一下属性

function Test() {}
Test.prototype.name = "Test"
console.log(Test.prototype)

放在控制台打印,会出现以下内容

image.png

可以看出,prototype已经有了一个名为name的属性。

那么这个prototype有什么作用呢?

实际上,我们把prototype称为原型对象,对象以原型对象为模板,从原型对象那继承方法和属性,这样说有点抽象,我们看以下例子

function Test() {}
var test = new Test()
Test.prototype.name = "Test"
console.log(test.name)

放在控制台打印,会出现以下内容

image.png

可以看出,在原型对象定义的属性,通过new生成的实例对象test也拥有,我们来打印一下实例对象test,得到

image.png

可以看出,实例对象test有一个名为__proto__的属性,而里面保存着我们给原型对象定义的name属性。

那么__proto__是什么?和prototype的关系是什么?

__proto__

我们发现,test.__proto__ 和 Test.prototype 的内容很相似,我们来验证一下

image.png

显而易见,test.__proto__ 和 Test.prototype 都指向同一个对象,他们是相同的,这也解析了,在我们访问test.name时,会先判断test是否有name属性,如果没有,就去test.__proto__ 去寻找name属性。

除了__proto__属性,我们发现test对象还有一个constructor属性,这是什么来的呢?

constructor属性

我们来看看constructor属性都有哪些内容

image.png

可以发现,constructor属性的内容很像Test函数,实际上,每个实例对象test都会从原型中继承一个constructor属性,该属性指向了用于构造此实例对象的构造函数,也就Test。

image.png

既然test.constructor是指向构造函数Test,那意味这我们也可通过constructor属性来实例化一个新的对象。

var test2 = new test.constructor()

正常情况下我们不会这样使用,除非因为某些原因没有原始构造器的引用,也就是没有了Test的引用。

原型链

以上我们理解了prototype、__proto__ 和 constructor, 那么他们的关系是如何的,我画了一个图便于理解:

image.png

打印实例test同样可以验证图上显示的流程

image.png

可以知道的是,实例test从原型对象Test.prototype继承方法和属性,原型对象同样可能有原型,Test.prototype从Object.prototype继承方法和属性。这样一层一层类推,我们把这种关系称为原型链

原型继承的范围

我们知道Object对象拥有许多方法,比如Object.key(),Object.is() 等等,但是我们发现实例对象test继承的方法并没有这些,这是为什么呢?

原因在于,定义在prototype属性上的方法或属性才会被继承,也就是说test实例只会继承Object.prototype上的方法,而不会继承Object上的方法。

这看起来有点费解,构造器本身是一个函数,怎么可以给函数添加方法呢,实际上函数也是一个对象类型,可参考 function

因此我们给Test函数添加属性或方法,test实例是不会继承的,如

function Test() {}
var test = new Test()
Test.attr = '属性'
console.log(Test.attr)   // 属性
console.log(test.attr)	 // undefined

总结

以上就是我对js原型和原型链的理解,深入分析了prototype、__proto__、constructor之间的关系,也分析了原型继承的范围。如果觉得对你有帮助,请帮忙点个赞+评论+收藏。

最后留下一道题给大家:

// 希望实现一个Number类型的加法,期待num为3
var num = 1
num.add(2)

console.log(num)