javascript 核心语言笔记 6 - 对象

对象是 JavaScript 的基本数据类型。是一种复合值:将不少值聚合在一块儿。对象能够看作是无序集合,每一个属性都是一个名/值对。这种基本数据结构还有不少叫法,好比「散列」(hash)、「散列表」(hashtable)、「字典」(dictionary)、「关联数组」(associative array)。JavaScript 还能够从一个称为 原型 的对象继承属性git

JavaScript 对象是动态的 —— 能够新增属性也能够删除属性,除了字符串、数字、布尔值、null 和 undefined 以外,JavaScript 中的值都是对象github

对象是可变的,经过引用操做对象原对象也会受到影响json

属性包括名字和值。名字是能够包含空字符串在内的 任意字符串,值能够是任意 JavaScript 值,或者(在 ECMAScript 5中)能够是一个 getter 或者 setter (或都有),每一个属性还有一些与之相关的值称为「属性特性」(property attribute):api

  • 可写(writable)
  • 可枚举(enumerable)
  • 可配置(configurable),代表是否能够删除或者修改

ECMAScript 5 以前,经过代码给对象建立的全部属性都是可写、可枚举和可配置的数组

除了包含属性以外,每一个对象还拥有三个相关的对象特性(object attribute):浏览器

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

下面这些术语用来区分三类 JavaScript 对象和两类属性:数据结构

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

建立对象

对象直接量

 1 var empty = {}
 2 var point = { x:0, y:0 }
 3 var point2 = { x:point.x, y:point.y + 1 }
 4 var book = {
 5     "main title": "JavaScript",
 6     "for": "all audiences",
 7     author: {
 8         firstname: "David",
 9         surname: "Flanagan"
10     }
11 }

在 ECMAScript 5 中,保留字能够用作不带引号的属性名。而后对于 ECMAScript 3 来讲,使用保留字做为属性名必须使用引号引发来。ECMAScript 5 中属性最后一个逗号会被忽略,但在 IE 中则报错dom

经过 new 建立对象

new 运算符建立并初始化一个新对象。new 后跟随一个函数调用。这里的函数称作构造函数(constructor),用来初始化一个新建立的对象。JavaScript 语言核心的原始类型都包含内置构造函数(另外一方面也证明了 JavaScript 中一切皆对象)函数

1 var o = new Object();
2 var a = new Array();
3 var d = new Date();
4 var r = new RegExp('js');

原型

每个 JavaScript 对象(null 除外)都和另外一个对象相关联,这个对象就是「原型」,每个对象都从原型继承属性工具

经过 new 建立的对象原型就是构造函数的 prototype 属性值,经过 new Object() 建立的对象也继承自 Obejct.property

没有原型对象的为数很少,Obejct.prototype 就是其中之一。它不继承任何属性,普通对象都具备原型。全部的内置构造函数都具备一个继承自 Object.prototype 的原型。例如,Date.prototype 的属性继承自 Object.prototype,所以由 new Date() 建立的 Date 对象的属性现时继承自 Date.prototype 和 Object.prototype,这一系列连接的原型对象就是所谓的「原型链」(prototype chain)

Object.create()

ECMAScript 5 定义了一个名为 Obejct.create() 的方法,用来建立一个新对象,其中第一个参数是这个对象的原型,第二个可选参数用来对对象的属性进行进一步描述,Object.create() 是一个 静态函数,不是提供给对象调用的方法

1 var o1 = Object.create({ x:1, y:2 });       // o1 继承了属性 x 和 y
2 var o2 = Obejct.create(null);               // o2 不继承任何属性和方法

在 ECMAScript 3 中能够用相似代码来模拟原型继承:

 1 function inherit(p) {
 2     if (p == null) throw TypeError();
 3     if (Object.create) return Object.create(p);
 4 
 5     var t = typeof p;
 6     if (t !== "object" && t !== "undefined") throw TypeError();
 7 
 8     function f() {}
 9     f.prototype = p;
10 
11     return new f();
12 }
13 
14 var o = { x: "test o" }
15 
16 var c = inherit(o);
17 
18 c.x = "test c";
19 
20 console.log(c.x);       // => "test c"
21 console.log(o.x);       // => "test o"

属性的查询和设置

1 var author = book.author;           // 取得 book 的 author 属性
2 var title = book["main title"];     // 使用 [] 访问属性时 [] 内必须是一个计算结果为字符串的表达式
3 
4 book.edition = 6;                   // 给 book 建立一个名为 edition 的属性,「.」号运算符后的标识符不能是保留字

