TDD测试驱动的javascript开发(二) - 深入学习javascript中prototype

TDD测试驱动的javascript开发(2) ------ 深入学习javascript中prototype

1. 原型模式

1.1 我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

简单的解释上面的话的意思就是:首先,我们要知道,面向对象的语言中类的存在,而javascript也是一门面向对象的语言(这句话说的可能有一些毛病,但是不影响),在javascript中定义一个类函数的时候,就默认创建了一个prototype属性,在这个prototype属性里的所有的属性和方法将被这个类的所有实例共享。看代码:

//创建一个Person类
function Person() {
}
//Person类prototype里包含了name、age属性和sayName()方法
Person.prototype.name = "defaultName";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
	return this.name;
};

var person = new Person();
var person3 = new Person();
TestCase("test extends",{
	"test person.sayName() should be equals person3.sayName()" : function() {
		assertEquals(person.sayName(),person3.sayName()); 
	}
});

1.2 在默认的情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

Person.prototype.constructor  指向  Person

当为对象实例添加一个属性时,这个属性就会 屏蔽 原型对象中保存的同名属性(它只会阻止我们访问原型中的那个属性,并不会修改那个属性),不过我们可以使用delete操作符删除实例属性,让我们可以继续访问原型中的属性,看代码:

//创建一个Person类
function Person() {
}
//Person类prototype里包含了name、age属性和sayName()方法
Person.prototype.name = "defaultName";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
	alert(this.name);
};

var person = new Person();
person.sayName();         // defaultName

var person2 = new Person();
person2.name = "person2";
person2.sayName();           //person2
//删除实例中的name属性
delete person2.name;
person2.sayName();          //defaultName
1.3 使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是存在于原型中,只有在给定的属性是存在于对象的实例中才会返回true

function Person() {
}
//Person类prototype里包含了name、age属性和sayName()方法
Person.prototype.name = "defaultName";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
	return this.name;
};

var person = new Person();

var person2 = new Person();
person2.name = "person2";

TestCase("test extends",{
	"test person should not be hasOwnProperty name" : function() {
		assertEquals(false,person.hasOwnProperty("name")); 
	},
	"test person2 should not be hasOwnProperty name" : function() {
		assertEquals(true,person2.hasOwnProperty("name")); 
	},
});


1.4 使用 in 操作符确定一个属性是原型中的属

in操作符只要通过对象能够访问到的属性就返回true,(hasOwnProperty()只在属性存在于实例中才返回true),因此,只要in操作符返回true,hasOwnProperty()返回false,就证明该属性是原型中的属性,看代码:

//创建一个Person类
function Person() {
}
//Person类prototype里包含了name、age属性和sayName()方法
Person.prototype.name = "defaultName";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
	alert(this.name);
};

var person = new Person();

var person2 = new Person();
person2.name = "person2";
person2.color = "blue";

TestCase("test extends",{
	"test Person  person.hasOwnProperty(name)" : function() {
		assertEquals(false,person.hasOwnProperty("name")); //name是原型中的属性,实例中不存在
	},
	"test Person  name in person" : function() {
		assertEquals(true,"name" in person);
	},
	"test Person  person2.hasOwnProperty(name)" : function() {
		assertEquals(true,person2.hasOwnProperty("name"));//name是实例对象的属性
	},
	"test Person  Person2 name in person2" : function() {
		assertEquals(true,"name" in person2);
	},
	"test Person  person2.hasOwnProperty(color)" : function() {
		assertEquals(true,person2.hasOwnProperty("color"));   //color是实例对象的属性
	},
	"test Person  color property in Person2" : function() {
		assertEquals(true,"color" in person2);		//color是实例对象的属性,in也同样可以访问到
	}
	
});

1.5枚举出实例的全部属性---- Object.keys()


function Person() {
}
//Person类prototype里包含了name、age属性和sayName()方法
Person.prototype.name = "defaultName";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
	return this.name;
};

var person = new Person();

var person2 = new Person();
person2.name = "person2";

var keys = Object.keys(Person.prototype); 

var keys2 = Object.keys(person2);

TestCase("test extends",{
	"test keys should be an array" : function() {
		assertArray("keys is not an array",keys); 
	},
	"test keys[0] should be equals name" : function() {
		assertEquals("name",keys[0]);
	},
	"test keys[1] should be equals age" : function() {
		assertEquals("age",keys[1]);
	},
	"test keys[2] should be equals age" : function() {
		assertEquals("sayName",keys[2]);
	},
	"test keys2 should be an array" : function() {
		assertArray("keys is not an array",keys2);
	},
	"test keys2[0] should be equals age" : function() {
		assertEquals("name",keys2[0]);
	}
});
1.6 更简单的原型语法-----字面量创建对象

function Person() {
	
}
Person.prototype = {
	name : "defaultName",
	age : 20,
	sayName : function() {
		return this.name;
	}
		
};

在之前创建函数的时候,我们知道,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor。而我们在这里使用的这种语法(字面量),本质上完全重写了默认的prototype对象,因此constructor对象也就变成了新对象的constructor的属性(指向Object构造函数),不再指向Person函数。

function Person() {
	
}
Person.prototype = {
	name : "defaultName",
	age : 20,
	sayName : function() {
		return this.name;
	}
};

var con = new Person();

