创建JavaScript自定义对象的方式

时间:2022-02-22 19:25:58

一、Object构造函数

1、使用new关键字创建一个Object的实例,然后为该实例添加属性或方法:

var person = new Object();
person.name = "zhangsan";
person.getName = function() {
	return this.name;
};
二、对象字面量

var person = {
	name: "zhangsan",
	getName: function() {
		return this.name;
	}
};
三、工厂模式

1、在JavaScript中虽然没有类的概念,不可以创建一个类,但是可以创建一种函数来封装创建对象的细节:

function getPersonInstance(name) {
	var obj = new Object();
	obj.name = name;
	obj.getName = function() {
		return this.name;
	};
	return obj;
}

// 创建对象
var person = getPersonInstance("zhangsan");
2、对于创建大量的相似对象时,工厂模式相比使用Object构造函数或对象字面量的方式可以避免产生大量重复的代码。但是工厂模式不能解决对象识别(对象的类型)的问题。

四、构造函数模式

1、ECMAScript中的构造函数可以用来创建特定类型的对象,像Object和Array之类的原生构造函数在运行时会自动出现在执行环境中。除此之外,还可以自定义构造函数,从而创建自定义对象:

function Person(name) {
	this.name = name;
	this.getName = function() {
		return this.name;
	};
}

// 创建对象
var person = new Person("zhangsan");
在写法上一般习惯是将构造函数的首字母大写,而非构造函数的首字母应该小写;构造函数本身也是一个函数,只是它用来创建对象而已。

2、使用new操作符调用构造函数的方式实际上会经历如下的四个过程:

  • 创建一个新对象;
  • 将构造函数的作用域赋值给这个新对象,所以this就指向了这个新对象;
  • 执行构造函数中的代码,为该对象添加属性或方法;
  • 返回新对象

3、如上代码所示,创建的person对象会有一个属性constructor(构造函数),该属性指向Person,即:person.constructor == Person为true,该属性可以用来标识对象类型,不过它同时也是Object的实例,因为所有对象均继承自Object。

4、创建自定义的构造函数可以将它的实例标识为一种特定的类型,所以构造函数模式相比工程模式而言,可以解决对象识别的问题。

5、这种方式定义的构造函数是定义在Global对象中的,在浏览器中就是window对象。

6、将构造函数当作普通函数使用

构造函数也是函数;对于任何函数,只要通过new操作符来调用,那它就可以作为构造函数;同样,对于任何函数只要不是通过new操作符来调用,那就是一个普通函数的调用;前面见证了使用new操作符来调用函数,如下是把构造函数当作普通函数来调用:

  • 采用如下方式调用,则属性和方法都会被添加给window对象,因为当在全局作用域中调用一个函数时,this对象总是指向Global对象,而Global对象在浏览器中就是window对象
// 把构造函数作为普通函数调用
Person("zhangsan");
alert(window.getName());
  • 如下调用方式,使用call或apply在某个特殊的作用域中调用Person()函数,这里是在对象obj的作用域中调用的,所以对象obj就拥有了所有属性和方法
// 在另一个对象的作用域中调用
var obj = new Object();
Person.call(obj, "shangsan");
alert(obj.getName());
7、构造函数方式的问题
因为ECMAScript中的函数也是对象,因此每定义一个函数也就是实例化了一个对象。所以,如果构造函数中定义了方法的话,则构造函数中的每个方法都要在每个实例上重新创建一遍,比如上面的Person构造函数,如果实例化了两个对象p1和p2,则p1和p2均拥有getName这个方法,但是它们各自的这个getName方法并不是同一个Function的实例。所以,此时的Person构造函数从逻辑上等价于如下的构造函数:
function Person(name) {
	this.name = name;
	this.getName = new Function("return this.name");
}
以上方式创建的函数,会导致不同的作用域链和标识符解析,但是创建Function实例的机制是相同的,所以使用new Person()方式创建的不同实例的同名方法是不相等的,也就是说加入有两个对象p1和p2,则p1.getName != p2.getName;所以问题就在于没有必要创建两个以上的完成同样功能的Function实例,并且这里的getName函数中有this对象存在,根本不用在执行代码前就把函数绑定到特定的对象上,解决办法就是把函数定义转移到构造函数的外部,如下:
function Person(name) {
	this.name = name;
	this.getName = getName;
}

