JavaScript的对象构造
参考书籍:《JavaScript高级程序设计》(第3版)
JavaScript
是一门面向对象的语言,即拥有类的概念。但ECMAScript
中没有类的概念,因此它的对象与其他基于类的语言不同,我们可以把ECMAScript
中的对象想象成散列表,由一组组键值对构成,值可以是数据或者函数。
1.object
构造函数模式
var person = new object();
person.name = "A";
person.sayName = function() {
alert(this.name);
};
上例创建了一个 $person$ 对象,并为其添加了 $name$ 属性和 $sayName(\ )$ 方法,是最简单的构造对象的方法。
2.对象字面量
var person = {
name: "XiaoMing",
sayName: function() {
alert(this.name);
}
};
上例创建了相同的 $person$ 对象,不同之处在于它是使用对象字面量定义的。以花括号表示开始和结束,属性名+冒号+值的格式定义属性,用逗号隔开多个不同的属性。
在使用对象字面量时,属性名也可以使用字符串。需要注意的是,JSON
只支持此种语法。
var person = {
"name": "XiaoMing",
"sayName": function() {
alert(this.name);
}
};
一般情况下,由于对象字面量语法的代码量少,开发人员大多采用此种方式构造对象。
3.工厂模式
不论是使用 $object$ 构造函数还是对象字面量,都有一个很明显的缺点,就是创建多个对象时会产生大量重复代码。因此人们采取了工厂模式的一个变种。
function createPerson(name) {
var o = new object();
o.name = name;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson("XiaoMing");
var person2 = createPerson("XiaoHong");
函数 $createPerson(\ )$ 接收参数并返回一个 $person$ 对象,每次调用均可以得到一个新的 $person$ 对象。以此,我们可以大量创建 $person$ 对象。
4.构造函数模式
ECMAScript
中的构造函数可以用于创建指定类型的对象,我们可以创建自定义构造函数,从而创建自定义对象。
function Person(name) {
this.name = name;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person("XiaoMing");
var person2 = new Person("XiaoHong");
此例使用 $Person(\ )$ 函数替代了 $createPerson(\ )$ 函数。可以发现, $Person(\ )$ 中,我们并没有显示的创建对象,而是直接将属性和方法赋予 $this$ 对象,并且在最后构造函数时使用了 $new$ 操作符。实际上,该操作经历了 $4$ 步:
- 创建对象
- 将构造函数作用域赋予新对象
- 执行构造函数
- 返回新对象
在此例中,我们创建的对象可以通过 $instanceof$ 操作符进行检验,这也正是使用构造函数模式的优点——对象实例被标识为特殊类型。
如果我们不使用 $new$ 操作符,而是直接调用构造函数,则属性会附加在 $window$ 对象上。如下所示
Person("XiaoMing");
windwo.sayName(); //"XiaoMing"
而要避免这种情况,可以使用严格模式。这样在非严格模式下默认转换为全局对象的 $null$ 或者 $undefined$ 不会转换,因此上述调用会抛出错误。严格模式可以以如下方式使用:
function Person(name) {
'use strict'; //开启严格模式
this.name = name;
}
5.原型模式
在原型模式之前,我们可以先讨论下构造函数模式的缺点。构造函数模式的缺点就是其中每个对象的每个方法都要重新创建一遍。例如之前的 $person1$ 对象和 $person2$ 对象,其 $sayName(\ )$ 方法不是同一个 $Function$ 实例,而是两个不同的实例。当然我们可以通过以下方法解决:
function Person(name) {
this.name = name;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
在全局作用域中定义 $sayName(\ )$ 函数。这种方法确实可以做到共享同一个实例,但问题是 $sayName(\ )$ 函数定义在全局作用域中,却只被 $Person$ 对象调用,而且由于是全局作用域,该函数可以被随意调用,不利于封装。为解决这个问题,我们可以使用原型模式。
什么是原型对象
我们创建的每个对象中都有一个 $prototype$ 属性,该属性是一个指针,指向一个包含可以被特定实例所共享的属性和方法。对于ECMAScript
中的引用类型而言, $prototype$ 是保存所有实例方法的地方。在构造函数模式中,我们提到了创建对象过程中的 $4$ 步,而 $prototype$ 可以理解为第 $1$ 步当中创建的对象的原型指针。无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为其创建一个 $prototype$ 属性,指向函数的原型对象。创建了构造函数之后,原型对象默认取得 $constructor$ 属性,这是一个指向 $prototype$ 属性所在函数的指针。通过 $isPrototypeOf(\ )$ 函数,我们可以判断是否是一个实例的原型对象。在EMCAScript5
中,新增的 $Object.getPrototype(\ )$ 方法可以获取对象的原型。
原型对象的优点就是可以让所有对象实例共享它的属性和方法。如下例所示:
function Person() {}
Person.prototype.name = "XiaoMing";
Perosn.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true
可以发现,使用原型模式后,新对象的方法是同一个方法,即实现了方法共享。
采用原型模式时,当我们为实例添加了一个属性时,该属性会屏蔽原型对象中的同名属性,但不会影响到原型中的属性。如果要再访问原型属性,可以调用 $delete$ 操作。判断一个属性是原型属性还是对象属性,我们可以通过 $hasOwnProperty(\ )$ 函数和 $in$ 操作符。 $hasOwnProperty(\ )$ 函数只有当属性是对象属性时才会返回 $true$ ,而 $in$ 操作符无论属性是对象属性还是原型属性,都会返回 $true$ 。如下例所示:
function Person() {}
Person.prototype = {
name: "XiaoMing",
sayName: function() {
alert(this.name);
}
};
var person = new Person();
person.name = "XiaoHong";
alert(person.name); //“XiaoHong"
alert(hasOwnProperty(person.name)); // true
alert("name" in person); //true
delete person.name;
alert(person.name); //"XiaoMing"
alert(hasOwnProperty(person.name)); //true
alert("name" in person); //true;
当 $hasOWnProperty(\ )$ 返回 $false$ 而 $in$ 返回 $true$ 时,我们可以确定该属性为原型属性;而两者都返回 $true$ 时,可以确定其为对象属性。在此例中,我们采用字面量语法构造 $prototype$ ,相比之下,这种语法更简洁。
原型模式很好的实现了共享,但显然对象中所有的属性都是共享是十分不利的,因此我们要采取构造函数模式和原型模式结合的方式构造对象。如下例所示:
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
};
在这里,我们重新定义了 $constructor$ 的值,因为每当创建一个新函数时,就会一起创建其 $prototype$ 对象和 $constructor$ 属性,使用字面量语法相当于重写了 $prototype$ 对象,因而使得 $constructor$ 属性也改变了(指向 $Object$ 构造函数),虽然用 $instanceof$ 操作符测试 $Person$ 依然为 $true$ ,但如果测试 $constructor$ 属性则就为 $false$ 了。因此需要重新定义 $cosntructor$ 属性,以确保通过该属性能够访问到正确的值。
通过结合构造函数模式和原型模式,我们可以保证属性的独立和方法的共享。
虽然通过构造函数模式和原型模式的结合,我们可以很好的构造对象,但是相比起其他面向对象语言,此种构造方式很奇怪。对此,我们可以采用动态原型模式。此种模式的本质是检查某个方法是否有效来决定是否初始化原型。如下例所示:
function Person(name) {
this.name = name;
if (typeof this.sayName != "function") {
person.prototype.sayName = function() {
alert(this.name);
};
}
}
在动态原型模式中,我们只有在 $sayName(\ )$ 方法不存在的情况下才会将起添加到原型当中,之后则不需要再修改原型。
6.寄生构造函数模式
寄生构造函数模式,本质是创建一个函数,用于封装创建对象的代码,再返回所创建的对象。
function Person(name) {
var o = new object();
o.name = name;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person = new Person("XiaoMing");
这种模式和工厂模式几乎一样,区别只在于其使用 $new$ 操作符创建对象,并且将包装函数作为构造函数。
这种模式用于特殊情况下创建对象的构造函数。如下例所示:
function SpecialArray() {
var a = new Array();
//添加值
a.push.apply(valuse, arguments);
//添加方法
a.toPipedString = function() {
return this.join("|");
};
return a;
}
var names = new SpecialArray("XiaoMing", "XiaoHong");
可以发现,通过此种模式,我们可以构造一个具有额外方法的数组。但要注意,此种模式返回的对象与构造函数及其原型没有关系,即不能通过 $instanceof$ 操作符确定类型,因此,一般不推荐使用此种模式。
7.稳妥构造函数模式
稳妥对象( $durable\ \ objects$ ),指的是没有公共属性,并且其方法也不引用 $this$ 的对象,最适合在一些安全环境中使用。其遵循类似于寄生构造函数的模式,不同之处在于不引用 $this$ ,不使用 $new$ 操作符。
function Person(name) {
var o = new object();
o.sayName = function() {
alert(name);
};
return o;
}
var person = Person("XiaoMing");
person.sayName(); //"XiaoMing";
以此种模式创建的对象,除了其本身的 $sayName(\ )$ 方法外,没有其他方式访问 $name$ 属性。
8.属性类型
ECMAScript
中有两种属性——数据属性和访问器属性,用于描述属性特性。需要注意的是,并非所有浏览器都支持下述方法。
(1) 数据属性
数据属性是一个数据值的位置,有 $4$ 个行为特性:
- $[[Configurable]]$ :表示能否通过 $delete$ 删除该属性,能否修改属性特性,能否修改为访问器属性,在上述例子中我们直接将属性定义在对象上,其 $[[Configurable]]$ 特性默认为 $true$ 。
- $[[Enumerable]]$ :表示能否通过 $for-in$ 循环返回属性。与 $[[Configurable]]$ 相同,默认为 $true$ 。
- $[[Writable]]$ :表示能否修改属性的值。默认为 $true$ 。
- $[[Value]]$ :保存属性的值。读写属性时从该位置读写。默认为 $undefined$ 。
要修改属性的特性,需要调用ECMAScript5
中的 $Object.defineProperty(\ )$ 方法。该方法接收三个参数——属性所在对象、属性名和一个描述符对象( $configurable$ , $enumerable$ , $writable$ 和 $value$ )。在使用该方法时,如果未指定,则 $configurable$ , $writable$ 以及 $enumerable$ 默认为 $false$ ,除非是对已定义的属性特性的修改。
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "XiaoMing"
});
在设置数据属性的值后,作出不允许行为时,非严格模式下会被忽略,严格模式下则会抛出错误。
(2)访问器属性
访问器属性包含 $getter$ 和 $setter$ 函数,分别用于读取和写入。访问器属性包含如下 $4$ 个特性:
- $[[Configurable]]$ :同数据属性。
- $[[Enumerable]]$ :同数据属性。
- $[[Get]]$ :读取属性时调用的函数,默认为 $undefined$ 。
- $[[Set]]$ :写入属性时调用的函数,默认为 $undefined$ 。
与数据属性相同,定义访问器属性时也要用到 $Object.defineProperty(\ )$ 方法。
var person = {
_age: 8;
};
Object.defineProperty(person, "age", {
get: function() {
return this._age;
},
set: function(newAge) {
if (newAge > 18) this._age = 18;
}
});
alert(person.age); //8
person.age = 19;
alert(person.age); //18
在非严格模式下可以不同时指定 $getter$ 与 $setter$ :只指定 $getter$ 则属性只读,相反只指定 $setter$ 则属性只写。严格模式下必须同时指定。对于不支持该方法的浏览器,要定义 $getter$ 和 $setter$ 一般使用 $__defineGetter__(\ )$ 方法和 $__defineSetter(\ )__$ 方法。需要注意的是,在不支持 $Object.defineProperty(\ )$ 的浏览器中无法修改 $configurable$ 和 $enumerable$ 。
(3)属性类型相关方法
多属性定义
在ECMAScript5
中定义了一个 $Object.defineProperties(\ )$ 方法用于定义多个属性。
var person = {};
Object.defineProperties(person, {
name: {
writable: false,
value: "XiaoMing"
},
_age: {
writable: true,
value: 8
},
age: {
get: function {
return this._age;
},
set: function(newAge) {
if(newAge > 18) age = 18;
}
}
});
读取属性特性
ECMAScript5
中定义的 $Object.getOwnPropertyDescriptor(\ )$ 方法,接收两个参数——属性所在对象和要读取的属性名,返回一个记录属性特性值的对象。
//接上段代码
var descriptor = Object.getOwnPropertyDescriptor(person, "_age");
alert(descriptor.value); //8
alert(descriptor.configurable); //false
在大多数情况下可能没有必要使用 $Object.defineProperty(\ )$ 等方法,但这对于理解JavaScript
中的对象有所帮助。