与C++、Java等强类型面向对象的编程语言不同,JavaScript对象是动态的——可以新增属性也可以删除属性,同一类型的不同对象完全可能具备不同的属性。
属性包括名字和值。属性名可以是包含空字符串在内的任意字符串,值可以是任意JavaScript值,或者(在ECMAScript 5中)可以是一个getter或setter函数(或两者都有)。除了名字和值之外,每个属性还有一些与之相关的值,称为“属性特性”(property attribute):
- 可写(writable attribute),表明是否可以设置该属性的值。
- 可枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性。
- 可配置(configurable attribute),表明是否可以删除或修改该属性。
除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute):
- 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象。
- 对象的类(class)是一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag)指明了(在ECMAScript 5中)是否可以向该对象添加新属性。
1. 创建对象
可以通过对象直接量、关键字new和(ECMAScript 5中的)Object.create()函数来创建对象。
1.1. 对象直接量
直接上代码段比较容易理解 :
var empty = {}; //没有任何属性的对象
var point = {x:0, y:0}; //两个属性
var point2 = {x:point.x, y:point.y+1}; //更复杂的值
var book={
"main title": "JavaScript", //属性名字里有空格,必须用字符串表示
'sub-title': "The Definitive Guide", //属性名字里有连字符,必须用字符串表示
"for": "all audiences", //"for"是保留字,因此必须用引号
author: { //这个属性的值是一个对象
firstname: "David", //注意,这里的属性名都没有引号
surname: "Flanagan"
}
};
在ECMAScript 5(以及ECMAScript 3的一些实现)中,保留字可以用做不带引号的属性名。然而对于ECMAScript 3来说,使用保留字作为属性名必须使用引号引起来。在ECMAScript 5中,对象直接量中的最后一个属性后的逗号将忽略,且在ECMAScript 3的大部分实现中也可以忽略这个逗号,但在IE中则报错。
个人很不推荐这种方式,除非只是简单地存储某些数据,并不打算抽象化某些事物也不赋予其任何行为,否则在大规模属性或原型函数的情况下不同的对象之间都有独立存储这些属性和成员函数,导致更多的内存占用,也不符合面向对象的程序设计思想。对象直接量生成的对象也不具备明确的类型信息,这在大规模代码的项目中代码将是非常难以维护的。
1.2. 通过new创建对象
var o = new Object(); //创建一个空对象,和{}一样
var a = new Array(); //创建一个空数组,和[]一样
var d = new Date(); //创建一个表示当前时间的Date对象
var r = new RegExp("js"); //创建一个可以进行模式匹配的EegExp对象
1.3. Object.create()
ECMAScript 5定义了一个名为Object.create()的方法,它创建一个新对象,其中第一个参数是这个对象的原型。Object.create()提供第二个可选参数,用以对对象的属性进行进一步描述。
var o1 = Object.create({x:1, y:2}); //o1继承了属性x和y
var o2 = Object.create(null); //o2不继承任何属性和方法
var o3 = Object.create(Object.prototype); //o3和{}和new Object()一样
2. 继承
这部分内容属于拓展阅读,刚好看到这儿就了解了一下类的继承,可以参考阮一峰的文章:
- Javascript继承机制的设计思想
- Javascript面向对象编程(一):封装
- Javascript面向对象编程(二):构造函数的继承
- Javascript面向对象编程(三):非构造函数的继承
- call apply bind 区别
2.1. 通过new设计类继承
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
2.2. 通过Object.create设计类继承
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
3. 检测属性
JavaScript对象可以看做属性的集合,我们经常会检测集合中成员的所属关系——判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnPreperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也可以做到这一点。
in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true:
var o = {x:1};
"x" in o; //true:"x"是o的属性
"y" in o; //false:"y"不是o的属性
"toString" in o; //true:o继承toString属性
对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false:
var o= {x:1};
o.hasOwnProperty("x"); //true:o有一个自有属性x
o.hasOwnProperty("y"); //false:o中不存在属性y
o.hasOwnProperty("toString"); //false:toString是继承属性
propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为true时它才返回true。某些内置属性是不可枚举的。通常由JavaScript代码创建的属性都是可枚举的,除非在ECMAScript 5中使用一个特殊的方法来改变属性的可枚举性,随后会提到:
var o=Object.create({y:2});
o.x=1;
o.propertyIsEnumerable("x"); //true:o有一个可枚举的自有属性x
o.propertyIsEnumerable("y"); //false:y是继承来的
Object.prototype.propertyIsEnumerable("toString"); //false:不可枚举
但是这儿要小心了,采用我们上一节的方法定义的继承类,是否自有属性可能会出乎我们的意料之外,先看下面代码:
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
rect.propertyIsEnumerable("x"); // true,本以为应该是false?
看起来有点前后矛盾,但是仔细想想应该是这样的,因为我们在构造函数中调用 Shape.call(this, width, height);
的时候将自身传给了父类,那么父类构造函数是在给子类传递过去的this设置属性,因此Shape中定义的x和y就成了Rectangle的自有属性了。
4. 枚举属性
除了for/in循环之外,ECMAScript 5定义了两个用以枚举属性名称的函数。第一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。
ECMAScript 5中第二个枚举属性的函数是Object.getOwnPropertyNames(),它和Object.keys()类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。在ECMAScript 3中是无法实现类似的函数的,因为ECMAScript 3中没有提供任何方法来获取对象不可枚举的属性。
5. 属性getter和setter
var o = {
//普通的数据属性
data_prop:value,
//存取器属性都是成对定义的函数
get accessor_prop() {/*这里是函数体*/},
set accessor_prop(value) {/*这里是函数体*/}
};
这个很容易理解,记录一下语法!
6. 属性的特性
一个属性包含一个名字和4个特性。数据属性的4个特性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置性(configurable)。存取器属性不具有值(value)特性和可写性,它们的可写性是由setter方法存在与否决定的,因此存取器属性的4个特性是读取(get)、写入(set)、可枚举性和可配置性。
为了实现属性特性的查询和设置操作,ECMAScript 5中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那4个特性。描述符对象的属性和它们所描述的属性特性是同名的,因此数据属性的描述符对象的属性有value、writable、enumerable和configurable。存取器属性的描述符对象则用get属性和set属性代替value和writable。其中writable、enumerable和configurable都是布尔值,当然,get属性和set属性是函数值。
通过调用Object.getOwnPropertyDescriptor()可以获得某个对象特定属性的属性描述符:
// 返回 {value:1, writable:true, enumerable:true, configurable:true}
Object.getOwnPropertyDescriptor({x:1}, "x");
//查询上文中定义的randam对象的octet属性
//返回{get:/*func*/,set:undefined,enumerable:true,configurable:true}
Object.getOwnPropertyDescriptor(random, "octet"); //对于继承属性和不存在的属性,返回undefined
Object.getOwnPropertyDescriptor({}, "x"); //undefined,没有这个属性
Object.getOwnPropertyDescriptor({}, "toString"); //undefined,继承属性
下面的例子说明了 Object.defineProperty 的用法:
var o = {}; //创建一个空对象
//添加一个不可枚举的数据属性x,并赋值为1
Object.defineProperty(o, "x", {
value: 1,
writable: true,
enumerable: false,
configurable: true
}); //属性是存在的,但不可枚举
o.x; //=>1
Object.keys(o); //=>[]
//现在对属性x做修改,让它变为只读
Object.defineProperty(o, "x", {writable:false});
//试图更改这个属性的值
o.x = 2; //操作失败但不报错,而在严格模式中抛出类型错误异常
o.x; //=>1
//属性依然是可配置的,因此可以通过这种方式对它进行修改:
Object.defineProperty(o, "x", {value: 2});
o.x; //=>2
//现在将x从数据属性修改为存取器属性
Object.defineProperty(o, "x", {get: function() {return 0;}});
o.x; //=>0
如果要同时修改或创建多个属性,则需要使用Object.defineProperties()。第一个参数是要修改的对象,第二个参数是一个映射表,它包含要新建或修改的属性的名称,以及它们的属性描述符,例如:
var p = Object.defineProperties({}, {
x: {value:1, writable:true, enumerable:true, configurable:true},
y: {value:1, writable:true, enumerable:true, configurable:true},
r: {
get: function(){return Math.sqrt(this.x*this.x+this.y*this.y)},
enumerable:true,
configurable:true
}
});
对于那些不允许创建或修改的属性来说,如果用Object.defineProperty()和Object.defineProperties()对其操作(新建或修改)就会抛出类型错误异常,比如,给一个不可扩展的对象新增属性就会抛出类型错误异常。造成这些方法抛出类型错误异常的其他原因则和特性本身相关,可写性控制着对值特性的修改,可配置性控制着对其他特性(包括属性是否可以删除)的修改。然而规则远不止这么简单,例如,如果属性是可配置的话,则可以修改不可写属性的值。同样,如果属性是不可配置的,仍然可以将可写属性修改为不可写属性。下面是完整的规则,任何对Object.defineProperty()或Object.defineProperties()违反规则的使用都会抛出类型错误异常:
- 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性。
- 如果属性是不可配置的,则不能修改它的可配置性和可枚举性。
- 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能将它转换为数据属性。
- 如果数据属性是不可配置的,则不能将它转换为存取器属性。
- 如果数据属性是不可配置的,则不能将它的可写性从false修改为true,但可以从true修改为false。
- 如果数据属性是不可配置且不可写的,则不能修改它的值。然而可配置但不可写属性的值是可以修改的(实际上是先将它标记为可写的,然后修改它的值,最后转换为不可写的)。
7. 对象的三个属性
每一个对象都有与之相关的原型(prototype)、类(class)和可扩展性(extensible attribute)。
7.1. 原型属性
原型属性是在实例对象创建之初就设置好的,通过对象直接量创建的对象使用Object.prototype作为它们的原型,通过new创建的对象使用构造函数的prototype属性作为它们的原型,通过Object.create()创建的对象使用第一个参数(也可以是null)作为它们的原型。
在ECMAScript 5中,将对象作为参数传入Object.getPrototypeOf()可以查询它的原型。要想检测一个对象是否是另一个对象的原型(或处于原型链中),可用isPrototypeOf()方法。例如,可以通过p.isPrototypeOf(o)来检测p是否是o的原型:
var p = {x:1}; //定义一个原型对象
var o=Object.create(p); //使用这个原型创建一个对象
p.isPrototypeOf(o); //=>true:o继承自p
Object.prototype.isPrototypeOf(o); //=>true:p继承自Object.prototype
isPrototypeOf()函数实现的功能和instanceof运算符非常类似。
7.2. 类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ECMAScript 3和ECMAScript 5都未提供设置这个属性的方法,并只有一种间接的方法可以查询它。默认的toString()方法(继承自Object.prototype)返回了如下这种格式的字符串:[object class]
function classof(o) {
if (o === null) return "Null";
if (o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8, -1);
}
但是这个方法也不能得到准确的类名,比如上面第2节中『继承』中创建的对象rect:
classof(rect); //=>Object
但是我们可以通过以下的方法获得其类名:
rect.constructor.name; // => Rectangle
7.3. 可扩展性
对象的可扩展性用以表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显式可扩展的,宿主对象的可扩展性是由JavaScript引擎定义的。在ECMAScript 5中,所有的内置对象和自定义对象都是可扩展的,除非将它们转换为不可扩展的,同样,宿主对象的可扩展性也是由实现ECMAScript 5的JavaScript引擎定义的。
ECMAScript 5定义了用来查询和设置对象可扩展性的函数。通过将对象传入Object.isExtensible(),来判断该对象是否是可扩展的。如果想将对象转换为不可扩展的,需要调用Object.preventExtensions(),将待转换的对象作为参数传进去。注意,一旦将对象转换为不可扩展的,就无法再将其转换回可扩展的了。同样需要注意的是,preventExtensions()只影响到对象本身的可扩展性。如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。
可扩展属性的目的是将对象“锁定”,以避免外界的干扰。对象的可扩展性通常和属性的可配置性与可写性配合使用,ECMAScript 5定义的一些函数可以更方便地设置多种属性。
Object.seal()和Object.preventExtensions()类似,除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。也就是说,不能给这个对象添加新属性,而且它已有的属性也不能删除或配置,不过它已有的可写属性依然可以设置。对于那些已经封闭(sealed)起来的对象是不能解封的。可以使用Object.isSealed()来检测对象是否封闭。
Object.freeze()将更严格地锁定对象——“冻结”(frozen)。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它自有的所有数据属性设置为只读(如果对象的存取器属性具有setter方法,存取器属性将不受影响,仍可以通过给属性赋值调用它们)。使用Object.isFrozen()来检测对象是否冻结。
Object.preventExtensions()、Object.seal()和Object.freeze()都返回传入的对象,也就是说,可以通过函数嵌套的方式调用它们:
//创建一个封闭对象,包括一个冻结的原型和一个不可枚举的属性
var o=Object.seal(Object.create(Object.freeze({x:1}), {
y: {value: 2, writable: true}
}));
8. 序列化对象
对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原JavaScript对象。这些方法都使用JSON作为数据交换格式,JSON的全称是"JavaScript Object Notation"——JavaScript对象表示法,它的语法和JavaScript对象与数组直接量的语法非常相近:
o = {x: 1, y: {z: [false, null, ""]}}; //定义一个测试对象
s = JSON.stringify(o); //s是'{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s); //p是o的深拷贝