Javascript进阶之路-论对象的重要性

要了解JavaScript对象,我们可以从对象创建、属性操作、对象方法这几个方面入手。概括起来,包括以下几模块:

1.创建对象
        1.1 对象直接量
        1.2 通过new创建对象
        1.3 Object.create
    2.属性管理
        2.1 属性查询和设置
        2.2 删除属性
        2.3 检测属性
        2.4 枚举属性
    3.属性封装
        3.1 属性getter和setter
        3.2 属性特性
    4. 对象的三个属性

4.1 原型属性

4.2 类属性

4.3 可扩展性
    5. 序列化对象

1.创建对象

1.1 对象直接量

对象直接量是创建对象最简单的方式,由若干名/值对组成映射表:

var point = {x: 0, y: 0 };

属性名也没有什么限制,可以是js的关键字或者任意字符串,如果是这两种情况,属性需要用双引号引起来:

var empty = {};
va point = {x: 0, y: 0 };
var book = {
"main title": "Javascript",
"sub-title": "The definitive Guide",
"for": "all audience",
author: {
firstName: "Davide",
lastName: "Flanagan"
}
};

对象直接量创建对象十分简单,但一般都不会这样使用。代码可复用性低,如果想要在其他地方使用该对象并且属性值不一样,那这么办?是不是又得重新创建一份代码?

1.2 通过new创建对象

通过new创建对象之前,先要创建一个函数,new把这个函数当做构造函数(constructor)。例如通过new创建一个Person对象:

function Person(){
//构造函数
} var person = new Person();

Javscript语言核心中的原始类型都包含内置构造函数:

var a = new Array();
var d = new Date();
var r = new RegExp(“js”);

1.3 Object.create()

在了解Object的create方法之前,我们想看看什么是原型。每一个Javascript对象(null除外)都和另一个对象相关联。“另一个”对象就是我们所说的原型。每一个对象都从原型继承属性。

所有通过对象直接量创建的对象都具有同一个原型对象Object.prototype。关键字new和构造函数创建的对象原型就是构造函数的prototype属性的值。通过new Array()创建对象的原型为Array.prototype,通过new Date()创建的对象原型为Date.prototype。原型暂介绍到这里。

Object.create方法包含两个参数,第一个参数是对象的原型,第二个参数可选,用于描述对象属性。使用很简单,只需传入所需的原型对象即可:

var o1 = Object.create({x: 1, y: 2 }); //原型为Object.prototype

如果想创建一个没有原型的对象,可通过传入null作为参数。这样创建的对象不会继承任何属性,也没有像toString这样的方法:

var o2 = Object.create(null); //没有原型

如果想创建一个普通的空对象,直接传入Object.prototype:

var o3 = Object.create(Object.prototype);

如果是自定义的对象,和创建空对象一样。直接传入对象名.prototype:

function Person(){
}
var o4 = Object.create(Person.prototype);

2.属性管理

2.1 属性查询和设置

对象的属性可通过点(.)或方括号([])运算符获取。如果使用点获取属性,属性名必须是简单的表示符。不能是保留字,比如,o.for或者o.class。

ar author = book.author; //正确
var name = author.surname; //正确
var title = book[“main title”]; //正确
var className = book.class; //错误

object[“property”]这种语法看起来更像数组,只是这个数组的元素是通过字符串索引而不是数字索引。这种数组就是我们所说的关联数组,也称为散列、映射或字典。Javascript对象都是关联数组。

既然对象是关联数组,那么Javascript也为我们提供了属性的遍历方式for/in。下面的例子利用for/in计算portfolio的总计值:

function getvalue(portfolio){
var total = 0.0;
for(stock in portolio){
var shares = portolio[stock];
var price = getquote(stock);
total += shares * price;
} return total;
}

继承:Javascript对象具有自有属性(own property),也有一些属性是从原型对象继承而来。我们先看看一个实现继承功能的函数inherit:

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();
}

假设要查询对象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.z = 3; //给q定义属性z
var s = q.toString(); //toString继承自Object.prototype
q.x + q.y // => 3:x和y分别继承自o和p

2.2 删除属性

delete运算符可以删除对象的属性:

delete book.author;
delete book[“main title”];

delete只能删除自有属性,不能删除继承属性。要删除继承属性,必须从定义这个属性的原型对象上删除它,而且这会影响到所有的继承自这个原型的对象。删除成功会返回true。

ar o = {x: 1};
delete o.x; //删除x,返回true
delete o.x; //x已经不存在了,什么都没做,返回true。
delete o.toString; //什么都没做,返回true。
delete不能删除可配置型为false的属性。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性:
delete Object.prototype //不能删除,属性是不可配置的
var x = 1;
delete this.x; //不能删除这个属性
function f() {}
delete this.f; //不能删除全局函数

2.3 检测属性

判断某个属性是否存在于某个对象中,可通过in运算符、hasOwnProperty()和propetyIsEnumerable()方法来检测。
    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.hasOenProperty("toString"); //false:toString是继承属性