做为关联数组的对象

当经过 [] 来访问对象属性时,属性名经过字符串来表示。字符串是 JavaScript 的数据类型,在程序运行时能够修改建立它们。所以,能够在 JavaScript 中使用下面这种代码来动态添加/查找属性:

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

继承

假设要查询对象 o 的属性 x,若是 o 中不存在 x,那么将会继续在 o 的原型对象中查询属性 x。若是原型对象中也没有 x,但这个原型对象还有原型,那么继续在这个原型对象的原型上执行查找,直到找到 x 或者找到一个原型是 null 的对象为止。能够看出来,原型的属性构成了一个「连接」,经过这个「链」能够实现属性的继承

 1 var o = {}
 2 o.x = 1;
 3 
 4 var p = inherit(o);
 5 p.y = 2;
 6 
 7 var q = inherit(p);
 8 q.z = 3;
 9 
10 var s = q.toString();   // => "[object Object]"
11 q.x + q.y               // => 3

属性访问错误

属性访问并不老是返回或设置一个值,下页场景给对象 o 设置 属性 p 会失败:

  • o 中的属性 p 是只读的(defineProperty() 方法中有一个例外,能够对可配置的只读属性从新赋值)
  • o 中不存在自有属性 p:o 没有使用 setter 方法继承属性 p,而且 o 的可扩展性(extensible attribute)是 false。若是 o 中不存在 p,并且没有 setter 方法可供调用,则 p 必定会添加至 o 中。若是 o 不是可扩展的,那么在 o 中不能定义新的属性

删除属性

使用 delete 运算符能够删除对象的属性,delete 运算符只能删除 自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,并且这会影响到全部继承自这个原型的对象)

若是删除成功或者删除了一个没有影响的值(不存在的属性),delete 表达式返回 true。当 delete 运算符的操做数不是一个对象的属性的时候也返回 true

 1 var o = { x: 1 }
 2 delete o.x;             // => true
 3 delete o.x;             // => true x 并不存在
 4 delete o.toString;      // => true toString 是继承属性
 5 delete 1                // => true 不是对象属性
 6 this.b = 1;
 7 delete b;               // => true 删除全局对象上的变量 b
 8 
 9 delete Object.property  // => false
10 var x = 1;
11 delete this.x;          // => false 不能删除这个属性,由于是经过 var 声明的
12 function f() {}
13 delete f                // => false 不能删除全局函数

检测属性

能够经过 in 运算符、hasOwnProperty() 方法和 propertyIsEnumerable() 方法来检测对象是否存在某属性,propertyIsEnumerable 只有检测到是自有属性且这个属性的可枚举性为 true 时它才返回 true

 1 var o = { x: 1 };
 2 "x" in o;                          // => true
 3 "y" in o;                          // => false
 4 "toString" in o                    // => true
 5 
 6 o.hasOwnProperty("x")              // => true
 7 o.hasOwnProperty("y")              // => false
 8 o.hasOwnProperty("toString")       // => false
 9 
10 var o = inherit({ y: 2});
11 o.x = 1;
12 o.propertyIsEnumerable("x")        // => true
13 o.propertyIsEnumerable("y")        // => false
14 o.propertyIsEnumerable("toString") // => false

还能够经过判断属性是不是 undefined 来模拟 in 运算符

1 o.x !== undefined;                 // => true
2 o.y !== undefined;                 // => false
3 o.toString !== undefined;          // => true

然而有一种场景只能使用 in 运算符而不能经过只判断 undefined 的方式。in 能够区分不存在的属性和存在但值为 undefined 的属性

1 var o = { x: undefined }
2 o.x !== undefined           // => false 存在 x,只是值为 undefined
3 o.y !== undefined           // => false
4 "x" in o                    // => true
5 "y" in o                    // => false
6 delete o.x                  // => true
7 "x" in o                    // => false delete 后 o 彻底不存在了

枚举属性

许多工具库给 Object.prototype 添加了新的方法或者属性(一般不建议这么作),这些方法和属性能够被全部对象继承并使用。然而在 ECMAScript 5 标签以前,这些添加的方法是 不能定义为不可枚举的,所以它们均可以在 for/in 循环枚举出来。为了不这和践状况,须要过滤 for/in 循环返回的属性,下面两种方法是最多见的:

