首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

【趣味JavaScript】一文让你读懂JavaScript原型对象与原型链的继承,探秘属性的查找机制重置版《下篇》

原型链

理解了以上这些是什么之后,那么接下来,我们就可以来研究一下什么是原型链了!

并且js中实现继承主要是依靠原型链来实现! 所以我们才需要学习原型链的原理!

在学习原型链之前,你先记住一个概念,就是原型链: 其实是实例对象和原型对象之间的链条!

我们之前不是说了 实例对象在搜索属性或者方法的优先级吗 ?

就是说如果当你调用一个属性或者方法时, 其实首先是会在当前实例对象上进行搜索, 如果没有那么就到构造函数中去进行搜索,如果还是没有,那么就到原型对象当中去是进行搜索

那么有人就会问了,如果 当前构造函数的原型对象中也没有呢? 接下来js会干什么呢?

揭晓谜底吧,请看下图:

如图

分析

以上这张图就是默认情况下,如果说person这个实例对象如果顺着__proto__这个链条没有在Person.prototype中找到想找的方法或数据,那么又会继续顺着__proto__这个链条往上走,继续寻找,

那么找谁呢?  这是js设计者规定的,默认就会找到一个Object.prototype原型对象当中去, 那么这样子的查找链条也就形成了原型链

并且既然这里有一个所谓的Object.prototype原型对象 那么这个对象也会有一个属性叫constructor的属性,来返指向其它的构造函数Object, 这里的Object.prototype原型对象自身其实也有一个__proto__, 因为我们说了,实例对象才有这个属性,所以还可以向上访问,不过这里再往上就只能返回null了,因为没有了!

小结

js其实在面向对象的设计之初就是依照近原则,当我们要使用一个方法和数据时,js会优先查找自身,如果没有就查找离自己最近的,这里也就指的是构造函数,如果自己没有,他就会沿着原型链__proto__这个链条,向上查找,如果还没有找到,它还会沿着原型链继续向上查找,直到找到Object.prototype原型对象

Object.prototype原型对象默认也是会有一些方法在里面的

如图

所以这里 其实就是解释了js中每个函数都存在原型对象属性prototype

并且在js中所有函数的默认的原型对象都是Object的实例对象,而且默认还形成一个层层嵌套的形式,这也就是默认原型链

那我们要Object的原型对象干嘛,它里面又没几个方法和属性来满足我们日常开发的需求对吧,

所以我们需要扩大我们的原型链条, 这就要谈论到继承了

原型对象中的this指向

当我们使用new操作符来执行一个函数的时候,这个时候,构造函数中的this会指向到该实例对象上

而原型对象中如果函数方法出现的this 那么也是指向的该实例对象

举个栗子

结果如下

由此可见构造函数中的this与原型对象中方法中的this 其实就是当前new出来的实例对象

原型链继承的实现

在js中继承就是通过原型链来实现的,那么到底如何实现呢?

我们废话不多说,直接看个案例!

代码

//猫类

function Cat(){

this.username='小猫';

}

//狗类

function Dog(){

this.username='小狗';

}

//老虎类

function tiGer(){

this.username='老虎';

}

//猫类的原型对象中有一个方法

Cat.prototype.behavior=function (){

console.log('【'+this.username+'】 这种动物真的会要咬人!!....');

console.log(this);//谁调用this归谁!

}

//实例化猫类

var cat=new Cat();

//把狗类的原型对象指向猫

Dog.prototype=cat;

//实例化狗类

var dog=new Dog();

//把老虎的原型对象指向狗

tiGer.prototype=dog;

//实例化老虎类

var tiger=new tiGer();

//调用方法

tiger.behavior();

dog.behavior();

这里我们修改了原型对象的指向, 也就是修改了构造函数的prototype属性值,对吧!

那么这样一来会造就什么样的情况呢?

简单一点说, 我们就会顺着一个: C实例−>C原型(B实例)−>B原型(A实例)−>A原型 这样一个过程来进行查找!

也就是实例tiger-->Tiger原型对象(dog实例)--->Dog原型对象(cat实例)--->Cat原型对象 进行查找

这里也很明显,tiGer的实例和原型对象都没有一个叫behavior的方法, 那么就会顺着一条线路,一直往上寻找

如图

当然你也可以通过修改__proto__来实现!

