JS 原型与原型链 学习笔记和归纳

js 原型与原型链:

原型:

​ “原型是一 种对象,被用在 ECMAScript 中实现继承结构、状态和行为。当构造函数创建对象时,那个对象隐含引用构造函数的关联原型,以此分解属性引用。通过程序中的表达式 constructor.prototype 可以引用到构造函数的关联原型,通过继承,添加给对象的属性 会被所有共享此原型的对象共享。”----------《ECMAScript规范-第三版_中文版》

首先我们根据规范可以知道,原型是一个具体的对象

几条总结:

  • 所有的函数对象都有一个prototype的属性,该属性的值是一个由Object构造函数创造出来的空对象的地址引用。
  • protorype属性所指向的原型对象中有一个名为constructor的属性,该属性存储的是与该原型对象相关联的构造函数的地址引用。
  • 所有的实例对象(包括由Function创建的函数对象也属于实例对象)都有一个__proto__属性(该属性并不是暴露给用户操作的),该属性的值是构建该实例对象的构造函数的原型的引用。
  • 在查找属性值的时候,会现在自身的对象中去查找,如果没有找到则**沿着__proto__**在隐式原型中查找。
  • Function函数的__ptoto__存储的是Function.prototype的地址引用。
  • 任何函数都可以通过new来产生对象,此时的函数就从普通函数成为了构造函数。
  • Object构造函数的原型的__proto__的值为null
  • 我们所定义的函数都是Function函数的实例。所以函数本身也是对象的一种。
  • 把一个对象赋值给一个变量,本质上该变量存储的是该对象所在内存的地址。形如:0xff3f33这样,也就是我们说的引用。计算机会拿着引用去寻找对象所在的区域,进而拿去取数据。
  • 对象的修改和访问都是通过引用来找到对象所在的内存的。
  • 赋值的本质操作是把该变量在内存中存的值拷贝然后写入到另一个变量所在的内存中。无论是地址引用函数原始数据。
  • JS中的全局构造函数,如:Array/String/Number/Object等函数都是Function函数实例化而来的。而Function本身不是任何函数的实例,而是js最原始存在的函数。
  • 所有函数的__proto__指向的都是Functionprototype
  • 所有我们默认情况下生成的对象,本质上都是由Object产生的,如:var obj = {}。只是封装的语法糖。
  • 读取对象的属性时,会自动通过原型链查找,给对象添加属性时,会直接添加属性并设置其值。
  • 所有由Object实例化出来的对象都指向同一个隐式原型。
  • InstanceofA instanceof B,instance的意思是:实例。即a是否是b的实例。instanceof的判别方式:如果B函数的显示原型对象在A实例的原型链上,返回true,反之false

相关函数与对象的关系图:

ws5wOx.jpg

上面的关系图清楚的显示了相关的对象联系。

原型链:

原型链是一种链式的结构,我们都知道在数据结构中,有一种数据结构叫做链表,可以通过指针来寻找到该对象的前面后后面是什么数据。和链表类似,在js当中,我们也使用了类似于链表的结构来实现相关数据的访问。

“当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。” ----------《MDN:继承与原型链》

当我们去访问一个对象的属性的时候,假如本对象中没有该属性,则会向更高层去寻找,更高层没有继续向更高层去寻找,以此类推。层层寻找的关系,我们把它称之为原型链。

原型链查找原理:

当我们去访问一个对象的属性的时候,假如本对象中没有该属性,此时并不会报错或者返回nudefine,js会进入到该对象的__proto__所指向的对象中找,如果还没有找到,继续进入到现在这个对象__proto__所指向的对象中去找,直到找到然后停止在__proto__这个链上的查找。如果没有找到就会抛出一个错误。

通常情况下我们遇到的是这样的情况:

实例:

function Animo(name){
	this.name = name
}
Animo.prototype.eat = function(){
	console.log("this is a action for " + this.name)
}
var tiger = new Animo("tiger")
tiger.eat()
//this is a action for tiger

我们从这个实例中我们可以看到在tiger实例当中并没有eat这个方法,但是js会去向tiger.__proto__所指向的对象中查找,发现有eat这个函数,于是就对调用隐式原型里的eat函数。

实例:

function Animo(name){
	this.name = name
}
Animo.prototype = {
    eat:function(){
		console.log("this is a action for " + this.name)
	}
}
var tiger = new Animo("tiger")
tiger.eat()
//this is a action for tiger

