理解原型对象
我们创建的每一个函数都有一个默认的prototype
属性,它指向一个对象,对象中默认的有一个叫做constructor
的属性,指向这个函数本身。
如上图,右侧的这个方框就是函数function的原型,也就是说prototype属性指向的这个对象就是原型。
当构造函数创建出一个新实例后,该实例会默认具有一个__proto__
属性,这个属性指向构造函数的原型对象。因此,如果我们把属性和方法都添加到原型对象中,不同的实例就可以访问到相同的属性和方法了。
我们先声明了一个空的构造函数Person,将一些属性和方法添加到了Person函数的prototype属性中,然后实例化了两个对象,根据后面的输出我们可以确定两个实例访问的是相同的对象,它们共享这些属性和方法。下图展示了例子中各个对象的关系:
实例与原型中属性的纠葛
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。换句话说,添加这个属性只会阻止我们去访问原型中的那个属性,但不会修改那个属性。你也可以理解为,当我们访问实例对象时,会优先访问它自身的属性和方法。
在这个例子中,我们给实例fun1
添加了属性name="Tom"
。
- 访问
fun1.name
时,先在实例fun1
中寻找属性name
,找到之后返回Tom
就不必再搜索原型了。 - 而访问
fun2.name
时,先在实例fun2
中没有找到属性name
,于是搜索原型,结果找到了值为wanghan
的属性name
,验证了在实例中添加属性不会修改原型中的同名属性。 - 访问
fun1.getName()
时,在在实例fun1
中没有找到方法getName()
,搜索原型找到了这个方法,执行其中的代码,此时方法中的this
已经指向了实例(回忆一下操作符new调用构造函数经历的过程),因此输出的是实例属性name
的值Tom
。
判断属性的位置
hasOwnProperty()
方法只在给定属性存在于实例对象中时返回true
。- 单独使用
in
操作符时,无论属性存在于实例中还是原型中,只要能够访问到,就会返回true
。
如果实例和原型中都存在着一个相同属性,结合这两种方法我们就可以判断,我们访问到的这个同名属性到底是实例中的还是原型中的。
更简单的原型语法
在前面的例子中,我们给原型对象添加对象的时候,输入了好多遍Person.prototype
,其实这些不必要的输入都是可以避免的,最常见的方法就是以对象字面量的形式创建对象。
这样创建对象是不是很轻松,而且输出的结果跟之前相比并没有什么变化。但是这里有一点需要注意,重写原型对象的实质是,我们重建了一个对象赋值给了函数的prototype
,所以新建的这个原型对象中默认的constructor
属性也是新建的,它不指向构造函数Person
,但是必要情况下我们可以手动让它指向Person
。
重写原型对象的弊端
由于实例与原型之间的松散连接关系,即使我们先创建实例,再给原型对象添加属性,我们也照样可以访问到这些属性。
但是重写原型对象之后就不一样了。
我们前面说过,重写原型对象实际上是新建了一个新的对象赋值给构造函数的prototype
。如果是先创建一个实例,那么实例指向最开始的一个只含有constructor
属性的原型对象,即使随后又新建了一个原型对象,它的指向也不会再发生变化。
要想解决这个问题就要牢记,要在重写原型对象之后新建实例,这样实例指向的就是重写之后的原型对象。
原型对象的问题
原型模式也不是没有缺点。
- 原型中的所有属性和方法都是被实例所共享的,共享方法(函数)是非常合适的,对于那些基本值的属性也还说的过去,但是对于包含引用类型值得属性来说,问题就非常突出了。 123456789101112function Person() {}Person.prototype={constructor: Person,name: "wanghan",friends: ["zhangmin","yangfan"]};var fun1 = new Person();var fun2 = new Person();fun1.friends.push("Tom");console.log(fun1.friends); //["zhangmin", "yangfan", "Tom"]console.log(fun2.friends); //["zhangmin", "yangfan", "Tom"]
原型对象中有一个字符串数组,然后创建了两个实例,操作实例fun1
向原型对象的数组中又添加了一个字符串Tom
,由于数组是引用类型值,并且两个实例共享原型对象中的属性,所以我们刚刚的修改也会在实例fun2
中体现出来。这个问题正是我们极少看到有人单独使用原型模式的原因所在。
- 原型对象直接在原型对象里定义了属性值,使所有实例默认情况下都取得相同的属性值,要克服这一缺点可以组合使用原型模式和构造函数模式。
后记
我画框图是用电脑自带的画图软件画的你敢信,举得例子也是自己手打出来的,运行正确之后就剪切到markdown上面。眼看着快要写完了,时间也快凌晨两点了,喜滋滋地准备收尾,谁知道电脑突然一黑,重启了。再次打开markdown之后我就绝望了,就剩下前两段摆在上面。我冷静了二十分钟,发了个朋友圈,然后重写到凌晨四点。嗨呀,好气呀。