TestCase("test extends",{
    "test con should be instance of Object" : function() {
        assertInstanceOf("con shoud be instance of Obejct", Object, con);
    },
    "test con should be instance of Person" : function() {
        assertInstanceOf("con shoud be instance of Person", Person, con);
    },
    "test con constructor should be equals Person" : function() {
        assertNotEquals(Person,con.constructor);                    //不再等于Person
    },
    "test con constructor should not be equals Object" : function() {
        assertEquals(Object,con.constructor);  
    },
    "test con constructor should be same as Person" : function() {
        assertNotSame(Person,con.constructor);
    },
    "test con constructor should not be same as Object" : function() {
        assertSame(Object,con.constructor);  
    }
});

1.7如何保留1.6中的constructor,在constructor的值比较重要的时候,需要我们来保存这个constructor的值,这个时候,我们可以在Person.prototype中重写constructor属性,并设置其值为Person。 -------但是注意:原生的constructor属性是不可以枚举的,如果这样设置了之后,constructor将变成可以枚举的。

function Person() {
	
}
Person.prototype = {
	constructor : Person,
	name : "defaultName",
	age : 20,
	sayName : function() {
		return this.name;
	}
};

var con = new Person();

TestCase("test extends",{
	"test con should be instance of Object" : function() {
		assertInstanceOf("con shoud be instance of Obejct", Object, con);
	},
	"test con should be instance of Person" : function() {
		assertInstanceOf("con shoud be instance of Person", Person, con);
	},
	"test con constructor should be equals Person" : function() {
		assertEquals(Person,con.constructor);               //完全重写了constructor,所以不再等于Object,但是还是Object的实例哦
	},
	"test con constructor should not be equals Object" : function() {
		assertNotEquals(Object,con.constructor);  
	},
	"test con constructor should be same as Person" : function() {
		assertSame(Person,con.constructor);
	},
	"test con constructor should not be same as Object" : function() {
		assertNotSame(Object,con.constructor);  
	}
});
补充:如果你的浏览器是兼容ECMAScript5,就可以使用defineProperty

//重设构造函数
Object.defineProperty(Person.prototype,"constructor",{
	enumerable:false,
	value:Persons
});

1.8原型对象的问题

原型中的属性是被所有实例所共享的,当然,这种共享对于函数非常合适,可是对于一些引用类型的属性来说,问题就出现了,因为有些时候我们是不想我们的引用类型的属性被共享:

function Person() {
	
}
Person.prototype = {
	name : "defaultName",
	age : 20,
	friends : ["fengfeng","tongtong"],
	sayName : function() {
		return this.name;
	}
};
var person = new Person();
person.friends.push("ty");

var person2 = new Person();

TestCase("test property",{
	"test person friends.length should be 3 " : function() {
		assertEquals(person.friends.length,3);  
	},
	"test person friends[2] should be ty" : function() {
		assertEquals("ty",person.friends[2]);  
	},
	"test person2 friends.length should be became 3 " : function() {
		assertEquals(person2.friends.length,3);      //person2的长度也变成了3
	},
	"test person2 friends should be equals person friends " : function() {
		assertEquals(person.friends,person2.friends); //person2的friends属性也发生了变化
	}
});

1.9 如何解决引用类型的属性不被共享-----组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.friends = ['tong','feng'];
}
Person.prototype = {
		constructor : Person,
		sayName : function() {
			return this.name;
		}
};

var person = new Person("tongtong",25);

var person2 = new Person("fengfeng",26);

person.friends.push("ty");

TestCase("test property",{
	"test person friends.length should be 3 " : function() {
		assertEquals(person.friends.length,3);  
	},
	"test person friends[2] should be ty" : function() {
		assertEquals("ty",person.friends[2]);  
	},
	"test person2 friends.length should be 2 " : function() {
		assertEquals(person2.friends.length,2);      //person2的长度还是2
	},
	"test person2 friends should not be equals person friends " : function() {
		assertNotEquals(person.friends,person2.friends); //person2的friends属性没有发生变化
	},
	"test person2 sayName should be equals person sayName" : function() {
		assertEquals(person.sayName,person2.sayName); //person.sayName===person2.sayName
	}
});

1.10 继1.9扩展 ------  动态原型模式

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.friends = ['tong','feng'];
	//method
	if (typeof this.sayName != "function") {
		Person.prototype.sayName = function() {
			return this.name;
		};
	}
}

只有在sayName()方法不存在的时候,才会将它添加到原型中,这段代码只会在初次调用构造函数的时候执行。这里对原型的修改,能够立即在所有实例中得到反映。

使用动态原型模式时,不能使用对象字面量重写原型,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例和新原型之间的联系。

1.11稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象,稳妥对象最适合使用在一些安全环境中(这些环境会禁止使用this和new),或者在防止数据被其他应用程序改动时使用。

稳妥构造函数的2个特点:

新创建对象的实例方法不引用this,      不使用new操作符调用构造函数

使用稳妥对构造函数模式创建的对象与构造函数之间没什么关系,因此instanceof操作符对这种对象没有意义。

function Person(name,age) {
	var o = new Object();
	o.sayName = function() {
		return name;
	};
	return o;
};

var person = Person("tongtong","25");

TestCase("test property", {
	"test person sayName should be tongtong " : function() {
		assertEquals(person.sayName(),"tongtong");  
	},
	"test person is not instanceof Person " : function() {
		assertNotInstanceOf(Person,person); 
	}
});

关于安全模式的2个平台

http://www.adsafe.org/

http://code.google.com/p/google-caja/