1 Object.prototype.test = 1;
2 var o = { a: 1, b:2, c: function() {} };
3 for (p in o) {
4     if (!o.hasOwnProperty(p)) continue;
5     console.log(p);
6 }
7 for (p in o) {
8     if (typeof o[p] === "function") continue;
9 }

除了 for/in 循环以外,ECMAScript 5 定义了两个用以枚举属性名称的函数。第一个是 Object.keys(),它返回一个数组,由对象中的 可枚举的自有属性名称 组成,第二个是 Object.getOwnPropertyNames(),它和上面的方法相似,只是它返回对象的 全部自有属性名称,不只仅是可枚举的属性

属性 getter 和 setter

在 ECMAScript 5 中,属性的值能够用一个或两个方法替代,这两个方法就是 getter 和 setter。由它们定义的属性称作「存取器属性」(accessor property),不一样于「数据属性」(data property),数据属性只有一个简单的值

当程序查询存取器属性的值时,JavaScript 调用 getter 方法(无参数)。这个方法返回属性的存取表达式值。当程序设置一个存取器属性的值时,调用 setter 方法,将赋值表达式右侧的值当作参数传入 setter。从某种意义上讲,这个方法负责「设置」属性值。能够忽略 setter 方法的返回值

使用存取器属性写入的属性不具备可写性(writable)。若是属性同时具备 getter 和 setter 方法,那么它是一个读/写属性。哪果它只有 getter 方法,那么它是一个只读属性。若是只有 setter 方法,那么它是一个只写属性,读取只写属性老是返回 undefined

 1 var p = {
 2   x: 1.0,
 3   y: 1.0,
 4 
 5   get r() {
 6     return Math.sqrt(this.x*this.x + this.y*this.y);
 7   },
 8   set r(newValue) {
 9     var oldValue = Math.sqrt(this.x*this.x + this.y*this.y);
10     var ratio = newValue/oldValue;
11 
12     this.x *= ratio;
13     this.y *= ratio;
14   },
15   get theta() {
16     return Math.atan2(this.y, this.x)
17   }
18 };
19 p.r             // => 1.4142135623730951

属性的特性

除了包含名字和值以外,属性还包含一些标识它们可写、可枚举和可配置的特性。ECMAScript 3 程序建立的属性都是可写、可枚举、可配置的,且没法对这些特性作出修改。ECMAScript 5 中却提供了查询和设置这些属性鹅的 API,这些 API 对于库的开发者来讲很是重要,由于:

  • 能够经过这些 API 给原型对象添加方法,并将它们设置成不可枚举的,让它们看起来更像内置方法
  • 能够经过这些 API 给对象定义不修改或删除的属性借此「锁定」这个对象

数据属性 的 4 个属性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置性(configurable)

存取器属性 不具备值(value)特性和可写性,它们的可写性是由 setter 方法存在与否决定,所以存取器属性的 4 个特性是读取(get)、写入(set)、可枚举性和可配置性

为了实现属性特性的查询和设置操做,ECMAScript 5 中定义了一个名为「属性描述符」(property descriptor)的对象,这个对象表明那 4 个特性。描述符对象的属性和它们所描述的属性特性是同名的。所以,数据属性的描述符对象的属性有 value, writable, enumerable 和 configurable。存取器属性描述符对象则用 get, set 属性代替 value, writable。其中 writable、enumerable 和 configurable 都是布尔值,get、set 都是函数值

经过调用 Object.getOwnPropertyDescriptor() 能够得到某个对象特定属性的属性描述符

 1 // => {value: 1, writable: true, enumerable: true, configurable: true}
 2 Object.getOwnPropertyDescriptor({ x: 1}, "x")
 3 var random = {
 4     get octet() {
 5         return Math.floor(Math.random() * 256)
 6     },
 7     get uint16() {
 8         return Math.floor(Math.random() * 65536)
 9     },
10     get int16() {
11         return Math.floor(Math.random() * 65536 - 32768)
12     }
13 }
14 // => {set: undefined, get: function, enumerable: true, configurable: true}
15 Object.getOwnPropertyDescriptor(random, "octet")
16 // => undefined
17 Object.getOwnPropertyDescriptor({}, "x")

从函数名字就能够看出来 Object.getOwnPropertyDescriptor() 只能获得自有属性的描述符。继承属性的特性须要遍历原型链

