原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。
在单独使用时,in操作符会遍历实例公开(可枚举)的属性,如果找到该指定属性则返回true,无论该指定属性是存在与实例中还是原型中。直接上代码:
1: function Animal(){
2:3: }4: Animal.prototype.name = "animal";
5: var tom = new Animal();
6: tom.age = 22;7: console.log("name" in tom); //true8: console.log("age" in tom); //true
以上代码虽然能够确定实例是否能够访问指定属性,但是我们不能确定指定属性到底是存在于实例上海市原型上,同时使用hasOwnPrototype()和in就可以确定指定属性到底是在实例上海市原型中。上代码:
1: function Animal(){
2:3: }4: Animal.prototype.name = "animal";
5: var tom = new Animal();
6: tom.age = 22;7: function hasPrototypeProperty(object,name){8: return !object.hasOwnProperty(name) && (name in object);9: }10: console.log(hasPrototypeProperty(tom,"name")); //true11: console.log(hasPrototypeProperty(tom,"age")); //false
hasPrototypeProperty()方法只有当该属性只存在于原型上时才会返回true。
在使用for-in循环时,返回的是所有对象能够访问的,可枚举的属性。既包括实例本身的属性也包括实例原型上的属性。要取得对象上所有可枚举的实例属性,可以使用ECMAScript5的Object.keys()方法。这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。上代码:
1: function Animal(){
2: }3: Animal.prototype.name = "animal";
4: Animal.prototype.canEat = true;
5: Animal.prototype.canRun = true;
6: var WangCai = new Animal();
7: console.log(Object.keys(WangCai)); // []
8: console.log(Object.keys(Animal.prototype)); //["name", "canEat", "canRun"]
如果你想得到所有的实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames() 方法,代码如下:
1: function Animal(){
2: }3: Animal.prototype.name = "animal";
4: Animal.prototype.canEat = true;
5: Animal.prototype.canRun = true;
6: var WangCai = new Animal();
7: console.log(Object.keys(WangCai)); // []
8: console.log(Object.getOwnPropertyNames(Animal.prototype)); //["constructor", "name", "canEat", "canRun"]
Object.keys()和Object.getOwnPropertyNames() 方法都可以用来取代for-in循环,但是注意只有IE9+,FF 4+,Safari 5+,Opera 12+ ,Chrome支持。
更简单的原型语法
在前面的例子中,每给原型添加一个属性或方法都要敲一遍Animal.prototype,对于有代码洁癖的我,那是最受不了的,更好的做法是用一个对象字面量来重写prototype,代码如下:
1: function Animal(name){
2: this.name = name;3: }4: Animal.prototype = {5: canEat : true,
6: canRun : true,
7: say : function(){
8: console.log("oh ~~~~我的名字叫"+this.name);
9: }10: };11: var tom = new Animal("tom");12: tom.say(); //oh ~~~~我的名字叫tom13: console.dir(Animal.prototype.constructor); //function Object() { [native code] }
在上面的代码中,我们把Animal.prototype设置为一个以字面量形式创建的新对象。结果相同,但是我们发现constructor已经不再指向Animal了,而是指向Object的构造函数,上一篇中提到,每创建一个新函数,就会同时创建他的prototype对象,这个对象也会获得constructor属性。而我们在这里重写了prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object的构造函数),不再指向Animal,这个时候constructor也就无法确定对象的类型了。(注意constructor就不再是存在于prototype上了)
我们也可以手动指定constructor,代码如下:
1: function Animal(name){
2: this.name = name;3: }4: Animal.prototype = {5: constructor: Animal,6: canEat : true,
7: canRun : true,
8: say : function(){
9: console.log("oh ~~~~我的名字叫"+this.name);
10: }11: };
以上代码,我们设置了一个constructor,从而确保了通过该属性能够访问到适当的值,但是这种方式重设constructor属性会导致它的[[enumerable]] 特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。在支持ECMAScript 5的浏览器上你可以用 Object.defineProperty()。示例代码如下:
1: function Animal(name){
2: this.name = name;3: }4: Animal.prototype = {5: constructor: Animal,6: canEat : true,
7: canRun : true,
8: say : function(){
9: console.log("oh ~~~~我的名字叫"+this.name);
10: }11: };12: //重新设置构造函数13: Object.defineProperty(Animal.prototype,"constructor",{14: enumerable : false,
15: value : Animal16: });
原型的动态性
前面几篇包括上面的重写prototype都涉及到了原型的动态性,所以我在这里总结一下。原型是伴随着一个新函数的创建而创建的,原型本身和一个新的Object实例并无区别,只是他多了一个constructor属性,这个constructor属性是指向那个新函数的。注意,原型是伴随着函数(那个新函数)存在的,和具体的实例并无直接联系,故每一个由这个新函数(构造函数)创建的新实例都共享原型的属性和方法(公开的),这也就是原型继承的基本。原型模式和构造函数的每个方法都是一个新的Function实例有着本质的区别,每个新的实例并无具体的方法实现(原型上定义的方法),只是通过原型链接口来访问原型上的具体实现。但是值得注意的是,通过构造函数创建的新实例指向的原型都是当初创建时构造函数所指的那个原型,就算我们在实例化以后改变该构造函数的原型(如重新声明原型为{}),该实例的原型依然是当初那个原型。而上面我们就是以字面量的方式重写了该原型,所以也就切断了已有实例和现在原型的关系。
原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有的原生引用类型(Object,Array,String...)都在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新的方法,甚至可以重写默认方法(不推荐,如果出现冲突,将很难处理)。
原型对象的问题
原型模式也不是没有缺点,原型模式的最大缺点就是每个实例对原型的修改都会影响到所有继承自该原型的实例(通过该原型伴随的函数创建的实例)。可是实例一般都要有属于自己的全部属性,而这个问题也是有解决方式的(下一篇 呵呵)
后记
如果在文中发现错误,请指正。推荐一个JS学习群(群里都是大神) 239147101