原型链与继承查找机制
当你访问一个对象的属性
或方法
时,如果这个对象本身没有这个属性
或方法
,那么js
会在这个对象的原型中寻找这个属性或方法,如果找到了,就会使用它, 如果还是找不到,就会在原型的原型中寻找,以此类推,直到找到为止, 而继承
的关键,也就在于自定义修改原型
的指向!
所以当你把之前的原型链
图分析透彻,你就会知道原型链
就是通过__proto__
属性形成的,任何对象普通对象
和函数对象
都有__proto__
属性,并且其核心思想
也就是通过__proto__
这个链条来进行查找数据
!
DOM原型链的形成
这其实也很好的解释了我们javascript
中DOM属性和方法
也是这样子进行查找的
html
<div id="connent"></div>
js
var oDiv = document.getElementById("connent");
console.log(oDiv.__proto__);
console.log(oDiv.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
结果如下
其实这样你就会知道当一个DOM
元素在使用某个属性和方法的时候,是怎么进行查找的它的链条
也是通过__proto__
来进行查找的,对吧! 其中顺序如下
↓HTMLDivElement
↓HTMLElement、
↓Element、
↓Node、
↓EventTarget
↓Object
通过__proto__
就形成了JavaScript
中与DOM文档对象模型
相关的概念了
这里也给大家简单介绍一下,方便理解!
HTMLDivElement
: 这是一个代表 HTML<div>
元素的类, 它继承了HTMLElement
的属性和方法,包括可以用来改变元素样式的属性和方法,当然这里我只是举个栗子,不一定就是div元素
根据你打印的情况决定!HTMLElement
: 这是一个基础类,代表任何 HTML 元素, 所有的HTML
元素都继承了HTMLElement
的属性和方法Element
: 这是一个基础类,代表任何HTML 或 XML
元素, 它定义了所有元素共享的属性
和方法
,例如getAttribute()
和setAttribute()
Node
: 这是所有DOM
节点的基类,包括元素、文本节点、注释
等, 它定义了一些通用的属性
和方法
,如parentNode
和childNodes
。EventTarget
: 这个接口表示可以添加或删除事件监听器的事件目标
Object
: 这个也就是顶层的Object构造函数
在w3c也有这些属性和方法的详细解释
如图
而且这些继承
关系这样子一直走下来查找的属性
和方法
的关键就是原型链
, 可以说没有原型链
就没有现在的javascript
当查找对象
的某个属性
或方法
的时候,首先在当前对象
中查找,如果没有去对象的__proto__
中去查找, 这样子一直到最顶层null
,而这样的__proto__
形成的一条查找链条
就是原型链
现在你可以感受一下是不是如此呢!
并且继承
也就是修改原型的指向,即__proto__
或prototype
以上这些类
和接口
一起构成了JavaScript
的 DOM API应用程序编程接口
, 这样来允许我们以代码编程方式操作网页中的元素内容、结构和样式。
DOM
中所有的属性
和方法
你都可以看做为一个原型链的继承关系!
其实你可以去通过js
创建一些xml、svg、普通元素
以及文档模型!
代码如下
// 创建一个新的XML文档
var xmlDoc = document.implementation.createDocument(null, null);
// 创建根元素
var root = xmlDoc.createElement("root");
xmlDoc.appendChild(root);
// 创建一个子元素
var child = xmlDoc.createElement("child");
// 设置子元素的内容
var childText = xmlDoc.createTextNode("This is a child element");
child.appendChild(childText);
// 将子元素添加到根元素
root.appendChild(child);
// 打印XML文档
console.log(xmlDoc.__proto__);
console.log(xmlDoc.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
var oDiv = document.getElementById("oDiv");
console.log(oDiv.__proto__);
console.log(oDiv.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
console.log(document.__proto__);
console.log(document.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
// 创建一个新的SVG文档
var svgNS = "http://www.w3.org/2000/svg";
var svgDoc = document.implementation.createDocument(svgNS, "svg", null);
// 添加根元素
var root = svgDoc.documentElement;
// 添加一个矩形元素
var rect = svgDoc.createElementNS(svgNS, "rect");
rect.setAttribute("x", 10);
rect.setAttribute("y", 10);
rect.setAttribute("width", 100);
rect.setAttribute("height", 100);
rect.setAttribute("fill", "blue");
root.appendChild(rect);
// 添加一个圆形元素
var circle = svgDoc.createElementNS(svgNS, "circle");
circle.setAttribute("cx", 120);
circle.setAttribute("cy", 120);
circle.setAttribute("r", 50);
circle.setAttribute("fill", "red");
root.appendChild(circle);
// 将SVG文档添加到HTML文档中
document.body.appendChild(root);
console.log(rect.__proto__);
console.log(rect.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
然后看看他们的__proto__
原型链的指针你就明白了!
如图
使用原型和原型链的好处
到这里学了那么多,我们使用原型链
到底有什么好处呢?
其实原型链
也可以适当的帮助我们优化代码,减少代码冗余,提高程序代码的复用性!
举个栗子
现在有一个属性
应该出现在每一个实例上,那我们就可以重用它,尤其是对于方法
或者函数
这种类型的属性!
比如说现在有多个实例字面量对象
,每一个对象都是一个容器,而里面都包含一个 getValue方法
可以用来访问的值对象本身内部的某值!
代码如下
const arr = [
{
value: '张三', getValue() {
return this.value; } },
{
value: '李四', getValue() {
return this.value; } },
{
value: '王五', getValue() {
return this.value; } },
];
但是你可以想一下,这样子做好吗? 每一个对象都基本上有同样的代码, 这就是冗余
且不必要的代码,并且我以前也说过,一个函数在一个对象中,就会开辟一块内存空间,如果代码巨大的情况下,这样子做非常耗费内存资源!
所以你可以尝试优化一下,当然优化的办法有很多,这里我们重点讨论的就是原型链
你可以试想一下将 getValue方法
移动到所有盒子的原型链[[prototype]]
上!
那么我们加以修改一下,变成如下形式
代码
//公共使用
const _ObjPublic={
getValue() {
return this.value;
}
}
const arr = [
{
value: '张三', __proto__:_ObjPublic },
{
value: '李四', __proto__:_ObjPublic },
{
value: '王五', __proto__:_ObjPublic },
];
console.log(arr[0].getValue());
console.log(arr[1].getValue());
console.log(arr[2].getValue());
效果如下
这样一来所有对象中的 getValue方法
都会根据原型链
的原理找到并引用相同的函数,降低了内存使用率!
但是上面这样一个一个手动去捆绑原型链
太麻烦了, 如果代码多了这样一个一个的去修改也是一件很大工程量的事情! 那么怎么办呢?
这时,我们就可以使用构造函数
方式创建实例对象
因为当我们使用构造函数
来构造的实例对象
它会自动为实例对象
设置 原型链(__proto__)
属性
构造函数
是使用 new
调用的函数
还记得吗!
其实在js
的设计之初,就给我们考虑过这些问题, 大致我分为以下几个用途:
- 优化和简化代码,实现代码重用
- 根据
__proto__
的链条实现属性和方法
的继承
,只要是这个__proto__
链条上的东西,都可以被调用到
所以在js
中就有了这样一个说法:js
是基于对象的脚本语言,但是也有人说js
是基于原型的脚本语言!
我们单纯的来说一下prototype原型对象
用它来实现代码重用与属性和方法
的共享!
所以为了避免了代码冗余,公共使用的属性
和方法
,我们是可以设置到原型对象
中的!
方法
构造函数名.prototype.属性=值;
构造函数名.prototype.方法=function(){
..代码段..
}
然后通过构造函数
来实例化
的所有实例对象
都可以使用该构造函数
对应原型对象
中的属性
和方法
!
也就是说这个类型的实例对象
就都会共享
这些属性
和方法
也就是通过原型链(__proto__)
在进行查找!
这样做的一个好处是:减少了内存占用, 并且也实现了代码重用!
也是使用原型
对象的一大优点
代码如下
function createPerson(name,age) {
this.name=name;
this.age=age;
}
createPerson.prototype.say=function () {
console.log('我的名字叫【'+this.name+'】, 我的年龄是:'+this.age);
}
var a=new createPerson('张三','33');
var b=new createPerson('李四','55');
a.say();
b.say();
console.log(a.say===b.say);
如图
大家可以看到,say方法
,我没有定义到实例对象
上,也没有定义到构造函数
当中,而是定义到了原型对象
里面!
并且这样子做就相当于所有的实例对象
都共享一个方法,那么它们的地址都是相等的了!
这样做的好处,在于节约内存开销
为什么这样说呢?
我们来看下面这张图:
如图
这就是让公用的方法
或者属性
在内存中只存在一份,所以prototype
就是这样来实现数据的共享, 不然的话你每一次new
都会在内存中创建一份属性
或者方法
出来
而不管我们实例化多少次对象出来,原型对象
里面的属性和方法
只生成一次,所以会节省内存, 同时提高代码的可重用性和可维护性 。
也就是说只要是通过 new
创建的实例对象
,无论多少次,它们的__proto__
都是指向构造函数的prototype
如图
所以我们给构造函数
的原型对象
添加一些方法,就能让创建的多个实例对象
共享同一个方法,减少内存的使用。
当然你也可以把所有的属性和方法
都添加到原型对象当中,构造函数
中就不用再去定义了,看情况来决定!
相当于构造函数
创建的每一个实例
都会自动将构造函数
的prototype
属性作为其 原型链__proto__
你完全可以使用Object.getPrototypeOf
方法来进行验证以下
console.log(Object.getPrototypeOf(a) === createPerson.prototype); //返回true
console.log(Object.getPrototypeOf(b) === createPerson.prototype); //返回true
console.log(createPerson.prototype.constructor === createPerson); //返回true
字面量与原型链之间的关系
在JavaScript
中的一些字面量语法
会隐式的创建原型链__proto__
这里我给大家举几个案例就会明白了~~
举栗
对于使用对象字面量创建的对象,__proto__
返回的是:Object的原型对象
你也可以理解为对象字面量没有 __proto__
的情况下,自动将Object.prototype
作为它们的__proto__
值
代码
var obj = {
}
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj) === Object.prototype); //返回true
对于使用数组字面量创建的对象,__proto__
返回的是:数组的原型对象
也就是说数组字面量
会自动将 Array.prototype
作为它们的 __proto__
值
代码
var arr = [];
console.log(arr.__proto__);
console.log(Object.getPrototypeOf(arr) === Array.prototype); //返回true
如果是正则表达式字面量
,则会自动将 RegExp.prototype
作为这些字面量的 __proto__
值
const regexp = /abc/;
console.log(Object.getPrototypeOf(regexp) === RegExp.prototype) // true
对于使用字符串字面量
方式创建的字符串对象,__proto__
返回的是:字符串的原型对象,也就是说
如果是字符串字面量
,则会自动将 String.prototype
作为这些字面量的 __proto__
值
var str = "";
console.log(str.__proto__);
console.log(Object.getPrototypeOf(str) === String.prototype) // true
如果是使用数字字面量
方式创建的数值对象,__proto__
返回的是:数值的原型对象,也就数值字面量
,则会自动将 Number.prototype
作为这些字面量的 __proto__
值
var num = 100;
console.log(num.__proto__);
console.log(Object.getPrototypeOf(num) === Number.prototype) // true
那么如果是函数呢,一个函数名称
其实也算是一种函数字面量
的形式!
那__proto__
返回的是:函数的原型对象
,也就是Function.prototype
也就是会自动将 Function.prototype
作为这些函数字面量
的 __proto__
值!
var fn = function () {
}
console.log(fn.__proto__);
console.log(Object.getPrototypeOf(fn) === Function.prototype) // true
所以说这又解释了为什么有些属性和方法
只是在特定的构造函数
上定义的, 而它们又自动在所有特定的实例对象
上才可以使用,对吧!
比如像 map()
这样的数组方法
只是在 Array.prototype
上定义的方法,而它又只会自动在所有数组实例
上可用,就是因为这个原因!
性能与原型链
了解原型继承
的模型是使用javascript
编写复杂代码的重要基础,另外我们还要注意代码中原型链的长度,在必要时可以将其分解,以避免潜在的性能问题!
因为原型链
上较深层的属性
和方法
的查找, 在时间上可能会对性能产生负面影响,这在性能至关重要的代码中可能会格外明显, 因为如果尝试访问不存在的属性
始终会遍历整个原型链
,也就是原型链中的每个
可枚举属性都将被枚举, 那么层次多了反而不好!
所以说我们在遍历对象的属性时,最好先判断一下 要检查对象是否具有在其自身
上是否有定义的属性,而不是让__proto__
自动的去搜索其原型链
上的某个地方! 必要的情况下可以使用hasOwnProperty()
判断
举个栗子
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype.addVertex = function (v) {
this.vertices.push(v);
};
const g = new Graph();
// 当前原型链为: g ---> Graph.prototype ---> Object.prototype ---> null
//检查对象自身是否有vertices属性 返回true
console.log(g.hasOwnProperty("vertices"));
//检查对象自身是否有nope属性 返回false
console.log(g.hasOwnProperty("nope"));
//检查对象自身是否有addVertex属性 返回false
console.log(g.hasOwnProperty("addVertex"));
//检查原型对象自身是否有addVertex属性 返回true
console.log(Object.getPrototypeOf(g).hasOwnProperty("addVertex"));
最后总结
原型链
其实是一种关系的链条, 它是让实例对象
和原型对象
之间产生关系一种链条!
而这个关系是通过原型([[Prototype]])
也就是__proto__
来进行关联的!
而也只有实例对象
才有这个__proto__
不标准的属性,当然这里的意思是有的游览器并不支持这个属性!
那么有了原型链
我们实例对象
在进行查找属性
的时候则按照以下规则:
首先在实例对象
上查找,如果有则使用自身带有的属性或方法,如果没有则通过__proto__
指向的原型对象
进行 查找,找到则使用, 如果找不到则继续向原型对象
的__proto__
进行查找, 找到则使用,以此类推, 如果最终未找到则会报错!
同时,我们也对构造函数
有了一个深入的了解,也就是构造函数
在实例化
的时候会生成一个叫prototype
的属性,它就是构造函数
的原型对象
, 这个对象中还有一个默认存在的属性constructor
用来指向原型对象
所在的构造函数
的指针!
实例对象
的__proto__
属性指向的是构造函数
的原型对象
,这个对象的指向是可以被修改的,从而实现层层继承
也就是说原型
的指向
是可以被改变
的, 也不管你是修改prototype
还是__proto__
最终原来本身指向的原型会指向到一个新的原型对象上从而通过__proto__
查找链条来实现继承
关系!