要想设置属性的特性,或者让新建属性具备某种特性,则须要调用 Object.defineProperty(),传入要修改的对象、要建立或者修改的属性的名称之前属性描述符对象:

 1 var o = {};
 2 Object.defineProperty(o, "x", {
 3     value: 1,
 4     writable: true,
 5     enumerable: false,
 6     configurable: true
 7 });
 8 // x 属性存在但不可枚举
 9 Object.keys()       // => []
10 
11 Object.defineProperty(o, "x", { writable: false })
12 o.x = 2             // 试图更改这个属性的值,会操做失败不报错,严格模式中则抛出类型错误异常
13 o.x                 // => 1
14 
15 // 将 x 从数据属性修改成存取器属性
16 Object.defineProperty(o, "x", { value: 2 })
17 Object.defineProperty(o, "x", { get: function() { return 0} }
18 o.x                 // => 0

传入 Object.defineProperty() 的属性描述符对象 没必要 包含全部 4 个特性。对于建立属性来讲,默认的特性值是 false 或 undefined。对于修改的已有属性来讲,默认的特性值没有作任何修改。注意,这个方法要么修改已有属性要么新建自胡属性,但 不能修改继承属性,想要同时修改或者建立多个属性则须要使用 Object.defineProperties(),使用方法能够参考 MDN 相关 api

对于那些不容许建立或者修改的属性来讲,若是用 Object.defineProperty() 对其操做就会抛出类型错误异常,好比给一个不可扩展的对象新增属性就会抛出类型错误异常。可写性控制着对特定值特性的修改,可配置性控制着对其它特性的修改,使用的时候如下状况会抛出类型错误异常:

  • 若是对象是不可扩展的,则能够编辑已有的自有属性,但不能给它添加新属性
  • 若是属性是不可配置的,则不能修改它的可配置性和可枚举性
  • 若是存取器属性是不可配置的,则不能修改其 getter 和 setter 方法,也不能将它转换为数据属性
  • 若是数据属性是不可配置的,则不能将它转换为存取器属性
  • 若是数据属性是不可配置的,则不能将它的可写性从 false 修改成 true,但能够从 true 修改成 false
  • 若是数据属性是不可配置且不可写的,则不能修改它的值,然而 可配置但不可写的属性值是能够修改的
     1 // 给 Object.prototype 添加一个不可枚举的 extend() 方法
     2 // 这个方法继承自调用它的对象,将做为参数什入的对象属性都复制
     3 Object.defineProperty(Object.prototype, "extend", {
     4     writable: true,
     5     enumerable: false,
     6     configurable: true,
     7     value: function(o) {
     8         var names = Object.getOwnPropertyNames(0);
     9 
    10         for (var i = 0, l = names.length; i < l; i++) {
    11             if (names[i] in this) continue;
    12 
    13             var desc = Object.getOwnPropertyDescriptor(o, name[i]);
    14             Object.defineProperty(this, names[i], desc)
    15         }
    16     }
    17 });

    getter 和 setter 的老式 API

    在ECMAScript 5标准被采纳以前,大多数 JavaScript 的实现(IE 除外)已经能够支持对象直接量语法中的 get 和 set 写法。这些实现提供了非标准的老式 API 用来查询和设置 getter 和 setter。这些 API 由 4 个方法组成,全部对象都拥有这些方法。__lookupGetter__() 和 __lookupSetter__() 用以返回一个命名属性的 getter 和 setter 方法,__defineSetter__() 和 __defineGetter__() 用以定义 getter 和 setter

    对象的三个属性

    每一个对象都胡与之相关的 原型(prototype)、(class)和 可扩展性(extensible attribute)

    原型属性

    原型属性是在实例对象建立之初就设置好的,ECMAScript 5 中,对象做为参数传入 Object.getPrototypeOf() 能够查看它的原型,在 ECMAScript 3 中,则没有与之等价的函数,但常用表达式 o.constructor.prototype 来检测一个对象的原型。经过 new 表达式建立的对象,一般继承一个 constructor 属性,这个属性指代建立这个对象的构造函数

    要想检测一个对象是不是另外一个对象的原型(或者处于原型链中),请使用 isPrototypeOf() 方法,这个方法和 instanceof 运算符很是相似,例如:

    1 var p = { x:1 };
    2 var o = Object.create(p);
    3 p.isPrototypeOf(o)                  // => true
    4 Object.prototype.isPrototypeOf(o)   // => true

    类属性

    对象的类属性是一个字符串,用以表示对象的类型信息。ECMAScript 3⁄5 都未提供设置这个属性的方法,并只有一种间接的方法能够查询它。默认的 toString() 方法(继承自 Object.prototype),返回了以下这种格式的字符串:

    [object class]

    因此能够经过 toString() 方法返回的字符串截取处理取到 class 名,不过不少对象继承的 toString() 方法被重写了,为了能调用正确的 toString() 版本,必须间接地调用 Function.call() 方法

     1 function classof(o) {
     2     if (o === null) return "Null";
     3     if (o === undefined) return "Undefined";
     4     return Object.prototype.toString.call(o).slice(8, -2);
     5 }
     6 classof(null)     // => "Null"
     7 classof(1)        // => "Number"
     8 classof("")       // => "String"
     9 classof(true)     // => "Boolean"
    10 classof({})       // => "Object"
    11 classof([])       // => "Array"
    12 classof(/./)      // => "Regexp"
    13 classof(new Date) // => "Date"
    14 function f() {}
    15 classof(new f())  // => "Object"

    可扩展属性

    可扩展性用以表示是否能够给对象是添加新属性。全部内置对象和自定义对象都是显式可扩展的,宿主对象的可扩展属性是由 JavaScript 引擎定义的,ECMAScript 5 中,全部的内置对象和自定义对象都是可扩展的,除非将它们转换为不可扩展的,宿主对象的可扩展性也是由实现 ECMAScript 5 的 JavaScript 引擎定义的

    ECMAScript 5 定义了用来查询和设置对象可扩展性的函数:Object.isExtensible(),若是将对象转换为不可扩展的,须要调用 Object.preventExtensions(),不过一量旦将对象转换为不可扩展的,就没法再转换回去了。

    Object.seal() 和 Object.preventExtensions() 相似,除了能将对象设置为不可扩展的,还能够将对象的全部自有属性都设置为不可配置的,也就是说不能给对象添加新的属性,已有的属性也不能删除或配置,已封闭(sealed)的对象是不能解封的,可使用 Object.isSealed() 来检测对象是否封闭

    Object.freeze() 将更严格地锁定对象 —— 「冻结」,它还能够将它自有的全部数据属性设置为只读,可使用 Object.isFrozen() 来检测对象是否冻结

    序列化对象

    对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5 提供了内置函数 JSON.stringify 和 JSON.parse() 用来序列化和还原 JavaScript 对象。这些方法都使用 JSON 做为数据交换格式,JSON的全称是「JavaScript Object Notation」—— JavaScript 对象表示法,正如其名,它的语法和 JavaScript 对象与数组直接量的语法很是相近

    ECMAScript 3 环境中能够引用 json2 类库来支持这两个序列化函数

    JSON 语法是 JavaScript 语法的子集,它并不能表示 JavaScript 里的全部值,函数、RegExp、Error 对象和 undefined 值不能序列化和不肯。JSON.stringify() 只能序列化对象可枚举的自有属性,关于 JSON 对象更多 API 能够参考 JSON.stringify

    对象方法

    toString() 方法

    toString() 方法没有参数,在须要将对象转换为字符串的时候,JavaScript 都调用这个方法

    1 var s = { x: 1, y: 1 }
    2 s.toString();       // => "[object Ojbect]"

    toLocaleString() 方法

    返回一个对象的本地化字符串。Object 中默认的 toLocaleString() 方法并不作任何本地化自身操做,它仅调用 toString() 方法并返回值。Date 和 Number 类对 toString() 方法作了定制,能够用它对数字、日期和时间作本地化的转换

    toJSON() 方法

    Object.prototype 实际上不有定义 toJSON() 方法,但对于须要执行序列化的对象来讲,JSON.stringify() 方法会调用 toJSON() 方法,若是存在则调用它,返回值便是序列化的结果,而不是原始对象,参见 Date.toJSON

    valueOf() 方法

    valueOf() 和 toString() 方法很是相似,但每每当 JavaScript 须要 将对象转换为某种原始值而非字符串 的时候才会用到它,尤为是转换为数字的时候。若是在须要使用原始值的上下文中使用了对象,JavaScript 就会自动调用这个方法,一样有些内置类自定义了 valueOf() 方法,好比 Date.valueOf

相关文章
相关标签/搜索