上面的实例,我们把Animo.prototype存了一个新的对象的地址引用,我们在前面说过,当我们声明了一个函数后它就有自身的原型,通过函数的prototype去指向它,虽然是一个对象。凡是当我们进行上面的赋值操作后,就和原来的对象失去了联系,原本我们默认的查找顺序是:tiger-> Animo.prototype->Object.prototype->null,这是我们默认的原型链。但是当我们重新赋值的时候,查找的顺序变了:tiger->obj->Object.prototype->null。其实这不是我们想要的结果。假如我们在原型改变之前就有一个实例:

function Animo(name){
	this.name = name
}
var tigerA = new Animo("tigerA")
Animo.prototype = {
    eat:function(){
		console.log("this is a action for " + this.name)
	}
}
var tiger = new Animo("tiger")

此时的两个变量是有着完全不同的原型链的。

tigerA的原型链:tigerA->Animo.prototype->Object.prototype->null.

tiger的原型链:tiger->obj->Object.prototype->null

为什么会出现这样的情况,是因为你在没有改变原型的情况下就已经实例化了一个对象。那么,该对象的__proto__存储的就是Animo.prototype里的地址值,也就是默认原型。当你改变原型的时候,也就是说,将Animo.prototype的值改变成了另一个地址值,当你实例化的时候,js会把构造函数中prototype村的原型的地址值给__proto__。但是此时你的prototype的值已经改变了,所以实例化对象的__proto__的值就是改变了的值。

还有一点需要注意,当你重新给构造函数.prtotype赋新的对象的时候,一定要带上constructor这个属性,

constructor:

​ “返回创建实例对象的 Object 构造函数的引用。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。” -------《MDN:constructor属性》。

所有的对象都会从他的原型上继承一个constructor属性。

var o = {};
o.constructor === Object; // true

var a = [];
a.constructor === Array; // true

我们手动实例化的对象本身没有这个属性,但是在对象的__proto__中有这个属性,该属性指向的是构建实例对象的构造函数。

以下内容来自MDN文档,他们写的很好。可以用来做笔记。

改变函数的constructor:

大多数情况下,此属性用于定义一个构造函数,并使用new和继承原型链进一步调用它。

function Parent() {}
Parent.prototype.parentMethod = function parentMethod() {};

function Child() {}
//Object.create的第一个参数是创建该函数的原型。
Child.prototype = Object.create(Parent.prototype); // re-define child prototype to Parent prototype

Child.prototype.constructor = Child; // return original constructor to Child

但为什么我们需要在这里执行最后一行?很不幸正确答案是 - 看情况而定。让我们来尝试定义在哪些情况下,重新分配原始构造函数会发挥重要作用,以及在什么时候它就是额外的未使用的(无效的)代码行。

试想下一种情况:该对象具有创建自身的create方法。

function Parent() {};
function CreatedConstructor() {}

CreatedConstructor.prototype = Object.create(Parent.prototype);

CreatedConstructor.prototype.create = function create() {
  return new this.constructor();
}

new CreatedConstructor().create().create();
// error undefined is not a function since constructor === Parent

我们可以来分析一下这段代码的原型链构成:

实例对象->obj->Parent.prototype->Object.prototype->null。这里的objObject.create创建出来的函数。现在,create函数存在于obj对象中。当我们new CreatedConstructor()时,生成一个实例对象。当我们执行.create()时我们会:

return new this.constructor();
//this.constructor()会被执行,这个this就是我们生成的实例对象。
//这个实例对象的原型指向的是Prarent.prototype。我们在上面可以知道。
//constructor的指向的是其原型所对应的构造函数,也就是我们这里的Parent构造函数。
//return new this.constructor() <===> return new Parent()
//但是我们很清楚的知道,Parent的prototype里并没有create()这个函数,所以会报错。最根本原因是因为
//construtor指向的是Parent而不是CreatedConstructor,所以说会报错。

我们来改一下代码:

function Parent() {};
function CreatedConstructor() {}

CreatedConstructor.prototype = Object.create(Parent.prototype);
CreatedConstructor.prototype.constructor = CreatedConstructor

CreatedConstructor.prototype.create = function create() {
  return new this.constructor();
}

new CreatedConstructor().create().create();

在这里我们将CreatedConstructor.prototype.constructor指向了CreatedConstructor,而在CreatedConstructor的原型中有我们想要的create函数。所以我运行该代码的时候不会报错。

总结:手动设置或更新构造函数可能会导致不同且有时令人困惑的后果。为了防止它,只需在每个特定情况下定义构造函数的角色。在大多数情况下,不使用构造函数,并且不需要重新分配构造函数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值