propertyIsEnumerable()方法:是hasOwnProperty的增强版,只有检测到自有属性并且这个属性是可枚举行为true时才返回true:

var o = inherit({y: 2});
o.x = 1;
o.propertyIsEnumerable("x"); //true: o有一个可枚举属的自有属性x
o.propertyIsEnumerable("y"); //false:y是继承来的
Object.prototype.propertyIsEnumerable("toString"); //false:不可枚举

2.4 枚举属性

通常使用for/in循环遍历对象属性,遍历的属性包括自有属性和继承属性。对象继承的内置方法是不可枚举的,但在代码中给对象添加的属性都是可枚举的。例如:

var o = {x: 1, y: 2, z: 3}; //三个可枚举的自有属性
o.propertyIsEnumeable("toString"); //false,不可枚举
for (p in o) //遍历属性
console.log(p); //输出x、y和z,不会输出toString

有时候我们只想遍历自有属性,并且属性不为函数:

for(p in o){
if(!o.hasOwnProperty(p)) continue;
if(typeof o[p] === "function") continue;
}

我们可通过枚举遍历功能实现可枚举属性的复制:

/*
* 把p中的可枚举属性复制到o中,并返回o
* 如果o和p含同名属性,则覆盖o中的属性
* 这个函数并不处理getter和setter以及复制属性
*/
function extend(o, p){
for(prop in p){ //遍历p中的所有属性
o[prop] = p[prop]; //将属性添加到o中
}
return o;
}

ES5定义了两个用以枚举属性名称的函数。第一个是Object.keys(),返回由对象中可枚举属自有属性名称组成的数组。第二个枚举函数是Object.getOwnPropertyNames(),和Object.keys()类似,它返回对象的所有自有属性,而不仅仅是可枚举属性。

3.属性封装

3.1 属性getter和setter

对象属性由名字、值和一组特性(attribute)构成的。在ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称做“存取器属性”,它不同于“数据属性”,数据属性只有一个简单的值。
    和数据属性不同,存取器属性不具有可写性(writeable atribute)。如果属性同时具有getter和setter方法,那么它是一个读/写属性。如果它只有getter方法,那么它是一个只读属性,如果它只有setter方法,那么它是一个只写属性。读取只写属性总是返回undefined。
    存取器属性定义语法也比较简单,函数定义没有使用function关键字,而是使用get或set:

var o = {
//普通的数据属性
data_prop: 1,
//存取器属性都是成对定义的函数
get accessor_prop(){/* 这里是函数体 */},
set accessor_prop(value){}
};

思考下面这个表示2D笛卡尔点坐标的对象。它有两个普通属性x和y分别表示x坐标和y坐标,它还有两个等价的存取器属性用来表示点的极坐标:

var p = {
//x和y是普通的可读写数据属性
x: 1.0,
y: 1.0,
//r是可读写的存取器属性,它有getter和setter
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);
var ratio = newValue / oldValue;
this.x *= ratio;
this.y *= ratio;
},
//theta是只读存取器属性,只有getter方法
get theta() { return Math.atan2(this.y, this.x); }
};

和数据属性一样,存取器属性是可以继承的,因此可以将上述代码中的p对象当做另一个“点”的原型。可以给性对象定义它的x和y属性,但r和theta属性继承而来:

var q = inherit(p);
q.x = 1, q.y = 1;
console.log(q.r);
cosole.log(q.theta);

3.2 属性特性

我们可以将存取器属性的getter和setter方法看成属性的特性。按照这个逻辑,我们也可把属性的值同样看着属性的特性。因此,可以认为一个属性包含一个名字和4个特性。
    数字属性的4个特性分别是它的值(value)、可写性(writeable)、可枚举性(enumerable)和可配置型(configurable)。
    存取器属性不具有值(value)特性和可写性,因此包含:读取(get)、写入(set)、可枚举性、可配置性。

ES5定义了一个名为“属性描述符”的对象,这个对象代表那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.getOwnProeprtyDescriptor({x: 1},"x");
//查询上文中定义的random对象的octet属性
//返回{get: /*func */, set: undefined, enumerable: true, configurable: true}
Object.getOwnPropertyDesciptor(random, "octet"); //对于继承属性和不存在属性,返回undefined
Object.getOwnPropertyDesciptor({}, "x");
Object.getOwnPropertyDesciptor({}, "toString");

从函数名就可以看出,Object.getOwnPropertyDesciptor()只能得到自有属性的描述符。要想获得继承属性的特性,需要遍历原型链(Object.getPrototypeOf())。

想要设置属性的特性,或者让新建属性具有某些特性,则需要调用Object.defineProperty(),包含三个参数:对象、属性名、属性描述符对象:

// 属性是存在的,但不可枚举
o.x; //=> 1
Object.keys(o) //=> []
//现在对属性x做修改,让它变成只读
Object.defineProperty(o, "x", {writable: true });
//视图更改这个属性的值
o.x = 2; //操作失败但不报错,而在严格模式中抛出类型错误异常
//属性依然是可配置的,因此可通过这种方式对它进行修改:
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: 2, writable: true, enumerable: true, configurable: true},
r: {
get: function(){ return Math.sqrt(this.x * this.x + this.y * this.y); },
enumerable: true,
configurable: true
}
});

getter和setter的老式API: 在ES5采纳之前,大多数Javascript的实现已经可以支持对象直接量语法中get和set写法。这些实现提供了非标准的老式API用来查询和设置getter和setter。这些API由四个方法组成,所有对象都拥有这些方法。

__lookupGetter__()和__lookupSetter__()用以返回一个命名属性的getter和setter方法。
    __defineGetter__()和__defineSetter__()用以定义getter和setter,第一个参数是属性名字,第二个参数是getter和setter方法。

var o = {};
o.__defineGetter__("x", function(){return 0;});
o.__defineSetter__("y", function(value){console.log("set value:" + value);});

4.对象的三个属性

每一个对象都有与之相关的原型(prototype)、类(class)、可扩展性(extensible attribute)。接下来讲述这些属性有什么作用。

4.1 原型属性

对象的原型属性是用来继承属性的,我们经常把“o的原型属性”直接叫做“o的原型”。在之前“创建对象”介绍了三种方式创建对象。通过对象直接量创建的对象使用Object.prototype作为它们的原型。通过new创建的对象使用构造函数的prototype属性作为它们的原型。通过Object.create()创建的对象使用第一个参数作为它们的原型。
    在ES5中,可通过Object.getPrototypeOf()查询对象原型。在ES3中,没有与之等价的函数,而是使用表达式o.constructor.prototype检查对象的原型。
    要想检测一个对象是否是另一个对象的原型(或处于原型链中),使用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

Mozilla实现的Javascript对外暴露了一个专门命名为__proto__属性,用以直接查询/设置对象原型。但IE和Opera不支持__proto__属性,所以不建议直接使用__proto__属性。

4.2 类属性

对象的类属性是一个字符串,用以表示对象的类型信息。ES3和ES5都为提供设置这个属性的方法,只有一种间接方式查询它。默认的toString()方法返回这种格式的字符串:[object class]。
    可通过调用toString()方法,然后提取已返回字符串的第八个到倒数第二个位置之间的字符。但有个麻烦是,很多对象继承的toString()方法重写了,为了能够调用正确的toString()版本,必须间接调用Function.call()方法。下面例子的classof函数可返回任意对象的类:

function classof(o){
if(o === null) return "Null";
if(o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8, -1);
}

4.3 可扩展性

对象的可扩展性用以表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显式可扩展的。在ES5中,可将对象转换为不可扩展的。
    Object.seal()方法除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。也就是说,不能给对象添加新属性,而且已有属性也不能删除和配置。
    Object.isSealed()方法用来检测对象是否封闭。
    Object.freeze()方法将更严格的锁定对象,除了拥有Object.seal()方法的功能外,还可以将自有的所有数据属性设置为只读(如果对象的存取器属性有setter方法,存取器属性不受影响, 仍可以通过给属性赋值调用它们)。
    Object.isFrozen()用来检测对象是否冻结。

5.序列化对象

对象序列化是指将对象的状态转换为字符串,也可以将字符串还原为对象。ES5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原Javascript对象。这些方法都使用JSON作为数据交换格式。例如:

o = {x: 1, y: {z: [false, null, ""]}}; //定义一个测试对象
s = JSON.stringify(o); //{"x":1,"y":{"z":[false,null,""]}}
p = JSON.parse(s); //p是o的深拷贝

JSON的语法是Javscript语法的子集,它并不能表示Javascript里的所有值。支持对象、数组、字符串、无穷大数字、true、false和null,并且它们可以序列化和还原。NaN、Inifinity和-Inifinity序列化结果都是null。函数、RegExp、Error对象和undefined值不能序列化和还原。
    这里在附加说一下对象的方法:
    toString()方法:它将返回一个表示调用这个方法的对象值的字符串。很多对象都重写了toString()方法,比如Array.toString()、Date.toString()以及Function.toStrring()。
    toJSON()方法:Object.prototype实际上没有定义toJSON()方法,但由于需要执行序列化的对象来说,JSON.stringify()方法会调用toJSON()方法。如果在带序列化的对象中存在这个方法,则调用它。
    valueOf()方法:valueOf()方法和toString()方法非常相似,但往往Javascript需要将对象转换为某种原始值而非字符串的时候才调用它,尤其是转换为数字的时候。有些内置类自定义了valueOf()方法,比如,Date.valueOf()。

如果本篇内容对大家有帮助,请点击页面右下角的关注。如果觉得不好,也欢迎拍砖。你们的评价就是博主的动力!下篇内容,敬请期待!

上一篇:UVA1601-The Morning after Halloween(双向BFS)


下一篇:Java基础之异常