function getName() {
	return this.name;
}
这样过后,把getName属性设置成等于全局的getName函数,即getName属性包含的是一个指向getName函数的指针,所以有该构造函数创建的多个实例就共享了在全局作用域中定义的同一个getName函数。但是在全局作用域中定义的函数实际上只能被某个对象调用,这样做却使得全局作用域变得名不副实;而且如果对象需要定义很多的方法,则就需要定义更多的全局函数,就会使得自定义的构造函数变得毫无封装性。
五、原型模式

1、创建的每个函数都有一个属性prototype(原型),该属性是一个指针,指向一个对象,而该对象的用途是包含可以由特定类型的所有实例共享的属性和方法;所以不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。如下:

function Person() {}
Person.prototype.name = "zhangsan";
Person.prototype.getName = function() {
	return this.name;
};
由于name属性和getName方法都是直接添加到Person的prototype中的,所以通过new Person()方式创建的多个对象共享了这些属性和方法,它们拥有的都是同一个属性和方法,可以通过"实例.name"的方式来访问name属性,这是通过查找对象属性的过程来实现的。

2、原型对象

     只要创建了一个新函数,则会根据一组特定的规则为该函数创建一个prototype属性,该属性指向了函数的原型对象,在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,该属性包含一个指向prototype属性所在函数的指针,所以对于Person函数来说,Person.prototype.constructor指向Person。

     创建了自定义的构造函数后,其原型对象默认只会取得constructor属性,至于其它方法则都是从Object继承而来的;当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性)指向构造函数的原型对象,ECMA-262第五版中称这个指针为[[Prototype]],没有标准的方式来访问[[Prototype]],但是在Firefox、Safari、Chrome浏览器中每个对象都支持属性__proto__,而在其它实现中该属性是完全不可见的。虽然所有的实现都无法访问[[Prototype]],但是可以通过原型对象的isPrototypeOf()方法来确定实例与原型对象之间是否存在这种关系,如果实例的[[Prototype]]指向了调用isPrototypeOf()方法的原型对象,则该返回true:Person.prototype.isPrototypeOf(person);在ECMAScript5中有个方法叫做Object.getPrototypeOf(),在所有支持的实现中,该方法返回[[Prototype]]的值:Object.getPrototypeOf(person) == Person.prototype,支持该方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+、Chrome。

     每当读取某个对象的属性时,都会进行一次搜索,首先从对象实例本身开始,找到了就返回属性值,如果没有找到,则继续搜索指针指向的原型对象,这就是多个对象实例共享原型对象所保存的属性和方法的基本原理。

     虽然可以通过对象实例访问保存在原型对象中的值,但是不能通过对象实例重写原型对象中的值,如果在某个实例中添加了一个属性,且该属性与原型对象中的某个属性同名,那么是在实例中创建该属性,该属性将会屏蔽掉原型对象中的那个同名属性。同时也可以使用delete操作符来删除某个实例属性,从而能够重新访问原型对象中的同名属性。

     方法hasOwnProperty()(从Object对象继承而来的)可以用来检测一个属性是存在于实例本身还是存在于原型对象中,当给定属性存在于实例中时返回true。

