Javascript高级编程学习笔记(21)—— 对象原型
JS中对象相关的最重要的恐怕就是原型链了
原型链也是JS中对象继承的实现的基础
接昨天的文章,我们使用构造函数创建对象的时候仍然存在一些问题
那就是所有的实例没法共用一个函数
这样无疑会造成极大的内存浪费
那么解决的办法是什么呢?
那就是通过对象的原型来实现
原型链
在JS中我们创建的每一个函数都有一个 prototype(原型)属性
该属性是一个指针指向一个对象,这个对象的用途是包含由特定类型所有实例的共享属性和方法
这个对象也就是我们常说的原型对象
原型链就是对象实例和对象的原型对象组成的继承链
而JS中所有的对象都继承自Object类,所以原型链的顶端也就是Object构造函数的prototype 指向的对象
可以参考下图,可能画的有点丑请多见谅
对于这样一个继承链,在其它的面向对象的语言中,最下面的有关对象的部分应该是公有的,和类的概念十分类似
而不太好理解的,就是有关构造函数的那一部分
造成JS继承灵活多变的地方就在于这里,JS中的函数也是一个对象
所以函数也有自己的原型链,为了让构造函数和被构造函数创建的实例之间联系起来,所以函数除了有自己的 [[Prototype]] 属性之外,还有prototype属性指向被其构造的对象实例的原型对象
与此同时被函数prototype属性指向的对象,其constructor属性也会指向这个函数,这样就使构造函数和对象紧密地结合在一起
PS. 上文中的 [[Prototype]] 属性,就是对象的__proto__属性,但是这一属性只在浏览器中实现,JS的其他实现,对象不一定会有 __proto__属性
在这些没有__proto__ 的实现中虽然无法访问,但是可以通过 isPrototypeOf() 来检测对象之间的这种联系
而在ES5以上的脚本实现中,还支持一个Object.getPrototypeOf() 用于获取一个对象的原型对象
原型模式
在此基础上,我们可以发现,一个构造函数构造的所有函数的实例都有办法能够访问到原型对象
那么一类对象的共享属性的存放位置就显而易见了,那就是将其存放到原型对象上
以昨天的例子来说,使用原型模式创建对象如下:
function Person(){} var pPrototype = Person.prototype; pPrototype.name = 'lhy'; pPrototype.job = 'font end'; pPrototype.age = 21; pPrototype.sayName = function(){ alert(this.name); } var person1 = new Person();
能够访问的原因在于,当代码读取对象的属性的时候 ,都会执行一次搜索,该搜索过程会从该对象实例开始
如果没有找到指定属性,则顺着原型链进行查找,直到Object.prototype为止
找到相应属性则返回,没找到则报未定义的错误
PS. 虽然我们可以通过实例对象访问原型对象的属性,但是我们无法通过实例对象来重写原型对象的属性
什么意思呢?就是说虽然我们可以通过实例对象的属性访问原型对象的某个属性,但是我们对实例对象对应属性的修改不会映射到原型对象上
如果我们在实例对象上设置了,一个本来只有原型对象上才有的属性,那么JS会为实例对象创建一个同名属性,并设置它的值
然后原型对象上的属性就被同名的属性屏蔽了,再也无法访问到原型对象上的这个属性,除非使用delete删除实例上的该属性
例如:
为了能够判断一个属性是来自于该实例,还是其原型对象
可以使用 hasOwnProperty() 方法来进行判断,对于来自实例的属性返回 true
Tips. in操作符不在for-in循环中单独使用的时候,用于判断对象上是否存在该属性,上面的方法与in操作符配合就可以判断属性到底来自实例,还是原型
function test(propertyName,obj){ if(obj.hasOwnProperty(propertyName)){ alert('来自实例'); )else if(propertyName in obj){ alert('来自原型'); } }
Ps. for-in 循环会遍历对象上的所有可枚举属性,包括来自原型的属性,需要注意的是,如果原型上有个不可枚举的属性,而实例上有个与之同名的可枚举属性,那么实例上的这个属性会被遍历出来,不会受原型的影响
除此而外JS提供了Object.keys() 方法,可以获取到对象所有可枚举属性的集合
原型模式还有另一种简化写法
function Person (){} Person.prototype = {name:"lhy",age:"21"};
但是这种写法存在一个问题,由于这里直接将一个对象指定为函数的原型对象
所以这里的函数原型的 constructor 属性也被改变了,这样我们就没法通过 constructor 来判断函数类型
那怎么办呢?
办法还是有的,那就是手动将要指定的原型对象的 constructor 设置为构造函数
不过要注意的是如果手动设置,那么该属性会被for-in 遍历出来,因为这种方式的就是为实例 创建一个 constructor 属性,以此来屏蔽不正确的constructor属性
所以最后还要将 constructor 属性特性描述符的 Enumerable 设为false,才能完美地解决
原型的动态性
从之前举的例子,和刚才简化的写法,我们不难发现原型是一个动态地对象
因为我们对原型对象的修改都会立即体现在实例对象上
同样的我们在操作原型时就要更加地小心,尤其是在重写原型对象时
拿上方的例子来说,虽然我们通过一些操作尽量减少了重写原型对象带来的差异,但还有一些是没法消除的
那就是我们重写原型对象时,会切断已有实例和重写的原型对象的实例,因为它们仍旧指向旧的原型对象
讲了这么多,那么原型模式是我们创建一类对象的最好的选择嘛?
虽然对于函数来说十分地合适,但对于一些引用类型的值可能就会有一些问题
比如我希望对象拥有自己独立的属性,那么原型模式可能就无法胜任了
那么怎么办呢?
构造函数与原型的组合模式
构造函数的缺点是没法共用数据,原型模式的缺点是没法有独立数据
那将他们组合起来不就行了吗
所以这种组合模式就产生了
function Person(name, job, age){ this.name = name; this.age = age; this.job = job; } Person.prototype.sayName = function(){ alert(this.name); }
这种模式基本上就能满足我们平时的所有需求,所以这也是我们广泛使用的方式
动态原型模式
上面的方法虽然从功能上说,无可挑剔,但是暴露在外的独立的原型属性的声明,让追求极致的程序员们十分难受
所以动态原型模式就出现了
function Person(name, job, age){ this.name = name; this.age = age; this.job = job; if(this.sayName !== 'function'){ Person.prototype.sayName = function(){ alert(this.name); } } }
寄生构造函数模式
虽然动态原型模式,已经能够完美地处理绝大部分情况,但是有些特殊情况,仍会存在问题
就像我们不建议给原生的对象的原型添加属性,但有时候我们确实需要根据某个原生的对象来创建一类对象,那么怎么办呢?
所以就有了寄生构造函数模式,其原理就是我不在原生的原型上添加,但我可以使用工厂方法,对其构造的原型对象进行加工,从而得到我想要的对象
function specialArray(){ var values = new Array(); values.push.apply(values,arguments); values.myName = function(){ return 'lhy'; } return values; }
这种只是改良了的工厂方法,存在的弊端和工厂方法几乎一致,无法确定对象类型等
所以使用场景很少
稳妥构造函数
我们知道在一些安全环境下,我们没法使用this、new等来创建,或者需要防止数据被其他应用程序篡改时使用
pS. 不使用new 是指不用new调用构造函数
function Person(name, job, age ){ var o = new Object(); o.sayName = function(){ alert(name); } return o; }
这样创建对象时传入的原始数据,只能通过返回的对象o 上面的方法进行访问,所以保证了原始数据的安全性
总的来说,创建一类函数使用组合模式的场景更为广泛,使用频率也更高