对象

对象


对象是JavaScript的基本数据类型。对象是一种复合值:它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。对象也可看作是属性的无序集合,每个属性都是一个名/值对。属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。这种数据类型也可以称为散列(hash)、散列表(hashtable)、字典(dictionary)、关联数组(associative array)

Javascript对象是动态的——可以新增属性也可以删除属性——但它们通常用来模拟静态对象以及静态类型语言中的“结构体”(struct)。有时,它们也用作字符串的集合(忽略名/值对中的值)。
除了字符串、数字、true、false、null和undefined之外,JavaScript中的值都是对象,尽管字符串、数字和布尔值不是对象,但它们的行为和不可变对象非常相似。如:(123).toString();原始值123本没有属性和方法,但js依旧让他可以调用方法。注意:这里的字面值必须用括号括起来

属性包括名字和值。属性名可以是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性。除了名字和值之外,每个属性还有一些与之相关的值,称为属性特性(property attribute)。

  • 可写(writable attribute),表明是否可以设置该属性的值。
  • 可枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性。
  • 可配置(configurable attribute),表明是否可以删除或修改该属性。

在ECMAScript 5之前,通过代码给对象创建的所有属性都是可写的、可枚举的和可配置的。在ECMAScript 5中则可以对这些特性加以配置。

除了包含属性之外,每个对象还拥有三个相关的对象特征(object attribute)。

  • 对象的原型(portotype)指向另外一个对象,本对象的属性继承自它的原型对象。
  • 对象的类(class)是一个标识对象类型的字符串。
  • 对象的扩展标记(extensible flag)指明了(在ECMAScript 5中)是否可以向该对象添加新属性。

最后,我们用下面这些术语来对三类JavaScript对象和两类属性作区分:

  • 内置对象(native object)是由ECMAScript规范定义的对象或类。例如,数组,函数,日期和正则表达式都是内置对象。
  • 宿主对象(host object)是由JavaScript解释器所嵌入的宿主环境(比如Web浏览器)定义的。
  • 自定义对象(user-defined object)是由运行中的JavaScript代码创建的对象。
  • 自有属性(own property)是直接在对象中定义的属性。
  • 继承属性(inherited property)是在对象的原型对象中定义的属性。

1. 创建对象

可以通过对象直接量、关键字new和(ECMAScript 5中的)Object.creat()函数来创建对象。

1.1 对象直接量

对象直接量是由若干名/值对组成的映射表,名/值对中间用冒号分隔,名/值对之间用逗号分隔,整个映射表用花括号括起来.属性名可以是JavaScript标识符也可以是字符串字面量(包括空字符串)。属性的值可以是任意类型的JavaScript表达式,表达式的值(可以是原始值也可以是对象值)就是这个属性的值.

//没有任何属性的对象
var empty = {};

//两个属性
var point = { x: 0, y: 0 };

//更复杂的值
var point2 = { x: point.x, y: point.y };

var book = {
    "main title": "JavaScript",    //属性名字里面有空格,必须用字符串表示
    "sub-title": "The Definitive Guide",    //属性名字里有连字符,必须用字符串表示
    "for": "all audiences",    //"for"是保留字,因此必须必须用引号。
    //注意:以上三种属性名虽然可以使用,但并不建议。

    author: {    //这个属性的值是一个对象
        firstname: "David",    //注意:这里的属性名都没有引号。
        surname: "Flanagan"
        }    //最后一个属性后面的逗号不需要写,写了的大部分实现会忽略,但是有些如IE将会报错。
};

对象直接量是一个表达式,这个表达式的每次运算都创建并初始化一个新的对象。每次计算对象直接量的时候,也都会计算它的每个属性的值。也就是说,如果在一个重复调用的函数中的循环体内使用了对象直接量,它将创建很多新对象,并且每次创建的对象的属性值也有可能不同。

1.2 通过new创建对象

new运算符创建并初始化一个新对象。关键字new后跟随一个函数调用。这里的函数称作构造函数(constructor),构造函数用以初始化一个新创建的对象。JavaScript语言核心中的原始类型都包含内置构造函数。

//JavaScript 提供原始数据类型字符串、数字和布尔的对象版本。但是并无理由创建复杂的对象。原始值快得多。因此,最好不要使用new创建对象。
//创建一个空对象,和 var o = {} 一样
var o = new Object();