3、原型对象与in操作符

     使用in操作符的两种方式,如下:

  • 单独使用:通过对象能够访问指定属性时返回true,无论该属性存在于实例对象中还是存在于原型对象中,使用方式为:"属性名" in 对象。结合hasOwnProperty方法使用就能确定一个属性是否存在且存在什么对象中。
  • 在for-in循环中使用:返回的是所有能够通过对象访问、可枚举(enumerated)的属性,既包括存在于实例中的属性,也包括存在于原型对象中的属性;屏蔽了原型对象中的不可不枚举属性([[Enumerable]]标记的属性)的实例属性也会在该循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的(IE8--例外)。此外,要取得对象上所有可枚举的的实例属性,可以使用ECMAScript5中的Object.keys()方法,返回一个包含所有可枚举属性的字符串数组:Object.keys(Person.prototype)。如果是要取得所有实例属性,而无论该属性是否可枚举,则可以使用Object.getOwnPropertyNames(对象)。支持这两个方法的浏览器包括:IE9+、Firefox4+、Safari5+、Opera12+、Chrome。

4、简单的原型语法

可以使用对象字面量的方式来重写原型对象,如下:

function Person() {}
Person.prototype = {
	name: "zhangsan",
	getName: function() {
		return this.name;
	}
};
注意:这样做之后,constructor属性不再指向原来的Person构造函数了。因为每创建一个函数,就会同时创建它的原型对象,该原型对象也会自动拥有属性constructor,而这种方式本质上完全重写了默认的原型对象,所以constructor属性也就变成了新对象的constructor属性,所以现在的constructor属性指向了Object构造函数。此时,使用instanceof操作符还能返回正确的结果,但是通过constructor已经无法确定对象的类型了,如下:

var person = new Person();
alert(person instanceof Object); // true
alert(person instanceof Person); // true
alert(person.constructor == Person); // false
alert(person.constructor == Object); // true
如果要保持constructor属性指向Person,可以用如下的方式特意将它设置回适当的值:

function Person() {}
Person.prototype = {
	constructor: Person,
	name: "zhangsan",
	getName: function() {
		return this.name;
	}
};
指定了constructor属性的值是Person构造函数,保证了通过该属性能够访问到指定的值。但是这样设置constructor属性会导致它的[[Enumerable]]特性被设置为true,而默认情况下是false,即原生的constructor属性是不可枚举的,所以,如果使用的是兼容ECMASript5的JavaScript引擎,可以使用Object.defineProperty()方法来重设构造函数的方式来解决这个问题,如下:

function Person() {}
Person.prototype = {
	name: "zhangsan",
	getName: function() {
		return this.name;
	}
};
// 重设构造函数
Object.defineProperty(Person.property, "constructor", {
	enumerable: false,
	value: Person
});
5、原型的动态性

对原型对象所做的任何修改能够立即从实例上反映出来,即便是先创建了实例后再修改原型也是如此。尽管可以随时为原型对象添加属性和方法,且能够立即在所有的实例对象中反映出来,但是如果是重写了整个原型对象,那么情况会不一样;因为调用构造函数创建实例对象时,会为实例对象添加一个指向最初原型对象的[[Prototype]]指针,而把原型对象修改为另一个对象,则就等于切断了构造函数与最初原型对象之间的关系,而且,实例中的那个指针仅指向原型对象,而不是指向构造函数。如下代码所示:

function Person() {}

var p = new Person();

Person.prototype = {
	constructor: Person,
	name: "zhangsan",
	getName: function() {
		return this.name;
	}
};

p.getName(); // 错误
在以上代码中,当创建了person对象后,重写了其原型对象,因为person指向的原型对象中并不包含getName方法,getName方法是在新的对象中定义的,所以person.getName()报错。如下图:

创建JavaScript自定义对象的方式

6、原生对象的原型

所有的原生引用类型都在其构造函数的原型对象上定义了方法,所以通过原生对象的原型,还可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型。为原生对象的原型添加属性和方法后,会在当前环境中有效。

7、原型对象的问题

由于原型对象中的所有属性和方法都是被所有实例共享的,这种共享对于方法来说很适合,对于基本数据类型的属性来说也是适合的,因为通过在实例上添加同名的属性可以隐藏掉原型对象中的对应属性,但是对于引用类型的属性来说,要格外注意,如下:

