第六章:对象
2017.02.24
对象
对象是JavaScript的基本数据类型。对象是一种复合值。对象可以看做是属性的无序集合,每个属性都是一个名/值对。属性名是字符串,可以把对象看成是从字符串到值得映射。
这种基本数据结构还有多种叫法:散列(hash)、散列表(hashtable)、字典(dictionary)、关联数组、(associative array)。
对象不仅仅是字符串到值得映射,除了可以保持自由的属性,JavaScript对象可以从一个称为原型的对象继承属性。对象的方法通常是集成的属性。这种“原型式继承”(prototypal inheritance)是JavaScript的核心特征。
JavaScript对象是动态的,可以新增属性也可以删除属性。但他们常用来模拟静态对象以及静态类型语言中的结构体,有时也用来做字符串的集合(忽略名/值队中的值)。
除了字符串、数字、true、false、null、undefined外,JavaScript中的值都是对象。
对象是可变的,我们通过引用而非值来操作对象。如果变量x是指向一个对象的引用,那么var y=x;
y也是指向同一个对象的引用,而非副本。通过变量y修改这个对象亦会对变量x造成影响。
对象最常用的方法是创建create、设置set、查找query、删除delete、检测test、枚举enumerate其属性。
属性包括名字和值。属性名可以是包含字符串在内的任意字符串,对象中不能存在两个同名的属性(非严格模式下不会报错,但是会覆盖)。属性还有“属性特性”(property attribute):
- 可写,表明是否可以设置该属性的值。
- 可枚举,表明是否可以通过for/in循环返回该属性。
- 可配置,表明是否可以删除或修改该属性。
ES5之前,通过代码给对象创建的所有属性都是可写的、可枚举的。可配置的。
除了包含属性外,每个对象还拥有三个相关的对象特性(object attribute):
- 对象的原型(prototype)指向另一个对象,本对象的属性继承自它的原型对象。
- 对象的类(class)是一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag)指明了是否可以向该对象添加新属性(ES5)。
【三类JavaScript对象】
- 内置对象(native object):由ES规范定义的对象或类。例如数组、函数、日期、正则表达式。
- 宿主对象(host object):由JavaScript解释器所嵌入的宿主环境(比如Web浏览器)定义的。客户端JavaScript中表示网页结构的HTMLElement对象均是宿主对象。既然宿主环境定义的方法可以当成普通的JavaScript函数对象,那么宿主对象也可以当成内置对象。
- 自定义对象(user-defined object):由运行中的JavaScript代码创建的对象。
【两类属性】
- 自有属性(own property):直接在对象中定义的属性。
- 继承属性(inherited property):在对象的原型对象中定义的属性。
创建对象
对象直接量、关键字new、Object.create()(ES5中)
对象直接量
创建对象最简单的方法是使用对象直接量。
对象直接量是由若干名/值对组成的映射表,名/值对中间用冒号分隔,名/值对之间用逗号分隔,整个映射表用花括号括起来。
属性名可以是JavaScript标识符也可以是字符串直接量(包括空字符串)。属性的值可以是任意类型的JavaScript表达式,表达式的值可以是原始值也可以是对象值,就是这个属性的值。
var empty={};
var point={x:0,y:0};
var book={
"main title":"JavaScript", //属性名里有空格,必须用字符串表示
"sub-title":"The Definitive Guide", //属性名里有连字符,必须用字符串表示
"for":"all audiences", //for是保留字,需用引号,(ES5以及ES3的一些实现可以不用引号)
author:{
firstname:"David",
surname:"Flanagan"
}
}
ES5中,对象直接量的最后一个属性后的逗号将忽略,ES3中大部分可以忽略,但是IE报错。
通过new创建对象
new运算符创建并初始化一个新对象。关键字new后跟随一个函数调用(构造函数 constructor)。构造函数用以初始化一个新创建的对象。JavaScript语言核心中的原始类型都包含内置构造函数。
var o=new Object(); //空对象,和{}一样
var a=new Array(); //空数组,和[]一样
var d=new Date(); //当前时间对象
var r=new RegExp("js"); //创建进行模式匹配的RegExp对象
原型
每一个JavaScript对象(null除外)都和另一个对象相关联。另一个对象就是原型,每一个对象都从原型继承属性。
所有通过对象直接量创建的对象都具有同一个原型对象,并可以通过JavaScript代码Object.prototype获得对原型对象的引用。
通过关键字new和构造函数创建的对象的原型就是构造函数的prototype属性的值。因此,同使用{}创建对象一样,通过new Object()创建的对象也继承自Object.prototype。同样,通过new Array()创建的对象的原型就是Array.prototype,通过new Date()创建的对象的原型就是Date.prototype。
> Object.prototype
{}
> Array.prototype
[]
> Date.prototype
Date {}
> RegExp.prototype
RegExp {}
> Function.prototype
[Function]
> Function.prototype.toString()
'function () {}'
> Number.prototype
[Number: 0]
> String.prototype
[String: '']
> Boolean.prototype
[Boolean: false]
Object.prototype没有原型对象。它不继承任何属性,其他对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自Object.prototype的原型。例如:Date.prototype的属性继承自Object.prototype,因此由new Date()创建的Date对象的属性同事继承自Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的"原型链"(prototype chain)。
Object.create()
ES5定义了名为Object.create()方法,他创建一个新对象,其中第一个参数是这个对象的原型。第二个参数可选,用以对属性进行进一步描述。
是静态函数,而不是提供给某个对象调用的方法。调用它的方法很简单,只需传入所需的原型对象即可。
var o=Object.create({x:1,y:1}); //o继承了属性x和y
通传入参数null来创建一个没有原型的新对象,通过此方式创建的对象不会继承任何东西,甚至不包括基础方法(比如toString)。也就是说,它不能和"+"运算符一起正常工作。
var o=Object.create(null); //不继承任何属性和方法
创建一个普通的空对象,需要传入Object.prototype。
var o=Object.create(Object.prototype); //同 {},new Object()一样
可以通过任意原型创建新对象(即可以使任意对象可继承)。
【通过原型继承创建一个新对象】
// 函数返回继承自原型对象p的属性的新对象,防止库函数无意间修改那些不受控制的对象。
function inherit(p){
if (p==null) throw TypeError(); //p是一个对象,但不能是null
if (Object.create) return Object.create(p);//如果存在Object.create()
var t=typeof p;
if (t!=="object" &&t!=="function") throw TypeError();
function f(){}; //定义一个空构造函数
f.prototype=p; //将其原型属性设置为p
return new f(); //使用f()创建p的继承对象
}
属性的查询和设置
var author=book.author;
var name=author.surname;
var title=book["main title"];
book.edition=6;
book["main title"]="ECMAScript";
ES3中点运算符后的标识符不能是保留字。必须以方括号的形式访问。比如,o["for"]。ES5可以再点运算符后使用保留字。
//node 6.9.5
> var a={for:1}
undefined
> a
{ for: 1 }
> a.for
1
使用方括号时,方括号内的表达式必须返回字符串或返回一个可以转换为字符串的值。例如数组中的arr[0]。
作为关联数组的对象
关联数组:元素通过字符串索引而不是数字索引。如object["property"]
JavaScript对象都是关联数组。
继承
查找对象o属性x,如果o中不存在x,将会在o的原型对象中查询属性x。如果原型中也没有x,但这个原想对象也有原型,那么继续向上执行查询,知道找到x或者查找到一个原型为null的对象为止。(应该是直到找到x或者找到Object对象或者查找到一个原型是null的对象为止)。
给对象o属性x赋值,如果o已经有熟悉x,那么这个操作改变这个已有属性x的值。如果o中不存在x,那么复制操作给o天剑新属性x。如果之前o继承了属性x,继承的属性将会被同名的新属性覆盖掉。
对象的原型属性构成了一个"链",通过"链"可以实现属性的继承。
属性访问错误
查找不存在的属性并不会报错会返回undefined。
book.subtitle;//undefined
如果对象不存在,师徒查询不存在的对象的属性会报错。null和undefined没有属性,因此会报错。
book.subtitle.length;//类型错误
避免出错的方法
var len=book&&book.subtitle&&book.subtitle.length;
ES5任何失败的属性设置操作都不会抛出类型错误异常。下属场景下给对象o设置属性p会失败:
- o属性p只读:不能给只读属性重新赋值(defineProperty()方法下有一个个例外,可以对可配置的只读属性重新赋值)
- o的属性p是继承属性,且只读:不能通过同名自有属性覆盖只读的继承属性。
- o中不存在自有属性p:o没有使用setter方法继承属性p,并且o的可扩展性为false。如果o中不存在p,而且没有setter方法,则p会添加到o中。但是如果o不可扩展,那么o不能定义新属性。
删除属性
delete运算符可以删除对象的属性。操作数应当是一个属性访问表达式。
delete book.author;
delete book["main title"];
delete只是断开属性和宿主对象的联系,而不去操作属性中的属性。
a={p:{x:1}};
b=a.p;
delete a.p; // 此时b为{x:1},a为{}
当delete删除成功或没有任何作用时,翻译true。如果delete后不是一个属性访问表达式,delete同样返回true。
o={x:1};
delete o.x;//true
delete o.x; //什么也没做,x已经不存在了,返回true
delete o.toString; //什么也没做,toString是继承来的。返回true。
delete 1; //无意义,返回true
delete不能删除可配置型为false的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。严格模式下(ES5),删除不可配置属性会报类型错误,非严格模式下(以及ES3)返回false。
delete Object.prototype; //不能删除,属性不可配置
var x=1;
delete this.x; //不能删除全局变量
function f(){};
delete this.f; //不能删除全局函数
非严格模式下删除全局对象的可配置属性式,可以省略对全局对象的引用。
this.x=1; //创建一个可配置的全局属性(没有用var)
delete x; //删除成功
检测属性
JavaScript对象可以看做属性的集合。检测属性集合中成员的所属关系,判断某个属性是否存在于某个对象中,使用in、hasOwnProperty()和protertyIsEnumerable()方法来完成。
in
in运算符左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true。
var o={x:1};
"x" in o; //true
"y" in o; //false
"toString" in o; //true :o继承toString属性
除了使用in,可以使用!==
判断一个属性是否是undefined。
var o={x:1};
o.x !== undefined; //true,o中有属性x
o.y !== undefined; //false,o中没有shuxingy
o.toString !== undefinede; //true :o继承toString属性
in运算符可以区分不存在的属性和存在但值为undefined的属性。
var o={x:undefined};
o.x!==undefined;//false,属性存在但值为undefined
"x" in o;//true,属性存在
delete o.x;
"x" in o;//false,属性不再存在
if(o.x!=null) o.x*=2;//如果存在且不为null或undefined,o.x乘以2
if(o.x) o.x*=2;//如果o含有属性且属性不能转换为false,o.x乘以2(undefined,null,false,"",0,NaN)
hasOwnProperty()
用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false。
var o={x:1};
o.hasOwnProperty("x"); //true
o.hasOwnProperty("y"); //false
o.hasOwnProperty("toString"); //false,继承属性返回false
propertyIsEnumerable
检测自有属性且这个属性的可枚举性为true时返回true。某些内置属性是不可枚举的,通常由JavaScript代码创建的属性都是可枚举的,除非在ES5中使用特殊方法改变属性的可枚举性。
var o=inherit({y:2});
o.x=1;
o.propertyIsEnumerable("x"); //true,o有一个可枚举的自有属性x
o.propertyIsEnumerable("y"); //false,y是继承来的
Object.prototype.propertyIsEnumerable("toString"); //false,不可枚举
枚举属性
for/in 遍历所有可枚举的属性(包括自有的和继承的),把属性名赋值给循环变量。
for(p in o){
if (!o.hasOwnProperty(p)) continue;//跳过继承的属性
}
for(p in o){
if (typeof o[p] === "function") continue;//跳过函数
}
Object,keys() ,返回一个数组,数组由对象中可枚举的自有属性的名称组成。(ES5)
Object.getOwnPropertyNames(),返回对象的所有自有属性的名称。(ES5)
属性getter和setter
对象属性由名字、值和一组特性构成。ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称作"存储器属性"(accessor property),它不同于”数据属性“(data property),数据属性只有一个简单地值。
当程序查询存储器属性的值时,JavaScript调用getter方法(无参数)。返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript调用setter方法,将赋值表达式右侧的值当做参数传入setter。
存取器属性不具有可写性。如果属性同时具有getter,setter方法,那么它是一个读写属性。如果只有getter或setter方法,则它是一个只读/写属性。读取只写属性总是返回undefined。
定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法:
var o={
//普通的数据属性
data_prop:value,
//存取器属性都是成对定义的函数
get accessor_prop(){/*函数体*/},
set accessor_prop(value){/*函数体*/}
};
【表示2D笛卡尔点坐标的对象】
> var p={x:1.0,
... y:1.0,
... get r(){return Math.sqrt(this.x*this.x+this.y*this.y);},
... set r(newvalue){
..... var oldvalue=Math.sqrt(this.x*this.x+this.y*this.y);
..... var ratio =newvalue/oldvalue;
..... this.x*=ratio;
..... this.y*=ratio;
..... },
... get theta(){return Math.atan2(this.y,this.x);}
... };
undefined
> p.r
1.4142135623730951
> p.r=2
2
> p.r
1.9999999999999998
> p
{ x: 1.414213562373095,
y: 1.414213562373095,
r: [Getter/Setter],
theta: [Getter] }
> p.theta
0.7853981633974483
> p.theta=1
1
> p.theta
0.7853981633974483
与数据属性一样,存取器属性是可以继承的。
属性的特性
数据属性的四个特性:值、可写性,可枚举性、可配置性。
存取器属性不具有值特性和可写性,他们的可写性由setter方法存在与否决定。存取器的四个特性:读取、写入、可枚举性、可配置型。
属性描述符
属性描述符(property descriptor)对象代表四个特性。描述符对象的属性和他们所描述的属性特性是同名的。
数据属性的描述符对象的属性有value、writable、enumerable、configurable。
存储器属性的描述符对象的属性有get、set、enumerable、configurable。
其中writable、enumerable、configurable都是布尔值,get属性和set属性是函数值。
Object.getOwnPropertyDescriptor()
通过Object.getOwnPropertyDescriptor()获取某个对象特定属性的属性描述符,对于不存在的属性和继承属性返回undefined:
Object.getOwnPropertyDescriptor({x:1},"x");//返回{ value: 1, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor(p,"r");//返回{ get: [Function: get r],set: [Function: set r], enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor(p,"theta");//返回 { get: [Function: get theta], set: undefined, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor({},"x");//undefined,不存在的
Object.getOwnPropertyDescriptor({},"toString");//undefined,继承的
获取继承属性的特性,需要遍历原型链。
Object.defineProperty()
设置属性的特性,或者想让新建属性具有某种特性,则需要调用 Object.defineProperty(),传入要修改的对象、要创建或修改的属性的额名称以及属性描述符对象:
var o={};
//添加不可枚举的数据属性x赋值为1
Object.defineProperty(o,"x",{value:1,writable:true,enumerable:false,configurable:true});
o.x; //1 ,属性存在
Object.keys(o);//[],不可枚举
//修改为只读
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.getOwnPropertyDescriptor(o,"x");//{ get: [Function: get],set: undefined, enumerable: false, configurable: true }
传入的属性描述符对象不必包含所有4个特性。对于新创建的属性来说,默认的特征值是false或undefined。对于修改的已有属性来说,默认的特征值没有做任何修改。注意:这个方法要么修改已有的自有属性要么新建自有属性,但不能修改继承属性。
Object.defineProperties()
如果要同时修改或创建多个属性,则需要使用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
}
});
p; //{ x: 1, y: 1, r: [Getter] }
对于不允许创建或修改的属性来说,使用 Object.defineProperty()
和Object.defineProperties()
对其操作就会抛出类型错误异常。
- 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性。
- 如果属性是不可配置的,则不能修改它的可配置型和可枚举性。
- 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能将它转换为数据属性。
- 如果数据属性是不可配置的,则不能将它转换为存取器属性。
- 如果数据属性是不可配置的,则不能将它的可写性从false修改为true,但可以从true修改为false。
- 如果数据属性是不可配置的且不可写的,则不能修改它的值。可配置但不可写属性的值是可以修改的(实际上是先将它标记为可写的,然后修改它的值,最后转换为不可写的)。
【复制属性的特性】
/*
* 给Object.property添加不可枚举的extend()方法
* 这个方法继承自调用它的对象,将作为参数传入的对象的属性一一复制
* 除了值之外,也复制属性的所有属性,除非在目标对象中存在同名的属性
* 参数对象的所有自身对象(包括不可枚举的属性也会一一复制)
*/
Object.defineProperty(Object.prototype,"extend",{
writable:true,
enumerable:false,
configurable:true,
value:function(o){
//得到所有的自有属性,包括不可枚举属性
var names=Object.getOwnPropertyNames(o);
//遍历
for(var i=0;i Object.getOwnPropertyDescriptor(Object.prototype,"extend")
{ value: [Function: value],
writable: true,
enumerable: false,
configurable: true }
对象的三个属性
原型(prototype)、类(class)、可扩展性(extensible attribute)
原型属性
对象的原型属性是用来继承属性的。我们经常把"o的原型属性"直接叫做"o的原型"。
Object.getPrototypeOf()
Object.getPrototypeOf() (ES5)查询原型。
var p={x:1};
var o=Object.create(p);
Object.getPrototypeOf(p);//{}
Object.getPrototypeOf(o);//{ x: 1 }
isPrototypeOf()
isPrototypeOf() 检测一个对象是否是另一个对象的原型(或处于原型链中)。p.isPrototypeOf(o)
检测p是否是o的原型,o是否继承自p
var p={x:1};
var o=Object.create(p);
p.isPrototypeOf(o); //true,o继承自p
Object.prototype.isPrototypeOf(o); //true,p继承自Object.prototype
类属性
对象的类属性是一个字符串,用以表示对象的类型信息。ES3和ES5都未提供设置这个属性的方法。并只有一种间接的方法可以查询它。默认的toString()方法返回了如下格式的字符串:[object class]
// classof() 返回传递给它的任意对象的类
function classof(o){
if(o===null) return "Null";
if(o===undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
}
classof(null) //"Null"
classof(1) //"Number"
classof("") //"String"
classof(true) //"Boolean"
classof({}) //"Object"
classof([]) //"Array"
classof(/./) //"RegExp"
classof(new Date()) //"Date"
classof(window) //"Window",客户端宿主对象。nodejs下会报错 ReferenceError: window is not defined
function f() {};
classof(f); //"function"
classof(new f()); //"Object"
可扩展性
对象的可扩展性用以表示是否可以给对象添加新属性。所有的内置对象和自定义对象都是显示可扩展的,宿主对象的可扩展性是由JavaScript引擎定义的。ES5中,所有内置对象和自定义对象都是可扩展的,除非将他们转换为不可扩展的。
ES5定义了查询和设置对象可扩展性的函数。
Object.esExtensible()
将对象传入Object.isExtensible(),判断对象是否是可扩展的。
var a={};
Object.isExtensible(a); //true
Object.preventExtensions()
如果想将对象转换为不可扩展的,需要调用Object.preventExtensions()
。注意,一旦将对象转换为不可扩展的,就无法再将其转换为可扩展的了。此方法只影响到对象本神的可扩展性,如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。
var a={x:1};
var b=Object.create(a)
Object.preventExtensions(b)
Object.isExtensible(b)//false
a.y=1;
b.y;//1
b.m=1
b.m;//undefined
Object.seal()
Object.seal()将对象设置为不可扩展的,还可以讲对象的所有自有属性都设置为不可配置的。对于封闭(sealed)起来的对象是不能解封的,可以通过Object.isSealed()
来检测对象是否封闭。
//接上:
Object.isSealed(a); // false
Object.isSealed(b); // true
Object.freeze()
Object.freeze()
将更严格地锁定对象——"冻结"(frozen)。将对象设置为不可扩展的,将其属性设置为不可配置的,将它自由的所有数据属性设置为只读的。(如果对象存取器属性具有setter方法,存取器属性将不受影响)。使用Object,isFrozen()
来检测对象是否冻结。
//接上
Object.isFrozen(a); //false
Object.isFrozen(b); //true
序列化对象
序列化对象(serialization)是将对象的状态转换为字符串,也可以将字符串还原为对象。
ES5 提供内置函数JSON.stringify()
和JSON.parse()
用来序列化和还原JavaScript对象。这些方法都使用JSON(JavaScript Object Notation,JavaScript对象表示法)作为数据交换格式。
o={x:1,y:{z:[false,null,""]}};
s=JSON.stringify(o);//'{"x":1,"y":{"z":[false,null,""]}}'
p=JSON.parse(s);//{ x: 1, y: { z: [ false, null, '' ] } }
JSON的语法是JavaScript语法的子集,它并不能表示JavaScript里的所有值。
JSON官网,在此不对json做深究。
【简单测试如下,基于node 6.9.5】
> JSON.stringify(undefined)
ndefined //无返回值
> JSON.stringify(null)
'null'
> JSON.stringify(NaN)
'null'
> JSON.stringify(0)
'0'
> JSON.stringify('')
'""'
> JSON.stringify(true)
'true'
> JSON.stringify(false)
'false'
> JSON.stringify({})
'{}'
> JSON.stringify([1,2,3])
'[1,2,3]'
> JSON.stringify(function(){})
undefined //无返回值
> JSON.stringify(new Date())
'"2017-02-24T08:40:02.442Z"'
> JSON.stringify(/./)
'{}'
> JSON.parse('null')
null
> JSON.parse('0')
0
> JSON.parse('""')
''
> JSON.parse('true')
true
> JSON.parse('false')
false
> JSON.parse('{}')
{}
> JSON.parse('[1,2,3]')
[ 1, 2, 3 ]
> JSON.parse('"2017-02-24T08:40:02.442Z"')
'2017-02-24T08:40:02.442Z'
对象方法
toString() valueOf() 可以参考JavaScript 中的 toString() 与 valueOf()
toLocaleString()
返回一个表示这个对象的本地化字符串。
Object中默认的toLocaleString() 方法并不做任何本地化自身的操作,它仅调用toString()方法并返回对应值。
Date和Number类对其做了定制,可以用它对数字、日期和时间做本地化的转换。
> (new Date()).toString()
'Fri Feb 24 2017 17:07:46 GMT+0800 (CST)'
> (new Date()).toLocaleString()
'2017-02-24 17:07:50'
Array类的toLocaleString对每个数组元素调用toLocaleString()方法转换为字符串。