前言
JavaScript是一门面向对象的语言,所有的对象都从原型继承属性和方法,那么什么是原型?对象与对象之间如何实现继承?
本文就带大家来深入理解下JavaScript中的原型,欢迎各位感兴趣的开发者阅读本文。
原理解析
接下来我们来逐步分析下原型与对象之间的关系。
原型对象
我们使用function
关键字来创建函数时,内存中会创建一个包含prototype
属性的对象,这个属性指向函数的原型对象,如下所示:
function Person() { } Person.prototype // {constructor: Person(), __proto__}
上述代码中我们创建了一个名为Person的函数:
- prototype属性指向的就是Person的原型(每个除null外的JS对象在创建的时候都会关联到另一个对象,这个关联的对象就是原型)
- 每个对象都会从原型“继承”属性
- 原型对象里包含
constructor
与__proto__
属性。
image-20210310173710555
我们画个图来描述下Person
与prototype
之间的关系
image-20210310195733848
用new运算符调用的函数便为构造函数,建议构造函数命名时将首字母大写。
函数实例与原型对象的关系
上个章节我们捋清了构造函数与原型对象的关系,接下我们来看下函数实例与原型对象之间的关系。
我们用运算符new
将上个章节创建的Person
函数进行实例化,得到person实例,代码如下:
// 实例化对象 const person = new Persion();
在上个章节中,我们知道原型对象有2个属性,其中__proto__
是每一个除null外的JavaScript对象都具有的一个属性,它指向该对象的原型对象。
接下来,我们来证明下person.__proto__
是否和Persion.prototype
相等,代码如下:
function Person() { } const person = new Persion(); console.log("函数实例的__proto__指针指向构造函数的原型对象: ", person.__proto__ === Person.prototype);
执行结果如下:
image-20210312172907318
除了使用
__proto__
来访问原型对象,我们还可以使用Object.getPrototypeOf()来获取。
证明出他们相等后,结合构造函数与原型对象可知他们三个之间的关系,如下所示:
image-20210310202939966
当我们实例化一个构造函数时,也会为这个实例创建一个
__proto__
属性,这个属性是一个指针,它指向构造函数的原型对象。
由于同一个构造函数创建的所有实例对象的
__proto__
属性都是指向其构造函数的原型对象,因此所有的实例对象都会共享构造函数原型对象上的属性和方法,因此,一旦原型对象上的属性或方法发生改变,所有的实例对象都会受到影响。
原型对象与构造函数的关系
上个章节我们分析了原型对象中__proto__
的指向,接下来我们来分析下constructor
的指向。每个原型对象都有一个constructor
属性,它指向该对象的构造函数。
接下来,我们来证明下Person.prototype.constructor
是否和Person
相等,代码如下:
function Person() { } const person = new Person(); console.log("原型对象与构造函数相等: ", Person.prototype.constructor === Person);
执行结果如下:
image-20210310211445102
证明出他们相等后,我们结合构造函数、函数实例、原型对象可知他们四个之间的关系,如下所示:
image-20210310212117231
获取对象原型,除了访问它的prototype外,我们还可以使用Object.getPrototypeOf()来获取。
实例属性的读取顺序
读取实例中的属性时,如果找不到,就会查找该对象原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
接下来,我们举个例子来证实下上述话语:
function Person() { } Person.prototype.name = "原型上的name属性"; const person = new Person(); person.name = "实例上的name属性"; console.log(person.name) // 实例上的name属性 delete person.name; console.log(person.name); // 原型上的name属性 delete Person.prototype.name; console.log(person.name); // undefined
我们来分析下上述例子:
- 向原型上添加了name属性
- 向实例上添加了name属性
- 此时,name的属性值为实例上的name属性
- 删除了实例上的name属性
- 此时,它就会找原型上的name属性,因此值为原型上的name属性
- 删除了原型上的name属性
- 此时,他就会找原型的原型的name值,原型的原型不存在name属性,因此返回undefined
在上面的分析中,我们在Person的原型的原型中没找到name属性,那么Person的原型的原型是什么呢?我们在谷歌浏览器的控制台来打印下看看,如下所示:
image-20210310222010988
正如上图结果所示,Person的原型的原型是一个对象,证明出了原型也是一个对象,那么我们就可以用最原始的方式创建它,代码如下所示:
const object = new Object(); object.name = "对象中的属性"; console.log(object.name); // 对象中的属性 console.log("object实例与Object的原型对象相等", object.__proto__ === Object.prototype); console.log("Object的原型对象与构造函数相等", Object.prototype.constructor === Object);
执行结果如下:
image-20210312175303383
知道原型也是对象后,结合我们上面所证明出来的内容,他们之间的关系如下所示:
image-20210310224603680
原型链
通过前面的分析我们知道了万物基于Object,那么Object的原型是什么呢?答案是null
我们在谷歌浏览器的控制台上验证下,结果如下所示:
image-20210310231037617
综合上述,他们最终的关系如下所示:
image-20210310232450892
图中橙色线条组成的链状结构就是原型链。
重写原型对象
我们在实现实现一些功能时,经常会用一个包含所有属性和方法的对象字面量来重写整个原型对象。
如下所示:
Person.prototype = { name: "神奇的程序员", age: "20", job: "web前端开发", sayName: function () { console.log(this.name); } }
- 将Person的原型指向了一个新的对象
- 原型上拥有三个属性和一个方法
- 对象中不存在constructor属性
由于重写的对象中不存在constructor属性,那么它的constructor属性将会指向Object。
我们来验证下,代码如下所示:
console.log("Person的原型对象的构造函数与Person构造函数相等", Person.prototype.constructor === Person)
执行结果如下:
image-20210312211047400
如果constructor的值很重要,那么我们就需要特意将constructor的指向改为构造函数了,代码如下所示:
Person.prototype = { name: "神奇的程序员", age: "20", job: "web前端开发", sayName: function () { console.log(this.name); }, constructor: Person } console.log("Person的原型对象与Person构造函数相等", Person.prototype.constructor === Person)
执行结果如下:
image-20210311000952986