代码如下

//猫类

function Cat() {

this.username = '小猫';

}

//狗类

function Dog() {

this.username = '小狗';

}

//老虎类

function tiGer() {

this.username = '老虎';

}

//猫类的原型对象中有一个方法

Cat.prototype.behavior = function () {

console.log('【' + this.username + '】 这种动物真的会要咬人!!....');

console.log(this);//谁调用this归谁!

}

//实例化猫类

var cat = new Cat();

//实例化狗类

var dog = new Dog();

//实例化老虎类

var tiger = new tiGer();

//修改原型链指针

tiger.__proto__= dog;

dog.__proto__= cat;

//console.log(tiger);

tiger.behavior();

原理分析

首先,定义了三个构造函数:Cat、Dog和Tiger,每个构造函数都有一个属性username, 分别赋值为小猫"、"小狗"和"老虎

接下来,在Cat类的原型对象中定义了一个方法behavior,该方法用于打印出动物的名字以及调用该方法的对象信息。

然后,通过实例化Cat类创建了一个名为cat的实例对象,并将Dog类的原型对象指向了cat。

这样就建立了一个继承关系,即Dog类会继承Cat类的属性和方法。

接着,通过实例化Dog类创建了一个名为dog的实例对象,并将Tiger类的原型对象指向了dog。同样地,这也建立了一个继承关系,即Tiger类会继承Dog类的属性和方法。

最后,通过实例化Tiger类创建了一个名为tiger的对象,并调用了它的behavior方法。

由于原型链上的继承关系,调用这个dog.behavior()和tiger.behavior()都会查找到最终的原型对象也就是Cat.prototype中的behavior方法进行调用!

当然如果这里再调用Object.prototype.__proto__往上就没有了,就会返回null

这样就形成了一个父子级别的关系,因为我们通过修改prototype或者__proto__形成了一个链条

毕竟原型对象,其实也是一个Object的实例,所以它也有一个__proto__属性,本身它在一个普通原型对象下的指向为Object.Prototype原型对象,也就是说所有函数的默认原型对象都是Object的实例, 但是这里我们把它修改了!

通过__proto__相连接, 每个继承父函数的实例对象都包含一个__proto__指针

最后会指向我们指定父函数的prototype原型对象

这样一直可以以此类推,进行迭代父函数的原型对象, 利用__proto__属性一直可以再往上一层继承。

在这个程中就形成了原型链

我们也可以使用Chrome并且打印一下实例对象来进行查看这个链条的走向!

console.log(tiger);

如图

这里如果眼尖的朋友可能已经注意到了一个问题,那就是constructor这个属性显示不见了, 构造函数的指向也不对了、原型的显示也不对了, 全部都指向了Cat构造函数, 当然从继承的效果上是不影响的!

我们可以用以下代码测试一下:

如图

原因:简单点说因为修改原型对象的时候,指向了另一个新的实例对象,所以把 constructor给丢失了!

如果你想看上去比较合理一点,加入以下代码

解决方案

修改之后如图

这就是原型链查找的关系,一层一层的链接关系就是:原型链

有些实例对象能够直接调用Object.prototype中的方法也是因为存在原型链的机制!

所以说JavaScript 中原型链用于实现继承就是这样实现的!

给大家专门准备了一张通用默认原型链原理图,拿去背吧!!

如图

基于原型链的继承

看了以上的案例和图例之后,我们应该就对javascript中的继承有个深入的理解了!

JavaScript的对象其实都会有一个指向一个原型对象的链条, 当我们试图访问一个对象的属性时,它不仅仅在该对象本身上去进行搜寻,还会搜寻该对象的原型,以及原型的原型依次层层向上搜索,直到找到一个名字匹配的属性为止, 或到原型链的末尾!

对象属性的继承

但是有一点我觉得值得注意,就是修改原型链 也就是使用{ __proto__: ... } 和 obj.__proto__ 有点不同,前者是标准且未被弃用的一种方式!

举个栗子

var obj={

a: 值1,

b: 值2,

__proto__: c

}

比如在像这样的对象字面量中,c的值必须为 null 或者指向另一个对象用来当做字面量所表示的对象的 原型链,而其他的如a 和 b将变成对象的自有属性, 这种语法读起来非常自然,并且兼容性也比较不错!