function Person() {}

Person.prototype = {
	constructor: Person,
	books: ["JavaScript", "C++"]
};

var p1 = new Person();
var p2 = new Person();

p1.books.push("MongoDB");
这里用p1对象在原型对象的books引用属性上添加了一个元素,导致p2对象访问books属性时也能反映出刚刚的添加动作,因为这个操作并不是为p1对象添加新的元素,不会屏蔽原型对象的同名属性。

六、组合使用构造函数模式和原型模式

1、组合使用构造函数模式和原型模式的方式是创建自定义对象最常见的方式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。所以,每个实例都会有自己的一份实例属性的副本,但同时又共享对方法的引用,好处就是最大限度的节省了内存,如下:

function Person(name, age) {
	this.name = name;
	this.age = age;
	this.books = ["JavaScript", "C++"];
}

Person.prototype = {
	constructor: Person,
	getName: function() {
		return this.name;
	}
};
七、动态原型模式

1、动态原型模式把所有信息都封装在构造函数中,而通过在构造函数中初始化原型对象(仅在必要的情况下),且保持了同时使用构造函数和原型的优点。也就是说,可以通过检查某个应该存在的方法是否有效来决定是否需要初始化原型对象。如下:

function Person(name, age) {
	this.name = name;
	this.age = age;
	
	if (typeof this.getName != "function") {
		Person.prototype.getName = function() {
			return this.name;
		};
	}
}
只在getName()方法不存在的情况下,才会将其添加到原型对象中,所以只会在第一次调用构造函数创建实例时才会添加该方法到原型对象中。不过在使用这种方式时,需要注意的是不能使用对象字面量来重写原型,因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的关系。

八、寄生构造函数模式

1、该模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。如下:

function Person(name, age) {
	var obj = new Object();
	obj.name = name;
	obj.age = age;
	obj.getName = function() {
		return this.name;
	};
	
	return obj;
}

var person = new Person("zhangsan", 21);
恍惚一看,这与工厂模式一模一样,但是注意函数名定义时首字母是大写的,说明把该函数当做构造函数来使用,而不是普通函数,而且使用的是new操作符来创建对象,并不是普通的函数调用。在该例子中,Person函数中创建了一个新对象,并以相应的属性和方法来初始化该对象,然后返回了该对象。构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数的末尾添加返回语句,就可以重写调用构造函数时返回的值。

该模式可以在特殊的情况下用来为对象创建构造函数。比如,要创建一个具有额外方法的特殊数组,由于不能直接修改Array的构造函数,所以可以使用该模式,如下:

function SpecialArray() {
	// 创建数组对象
	var arr = new Array();
	
	// 添加元素
	arr.push.apply(arr, arguments);
	
	// 添加方法
	arr.toPipedString = function() {
		return this.join("|");
	};
	
	// 返回数组对象
	return arr;
}

var colors = new SpecialArray("red", "blue");
colors.toPipedString();
对于寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性之间没有任何关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同,所以不能依赖instanceof操作符来确定对象类型。

九、稳妥构造函数模式

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

2、稳妥构造函数模式与寄生构造函数模式类似,但是有两点不同:

  • 新创建对象的实例方法不引用this
  • 不使用new操作符调用构造函数
如下:
function Person(name, age) {
	var obj = new Object();
	
	// 可以在这里定义私有变量和函数
	
	obj.getName = function() {
		return name;
	};
	
	return obj;
}

var person = Person("zhangsan", 21);
person.getName();
在该模式创建的对象中,除了使用getName方法外,没有其它办法可以访问其数据成员。这里的person变量指向的就是一个稳妥对象,即使有其它代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。
与寄生构造函数模式一样,使用稳妥构造函数模式创建的对象与构造函数之间也没有任何关系,所以instanceof操作符对这种对象也是没有意义的。

参考书籍:《JavaScript高级程序设计》(第三版