//创建一个空数组,和 var a = [] 一样
var a = Array();

//创建一个String对象,和 var x1 = "" 一样
var x1 = new String();

//创建一个Number对象,可使用 var x2 = 0; 代替
var x2 = new Number();

//创建一个Boolean对象,可使用 var x3 = false; 代替
var x3 = new Boolean();

//创建一个Function对象,可使用 var x4 = function() {}; 代替
var x4 = new Function();

//创建一个可以进行模式匹配的EegExp对象,可使用 var r = /()/; 代替
var r = new RegExp();

//创建一个表示当前时间的Date对象
var d = new Date();

//普通函数的写法
function fun(a b){
    return a * b;
}

//除了上述内置构造函数,用自定义构造函数来初始化新对象也十分常见。
//构造函数的写法
function Person(name, age, add){
    this.name = name,
    this.age = age,
    this.add = add,
    this.print = function(){
                    console.log("name:" + this.name +
                    "\nage:" + this.age + "\naddress;" + this.add);
                    }
}
var p = new Person("xiaoming",15,"JiLin");

1.3 原型

在讲述第三种对象创建技术之前,我们应当首先解释一下原型。每一个JavaScript对象(null除外)都和另一个对象相关联。“另一个”对象就是我们熟知的原型,每一个对象都从原型继承属性(这里注意JS的属性可以是函数,即可以继承方法)。

所有通过对象直接量创建的对象都具有同一个原型对象,并可以通过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.prototypeObject.prototype。这一系列链接的原型对象就是所谓的“原型链(prototype chain)”。
2.2节讲述属性继承的工作机制。8.1节将会讲到如何获取对象的原型。

1.4 Object.create()

ECMAScript 5定义了一个名为Object.create()的方法,它创建一个新对象,其中第一个参数是这个对象的原型。该方法提供第二个可选参数,用以对对象的属性进行进一步描述。第7节将会讲述第二个参数。

Object.create()是一个静态函数。使用它创建一个对象,只需传入所需的原型对象即可。

//o1继承了属性x和y
var o1 = Object.create({ x: 1, y: 2});

//通过传入null创建一个没有原型的对象。这种方式创建的对象不会继承任何东西,甚至不包括基础方法,比如toString(),也就是说,它将不能和“+”运算符一起正常工作。
var o2 = Object.create(null);

//创建一个普通的空对象,需要传入Object.ptototype;
var o3 = Object.create(Object.prototype);    //o3和{}和new Object()一样。

可以通过任意原型创建新对象(换句话说,可以是使任意对象可继承),这是一个强大的特性。


2. 属性的查询和设置

可以通过点(.)或方括号([])运算符来获取属性的值。运算符左侧应当是一个表达式,他返回一个对象。对于点(.)来说,右侧必须是一个以属性名称命名的简单标识符。对于方括号来说([]),方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名字。

//得到book的“author”属性
var author = book.author;

//得到author的“surname”属性
var name = author.surname;

//得到book的“main title”属性
var title = book["main title"];

和查询属性值的写法一样,通过点和方括号也可以创建属性或给属性赋值

//给book创建一个名为“edition”的属性
book.edition = 6;

//给“main titile”属性赋值
book["main title"] = "ECMAScript";

ECMAScript 3中,点运算符后的标识符不能是保留字,如果一个对象的属性名是保留字,则必须是用方括号的形式访问。ECMAScript 5 对此放宽了限制,可以在点运算符后直接使用保留字。注意:虽然可以使用,但不建议这样做。见1.2节

当使用方括号时,我们说方括号内的表达式必须返回字符串。其实更严格地讲,表达式必须返回字符串或返回一个可以转换为字符串的值。

2.1 作为关联数组的对象

上文提到,object.propertyobject["property"]表达式的值相同。第一种语法使用点运算符和一个标识符,这和 C 和 JAVA 中访问一个结构体或对象的静态字段非常相似。第二种语法使用方括号和一个字符串,看起来更像数组,只是这个数组元素是通过字符串索引而不是数字索引。这种数组就是我们所说的关联数组(associative array),也称做散列、映射或字典(dictionary)。JS对象都是关联数组。

通过点运算符访问对象的属性时,属性名用一个标识符来表示。标识符必须直接出现在JS程序中,他们不是数据类型,因此程序无法修改它们。反过来讲,当通过方括号[]来访问对象的属性时,属性名通过字符串来表示。字符串是JS的数据类型,在程序运行时可以修改和创建它们。例如:

var addr = "";
for(i = 0; i < 4; i++){
    addr += customer["address" + i] + '\n';
    }

这个例子主要说明了使用数组写法和用字符串表达式来访问对象属性的灵活性。

2.2 继承

JS对象具有“自有属性”(own property),也有一些属性是从原型对象继承而来的。为了更好的理解这种继承,必须更深入地了解属性访问的细节。
假如要查询对象 o 的 x 属性,如果 o 中不存在 x ,那么将会继续在 o 的原型对象中查询属性 x 。如果原型对象中也没有 x ,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查询,直到找到一个原型是 null 的对象为止。可以看到,对象的原型属性构成了一个“链”,通过这个“链”可以实现属性的继承。

//在EMCAScript 3 中可以用下列 inherit 函数模拟原型继承
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的继承对象
}

//o从Object.prototype所引用的对象继承属性
var o = {};

//给o定义一个属性x
o.x = 1;

//p继承o和Object.prototype
var p = inherit(o);

//给p定义一个属性y
p.y = 2;

//q继承p,o和Object.ptototype
var q = inherit(p);

//给q定义一个属性z
q.z = 3;

//toString继承自Object.prototype
var s = q.toString();
q.x + q.y    //=>3 x和y分别继承自o和p

现在假设给对象 o 的属性 x 赋值,如果 o 中已经有属性 x (这个属性不是继承来的),那么这个赋值操作只改变这个已有属性 x 的值。如果 o 中不存在属性 x,那么赋值操作给 o 添加一个新属性 x 。如果之前 o 继承了属性 x ,那么这个继承的属性就被新创建的同名属性覆盖了。
属性赋值操作首先检查原型链,以此判定是否允许赋值操作。例如,如果 o 继承了一个只读属性 x ,那么赋值操作是不允许的(2.3节将对此进行详细讨论)。如果允许属性赋值操作,它也总是在原始对象上创建新属性(不与原型链上的任意属性同名)或 创建原型链上已存在的同名属性(此时为覆盖操作) 或 对已有的属性赋值,而不会去修改原型链。在JS中,只有在查询属性时才会体会到继承的存在,而设置属性则和继承无关,这是JS的一个重要特性,该特性让程序员可以有选择地覆盖(override)继承的属性。

//一个用来继承的对象
var unitcircle = { r: 1 };

//c继承属性r
var c = inherit(unitcircle);

//c定义两个属性
c.x = 1; c.y = 1;

//c覆盖继承来的属性
c.r = 2;
unitcircle.r;    //=>1,原型对象没有修改

属性赋值要么失败,要么创建一个属性(同名覆盖属性或新属性),要么为原始对象中的已有属性赋值,但有一个例外。如果 o 继承了属性x,而这个属性是一个具有 setter 方法的 accessor 属性(参照第6节)那么这时将调用 setter 方法而不是给 o 创建一个属性 x。需要注意的是,setter 方法是由对象 o 调用的,而不是定义这个属性的原型对象调用的。因此如果 setter 方法定义任意属性,这个操作只是针对 o 本身,并不会修改原型链。如

//该对象中的set lang就是上面所说的setter方法,即更改器方法
var person = {
  firstName:'bill',
  lastName: 'gates',
  language:'',
  set lang(lang){
    this.language = lang;
  }
}


var x = inherit(person);

//这里不是属性赋值操作,而是对象x(注意不是对象person)调用从person对象中继承的set lang方法
//(lang这种特殊的方法使用时就像访问属性或给属性赋值一样),lang中的this此时为对象x,所
//以下面的语句类似构造函数一样,给对象x添加属性language,并赋值为‘zh-cn’
x.lang = 'zh-cn';
//此时x的内容为{language:'zh-cn',__proto__:Object}
//而不是{lang:'zh-cn',__proto__:Object}

2.3 属性访问错误

查询一个不存在的属性并不会报错,而是会返回 undefined 。但是如果对象不存在,那么试图查询这个不存在的对象的属性就会报错。null 和 undefined值都没有属性,因此查询这些值的属性会报错。假设在程序中有一个没有被初始化或者有意无意被赋值为 null 或 undefined 的变量,查询了它的属性。var person; person.name; //抛出一个类型错误异常。下面提供了两种避免访问 null 或 undefined 对象的属性的方法:

//一种冗余但很易懂的方法
var len = undefined;
if(book){
    if(book.subtitle) len = book.subtitle.length;
}
//一种更简练的方法,获取 subtitle 的 length 属性或 undefined
var len = book && book.subtitle && book.subtitle.length;    
//与其他编程语言不太相同,JS中逻辑与或的值是第一个能确定此逻辑表达式真假的值。比如:0&&16,
//则该表达式的值为0,若1&&16则该表达式的值为16。逻辑或同理。JS中逻辑表达式的值是Number不是Boolean

当然,给 null 和 undefined 设置属性也会报类型错误。给其他值设置属性也不总是成功,有一些属性是只读的,不能重新赋值,有一些对象不允许新增属性,但让人颇感意外的是,这些设置属性的失败操作不会报错。

//内置构造函数的原型是只读的
Object.protoype = 0;    //赋值失败,但没报错,Object.prototype没有修改

这是一个历史遗留问题,这个 bug 在EMCAScript 5的严格模式中已经修复。在严格模式中,任何失败的属性设置操作都会抛出一个类型错误异常。

尽管属性赋值成功或失败的规律看起来很简单,但要描述清楚并不容易。在这些场景下给对象 o 设置属性 p 会失败:

  • o中的属性p是只读的:不能给只读属性重新赋值(defineProperty()方法中有一个例外,可以对可配置的只读属性重新赋值)。
  • o中的属性p是继承属性,且他是只读的:不能通过同名自有属性覆盖只读的继承属性。
  • o中不存在自有属性p。o没有继承属性p,o没有setter方法或继承的setter方法创建p,并且o的可扩展性(extensible attribute)是false。 (这一条是自己的理解,不确定是否正确?)

3. 删除属性

delete 运算符可以删除对象的属性。它的操作数应当是一个属性访问表达式。让人感到意外的是,delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性,如:

delete book.author;    //book不再有属性author
delete book["main title"];    //book也不再有属性“main titile”

var 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的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。在严格模式下,删除一个不可配置属性会报一个类型错误。在非严格模式中(以及EMCAScript 3中),在这些情况下的delete操作会返回false:

delete Object.protoype;    //不能删除,属性是不可配置的
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;    //正常工作

4. 检测属性

判断某个属性是否存在于某个对象中。

  • 使用 in 运算符,其左侧是属性名(字符串),右侧是对象。如果对象的z自有属性或继承属性中包含这个属性则返回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.
var o = inherit({ y:2 });
o.x = 1;
o.propertyIsEnumerable("x");  //true:o有一个可枚举的自有属性
o.propertyIsEnumerable("y");    //false:y是继承来的
Object.prototype.propertyIsEnumerable("toString");    //false:不可枚举。
  • 还有一种简便的方式,使用“!==”判断一个属性是否是undefined来判定该属性是否存在某个对象中。
var o = {x:1};
o.x !== undefined;    //true:o中有属性x
o.y !== undefined;    //false:o中没有属性y
o.toString !== undefined;    //true:o继承了toString属性

//然而,某些属性有可能存在于对象中,只是值为undefined,这种情况下需要使用in运算符。
var o = {x:undefined};
o.x !== undefined;    //false: 属性存在,但值为undefined。也就是说,!==为true可推出属性一定存在,但是!==为false不能推出属性一定不存在
o.y !== undefined;    //false:属性不存在
"x" in o;    //true:属性存在
"y" in o;    //false:属性不存在
delete o.x;    //删除属性x
"x" in o;    //false:属性不再存在

注意:上面使用的是!==不是!=,两者区别:

//!=不能区分null和undefined
var a = null
a != null;    //false
a != undefined;    //false

//!==
a !== null;    //false
a !== undefined;    //true

//==和===同理,==不能区分null和undefined

//实际使用举例
//如果o中含有属性x,且x的值不是null或undefined,则o.x乘以2
if (o.x != null)o.x *= 2;
//等价于
if (o.x != undefined) o.x *= 2;

//如果o中含有属性x,且x的值不能转换为false,o.x乘以2
if (o.x) o.x *= 2;
//即如果x是undefined,null,false,‘’,0或NaN时,o.x保持不变。这里涉及到JS的类型转换,具体细节可上网搜索。

5 枚举属性

xxx







参考文献

[1] JavaScript权威指南(第6版)







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值