源地址:
虽然JavaScript中已经自带了很多内建引用类型,你还是会很频繁的需要创建自己的对象。JavaScript编程的很大一部分都是在操纵对象。深入理解JavaScript对象是怎么运行的是全面理解JavaScript的一个关键。记住JavaScript中的对象是动态的,这意味着他们可以在任何代码执行的地方被修改。不像基于类的语言在定义类时就锁定了对象,JavaScript中的对象没有这些限制。
定义属性
回忆一下第一章中有两种创建自己对象的方法,使用Object构造器或者使用对象字面量表示法。例如:
var person1 = { name: 'Nicholas'};var person2 = new Object();person2.name = 'Nicholas';person1.age = 'Redacted';person2.age = 'Redacted';person1.name = 'Greg';person2.name = 'Michael';
person1和person2对象都有一个name属性。在这个例子的后面,两个对象都被赋予了一个age属性。这可以在对象定义之后马上进行或者更晚一点。你所创建的对象总是可以修改的,除非你特别制定了一些东西(在本书的后面你会可拿到)。例子的最后部改变了每个对象的name属性值。对象的属性值可以在任何时候被改变。
当一个属性初次被添加到一个对象时,JavaScript在对象上使用了一个叫做[[put]]的内部方法。[[put]]方法在对象中创建了一个点来存储对象。你可以想象这类似于为一个哈希表初次添加一个键。因此在上面的例子中,当name和age属性初次在对象中定义时,一个[[put]]方法在每个对象中运行了一次。
调用[[put]]方法的结果是在对象中创建了一个自己的属性。一个自己的属性简单来说意味着这个特定的实例拥有这个属性。一个属性直接存储在实例中并且所有对于改属性的操作都需要经过对象。自己的属性和原型属性有着本质的区别,我们将在第四章中讨论这一点。
当一个已经存在的对象被赋予了一个新值时,一个叫做[[set]]的方法被调用。这个方法简单的用另一个值替换原有的属性值。在前面的例子中,将name属性赋予一个新值导致调用了[[set]]。看图3.1可以理解person1对象在幕后究竟发生了什么:
探查属性
由于属性可以在任何时间点被添加,有时需要检查属性是否存在于对象中。新JavaScript开发者进场会错误的使用下面的模式来探查一个属性是否存在:
//错误的做法 if(person1.age){ //对age进行一些操作 }
这个模式的问题在于类型强制性会影响输出结果。如果值为真值(一个对象,一个非空字符串,一个非零数字,或者true)那么if条件将会判定为真,如果值为假值那么if条件将会判定为假(null,undefined,0,false,或者一个空字符串)。既然属性可以包含任何一个假值,那么上述例子的结果可能会出现错误。例如,如果person1.age的值为0,那么if条件将不会得到满足,即使此时改属性存在。探测属性是否存在的一种更可靠的方法是使用in操作符。
in操作符针对一个给定的名字在一个特定的对象中寻找属性,一旦找到便返回true。事实上,in操作符实在检查哈希表中是否有给定的键值。例如:
console.log("name" in person1); //true console.log("age" in person1); //true console.log("title" in person1); //true
记住,对象中的方法仅仅只是一个包含了函数的属性因此你可以用同样的方法检查方法是否存在:
var person1 = { name: "Nicholas", sayName: function(){ console.log(this.name); }};console.log("sayName" in person1); //true
在大多数情形中,in操作符是探测属性是否存在于对象中的最好方法。它的另一个好处是不会去运行属性的值,这在属性的值会引起错误时十分重要。
在某些情形下,你想要检查一个属性是否是对象自己拥有的属性。in操作符会检查对象自己的属性以及对象原型的属性(在第四章中将会讨论)。hasOwnProperty()方法 – 所有对象都有这个方法 – 如果对象自己拥有属性的话会返回true。例如:
var person1 = { name: "Nicolas", sayName: function(){ console.log(this.name); }};console.log("name" in person1); //true console.log("sayName" in person1); //true console.log("toString" in person1); //true console.log(person1.hasOwnProperty("toString")); //false
在这个例子中,name是person1自己拥有的对象,因此无论是in操作符还是hasOwnProperty()方法都返回true。toString方法,是一个所有对象中都具有的原型方法。对此,in操作符返回true而hasOwnProperty()返回false。二者之间有很大的区别,这将在第四章中进行讨论。
移除属性
正如属性可以在任何时间被添加到对象中一样,它们也可以在任何时间被移除。简单的将属性设置为null并不能真正将属性完全从对象移除。这样做仅仅是调用了[[Set]]方法将值设定为null。正如你在前面部分看到的,这样的一个操作仅仅是替换了值。你需要delete操作符将属性完全从对象中移除。
delete操作符将针对一个对象属性起作用并将调用一个外部操作[[Delete]]。你可以将这个操作想象成从一个哈希表中移除了一个键值对。当delete操作符运行成功时,它返回true(有些属性是无法移除的,这将在后面的章节中讨论)。例如:
var person1 = { name: "Nicholas",};console.log("name" in person1); //true delete person1.name; //true - 没有输出 console.log("name" in person1); //false console.log(person1.name); //undefined
在这个例子中,name属性从person1对象中移除。在操作成功之后in操作符返回了false。同样需要注意试图获取一个不存在的属性将会返回一个undefined。图3-2显示了delete操作符如何影响一个对象:
枚举
默认情况下,你为一个对象添加的所有属性都是可枚举的。可枚举属性的内部[[Enumerable]]属性设置为true并在for-in循环中显示。for-in循环枚举了对象中的所有可枚举属性,依次获取属性名。例如,下面的例子依次输出对象的属性名和值:
var property; for(property in object){ console.log("Name" + property); console.log("Value" + object[property]);}
for-in每次循环,property变量都会被赋予对象中的下一个可枚举属性知道所有的属性都被使用。此时,循环结束,代码继续执行。这个例子中使用了方括号标示符来获取对象中的属性值并将它输出到控制台中。这是方括号标示符在JavaScript中一个主要的用法。
ECMAScript中引入了Object.keys()方法来获取一个可枚举属性名的数组。因此如果你只想要获取一个属性的列表,你可以使用这个方法来省下很多代码:
var properties = Object.keys(object); //如果你想要模仿for-in var i,len; for(i=0, len=properties.length; i < len; i++){ console.log("Name:" + properties[i]); console.log("Value:" + object[properties[i]]);}
这个例子中使用了Object.keys()来从一个对象中获取所有可枚举对象。接着,我们使用了一个for循环来迭代这些属性并输出名字和值。一般的,你可以在不需要迭代属性名时使用Object.keys。
使用for-in循环和使用Object.keys()返回的可枚举属性之间有差别。for-in循环同时返回了原型中的可枚举属性而Object.keys()仅仅反悔了对象自己的属性。原型属性和自己属性的差别在第四章中将会具体讲解。
记住并不是所有的对象都是可枚举的。事实上,大部分对象中的原生方法都不是可枚举的(它们的[[Enumerable]]属性被设置为false)。你可以使用propertyIsEnumerable()方法来检查一个对象是否是可枚举的,该方法存在于所有的对象中:
var person1 = { name: "Nicholas"};console.log("name" in person1); //true console.log(person1.propertyIsEnumerable("name")); //true var properties = Object.keys(person1);console.log("length" in properties); //true console.log(properties.propertyIsEnumerable("length")); //false
在上面这个例子中,name属性在person1中被定义,它是可枚举的。在数组properties数组中,length属性并不是可枚举的。JavaScript定义length属性为不可枚举的(你会发现许多原生属性默认都是不可枚举的)。
属性的类型
事实上,属性共分为两种类型:数据属性和存取器属性。数据属性是包含一个值的属性。默认的[[Put]]方法创建一个数据属性。在本章中前面的所有例子中都使用了data属性。存取器属性是不包含值但是在属性被读取时调用一个函数的属性,叫做获取器(getter),一个写入一个属性时调用的函数,叫做设置器(setter)。存取器属性实际上并不需要同时包含获取器和设置器,包含其中一个即可。
定义一个存取器属性的语法很特殊,它使用了一个对象字面量表示法:
var person1 = { _name: "Nicholas", get name(){ console.log("Reading name"); return this._name; }, set name(value){ console.log("Setting name to %s", value); this._name = value; }};console.log(person1.name); //"Reading name"然后"Nicholas" person1.name = "Greg"; console.log(person1.name); //"Setting name to Greg"然后"Greg"
这个例子中定义了一个叫做name的存取器属性。对象中包含了一个叫做_name的数据属性包含了实际的值。name的获取器和设置器使用了一种类似函数定义的语法但是没有function关键字。存取器前面使用了特殊的关键字get和set,后面是括号,然后就是函数体。对于一个获取器,它会获取一个值并返回而设置器将接收一个变量并将它赋值给一个属性。虽然在这个例子中使用了_name来存数属性数据,你可以简单的将数据存储在变量中甚至在另一个对象中。这个例子简单地为属性的行为添加的日志输出;如果你只需要将数据存储在一个属性中这并不是使用存取器属性的一个理由。
你并不需要同时定义一个获取器和一个设置器,你可以选择定义其中一个或者两个。如果你只定义了获取器,那么属性是只读的,因此当你为属性赋值时在非严格模式下会失败,在严格模式下则会抛出错误。如果你只定义了设置器,那么属性是只写的,因此当你想要读取属性值时,在非严格模式下会失败,在严格模式下会抛出错误。
属性(Property Attributes)
在ECMAScript5之前,我们并不能去设置是否应该为可枚举的。事实上,你根本没有办法去获取属性的内部属性。ECMAScript5通过引入一些可以改变属性内部属性的方法来改变了这种情况。现在,我们也可以使用各种方法来创建类似于JavaScript内建属性的属性了。
一般属性
数据属性和存取器属性共享两种属性。一个是[[Enumerable]],它决定属性是否是可枚举的。另一个是[[Configurable]],它决定属性是否可以被改变。一个可配置的属性可以使用delete操作符来移除并在任何时间点可以改变改变它的属性(这意味着属性可以从数据属性变为存取器属性或者相反)。默认的,你在对象中声明的所有属性都是可枚举可配置的。
你可以使用Object.defineProperty()方法来改变属性的属性。这个方法接收三个参数,拥有该属性的对象,属性名,以及一个包含设置的属性的对象(叫做属性描述符)。这个描述符拥有在内部属性中相同名字的属性,但是没有方括号。因此你可以使用enumerable来设置[[Enumerale]],使用configurable来设置[[Configurable]]。例如,假设你要设置一个不可枚举不可配置的属性:
var person1 = { name: "Nicholas"};Object.defineProperty(person1, "name", { enumerable: false});console.log("name" in person1); //true console.log(person1.propertyEnumerable("name")); //false var properties = Object.keys(person1); console.log(properties.length); //0 Object.defineProperty(person1, "name", { configurable: false}); //试着去删除属性 delete person1.name; console.log("name" in person1); //true console.log(person1.name); //"Nicholas" Object.defineProperty(person1, "name", { //erroe!!! configurable: true});
在上面的代码中,name属性像通常一样被定义但是将其[[Enumerable]]属性改为false。propertyIsEnumerable()方法现在返回false因为它读到了[[Enumerable]]的值。之后,name被设置为不可配置的。于是delete操作失败了因此name属性还依然存在于person1对象中。在name属性上调用Object.defineProperty()同样不会导致任何改变。事实上,name属性作为一个属性被锁定在了person1中。
代码的最后一部分试着重新将那么定义为可配置的。然而,程序抛出了一个错误因为一个非可配置属性永远不能再次被定义为可配置的。同时,试着改变存取器属性中的一个数据或者反之都会抛出一个错误。
当JavaScript运行在严格模式下时,试着删除一个非可配置属性将会导致错误。在非严格模式下,这个操作会默认失败。
数据属性
数据属性拥有两个额外属性。第一个是[[Value]],它包含属性的值。当你创建一个对象的属性使这个属性自动被赋值。无论是一个值还是一个函数,所有的属性值都会被存储在[[Value]]中。第二个属性是[[Writable]],它是一个用来表明这个属性是否可写的布尔值。默认情况下,所有的属性都是可写的,除非你有特别的指明。
有了这两个额外属性,你可以使用Object.defineProperty()来定义一个数据属性,即使这个属性不存在。考虑下面的代码:
var person1 = { name: "Nicholas"};
你在本章中已经多次见过这段代码。你可以使用下面的代码达到同样的目的(虽然繁琐了一些):
var person1 = { };Object.defineProperty(person1,"name",{ value: "Nicholas", enumerable: true, configurable: true, writable: true});
Object.defineProperty()方法接收三个参数:一个对象,属性的名字,以及一个包含属性信息的描述对象。当Object.defineProperty()被调用时,它首先检查看这个属性是否存在。如果这个属性不存在,那么一个新的属性会被创建。在这个例子中,name属性不存在因此创建一个新的name属性。在定义一个新属性时非常重要的一点是指明该属性的所有属性,因为如果不特别说明的话所有的布尔属性将会被默认为false。例如,下面创建的name属性就是不可枚举,不可配置,并且不可写的:
var person1 = { };Object.defineProperty(person,"name"{ value: "Nicholas"});console.log("name" in person1); //trueconsole.log(person1.propertyIsEnumerable("name")); //falsedelete person1.name;console.log("name" in person1); //trueperson1.name = "Greg";console.log(person.name); //"Nicholas"
在这段代码中,你除了读取值之外不能对name属性进行任何操作,其余所有的对象都是被锁定的。
和不可配置属性一样,在严格模式下当你视图改变一个不可写属性的值时会抛出一个错误。在非严格模式下,该操作会默认失败。
存取器属性
存取器属性也有两个额外的属性。因为存取器属性并不存储值,因此它没有[[Value]]和[[Writable]]。它拥有[[Get]]和[[Set]],它们分别包含了获取函数和设置函数。由于在字面量形式中已经有了获取函数和设置函数,你只需要定义其中的一个属性就可以创建存取器属性。
如果你试图定义一个同时拥有数据和存取器性质的属性,你将得到一个错误。
使用存取器属性属性来定义存取器属性的一大好处是你可以在已经存在的对象中定义存取器属性(如果用对象字面量形式的话,只能在对象定义阶段来定义存取器属性。)和数据属性一样,你可以指定存取器属性是否可配置可枚举。考虑下面的例子:
var person1 = { _name: "Nicholas", get name(){ console.log("Reading name"); return this._name; }, set name(value){ console.log("Setting name to %s", value); this._name = value; }}
下面的代码将会达到同样的目的:
var person1 = { _name: "Nicholas"};Object.defineProperty(person1,"name",{ get: function(){ console.log("Reading name"); return this._name; }, set: function(value){ console.log("Setting name to %s", value); this._name = value; }, enumerable: true, configurable: true});
注意到Object.defineProperty()中的get和set键都是包含一个函数的数据属性。在这里你不可以使用对象字面量存取器形式。
设置其他属性允许你改变存取器属性允许的方式。例如:你可以像下面一样创建一个不可配置,不可枚举,不可写的属性:
var person1 = { _name: "Nicholas"};Object.defineProperty(person1,"name",{ get:function(){ console.log("Reading name"); return this._name; }});console.log("name" in person1); //trueconsole.log(person1.propertyIsEnumerable("name")); //falsedelete person1.name; //trueconsole.log("name" in person1); //trueperson1.name = "Greg"; console.log(person1.name); //"Nicholas"
在这段代码中,name是一个只有读取器的存取器属性,这意味着值只可以读但是不能以任何方式改变。
如果使用对象字面量表示法定义存取器属性,在严格模式下当你试图改变一个没有设置器的存取器属性时将会得到一个错误。在非严格模式下,该操作将会默认失败。当试图读取一个只有设置器的存取器属性的值时也会得到相同的结果。
定义多重属性
在一个对象中使用Object.defineProperties()可以一次性定义多重属性。改方法接收两个参数,一个对象,以及一个包含所有属性信息的对象。第二个参数的键都是属性名,值都是对象属性的描述信息。例如:
var person1 = { };Object.defineProperties(person1,{ _name: { value: "Nicholas", enumerable: true, configurable: true, writable: true }, name: { get: function(){ console.log("Reading name"); return this._name; }, set: function(value){ console.log("Setting name to %s", value); this._name = value; }, enumerable: true, configurable: true }});
这个例子中定义了一个数据属性_name来包含信息,一个存取器属性_name。你可以使用Object.defineProperties()定义任何数量的属性,包括已经存在或者不存在属性。这等同于多次调用Object.defineProperty()。
获取属性的属性(Retrieving Property Attributes)
你也可以使用Object.getOwnPropertyDescriptor()来获取属性的属性。正如这个方法的名字所暗示的,它只对对象自身的属性起作用。这个方法接收两个参数,一个对象以及需要获取的属性名。如果这个属性存在,那么你将得到一个带有四个属性的描述符对象:configurable,enumerable以及另外两个特定的属性。即使你没有特别的设置其中一个属性,你仍然会得到包含特定属性的一个描述符对象。例如:
var person1 = { name: "Nicholas"}var descriptor = Object.getOwnPropertyDescriptor(person,"name");console.log(descriptor.enumerable); //trueconsole.log(descriptor.configurable); //trueconsole.log(descriptor.writable); //trueconsole.log(descriptor.value); //"Nicholas"
在上面的例子中,一个叫做name的属性在开头使用对象字面量表示法被定义。在这个属性上调用Object.getOwnPropertyDescriptor()方法返回了一个包含enumerable,configurable,writable以及value属性的一个描述符对象,即使我们没有使用Object.defineProperty()来显式的定义这几个对象。
防止对象被修改
和属性一样,对象也有内部的属性来指导它们的行为。其中的一个属性就是[[Extensible]],它是一个用于指明对象本身是否可以被修改的布尔值。你创建的所有对象都是默认可扩展的,这意味着你可以在任何时间点为这个对象添加新的属性。在前面的例子中你看到了很多次这样的操作。通过将[[Extensible]]设置为false,你可以阻止新的属性被添加到一个对象中,下面有三种方法可以完成这件事。
组织扩展性
第一种方法是使用Object.preventExtensions()创建一个不可扩展的对象。这个方法接收一个参数,即你想要创建的不可扩展对象。一旦这个对象被设置为不可扩展,它就再也不能添加新的属性并且该对象永远不能再次变为可扩展的对象了。你可以使用Object.isExtensible()来检查[[Extensible]]的值。例如:
var person1 = { name: "Nicholas"}console.log(Object.isExtensible(person1)); //trueobject.preventExtensions(person1); console.log(Object.isExtensible(person1)); //falseperson1.sayName = function(){ console.log(this.name);}; console.log("sayName", in person1); //false
在这段代码中,person1对象被设置为不可扩展的因此sayName()方法永远不能被添加到对象中。
封闭对象
创建一个不可扩展对象的第二种方法是封闭对象。一个被封闭的对象是不可扩展的,并且它的所有属性都是不可配置的。这意味着你不仅不能为对象添加新的属性而且也不能移除属性或者修改属性的类型。被封闭的对象只能读写属性的值。
Object.seal()方法被用来封闭一个对象。事实上在使用这个方法时,对象的[[Extensible]]属性以及对象所有属性的[[Configurable]]属性都被设置为false。你可以通过Object.isSealed()来检查一个对象是否被封闭。下面是一个例子:
var person1 = { name: "Nicholas"};console.log(Object.isExtensible(person1)); //trueconsole.log(Object.isSealed(person1)); //false Object.seal(person1); console.log(Object.isExtensible(person1)); //false;console.log(Object.isSealed(person1)); //trueperson1.sayName = function(){ console.log(this.name);}console.log("sayName" in person); //falseperson1.name = "Greg";console.log(person1.name); //"Greg" delete person1.name; console.log("name" in person1); //trueconsole.log(person1.name); //"Greg" var descriptor = Object.getOwnPropertyDescriptor(person1,"name");console.log(descriptor.configurable); //false
上面这段代码封闭了person1因此所有的修改都无效。因为所有的封闭对象都是不可扩展的,所以Object.isExtensible()反悔了false。试图添加一个sayName()方法以及删除person1.name都默认失败。
冻结对象
最后一种创建不可扩展对象的方法是冻结它。一个冻结对象是不可扩展的,所有的属性都是不可配置的,并且所有的数据属性都是不可写的。本质上来说,冻结对系那个是一个所有数据属性只能读取的封闭对象。冻结对系那个不能做任何修改并且不能再次变回非冻结对象;对象在被冻结时依然保留着相同的状态。你可以通过使用Object.freeze()来冻结一个对象并且使用Object.isFrozen()来检查一个对象是否被冻结。下面是一个例子:
var person1 = { name: "Nicholas"};console.log(Object.isExtensible(person1)); //trueconsole.log(Object.isSealed(person1)); //falseconsole.log(Object.isFrozen(person1)); //false Object.freeze(person1); console.log(Object.isExtensible(person1)); //falseconsole.log(Object.isSealed(person1)); //trueconsole.log(Object.isFrozen(person1)); //true person1.sayName = function(){ console.log(this.name);};console.log("sayName" in person1); //falseperson1.name = "Greg";console.log(person1.name); //"Nicholas"delete person1.name;console.log("name" in person1); //trueconsole.log(person1.name); //"Nicholas" var descriptor = Object.getOwnPropertyDescriptor(person1,"name"); console.log(descriptor.configurable); //falseconsole.log(descriptor.writable); //false
在这个例子中,person1对象被冻结。冻结对象可以看做既封闭又不可扩展的对象。因此Object.isSealed()返回true且Object.isExtensible()返回false。name属性不能被改变,因此即使它被赋值为”Greg”,操作也会默认失败。
总结
将JavaScript对象看做哈希映射 – 其中属性仅仅是键值对 –将有利于我们理解JavaScript对象。对象的属性可以使用点标示符或者方括号标示符来获取。你可以在任何时候通过赋值来添加一个JavaScript对象属性。属性也可以在任何时候使用delete操作符来删除。你可以使用in操作符来检查一个属性是否存在于对象中,或者使用hasOwnProperty()来检查。所有的对象属性默认都是可枚举的,这意味着你可以使用for-in循环来遍历属性或者使用Object.keys()来查看所有键。
对象的属性分为两种类型:数据属性和存取器属性。数据属性用来存储数据的值并且可读可写。当一个数据属性包含一个函数是,它被认为是这个对象的方法。存取器属性本身不存储数据,但而是使用获取器和设置器来进行特定的操作。你可以通过对象字面量表示法来直接创建数据属性和存取器属性。
所有的属性包含一些特定的性质,这些性质决定了属性怎么运行。数据属性和存取器属性都有[[Enumerable]]和[[Configurable]]性质。数据属性还有[[Writable]]和[[Value]]性质,而存取器属性有[[Getter]]和[[Setter]]性质。默认情况下,[[Enumerable]]和[[Configurable]]性质都是true,而数据属性的[[Writable]]也被设置为true。你可以使用Object.getOwnPropertyDescriptor()来获取这些性质。
有三种方法来创建不可扩展对象。使用Object.preventExtensions(),对象允许添加额外的属性。使用Object.seal()可以创建一个封闭对象,封闭对象也是一个不可配置对象即它的所有属性都是不可配置的。Object.freeze()对象可以创建一个冻结对象,它可以看做一个数据属性不可写的封闭对象。在使用不可扩展对象的时候要特别注意使用严格模式,以便不恰当的对象操作会抛出一个错误。