我们来看个实际的小案例

代码如下

const obj = {

a: '张三',

b: '李四',

__proto__: {

b: "王五",

c: "绿巨人",

},

};

console.log(obj);

console.log(obj.a);

delete obj.b;

console.log(obj.b);

console.log(obj.c);

分析

当前obj的原型链中具有属性 b和c两个属性如果obj.__proto__.__proto__ 依照之前的图例肯定是访问到Object.prototype最后obj.__proto__.__proto__.__proto__则是 null, 这里就是原型链的末尾,值为null完整的原型链看起来像这样:{ a: 张三, b: 李四 } ---> { b:王五, c: 绿巨人 } ---> Object.prototype ---> null

那么要说继承关系的话,那就是__proto__ 设置了原型链,也就是说它在这里的原型链被指定为另一个对象字面量!

即便是这里我使用了delete obj.b删除了属性b,也会从__proto__这个链条找到原型链中所继承来的属性b

如图

但是注意了如果这里我没有使用delete obj.b来删除了属性b 那么,当我们调用obj.b返回的则是李四而不是王五,这里其实叫做属性遮蔽(Property Shadowing),意思是虽然没有访问到王五,但这只是被遮住了而已,并不是被覆盖和删除的意思!

当然我们也可以根据这个原理来创建更长的原型链,并在原型链上查找一个属性

代码如下

const obj = {

a: 1,

b: 2,

// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。

__proto__: {

b: 3,

c: 4,

// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。

__proto__: {

d: 5,

},

},

};

console.log(obj.d); // 输出5

那么它的原型链就是如下这样:

