第一部分 JavaScript语言核心 第6章 对象(待续)
对象时JS基本的数据类型。
对象时一种复合值:它将很多值(原始值或者其他对象)聚合在一起,可以通过名字访问这些值。
对象也可以看做是属性的无序集合,每个属性都是一个键值对。属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。
但对象不仅仅是字符串到值的映射,除了可以保持自有的属性,JS对象还可以从一个称为原型的对象继承属性。对象的方法通常是继承的属性。这种”原型式继承“是JS的核心特征。
JS对象时动态的,可以新增和删除属性。但它们通常用来模拟静态对象以及静态类型语言中的”结构体“。有时它们也用做字符串的集合(忽略键值对中的值)。
除了字符串、数字、true、false、null和undefined之外,JS中的值都是对象。
尽管字符串、数字、布尔值不是对象,但它们的行为和不可变对象非常类似。
对象是可变的,我们通过引用而非值来操作对象。如果变量x是指向一个对象的引用,那么执行代码var y = x;变量y也是指向同一个对象的引用,而非这个对象的副本。通过变量y修改这个对象也会对变量x造成影响。
对象最常见的用法是创建create、设置set、查找query、删除delete、检测test和枚举enumerate它的属性。
属性名可以包含空字符串在内的任意字符串,但对象不能存在两个同名的属性。
属性还有一些与之相关的值,称为”属性特性“:
可写:表明是否可以设置该属性的值。
可枚举:表明是否可以通过for/in循环返回该属性。
可配置:表明是否可以删除或修改该属性。
除了包含属性外,对象还拥有三个相关的对象特性:
对象的原型指向另外一个对象,本对象的属性继承自它的原型对象。
对象的类是一个标识对象类型的字符串。
对象的扩展标记指明了是否可以向该对象添加新属性。
三类JS对象和两类属性分类:
内置对象:如数组、函数、日期和正则表达式都是内置对象。
宿主对象:由JS解释器所嵌入的宿主环境(如浏览器)定义的。客户端JS中表示网页结构的HtmlElement对象均是宿主对象。既然宿主环境定义的方法可以当成普通的JS函数对象,那么宿主对象也可以当成内置对象。
自定义对象:是由运行中的JS代码创建的对象。
继承属性:是在对象的原型对象中定义的属性。
自有属性:是直接在对象中定义的属性。
6.1 创建对象
可以通过对象直接量、关键字new和Object.create()函数来创建对象。
6.1.1 对象直接量
用对象直接量是创建对象最简单的方式:
var empty={}; //没有任何属性的对象。
var point={x:0,y:0}; //两个属性
var point2 ={x:pint.x,y:pint.y+1}; //更复杂的值
var book ={
"main title":"JavaScript", //属性名里有空格,必须用字符串表示
'sub-title':"The Definitive Guide", //属性名里有连字符,必须用字符串表示
"for":"all audiences", //for是保留字,必须用引号
author:{ //这个属性值是一个对象
firstname:"David", //注意,这里的属性名没有引号
surname:"Flanagan"
}
};
对象直接量是一个表达式,这个表达式的每次运算都创建并初始化一个新的对象。
每次计算对象直接量的时候,也都会计算它的每个属性的值。如果在一个重复调用的函数中的循环体内使用了对象直接量,他将创建很多新对象,并且每次创建的对象的属性值也有可能不同。
6.1.2 通过new创建对象
new运算符创建并初始化一个新对象。关键字new后跟一个构造函数,构造函数用来初始化一个新创建的对象。
JS语言核心中的原始类型都包含内置构造函数。如:
var o = new Object(); //创建一个空对象,和{}一样
var a = new Array(); //创建一个空数组,和[]一样
var d = new Date(); //创建一个表示当前时间的Date对象
var r = new RexExp("js"); //创建一个可以进行模式匹配的EegExp对象
6.1.3 原型
原型就类似C#中的类,是对象的一个模板。
每个JS对象(null)除外都和另一个对象相关联。”另一个“对象就是我们熟知的原型,每一个对象都从原型继承属性。
所有通过对象直接量创建的对象都具有同一个原型对象,并可以通过JS代码Object.prototype获得对原型对象的引用。
通过关键字new和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。因此,同使用{}创建对象一样,通过new Object()创建的对象也继承自Object.prototype。
同样,通过new Array()创建的对象的原型就是Array.prototype,通过new Date()创建的对象的原型就是Date.prototype。
没有原型的对象位数不多,Object.prototype就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都有原型。
所有的内置构造函数(以及大部分自定义构造函数)都具有一个继承自Object.prototype的原型。如:
Date.prototype的属性继承自Object.prototype,因此由new Date()创建的Date对象的属性同时继承自Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的”原型链“。
6.1.4 Object.creat()
Object.create(parm1,parm2);//用来创建新对象,第1个参数是这个对象的原型。第2个可选参数是对对象的属性进行进一步描述。
Object.create()是一个静态函数,而不是提供给某个对象调用的方法。
使用方法只需传入所需的原型对象即可:
var o1 = Object.create({x:1,y:2}); //o1继承了属性x和y
可以传入参数null来创建一个没有原型的新对象,这种方式创建的对象不会继承任何东西,也不包括基础方法,比如toString(),就不能和"+"运算符一起工作。
var o2 = Object.create(null); //o2不继承任何属性和方法
创建普通空对象(比如通过{}或new Object()创建的对象),需要传入Object.prototype:
var o3 = Object.create(Object.prototype); //o3和{}、new Object()一样
可以通过任意原型创建新对象(可以使任意对象可继承),是一个强大的特性:
例6-1:通过原型继承创建一个新对象
//inherit()返回了一个继承自原型对象p的属性的新对象
//这里使用ECMAScript 5中的Object.create()函数(如果存在的话)
//如果不存在Object.create(),则退化使用其他方法
function inherit(p)
{
if(p==null) throw TypeError(); //p是一个对象,但不能是null
if(Object.create) //如果Object.create()存在
return Object.create(p); //直接使用它
var t = typeof p; //否则进行进一步检测
if(t!=="object" && t!=="function") throw TypeError();
function f(){}; //定义一个空构造函数
f.prototype = p; //将其原型属性设置为p
return new f(); //使用f()创建p的继承对象
}
注意:inherit()并不能完全代替Object.create(),它不能通过传入null原型来创建对象,而且不能接受可选的第二个参数。
inherit()函数的一个用途就是防止库函数无意间(非恶意地)修改那些不受你控制的对象。
不是将对象直接作为参数传入函数,而是将它的继承对象传入函数。当函数读取继承对象的属性时,实际上读取的是继承来的值。如果给继承对象的属性赋值,则这些属性只会影响整个继承对象自身,而不是原始对象:
var o = {x:"dont change this value"};
library_function(inherit(o)); //房子对o的意外修改
6.2 属性的查询和设置
可以通过点.和方括号[]运算符来获取属性的值。运算符左侧应当是一个表达式,它返回一个对象。
对于点.来说,右侧必须是以属性名称命名的简单标识符。
对于方括号[]来说,方括号类必须是一个计算结果为字符串的表达式,这个字符串就是属性名字:
var author = book.author; //得到book的“author”属性
var name = author.surname; //得到获得author的"surname"属性
var title = book["main tilte"]; //得到book的"main title"属性
创建属性和给属性赋值:
book.edition = 6; //给book创建一个名为"edition"的属性
book["main title"] = "ECMSScript"; //给"mai title"属性赋值
6.2.1 作为关联数组的对象
object["proerty"];
这种获取属性方式更像数组,只是索引是字符串不是数字。这种数组就是关联数组,也称作散列、映射或字典。JS对象都是关联数组。
JS是弱类型语言,对象可以创建任意数量的属性。
这个函数给portifolio添加新的股票:
function addstock(porfolio,stockname,shares){
profolio[stockname] = shares;
}
for/in遍历关联数组:
计算porffolio的总计值:
funcion getvalue(portfolio){
var total = 0.0;
for(stock in portfolio){ //遍历portfolio中的每只股票
var shares = portfolio[stock]; //得到每只股票的份额
var price = getquote(stock); //查找股票价格
total +=shares*price; //将结果累加至total中
}
return total; //返回total的值
}
6.2.2 继承
JS对象具有“只有属性”,也有一些属性从原型对象继承来的。
假设要查询对象o的属性x,如果o中不存在x,那么将会继续在o的原型对象中查询属性x。如果原型对象中也没有x,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查询,知道找到x或者查找到一个原型是null的对象为止。
对象的原型属性构成了一个“链”,通过这个“链”可以实现属性的继承。
var o={}; //o从Object.prototype继承对象的方法
o.x =1; //给o定义一个属性x
var p= inherit(o); //p继承o和Object.prototype
p.y = 2; //给p定义一个属性y
var q=inherit(p); //q继承p、o和Object.prototype
q.x+q.y //=>3:x和y分别继承自o和p
现在假设给对象o的属性x赋值,如果o中已经有属性x(这个属性不是继承来的),那么这个赋值操作只改变这个已有属性x的值。
如果o中不存在属性x,那么赋值操作给o添加一个新属性x。如果之前o继承自属性x,那么这个继承的属性就被新创建的同名属性覆盖了。
属性赋值操作首先检查原型链,以此判定是否允许赋值操作。
例如,如果o继承自一个只读属性x,那么赋值操作时不允许的。
如果允许属性赋值赋值操作,它也总是在原始对象上创建属性或对已有的属性赋值,而不会修改原型链。
在JS中,只有在查下属性时才会体会到继承的存在,而设置属性则和继承无关,这是JS的一个重要特性,该特性让程序员可以有选择地覆盖(override)继承的属性。
var unitcircle={r:1}; //一个用来继承的对象
var c=inherit(unitcircle); //c继承属性r
c.x=1;c.y=1; //c定义两个属性
c.r =2; //c覆盖继承来的属性
unitcircle.r; //=>1,原型对象没有被修改
属性赋值要么失败,要么创建一个属性,要么在原始对象中设置属性,但有一个例外,如果o继承自属性x,而这个属性是一个如有setter方法和accessor属性,那么这是将调用setter方法而不是给o创建一个属性x。
需要注意的是,setter方法是有对象o调用的,而不是定义这个属性的原型对象调用的。因此如果setter方法定义任意属性,这个操作只是针对o本身,并不会修改原型链。
6.2.3 属性访问错误
属性访问并不总是返回或设置一个值。
查询一个不存在的属性并不会报错,如果对象o自身的属性或继承的属性中均未找到属性x,属性访问表达式o.x返回undefined。
book.subtitle; //没有这个属性则返回:undefined
但是,如果对象不存在,那么查询这个对象的属性就会报错。null和undefined值都没有属性,因此查询这些值的属性会报错:
//抛出一个类型错误异常,undefined没有length属性
var len = book.subtitle.length;
除非确定book和book.subtitle都是对象,否则不能这样写表达式book.subtitle.length,因为这样会报错,下面提供两种避免出错的方法:
//一种冗余但很易懂的方法
var len = undefined;
if(book){
if(book.subtitle) len = book.subtitle.length;
}
//一个更简练的常用方法,获取subtitle的lenght属性或undefined
var len = book && book.subtitle && book.subtitle.length;
给null和undef设置属性也会报类型错误。给其他值设置属性也不总是成功,有一些属性是只读的,不能重新复制,有一些对象不允许新增属性,但让人颇感意外的是,这些设置属性的失败操作不会报错:
//内置构造函数的原型是只读的
Object.prototype =o; //赋值失败,但没报错,Object.prototype没有修改
这是历史遗留问题,在ECMAScript 5严格模式中已经修复。在严格模式中,任何失败的属性设置操作都会抛出一个类型错误异常。
这些场景下给对象o设置属性p会失败:
o中的属性p是只读的:不能给只读属性重新赋值(defineProperty()方法中有一个例外,可以对可配置的只读属性重新赋值)。
o中的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性。
o中不存在自有属性p:o没有使用setter方法继承属性p,并且o的可扩展性是false。如果o中不存在p,而且没有setter方法可做调用,则p一定会添加至o中。但如果o不是可扩展的,那么在o中不能定义新属性。
6.3 删除属性
delete运算符可以删除对象的属性。
delete book.author; //book不再有属性author
delete book["main title"]; //book也不再有属性"main title"
delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性:
a={p:{x:1}};
b=a.p;
delete a.p;
执行这段代码后b.x的值依然是1。由于已经删除的属性的引用依然存在,因此在JS的某些实现中,可能因为这种不严谨的代码而造成内存泄漏。所以在销毁对象的时候,要遍历属性中的属性,依次删除。
delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。
当delete表达式删除成功或没有任何副作用(比如删除不存在的删除)时,它返回true。如果delete后不是一个属性访问表达式,delete同样返回true:
o ={x:1}; //o有一个属性x,并继承属性toString
delete o.x; //删除x,返回true
delete o.x; //什么都没做(x已经不存在了),返回true
delete o.toString; //什么也没做(toString是继承来的),返回true
delete 1; //无意义,返回true
delete不能删除那些可配置性为false的属性(尽管可以删除不可扩展对象的可配置属性)。
某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。在严格模式中,删除一个不可配置属性会报一个类型错误。在非严格模式中(以及ECMAScript3中),在这些情况下的delete操作会返回false:
delete Object.prototype; //不能删除,属性是不可配置的
var x=1; //声明一个全局变量
delete this.x; //不能删除这个属性
function f(){} //声明一个全局函数
delete this.f; //也不能删除全局函数
当在非严格模式中删除全局对象的可配置属性时,可以省略对全局对象的引用,直接在delete操作符后跟要删除的属性名即可:
this.x =1; //创建一个可配置的全局属性(没有用var)
delete x; //将它删除
然而在严格模式中,delete后跟随一个非法的操作数(比如x),则会抱一个预发错误,因此必须显式指定对象及其属性。
delete x; //在严格模式下报语法错误
delete this.x; //正常工作
6.4 检测属性
JS对象可以看做属性的集合,我们经常会检测集合中成员的所属关系,判断某个属性是否存在某个对象中。可以通过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()的增强版,只有检测到是只有属性且这个属性是可枚举的才会返回true。某些内置属性是不可枚举的。
通常由JS代码创建的属性都是可枚举的,除非在ECMA5中使用一个特殊的方法来改版属性的可美剧性,随后会提到:
var o=inherit{{y:2}};
o.x=1;
o.propertyIsEnumerable("x"); //true:o有一个可枚举的自有属性x
o.propertyIsEnumerable("y"); //false:y是继承来的
Object.prototype.propertyIsEnumerable("toString"); //false:不可枚举
除了使用in运算符之外,另一种更简便的方法时使用"!=="判断一个属性是否undefined:
var o={x:1}
o.x !==undefined; //true:o中有属性x
o.y !==undefined; //false:o中没有属性y
o.toString !== undefined; //true:o继承了tostring属性
然而有一种场景只能使用in运算符而不能使用上述属性访问的方式。in可以区分不存在的属性和存在但值为undefined的属性。例如下面代码:
var o={x:undefined} //属性被显式赋值为undefined
o.x !== undefined //false:属性存在,但值为undefined
o.y !== undefined //false:属性不存在
"x" in o //true:属性存在
"y" in o //false:属性不存在
delete o.x; //删除了属性x
"x" in o //false:属性不再存在
注意:上述代码中用的是"!=="运算符,而不是"!="。
"!=="可以区分undefined和null。有时则不必作这种区分:
//如果o中含有属性x,且x的值不是null或undefined,o.x乘以2
if(o.x!=null) o.x *=2;
//如果o中含有属性x,且x的值不能转换为false,o.x乘以2
如果x是undefined、null、false、” “、0或NaN,则它保持不变
if(o.x) o.x*=2;

浙公网安备 33010602011771号