《原型链重置版》一万多字让你读懂JavaScript原型对象与原型链的继承,探秘属性的查找机制! (6)

简介: 其实javascript中很多方面都跟原型链有关系,你如果没有弄懂,就等同于没有学会javascript...

logo.png

5.gif

原型链与继承查找机制

当你访问一个对象的属性方法时,如果这个对象本身没有这个属性方法,那么js会在这个对象的原型中寻找这个属性或方法,如果找到了,就会使用它, 如果还是找不到,就会在原型的原型中寻找,以此类推,直到找到为止, 而继承的关键,也就在于自定义修改原型的指向!

所以当你把之前的原型链图分析透彻,你就会知道原型链就是通过__proto__属性形成的,任何对象普通对象函数对象都有__proto__属性,并且其核心思想也就是通过__proto__这个链条来进行查找数据!

DOM原型链的形成

这其实也很好的解释了我们javascriptDOM属性和方法也是这样子进行查找的

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__);

结果如下

12.png

其实这样你就会知道当一个DOM元素在使用某个属性和方法的时候,是怎么进行查找的它的链条也是通过__proto__来进行查找的,对吧! 其中顺序如下

↓HTMLDivElement
↓HTMLElement、
↓Element、
↓Node、
↓EventTarget
↓Object

通过__proto__就形成了JavaScript中与DOM文档对象模型相关的概念了

这里也给大家简单介绍一下,方便理解!

  1. HTMLDivElement: 这是一个代表 HTML <div> 元素的类, 它继承了 HTMLElement 的属性和方法,包括可以用来改变元素样式的属性和方法,当然这里我只是举个栗子,不一定就是div元素根据你打印的情况决定!
  2. HTMLElement: 这是一个基础类,代表任何 HTML 元素, 所有的 HTML元素都继承了 HTMLElement 的属性和方法
  3. Element: 这是一个基础类,代表任何 HTML 或 XML 元素, 它定义了所有元素共享的属性方法,例如 getAttribute()setAttribute()
  4. Node: 这是所有DOM节点的基类,包括元素、文本节点、注释等, 它定义了一些通用的属性方法,如 parentNodechildNodes
  5. EventTarget: 这个接口表示可以添加或删除事件监听器的事件目标
  6. Object: 这个也就是顶层的Object构造函数

在w3c也有这些属性和方法的详细解释

如图

13.png

而且这些继承关系这样子一直走下来查找的属性方法的关键就是原型链, 可以说没有原型链就没有现在的javascript

当查找对象的某个属性方法的时候,首先在当前对象中查找,如果没有去对象的__proto__中去查找, 这样子一直到最顶层null,而这样的__proto__形成的一条查找链条就是原型链 现在你可以感受一下是不是如此呢!

并且继承也就是修改原型的指向,即__proto__prototype

以上这些接口一起构成了JavaScriptDOM 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__原型链的指针你就明白了!

如图

14.jpg

使用原型和原型链的好处

到这里学了那么多,我们使用原型链到底有什么好处呢?

其实原型链也可以适当的帮助我们优化代码,减少代码冗余,提高程序代码的复用性!

举个栗子

现在有一个属性应该出现在每一个实例上,那我们就可以重用它,尤其是对于方法或者函数这种类型的属性!

比如说现在有多个实例字面量对象,每一个对象都是一个容器,而里面都包含一个 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());

效果如下

optimize-1.png

这样一来所有对象中的 getValue方法 都会根据原型链的原理找到并引用相同的函数,降低了内存使用率!

但是上面这样一个一个手动去捆绑原型链太麻烦了, 如果代码多了这样一个一个的去修改也是一件很大工程量的事情! 那么怎么办呢?

这时,我们就可以使用构造函数方式创建实例对象

因为当我们使用构造函数来构造的实例对象它会自动为实例对象设置 原型链(__proto__)属性

构造函数是使用 new 调用的函数 还记得吗!

其实在js的设计之初,就给我们考虑过这些问题, 大致我分为以下几个用途:

  1. 优化和简化代码,实现代码重用
  2. 根据__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);

如图

1-1.png

大家可以看到,say方法,我没有定义到实例对象上,也没有定义到构造函数当中,而是定义到了原型对象里面!

并且这样子做就相当于所有的实例对象都共享一个方法,那么它们的地址都是相等的了!

这样做的好处,在于节约内存开销 为什么这样说呢?

我们来看下面这张图:

如图

2.png

这就是让公用的方法或者属性在内存中只存在一份,所以prototype就是这样来实现数据的共享, 不然的话你每一次new都会在内存中创建一份属性或者方法出来

而不管我们实例化多少次对象出来,原型对象里面的属性和方法只生成一次,所以会节省内存, 同时提高代码的可重用性和可维护性 。

也就是说只要是通过 new 创建的实例对象,无论多少次,它们的__proto__都是指向构造函数的prototype

如图

2-1.jpg

所以我们给构造函数原型对象添加一些方法,就能让创建的多个实例对象共享同一个方法,减少内存的使用。

当然你也可以把所有的属性和方法都添加到原型对象当中,构造函数中就不用再去定义了,看情况来决定!

相当于构造函数创建的每一个实例都会自动将构造函数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__查找链条来实现继承关系!


1.gif


4.png


9.gif

相关文章
|
13天前
|
JavaScript 前端开发 CDN
前端 JS 经典:package.json 属性详解
前端 JS 经典:package.json 属性详解
12 1
|
16天前
|
XML JavaScript 前端开发
如何在JavaScript中设置多个样式属性?
【6月更文挑战第29天】如何在JavaScript中设置多个样式属性?
33 3
|
16天前
|
前端开发 JavaScript
使用JavaScript动态更改元素的CSS属性
【6月更文挑战第29天】使用JavaScript动态更改元素的CSS属性
24 3
|
10天前
|
存储 Web App开发 JavaScript
浏览器【详解】Cookie(含Cookie的起源,属性,个数和大小限制,作用,优点,缺点,JS 的操作方法等)
浏览器【详解】Cookie(含Cookie的起源,属性,个数和大小限制,作用,优点,缺点,JS 的操作方法等)
11 0
|
10天前
|
JavaScript C++
js【详解】原型 vs 原型链
js【详解】原型 vs 原型链
8 0
|
12天前
|
JavaScript
js 内建对象的拓展 shim/polyfill ( 内含js 判断对象的属性是否存在的方法)
js 内建对象的拓展 shim/polyfill ( 内含js 判断对象的属性是否存在的方法)
11 0
|
14天前
|
JavaScript
js 排序—— sort() 对普通数组、对象数组(单属性/多属性)排序
js 排序—— sort() 对普通数组、对象数组(单属性/多属性)排序
8 0
|
2月前
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
43 0
|
2月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
30 0