{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null

其实就是这样就可以嵌套很多层出来,让对象看起来更加有层次结构,也方便管理一些特殊的数据!

对象方法的继承

方法或者函数的继承在js中其实和属性的继承也没有差别!

特别要说明的其实也就是this的指向,当继承的方法被调用时,this值指向的是当前继承的对象,而不是拥有该函数属性的对象

代码说明

const parent = {

username: '张三',

age:35,

method() {

return '我的年龄是'+(this.age + 1)+'岁';

}

}

console.log(parent.method()); //输出36

//然后我们通过child继承了parent的对象

const child = {

__proto__: parent,

}

console.log(child.method());//输出36

child.age = 5;

console.log(child.method());

代码分析

当调用 parent.method 时,this指向了parent所以按照正常逻辑执行 所以输出36

然后我们通过child继承了parent的对象

现在调用 child.method 时,this虽然指向了child,但是又因为 child 继承的是 parent 的方法,首先在 child 上寻找有没有method方法, 但由于child本身没有名为method方法,则会根据原型链__proto__找上去,最后找到即 parent.method方法来执行!

然后我们在 child添加一个age属性赋值为5, 这会就会遮蔽parent上的age属性

child对象现在的看起来是如下这样的:{ age: 5, __proto__: { username: '张三', age: 35, method: [Function] } }

最后输出:6, 是因为child 现在拥有age属性,就不会去找parent对象中的age属性了, 但是方法还是会去找parent中的method方法, 而方法中的this.age现在表示 child.age然后再这个基础上+1结果就是这样了!

更多参考案例

function Test() {

this.username = '张三';

this.age = 33;

this.job='软件开发';

}

Test.prototype.say=function (){

return '我的名字叫:'+this.username+',我的年龄是:'+this.age+'我的职业是:'+this.job;

}

var test=new Test();

//新建一个对象,并且修改原型链

var obj = {

username:'李四',

age:'35',

__proto__:test

}

console.log(obj);

console.log(obj.username);

console.log(obj.age);

console.log(obj.say());

另类继承实现方法

修改构造函数this指向从而实现继承

我们有时候可以借助call方法来实现简单的继承效果!

举个栗子

function Animal(name,age,food){

this.username=name;

this.age=age;

this.eat=function (){

console.log('这只['+this.username+']动物要吃['+food+']');

}

}

Animal.prototype.color='黑色';

Animal.prototype.say=function (){

console.log('我的名字叫'+this.name);

}

function Panda(name,age,eat){

this.like='玩耍';

Animal.call(this,name,age,eat); //借用一下

}

var p1=new Panda('熊猫盼盼',18,'竹叶');

//打印输出

console.log(p1);

p1.eat();

结果

这个案例中,应用了call修改this指向来达到一个共享的目的!

但是这种使用call等方法来修改this严格意义上来讲,只能算借用!

因为这种方式有一个很大的缺点,就是不能继承所谓父类中原型里面的属性和方法,不然你看上图,打印的结果当中并没有出现Animal类原型对象中的color属性和say方法

所以这种继承方式如何和prototype修改原型方式结合一起使用就会有意想不到的效果,并且参数的传递也会更加灵活多变!

举个栗子

//定义构造函数

function Person(userename,age,sex){

this.name=userename;

this.age=age;

this.sex=sex;

this.type='人类';

}

Person.prototype.say=function(){

console.log("hello world");

}

function Student(username,age,sex,score){

//借用Person构造函数

Person.call(this,username,age,sex);

//定义属性

this.score=score

}

//改变原型指向

Student.prototype=new Person();//不传值

Student.prototype.behavior=function(){

console.log("英语学习!!");

}

var s1=new Student("张三",15,"男","100分")

//打印结果看看

console.log(s1);

console.log(s1.type);

console.log(s1.name);

console.log(s1.age);

console.log(s1.sex);

console.log('考试得分:'+s1.score);

s1.behavior();

s1.say();

代码分析

从上面的代码中,我们可以看到构造函数Student中我们借用了Person构造函数, 然后在通过prototype修改原型指向,这样一来,不仅可以获取到父类构造函数中的属性和方法 也可以获取到父类原型对象中的属性和方法

这时都可以通过__proto__这个链条拿到!

如图

通过循环复制实现继承

我们的原型对象prototype既然是一个对象,那么我们也可以通过循环复制的手法把父级原型对象里面的属性和方法拷贝到目标原型对象下,同时也可以结合call方法借用构造函数中的属性和方法

代码如下

如图

__proto__的兼容性

根据MDN官方的建议,其实__proto__是被弃用了的!

那么到底我们平常使用什么来修改原型的指针呢?

在 JavaScript 中,你的确是可以通过直接修改实例对象的 __proto__ 属性来达到目的!

__proto__ 是一个非标准的属性,它在大多数的js环境中都可以使用,包括浏览器和Node.js 但是由于这个属性是非标准的,它在一些环境中可能不可用,或者在未来的标准中可能会被弃用,也就是说虽然一些浏览器仍然支持__proto__,但也许已从相关的web标准中移除,也许正准备移除或者出于兼容性而保留!

__proto__浏览器兼容性如下表

所以如果可以的话我们尽量不使用__proto__而改成其他,例如:Object.setPrototypeOf方法

举个栗子

让我们使用Object.setPrototypeOf来实现一个简单的继承

代码如下

function Test(){

}

Test.prototype.company='重庆科技';

function Test2(){

}

Test2.prototype.num=100;

function Test3(){

}

Test3.prototype.username='张三';

//实现继承

Object.setPrototypeOf(Test2.prototype, Test.prototype);

Object.setPrototypeOf(Test3.prototype, Test2.prototype);

var t3=new Test3();

console.log(t3);

效果

再看一个案例!

const a = { company : '重庆科技' };

const b = { age: 33 };

const c = { username: "张三" };

//实现继承

Object.setPrototypeOf(a, b);

Object.setPrototypeOf(b, c);

console.log(a);

console.log(a.username);

console.log(a.age);

console.log(a.company);

效果如下

所以我觉得可以的情况下,尽量使用标准的Object.setPrototypeOf方法来实现继承

因为Object.setPrototypeOf方法基本上被所有现代浏览器引擎所支持, 并且也允许动态地修改对象的原型

原型链与继承查找机制

当你访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,那么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 元素的类, 它继承了 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__)在进行查找!

这样做的一个好处是:减少了内存占用, 并且也实现了代码重用! 也是使用原型对象的一大优点

代码如下

如图

大家可以看到,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__查找链条来实现继承关系!

xxxxx

xxxx

xxxx

xxxx

"点赞""评论""收藏"

大家的支持就是我坚持下去的动力!

如果以上内容有任何错误或者不准确的地方,

欢迎在下面留言指出、或者你有更好的想法,

欢迎一起交流学习!

技术交流

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OMnz7LgawbV8jwhgVwuJEzQQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券