我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途包含可以有特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
比较一下原型内的方法地址是否一致:
为了更好的了解构造函数的声明方式和原型模式的声明方式,通过下面的图来了解以下:
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。_proto_属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性prototype中的constructor。通过这两个属性,就可以访问到原型里的属性和方法了。
PS:IE浏览器在脚本访问_proto_会不能识别,火狐和谷歌浏览器及某些浏览器均能识别。
判断一个对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试。
原型模式的执行流程:
原型模式的执行流程:
1.先查找构造函数实例里的属性和方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
如果想要box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可:
如何判断属性是构造函数里的实例里,还是在原型里?可以使用hasOwnPrototype()函数来验证:
in操作符会在通过能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
我们可以通过hasOwnPrototype()方法检测属性是否存在与实例中,也可以通过in来判断实例或原型中是否存在属性。那么结合这两种方法,可以判断原型中是否存在属性。
为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。
如果想让字面量方式的constructor指向实例对象,可以这样做:
PS:字面量方式为什么constructor会指向Object呢?因为Box.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它的prototype,这个对象也会自动获取constructor属性。所以,新对象的constructor重写了Box原来的constructor,因此会指向新对象,那么新对象没有指定构造函数,那么就默认为Object。
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。
原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方式,并且内置的应用类型本身也可以使用原型。
PS:尽管给原生的内置引用类型添加方法使用起来特别方便,但不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。
原型中所有属性是被很多实例共享的,共享对于函数非常适合,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:
PS:数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化的数据需要保留自己的特性,而不能共享。为了解决构造传参和共享问题,可以组合构造函数和原型模式:
PS:这种混合模式很好的解决了传参和引用共享的大难题,是创建对象比较好的方法。
原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起,我们可以使用动态原型模式。
当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用,就不会初始化,并且第二次创建对象,原型也不会再初始化了。这样即得到了封装,又实现了原型方法共享,并且属性都保持独立。
PS:使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会切断实例和原型之间的关系。