全面解析js中的原型,原型对象,原型链


Posted in Javascript onJanuary 25, 2021

理解原型

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。看如下例子:

function Person(){
}
Person.prototype.name = 'ccc'
Person.prototype.age = 18
Person.prototype.sayName = function (){
 console.log(this.name);
}

var person1 = new Person()
person1.sayName() // --> ccc

var person2 = new Person()
person2.sayName() // --> ccc

console.log(person1.sayName === person2.sayName) // --> true

理解原型对象

根据上面代码,看下图:

全面解析js中的原型,原型对象,原型链

需要理解三点:

  1. 我们只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,指向函数的原型对象。即Person(构造函数)有一个prototype指针,指向Person.prototype
  2. 默认情况下,每个原型对象上都会创建一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针
  3. 每个实例的内部都有一个指针(内部属性) ,指向构造函数的原型对象。即 person1 和person2 身上都有一个内部属性__proto__(在ECMAscript中管这个指针叫[[prototype]],虽然在脚本中没有标准的方式访问[[prototype]],但是firefox,ie,chrome都支持一个属性叫__proto__) 指向Person.prototype

注意:person1 和person2 实例与构造函数之间没有直接的关系。

在之前我们提到,所有实现中无法访问到[[prototype]],那我们如何知道实例和原型对象之间是否存在关系呢?这里可以通过两个方法来判断:

  • 原型对线上的方法:isPrototypeOf(),如:console.log(Person.prototype.isPrototypeOf(person1)) // --> true
  • ECMAscript5中新增的一个方法:Object.getPrototypeOf(),这个方法返回[[prototype]]的值。如:console.log(Object.getPrototypeOf(person1) === Person.prototype) // --> true

实例属性与原型属性的关系

前面我们提到过,原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问。虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而改属性与实例原型中的一个属性同名,那就会在实例上创建该属性并屏蔽原型中的那个属性。如下:

