本文内容,取自《Javascript高级程序设计》第三版,第六章
什么是构造函数
构造函数的特点:其本身是函数,首字母大写用来与其他函数进行区分。作用只不过可以用来创建对象而已。
function Person(name, age){
this.name = name;
this.age = age;
}
像Object和Array这样的原生构造函数,直接在原型环境中可以使用。Person是我们创建的自定义的构造函数。 构造函数和其他函数没有什么不同。任何函数只要通过new操作符调用,那它就是构造函数。如果不是通过new调用,那就是普通函数。
创建对象
创建对象的方式有两种:
第一种,使用Object构造函数;
第二种,使用对象字面量。
var obj = new Object();//第一种
var obj2 = {};//第二种
想要达到目的:创建多个相似的对象,并且这些对象能够共享一些数据,对象之间互不影响。
创建对象的演变过程:工厂模式 -> 构造函数模式 -> 原型模式 -> 组合模式
工厂模式
function createPerson(name, age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
}
return o;
}
var person1 = createPerson('zhangsan', 25);
var person2 = createPerson('lisi', 26);
优点:解决了创建多个相似对象的问题
缺点:没有解决对象识别的问题(即怎样知道一个对象的类型)
构造函数模式
function Person(name, age){
this.name = name;
this.age = age;
this.say = function(){
console.log(this.name)
}
}
var person1 = new Person('zhangsan', 25);
var person2 = new Person('lisi', 26);
使用new操作符,调用构造函数实际经历以下步骤:
(1)创建一个新的空对象
(2)空对象继承构造函数的原型
(3)this指向这个新的对象,并执行构造函数中的代码(为这个新对象添加属性和方法)
(4)返回新对象
var obj={};
obj.__proto__ = Person.prototype;
Person.call(obj);
return obj;
每个Person实例都有一个constructor(构造函数)属性,该属性指向Person构造函数
console.log(person1.constructor === Person) //true
console.log(person2.constructor === Person) //true
对象的constructor属性最初设计的目的是用来标识对象类型的。检查对象类型,可以使用instanceof
console.log(person1 instanceof Person)//true
console.log(person1 instanceof Object)//true
//所有对象都继承自Object
优点:创建自定义的构造函数,意味着将来可以将它的实例标识为一种特定类型,解决了工厂模式的问题。
缺点:每一个方法都要在每一个实例上重新创建一遍。
console.log(person1.sayName === person2.sayName)//false
原型模式
每创建一个函数都有一个原型(prototype)属性,原型属性指向函数原型对象,这个对象包含所有实例共享的属性和方法。
function Person(name, age){
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 25;
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName === person2.sayName);//true
prototype就是通过调用构造函数而创建的那个实例对象的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性指向prototype属性所在函数的指针,Person.prototype.constructor指向Person。原型对象默认只会取得constructor属性,其他方法都是从Object继承而来的。
当调用构造函数创建一个实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。这个连接在于实例与构造函数的原型对象之间,而不是实例与构造函数之间。
person1和person2内部都有一个指向Person.prototype的指针
//判断对象之间是否存在继承关系
console.log(Person.prototype.isPrototypeOf(person1)) //true
//取得一个对象的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype)
利用原型实现了继承。每当代码读取某个属性时,都会进行一次搜索,目标是具体给定名称的属性。假如name,搜索首先从对象实例本身开始,如果在实例中找到了name,则返回name的值;如果没有找到,则继续搜索指针指向的原型对象[[prototype]],在原型对象找name,如果找到就name的值,没有找到再继续往上找。这正是实例共享原型所保存的属性和方法的基本原理。
function Person(name, age){
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 25;
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.name = 'lisi';
console.log(person1.name);//'lisi'
delete person1.name;
console.log(person1.name);//'zhangsan'
虽然可以通过实例访问保存在原型对象上的值,但是不能通过实例重写原型中的值。如果实例中添加某个属性,原型中有就会屏蔽掉原型中的那个属性,不会修改那个属性。如果delete掉实例中的属性,能够重新访问原型中的属性。
function Person(name, age){
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 25;
Person.prototype.sayName = function(){
console.log(this.name);
}
//检测属性是存在实例中还是存在原型中
var person1 = new Person();
person1.name = 'lisi';
console.log(person1.hasOwnProperty('name'));//true
delete person1.name;
console.log(person1.hasOwnProperty('name'));//false
//in操作符,只要对象能访问到的属性就返回true
console.log('name' in person1);//true
不可枚举属性[[enumerable]]标记为false,constructor和prototype属性都是不可枚举的
for..in循环,返回所有能够通过对象访问的,可枚举的属性。(自身可枚举,原型可枚举)
Object.keys()方法,返回一个包含所有可枚举的实例属性的字符串数组(自身可枚举) Object.getOwnPropertyNames()方法,返回所有实例属性,无论是否可枚举(自身所有)
重写整个原型对象
function Person(name, age){
}
Person.prototype. = {
//这个构造函数属性不再指向Person
//constructor: Person,
name : 'zhangsan',
age : 25,
sayName : function(){
console.log(this.name);
}
}
默认情况,每创建一个函数对象都有一个prototype,这个对象会自动获得constructor属性。但是上面重写了原型对象,constructor属性的指向不再是Person构造函数,而是Object构造函数。导致通过constructor已经无法确定对象的类型。 如果像上面一样显示的加上constructor这个属性,会导致这个属性的[[enumerable]]特性被设置为true。默认情况下,原生的constructor属性不可枚举。 可以通过以下方法定义这个属性。
Object.defineProperty(Person.prototype, 'constructor',{
enumerable: false,
value: Person
})
function Person(name, age){
}
var person1 = new Person();
Person.prototype = {
//这个构造函数属性不再指向Person
constructor: Person,
name : 'zhangsan',
age : 25,
sayName : function(){
console.log(this.name);
}
}
person1.sayName();//报错
实例和原型之间的连接只不过是一个指针,而不是一个副本,原型上的修改,会影响到所有实例,无论是新创建的实例,还是后来创建的实例。 调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的关系。
实例中的指针仅指向原型,而不是指向构造函数
原型模式:
优点:解决了共享数据问题,共享函数非常合适,
缺点:但是共享引用类型的属性这个又有问题。一个实例改了引用类型的属性值,所有属性就会生效。以及忽略了构造函数传递初始化参数的环节,这样默认情况下都将取得相同的属性值。
function Person(name, age){
}
Person.prototype = {
constructor: Person,
name : 'zhangsan',
age : 25,
friends: ['lili','shasha']
sayName : function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('junjun');
//person1的修改,导致person2数据改变
console.log(person2.friends);//['lili','shasha', 'junjun']
组合模式
组合模式就是使用构造函数模式和原型模式组合的模式。
构造函数模式用于定义实例属性,而原型属性用于定义方法和共享的属性。结合两种模式,每个实例都会有自己的一份属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存,同时混合模式还支持向构造函数传递参数。
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ['lili', 'shasha'];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person('zhangsan', 25);
var person2 = new Person('lisi', 26);
person1.friends.push('junjun');
console.log(person1.friends);//['lili', 'shasha', 'junjun']
console.log(person2.friends);//['lili', 'shasha']
寄生构造函数模式
基本思想,创建一个函数,该函数的作用仅仅是封装创建的代码,然后返回新创建的对象。
构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
稳妥构造函数模式
基本思想,没有公共属性,而且其方法也不引用this的对象,不使用new操作符调用构造函数