面向对象 — 原型(一)
本文主要介绍JavaScript中的原型和原型链的基本概念和作用
通过前一篇文章面向对象 — 从基本介绍到构造函数的阅读,我们知道了JavaScript是一门面向对象的语言,但是,作者Brendan Eich在设计Javascript的时候并没有打算引入 “类”的概念,因为Javascript在当时作为一门简易的脚本语言,并不需要那么正式。但是最后,Brendan还是为其设计了一套完整的面向对象的机制,包括继承(关于Javascript中继承的实现可移步我另一篇文章:面向对象 — 继承)。
在这一套机制里,首先,借鉴了Java和C++的语法引入了new运算符,同时也借鉴了这些语言里在使用new运算符的时候就会调用类的constructor(构造函数),所以干脆就设计成了new一个构造函数来实现实例化。所以在JavaScript里,new运算符后面跟的不是类,而是构造函数。
1 | function Person (name, age) { |
这样的话我们可以使用一个构造函数new多个实例,而且这些实例都拥有在构造函数里定义的属性和方法,但是如果我们尝试输出一下:
1 | console.log(dary.intro === brendan.intro) // false |
你会发现,这会输出 false
,因为每个实例都拥有自己的属性和方法的副本,它们互相独立互不干扰。这样不仅无法做到数据共享,更是极大的浪费资源(两个intro方法体一模一样,但却存了两份)。考虑到这一点,Brendan决定为每一个函数设置一个prototype属性,即原型。
原型是函数的伴生体,什么是伴生体?看过红楼梦的都知道,贾宝玉之所以名叫贾宝玉是因为他出生的时候嘴里就含着一块玉,那么原型之于函数就相当于那块玉之于贾宝玉。
每个函数在被创建的时候就会有一个 prototype
属性,这个属性是一个指针,指向一个对象,而这个对象就是这个函数的原型对象,它是用来共享所有实例的属性和方法的地方。换句话说,写在这个对象里的属性和方法是所有实例公用的,这样的好处主要有两点:一、可以实现数据共享,多个实例共享相同的数据;二、节约资源,不需要每个实例存一份副本了。所以,我们的代码可以这样改写:
1 | function Person (name, age) { |
当我们把intro方法写在构造函数的原型上的时候,我们看到每一个通过Person实例化出来的对象都会调用同一个原型上的intro方法。也就是说,intro方法并不属于某一个实例,dary和brendan这两个对象本身都应该没有intro方法,我们在浏览器控制台打印了这两个对象也发现了这一点:
但是我们看到,实例对象除了自己本身有的属性name个age以外,还有一个__proto__
属性,这个属性是不可枚举的,但是大部分浏览器都可以直接输出并且使用它。从这实例对象的这个属性里我们都发现了intro,所以处于好奇,我们输出一下它们俩:
1 | console.log(dary.__proto__.intro === brendan.__proto__.intro) // true |
我们会惊奇的发现,得到的结果为true
,说明它俩是同一个方法,而这个方法我们是在Person的原型上定义的,所以我们有一个大胆的猜测:
1 | console.log(dary.__proto__ === Person.prototype) // true |
我们发现,这三句表达式输出结果均为true
,这下就说得通了,实例对象有一个不可枚举的属性 __proto__,这个属性是一个指针,指向了其构造函数的prototype也就是原型对象,实例可以通过 __proto__ 访问到构造函数的原型上的方法。
简单讲:实例的 __proto__ 指向构造函数的prototype。
而当我们打开Person.prototype的时候还会发现里面有一个不可枚举的constructor
属性,这个属性指回当前构造函数本身,也就是说:
1 | console.log(Person.prototype.constructor === Person) // true |
由此,我们可以得到如下图所示的关系:
由此图我们可以看出:当dary要访问某个属性或者方法的是,应该先从自己身上查找,如果有,则直接使用,如果没有,则从 __proto__ 属性找到了Person.prototype,可以调用Person.prototype上的方法。
这时我突发奇想,尝试着 dary.toString()
发现可以成功调用,但是我在dary本身以及Person.prototype上都没有定义这个方法,我又试着把Person.prototype输出一下,结果惊讶到我了:
仔细思考一下也就明白了,既然Person.prototype是一个对象,就是说它也应该是一个实例,那么他的构造函数是谁?作为一个普通对象来讲,应该都是 new Object()
吧?所以我试着输出:
1 | console.log(Person.prototype.__proto__ === Object.prototype) // true |
当我看到这个结果为 true
的时候我就彻底明白了,函数的原型本质就是一个普通对象,所以他是来自Object的实例,因此,原型对象的 __proto__ 属性指向Object.prototype。
由此,我们可以得到下图:
那Object.prototype不也是一个普通对象么?那它的__proto__ 呢?
我试着console了一下Object.prototype.__proto__
,结果得到了null
,我想这应该是Javascript故意这么设计的吧。
所以,原型(prototype)是函数的伴生体,实例的 __proto__ 指向构造函数的prototype,Object.prototype.__proto__ === null。记住这两句,原型你就明白了一大半了。
并且,文章读到这里,我们也就能明白Javascript一切皆为对象这句话的真正含义了,因为Javascript中任意数据都能沿着自己的原型链最终找到Object.prototype
,任意数据都能调用Object.prototype上的方法。
附上几个与原型相关的常用属性和方法列表
prototype
构造函数的原型__proto__
也叫[[prototype]]
隐式原型,实例对象上的属性,指向构造函数的prototypeinstanceof
运算符,判断一个对象是否是构造函数的实例1
2console.log(dary instanceof Person) // true
console.log(dary instanceof Object) // truehasOwnProperty
判断对象上是否存在某个属性,并且这个方法会过滤到原型上的属性1
2
3console.log(dary.hasOwnProperty('name')) // true
console.log(dary.hasOwnProperty('intro')) // false
console.log(dary.hasOwnProperty('abc')) // falseisPrototypeOf
检查一个对象是否存在于另一个对象的原型链上1
2console.log(Person.prototype.isPrototypeOf(dary)) // true
console.log(Object.prototype.isPrototypeOf(dary)) // true
代码不是万能的,但不写代码是万万不能的
Dary记
-
更多干货,尽在公众号
转载请注明来源,文末有原始链接。欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 dary1112@foxmail.com
创作不易,您的打赏是我更新的动力
-
支付宝
-
微信
文章标题:面向对象 — 原型(一)
文章字数:1.9k
本文作者:Dary
发布时间:2019-11-20, 21:42:00
最后更新:2020-03-14, 11:06:44
原始链接:http://www.xiongdalin.com/2019/11/20/oop-02/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。
Built By Dary