function Person() {}
Person.prototype.name = "ccc";
Person.prototype.age = 18;
Person.prototype.sayName = function() {
 console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = 'www' // 在person1中添加一个name属性
person1.sayName() // --> 'www'————'来自实例'
person2.sayName() // --> 'ccc'————'来自原型'

console.log(person1.hasOwnProperty('name')) // --> true
console.log(person2.hasOwnProperty('name')) // --> false

delete person1.name // --> 删除person1中新添加的name属性
person1.sayName() // -->'ccc'————'来自原型'

我们如何判断一个属性,到底是实例上的属性还是原型上的属性?这里可以通过hasOwnProperty()方法来检测一个属性是存在于实例中还是存在于原型中。(此方法继承于Object)

下图详细分析了上面例子在不同情况下的实现与原型的关系:(省略了Person构造函数的的关系)

全面解析js中的原型,原型对象,原型链

更简单的原型语法

我们不可能总像之前的例子一样,没添加一个属性和方法就要敲一遍,Person.prototype。为了减少不必要的输入,更常见的方法是像下面这样:

function Person(){}
Person.prototype ={
 name: 'ccc',
 age: 18,
 sayName: function () {
 console.log(this.name)
 }
}

在上面代码中,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外,constructor属性不再指向Person了。前面我们介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。但是在我们使用的新语法中,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数了。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。如下:

var person1 = new Person()
console.log(person1 instanceof Object) // --> true
console.log(person1 instanceof Person) // --> true
console.log(person1.constructor === Person) // --> false
console.log(person1.constructor === Object) // --> true

这里用instanceof操作符测试Object和Person仍然返回true,constructor属性则等于Object,不等于Person了,如果constructor真的很重要可以像下面这样写:

function Person(){}
Person.prototype ={
 constructor: Person, // --> 重设
 name: 'ccc',
 age: 18,
 sayName: function () {
 console.log(this.name)
 }
}

但是这会引起一个新问题,用上述方式重置constructor属性会导致它的[[Enumerable]]特性被设置为true。而默认情况下,原生的constructor属性是不可枚举的。因此如果你要使用兼容ECMAscript5的JavaScript引擎,可以试一试Object.defineProperty()。

function Person(){}
Person.constructor = {
 name: 'ccc', 
 age: 18,
 sayName: function(){
 console.log(this.name)
 }
}
// 重设构造函数,只适用于ECMAscript5兼容的浏览器
Object.defineProperty(Person.constructor, "constructor", {
 enumerable: false, 
 value: Person
})

原型的动态性

由于原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能立即从实例上反映出来。比如:

function Person(){}
var person1 = new Person()
Person.prototype.sayHi= function(){
 console.log('hi')
}
person1.sayHi()

上述代码我们先创建了一个Person实例,并将其保存在person1中,然后在Person.prototype中添加了sayHi()方法。即使person1是添加新方法之前创建的,但它仍然可以访问这个方法。原因是实例与原型之间的松散的连接关系。
尽管可以随时为原型添加属性和方法,并立即能够在实例中反映出来。但是如果重写整个原型对象,那么情况就不一样了。看如下代码:

function Person(){}
var person1 = new Person()

Person.prototype = {
 name: 'ccc',
 age: 18,
 sayName: function(){
 console.log(this.name)
 }
}

person1.sayName() // --> error

看下图分析:

全面解析js中的原型,原型对象,原型链

调用构造函数时为实例添加了一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

理解原型链

原型链是实现继承的主要方法。其基本思想是让一个引用类型继承另一个引用类型的属性和方法。在理解原型链之前,我们首先得捋一下,原型,原型对象,实例之间的关系:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象等于另一个类型的实例会怎么样?显然,这个原型对象将会包含一个指向另一个原型的指针。先看代码在看图:

function SuperType(){
 this.property = true
}

SuperType.prototype.getSuperValue = function(){
 return this.property
}

function SubType(){
 this.subProperty = false
}

// 继承了SuperType
SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function (){
 return this.subProperty
}

var instance = new SubType()
console.log(instance.getSuperValue()) // --> true

上述代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。

全面解析js中的原型,原型对象,原型链

分析上图:instance 指向SubType原型,SubType的原型又指向SuperType的原型。getSuperValue()方法仍然还在SuperType.prototype中,但property则位于SubType.prototype中。这是因为property是一个实例属性,而getSuperValue()则是一个原型方法。既然SubType.prototype现在是SuperType的实例,那么property当然就位于该实例中。此外要注意,instance.constructor现在指向的是SuperType,这是因为原来的SubType.prototype中的constructor被重写了的缘故。
为什么会返回true?
分析:调用instance.getSuperValue()方法会经历三个搜索步骤:

搜索实例
搜索SubType.prototype
搜索SuperType.prototype,直到这里才找到方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

别忘记默认的原型

要知道,所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会有toString(),valueOf()方法的原因。所以完整的原型链应该如下:
看下图,subType的内部:

全面解析js中的原型,原型对象,原型链

详细图解:

全面解析js中的原型,原型对象,原型链

总之一句话,SubType继承了SuperType,而SuperType继承了Object。当调用instanct.toString()的时候,实际上调用的是保存在Object.prototype中的那个方法。

确定原型和实例的关系

当一个原型链很长的时候,想要确定原型和实例的关系,总共有两种方法:

使用instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。

console.log(instance instanceof Object) // --> true
console.log(instance instanceof SuperType) // --> true
console.log(instance instanceof SubType) // --> true

使用isPrototypeOf()方法,跟instanctof判别方法类似,只要原型链中出现过的原型,都会返回true。

console.log(Object.prototype.isPrototypeOf(instance)) // --> true
console.log(SuperType.prototype.isPrototypeOf(instance)) // --> true
console.log(SubType.prototype.isPrototypeOf(instance)) // --> true

谨慎地定义方法

子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。如下:

function SuperType(){
 this.property = true;
}
SuperType.prototype.getSuperValue = function(){
 return this.property
}

function SubType(){
 this.subProperty = false;
}

// 继承了 SuperType
SubType.prototype = new SuperType()

// 添加新方法
SubType.prototype.getSubValue = function(){
 return this.subProperty
}
// 重写超类型中的方法
SubType.prototype.getSuperValue = function(){
 return false
}

var instance = new SubType()
console.log(instance.getSuperValue()) // --> false
var instanceSuper = new SuperType()
console.log(instanceSuper.getSuperValue()) // -> true

上述代码中,第一个方法getSubValue()被添加到了SubType中。第二个方法getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的那个方法。即当通过SubType的实例调用getSuperValue()时,调用的就是这个重新定义的方法,但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。还有一点,在通过原型链实现继承的时候,不能使用对象自变量创建原型方法,因为这样会重写原型链,导致原型链被切断。

原型链的问题

通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性就变成了现在的原型属性了,这就会导致属性被共享。看如下代码:

function SuperType(){
 this.colors = ['white', 'blue']
}

function SubType(){
}

// 继承了SuperType
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('red')

var instance2 = new SubType()
console.log(instance1.colors) // -->["white", "blue", "red"]
console.log(instance2.colors) // -->["white", "blue", "red"]

在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。因此,在实践中很少会单独使用原型链。

以上就是图解js中的原型,原型对象,原型链的详细内容,更多关于js中的原型,原型对象,原型链的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
javascript tips提示框组件实现代码
Nov 19 Javascript
通过jQuery源码学习javascript(三)
Dec 27 Javascript
Javascript中arguments对象详解
Oct 22 Javascript
使用Plupload实现直接上传附件至七牛云存储
Dec 26 Javascript
JavaScript针对网页节点的增删改查用法实例
Feb 02 Javascript
jquery实现左右滑动菜单效果代码
Aug 27 Javascript
jQuery实现仿QQ空间装扮预览图片的鼠标提示效果代码
Oct 30 Javascript
使用JavaScript根据图片获取条形码的方法
Jul 04 Javascript
Bootstrap datepicker日期选择器插件使用详解
Jul 26 Javascript
详解基于Koa2开发微信二维码扫码支付相关流程
May 16 Javascript
详解Node.JS模块 process
Aug 31 Javascript
vue制作toast组件npm包示例代码
Oct 29 Javascript
js中实现继承的五种方法
Jan 25 #Javascript
Vue中的nextTick作用和几个简单的使用场景
Jan 25 #Vue.js
Vue使用Ref跨层级获取组件的步骤
Jan 25 #Vue.js
javascript实现点击产生随机图形
Jan 25 #Javascript
如何在Vue项目中添加接口监听遮罩
Jan 25 #Vue.js
json.stringify()与json.parse()的区别以及用处
Jan 25 #Javascript
使用vue3重构拼图游戏的实现示例
Jan 25 #Vue.js
You might like
调整优化您的LAMP应用程序的5种简单方法
2011/06/26 PHP
PHP中的事务使用实例
2015/05/26 PHP
PHP设计模式之原型模式定义与用法详解
2018/04/03 PHP
js 方法实现返回多个数据的代码
2009/04/30 Javascript
javaScript checkbox 全选/反选及批量删除
2010/04/28 Javascript
javascript 从if else 到 switch case 再到抽象
2010/07/17 Javascript
javascript attachEvent绑定多个事件执行顺序问题
2010/10/20 Javascript
JavaScript高级程序设计阅读笔记(六) ECMAScript中的运算符(二)
2012/02/27 Javascript
jQuery中:button选择器用法实例
2015/01/04 Javascript
jquery彩色投票进度条简单实例演示
2020/07/23 Javascript
浅析javascript异步执行函数导致的变量变化问题解决思路
2016/05/13 Javascript
jQuery过滤特殊字符及JS字符串转为数字
2016/05/26 Javascript
javascript实现简单的ajax封装示例
2016/12/28 Javascript
jQuery制作全屏宽度固定高度轮播图(实例讲解)
2017/07/08 jQuery
微信通过页面(H5)直接打开本地app的解决方法
2017/09/09 Javascript
ES6下子组件调用父组件的方法(推荐)
2018/02/23 Javascript
用图片替换checkbox原始样式并实现同样的功能
2018/11/15 Javascript
Layui之table中的radio在切换分页时无法记住选中状态的解决方法
2019/09/02 Javascript
微信小程序之左右布局的实现代码
2019/12/13 Javascript
TensorFlow实现Batch Normalization
2018/03/08 Python
详解Python的hasattr() getattr() setattr() 函数使用方法
2018/07/09 Python
Django中使用Celery的方法示例
2018/11/29 Python
Python代理IP爬虫的新手使用教程
2019/09/05 Python
TENSORFLOW变量作用域(VARIABLE SCOPE)
2020/01/10 Python
pycharm中使用request和Pytest进行接口测试的方法
2020/07/31 Python
Python提取视频中图片的示例(按帧、按秒)
2020/10/22 Python
Python中使用aiohttp模拟服务器出现错误问题及解决方法
2020/10/31 Python
荷兰网上鞋店:Ziengs.nl
2017/01/02 全球购物
美国领先的在线邮轮旅游公司:CruiseDirect
2018/06/07 全球购物
英国在线购买轮胎、预订汽车、汽车维修和装配网站:Protyre
2020/04/12 全球购物
C语言面试题
2015/10/30 面试题
军训考核自我鉴定
2014/02/13 职场文书
企业三严三实学习心得体会
2014/10/13 职场文书
拾金不昧感谢信
2015/01/21 职场文书
JavaScript利用html5新方法操作元素类名详解
2021/11/27 Javascript
JAVA长虹键法之建造者Builder模式实现
2022/04/10 Java/Android