1)在javascript中,只有查询属性时才会体会到继承的存在,而设置属性则和继承无关,这使程序员可以有选择的覆盖继承的属性。
2)如果o继承了原型上的属性x,而这个属性是具有setter方法的存取器(accessor)属性,这时会调用setter方法而不是给o创建一个属性x。
注意: setter方法是由对象o调用的,而不是定义这个属性的原型调用的,因此setter定义任何属性,这个操作只针对o本身,而不会改变原型链。
3)属性访问并不总是返回或设置一个值,访问 null 或 undefined 的属性就会报错,为了预防这种报错,开发者们精简出一种有趣的写法: [obj] [&& obj.valueName][ && obj.valueName]
// book 是一个对象 if (book) { if (book.subtitle) len = book.subtitle.length; } // 与下方效果相同 var len = book && book.subtitle && book.subtitle.length;
4)给除 null 和 undefined 外的对象设置属性也不总是成功的,有部分属性是只读的,不能重新赋值、有一些对象不允许新增属性,不过意外的是这些设置属性的失败操作不会报错。
Object.prototype = 0; // 赋值失败,但没报错,Object.prototype 没有修改
这个BUG在ECMAScript 5 的严格模式已被修复
5)设置属性失败的规律总结:
- o中的属性p是只读的: 不能给只读属性重新赋值(defineProperty() 方法中有一个例外,可以对可配置的只读属性重新赋值)。
- o中的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性
- o中不存在自有属性p: o没有使用 setter 方法继承属性 p,并且 o 的可扩展性(extensible attribute)是false。如果o中不存在p,而且没有setter方法可供调用,则p一定会添加至o中。但如果o不是可拓展的,那么o中不能定义新属性。
6)javascript中的删除(delete)颇为有趣,它只是断开属性与所属对象的联系,而不是销毁该属性。
var a = { p: { x: 1 } }; b = a.p; delete a.p; 执行该端代码后,a.p 会返回 undefined ,但 b.x 的值依旧是 1。
由于已经删除的属性的引用依然存在,因此在javascript的某些实现中,可能因为这种不严谨的写法而造成内存泄漏,所以在销毁对象的时候,要遍历属性中的属性,依次删除。
7) 当delete表达式删除成功或没有任何副作用(比如删除不存在的属性时),它返回true, 如果delete后不是一个属性访问表达式,delete同样返回 true;
8) delete 不能删除那些可配置性为false的属性,但可以删除不可扩展对象的可配置属性。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性,在严格模式下,删除一个不可配置的属性会报一个类型错误。在非严格模式下,则会返回false;
非严格模式下:
this.x = 1;
delete x;
严格模式下:(必须显示的指定对象及其属性)
this.x = 1;
delete this.x;
9) 检查属性的方式:
in运算符 -- 左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性包含这个属性则返回 true。
hasOwnPreperty()方法 -- 检测给定的名字是否是对象的自有属性。对于继承属性他将返回 false
propertyIsEnumberable() -- 只有检查到是自有属性且这个属性的可枚举性是 true 时,它才返回 true。某些内置属性是不可枚举的。通常使用javascript代码创建的属性都是可枚举的,除非在ECMAScript 5 中使用一个特殊的方法来改变属性的可枚举性。
判断属性查询后的值是否为 undefined,这种方式不适用属性存在但值为 undefined 的情况。
10) 枚举属性的方式
for/in 循环 -- 可以在循环体中遍历对象所有可枚举的属性(包括自有属性可继承属性),把属性名称赋值给循环变量。
- 对象继承的内置方法是不可枚举的【非内置的方法是可以被枚举的】,但在代码中给对象添加的属性都是可枚举的(除非将它们转换为不可枚举的,)
- var a = Object.create( { x: 1 } ); a.y = 2; a.c = 3; for (str in a) { console.log(str); } // x y c
- 许多实用工具库给Object.prototype添加了新的方法或属性,这些方法和属性可以被所有对象继承并使用。在ECMAScript之前这些方法是不能被定义为不可枚举的,因此它们都可以在for/in循环中被枚举出来。
for ( p in o){
if (!o.hasOwnProperty(p))continue; // 跳过继承的属性
}
for (){
if (typeof o[p] === "function") continue; // 跳过方法
}
- ECMAScript 5 中定义了两个枚举属性名称的函数。第一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称构成。第二个是Object.getOwnPropertyNames(), 返回对象所有自有属性的名称而不仅仅是可枚举的属性。同样返回一个数组
11) 属性的特性
除了包含名字和值之外,属性还包含一些 标识它们可写、可枚举和可配置的特性。ES3中无法设置这些属性,所有使用js代码定义的属性都是可写、可枚举、可配置的,但ES5定义了查询和设置这些属性特性的API。
这里假设将getter和setter看做属性的特性,因此,可以认为一个属性包含一个名字和4个特性。数据属性的4个特性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置性(configruable)。
Object.getOwnPropertyDescriptor() -- 可以获得某个对象特定属性的属性描述符(自有属性):
// 返回 { value: 1, writable: true, enumerable: true, configurable: true } Object.getOwnPropertyDescriptor({x:1}, 'x'); // 查询上文中定义的randam对象的octet属性 // 返回 { get: /*func*/, set:undefined, enumerable: true, configurable: true } Object.getOwnPropertyDescriptor(random, 'octet'); // 对于继承属性和不存在的属性,返回undefined Object.getOwnPropertyDescriptor({}, 'x'); // undefined 没有这个属性 Object.getOwnPropertyDescriptor({}, 'toString'); // undefined, 继承属性
Object.defineProperty() -- 设置属性的特性,参数为 要修改的对象、要创建或修改的属性的名称以及属性描述符对象:
var o = {}; // 设置一个不可枚举属性,并赋值为1 Object.defineProperty(o, 'x', { value: 1, writable: true, enumerable: false, configurable: true, }); // 属性是存在的,但不可枚举 o.x; // => 1 Object.keys(o); // => [] // 现在对属性x做修改,让它变成只读 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.defineProperty()的属性描述符对象不必包含所有4个特性。对于新创建的属性,默认的特性值是false或undefined。对于修改的已有属性,默认的特性值没有做任何修改。
注意:这个方法要么修改已有属性要么新建自有属性,但不能修改继承属性。
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, }, });
任何对 Object.defineProperty() 或 Object.defineProperties() 违反规则的使用都会抛出类型错误异常:
- 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能添加新属性
- 如果属性是不可配置的,则不能修改它的可配置型和可枚举性
- 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能转换为数据属性
- 如果数据属性是不可配置的,则不能将它转换为存取器属性。
- 如果数据属性是不可配置的,则不能将它的可写性从false修改为 true,但可以从true修改为false。
- 如果数据属性是不可配置且不可写的,则不能修改它的值。然而可配置不可写属性的值是可修改的(操作流程是: 先将它标记为可写的,然后修改它的值,最后转换为不可写的)
12)对象的三个属性
每个对象都有与之相关的原型(prototype)、类(class)和可扩展性(extensible attribute)。
原型属性
对象的原型属性是用来继承属性的,我们经常把“o的原型属性”直接叫做“o的原型”。
Object.getPrototypeOf() -- 在ECMAScript 5 中,将对象作为参数传入可以查询它的原型.
isPropertyOf() -- 可以通过 p.isPropertyOf(o) 来检测p是否是o的原型
var p = {x: 1}; var o = Object.create(p); // 使用p作为原型创建一个对象 p.isPrototypeOf(o) // => true: o继承至p Object.prototye.isPrototyeOf(o) // => true: p 继承自 Object.prototype
在ECMAScript 3中,则没有与之等价的函数,但经常使用表达式o.constructor.prototype来检查一个对象的原型。通过 new 表达式创建的对象,通常会继承一个constructor属性,这个属性指代创建这个对象的构造函数。【注意: 这个方式检查对象原型并不可靠】
通过对象直接量或Object.create()方法创建的对象都包含一个名为constructor的属性,这个属性代指构造函数。因此,constructor.prototype才是对象直接量的真正的原型,但对于通过Object.create() 创建的对象则往往不是这样。???
类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ECMAScript 3 和 ECMAScript 5 都未提供设置这个属性的方法,并只有一种间接的方法可以查询它。默认的toString()方法(继承自Object.prototype)返回如下这种格式的字符串:
[object class]
因此想要获得对象的类,可以调用对象的 toString() 方法,然后提取已返回字符串的第8个到倒数第二个位置之间的字符。不过,很多对象继承的toString() 方法是被重写过的,为调用正确的 toString() 版本,必须间接地调用call() 方法。
// 在ECMAScript5中不需要对null 和 undefined做处理
function classof(o) { if (o === null) return "null"; if (o === undefined) return "undefined"; return Object.prototype.toString.call(o).slice(8, -1); }
通过内置构造函数(例如: Array、Date。。。)创建的对象包含“类属性”,它(此处的它应该指代对象包含的“类属性”???)与构造函数名称相匹配。
宿主对象也包含有意义的 “类属性”,但这和具体的Javascript实现有关。
通过对象直接量和 Object.create() 创建的对象的类属性是 “Object”,对于那些自定义构造函数创建的对象来说也一样,类属性是 “Object”,因此,对于自定义的对象,没办法通过自定义属性来区分对象的类。
classof(null) // => null classof(1) // => Number classof('') // => String classof(false) // => Boolean classof({}) // => Object classof([]) // => Array classof(/./) // => Regexp classof(new Date()) // => Date classof(window) // => Window 这是客户端宿主对象 function f() {} // 定义一个自定义的构造函数 classof(new f()) // => Object
可扩展性
对象的可扩展性表示是否可以给对象添加新属性。
在ECMAScript 5中,所有的内置对象和自定义对象都是显示可扩展的,除非将它们转换为不可扩展的。
宿主对象的可扩展性是由实现ECMAScript 5的Javascript引擎定义的。
Object.esExtensible() -- 判断对象是否可扩展
Object.prevertExtensions() -- 将对象转换为不可扩展的,方式:将待转换的对象作为参数传进去。注意:一旦将对象转换为不可扩展的,就无法再将其转换会可扩展的,此外,不可扩展性只会作用于该对象,假设给该对象的原型添加属性,这个对象仍然会继承原型上所添加的属性。
Object.seal() -- 封闭,将对象设置为不可扩展,同时将对象的属性设置为不可配置的。也就是说,不能给这个对象添加新属性,同时它已有的属性也不能删除或配置,不过它已有的可写属性依然可设置。
Object.isSealed() -- 封闭起来的对象是不可以解封的,只能使用该方法判断对象是否封闭
Object.freeze() -- “冻结(frozen)”,除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它自有的所有数据属性设置为只读(如果对象的存取器属性有setter方法,存取器属性将不受影响,仍可以通过给属性赋值调用它们)。
Object.isFrozen() -- 检测对象是否冻结
Object.preventExtensions()、Object.seal()和Object.freeze()都返回传入的对象,也就是说,可以通过函数嵌套的方式调用它们:
// 创建一个封闭对象,包括一个冻结的原型和一个不可枚举的属性 var o = Object.seal(Object.create(Object.freeze({x:1}), { y: { value: 2, writable: true } });
13)序列化对象
对象序列化(serialization)是指将对象的状态转换为字符串,也可以将字符串还原为对象。ECMAScript 5 提供了内置的方法 JSON.stringify() 和 JSON.parse() 用来序列化和还原Javascript对象。这些方法都使用JSON作为数据交换格式,JSON的全称是“Javascript Object Notation” —— JavaScript 对象表示法,它的语法和Javascript对象与数组直接量的语法非常相近。
o = {x:1, y:{z:[false, null, ""]}} // 定义一个测试对象 s = JSON.stringify(o); // s 是定义 '{"x":1, "y":{"z":[false, null, ""]} }' p = JSON.parse(s); // p是o的深度拷贝
JSON的语法是Javascript语法的子集,它并不能表示JavaScript里的所有值。
- 支持对象、数组、字符串、无穷大数字、true、false和null,并且它们可以序列化和还原。
- NaN、Infinity、-Infinity序列化的结果是 null。
- 日期对象序列化的结果是ISO格式的日期字符串(参照Date.toJSON() 函数),但JSON.parse()依然保留它们的字符串形态,而不是还原成原始日期对象。
- 函数、RegExp、Error对象和undefined值不能序列化和还原。
JSON.stringify() 只能序列化可枚举的自有属性。对于一个不能序列化的属性来说(例如: 属性值为函数时),在序列化后的输出字符串中会将这个属性省略掉。
JSON.stringify() 和 JSON.parse() 都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。
参考文献: JavaScript权威指南 第6版