JavaScript 对象

对象

语法

对象可以通过两种形式定义:声明(文字)形式和构造形式。

文字形式:
var myObj = {
    key: value
    // ...
};
构造形式:
var myObj = new Object();
myObj.key = value;

构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字形式中可以添加多个键值对,但是在构造形式中必须逐个添加属性。

类型

对象是 JavaScript 的基础。

简单基本类型:
  • string
  • number
  • boolean
  • null
  • undefined
  • object

【注意】:

  1. 简单基本类型本身并不是对象。
  2. null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug。实际上,null 本身是基础类型。

【关于注意中的第二点的原理】:
不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。

【错误纠正】:常见的错误说法“JavaScript 中万物皆是对象”。实际上,JavaScript 中有许多特殊的对象子类型,可以称之为复杂基本类型。函数就是对象的一个子类型,从技术角度来说是“可调用的对象”。在 JavaScript 中,函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数。数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂一些。

内置对象

JavaScript 中还有一些对象子类型,通常被称为内置对象。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。但是在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。

【示例】:

var str = "I am a string";
typeof str; // "string"
str instanceof String; // false

var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true

【解释】:
原始值 “I am a string”,是一个字面量,不是一个对象。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符串等,那需要将其转换为 String 对象。JavaScript 装箱操作会自动把字面量转换成一个 String 对象。

【建议】:
JavaScript 社区中的大多数人都认为能够使用文字形式时就不要使用构造形式。

【注意】:

  1. null 和 undefined 没有对应的构造形式,只有文字形式。
  2. Date 只有构造形式,没有文字形式。
  3. 对于 Objecy、Array、Function 和 RegExp 来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。在某些情况下,相比用文字形式创建对象,构造形式可以提供一些额外选项。由于这两种形式都可以创建对象,所以首选更简单的文字形式。建议只在需要那些额外选项时使用构造形式。
  4. Error 对象很少在代码中显式创建,一般是在抛出异常时自动创建。也可以使用 new Error() 这种构造形式来创建,不过一般来说用不着。

内容

对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。

【简述】:
需要强调的一点是,当我们说“内容”时,似乎在暗示这些值实际上被存储在对象内部,但是这只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。

属性访问

  • 属性访问:.操作符,例如 myObject.name。
  • 键访问:[]操作符,例如 myObject["a"]。

【区别】:
属性访问需要满足标识符的命名规范,而键访问可以接受任意 UTF-8/Unicode 字符串作为属性名。例如,如果要引用名称为“Super-star”的属性,就只能使用键访问,因为“super-star”不是一个有效的标识符属性名。

【注意】:
在对象中,属性名都是都是字符串。如果使用字符串以外的其他值作为属性名,那么首先该属性名会被转换为一个字符串。需要额外注意的一点,在数组下标中使用的是数字,在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。

var myObject = {};

myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"]; // "foo";
myObject["3"]; // "bar";
myObject["[object Object]"]; // "baz";

可计算属性名

键访问可以通过表达式来计算属性名。但是属性访问和文字形式声明对象则不行。

ES6 新增了可计算属性名,允许文字形式中使用 [] 包裹一个表达式来当作属性名。

【示例】:

var prefix = "foo";

// ES6
var myObject = {
    [prefix + "bar"]: "hello",
    [prefix + "baz"]: "world"
};
// ES6 之前
var myObject = {};
myObject[prefix + "bar"] = "hello";
myObject[prefix + "baz"] = "world";

myObject["foobar"]; // hello
myObject["foobaz"]; // world

属性与方法

【前言】:
如果访问的对象属性是一个函数,有些开发者喜欢使用不一样的叫法以作区分。由于函数很容易被认为是属于某个对象,在其他语言中,属于对象(也被称为“类”)的函数通常被称为“方法”,因此把“属性访问”说成是“方法访问”也就不奇怪了。有意思的是,JavaScript 的语法规范也作出了同样的区分。

【本书作者的见解】:
从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”似乎有点不妥。确实有些函数具有 this 引用,有时候这些 this 确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把一个函数变成一个“方法”,因为 this 是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。无论返回值是什么类型,每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别。

【举例】:

function foo() {
    console.log("foo");
}

var someFoo = foo; // 对 foo 的变量引用

var myObject = {
    someFoo: foo
};

var myObject2 = {
    bar: function() {
        console.log("bar");
    }
};

var someBar = myObject.bar;

【解释】:

  1. someFoo 和 myObject.someFoo 只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。唯一的区别就在于 myObject.someFoo 中的 this 会被隐式绑定到一个对象。
  2. 即使在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象。它们只是对于相同函数对象的多个引用。

数组

数组除了支持属性访问外,也支持键访问,并且有一套更加结构化的值存储机制(不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是非负整数。

数组也是对象子类型,因此可以给数组添加属性。

【示例】:

var myArray = ["foo", 42, "bar"];

myArray.baz = "baz";
console.log(myArray.length); // 输出:3
console.log(myArray.baz); // 输出:"baz"

【解释】:
给数组添加属性,不会影响数组的长度。这是因为 JS 引擎对数组和对象的行为和用途进行了相应的优化,虽然数组也是对象,但其拥有更复杂的行为与存储机制。所以最好只用对象来存储键值对,只用数组来存储数值下标值对。

【注意】:
如果向数组添加一个“看起来”像是数字的属性,该属性会成为数组的一个数值下标,因此会修改数组的内容而不是添加一个属性。

var myArray = ["foo", 42, "bar"];

myArray["3"] = "baz";
console.log(myArray.length); // 输出:4
console.log(myArray[3]); // 输出:"baz"

属性描述符

在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。但是从 ES5 开始,所有的属性都具备了属性描述符。

【示例】:

var myObject = {
    a: 2
};

console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
/**
 输出:
 {
    value: 2,
    writable: true,
    enumerable: true,
    configurable: true
 }
 */
  1. 在创建普通属性时属性描述符会使用默认值,即 writable: true, enumerable: true, configurable: true。
  2. 可以使用 Object.defineProperty() 添加一个新属性或者修改一个已有属性(前提是该属性的 configurable: true)的属性描述符。

【示例】:

var myObject = {};

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});

console.log(myObject.a); // 输出:2
1. Writable

writable 决定是否可以修改属性的值。

【示例】:

var myObject = {};

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写!
    configurable: true,
    enumerable: true
});

myObject.a = 3;
console.log(myObject.a); // 输出:2

// 案例二:
"use strict";
var myObject = {};

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写!
    configurable: true,
    enumerable: true
});

myObject.a = 3; // TypeError

【解释】:当 writable: false 时,对于属性值的修改静默(silently failed)失败。在严格模式下,会报 TypeError 错误。

2. Configurable

Configurable 决定属性是否可以配置。只要属性可配置,就可以使用 defineProperty() 方法来修改属性描述符。

【示例】:

var myObject = {
    a: 2
};

myObject.a = 3;
console.log(myObject.a); // 输出:3

Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置!
    enumerable: true
});

console.log(myObject.a); // 输出:4;
myObject.a = 5;
console.log(myObject.a); // 输出:5

Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
});
// TypeError

【解释】:

  1. 不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。因此,把 configurable 修改成 false 是单向操作,无法撤销!

【注意】:在 configurable: false 时,仍旧可以把 writable 的值从 true 修改为 false,但是无法从 false 修改为 true。

【示例】:configurable: false 还会禁止删除该属性。

var myObject = {
    a: 2
};

console.log(myObject.a); // 输出:2

delete myObject.a;
console.log(myObject.a); // 输出:undefined

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: false,
    enumerable: true
});

console.log(myObject.a); // 输出:2;
delete myObject.a;
console.log(myObject.a); // 输出:2

【注意】:delete 只用来直接删除对象(可删除)属性。如果对象的某个属性是某个对象/函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象/函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具,它就是一个删除对象属性的操作,仅此而已。

3. Enumerable

控制属性是否会出现在对象的属性枚举中,例如 for...in 循环。

如果把 enumerable 设置成 false,该属性就不会出现在枚举中,但是仍然可以正常地访问该属性。相对地,设置成 true 就会使其出现在枚举中。

【示例】:

var myObject = {
    a: 2,
    b: 3,
    c: 4
};

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: false,
    enumerable: false
});

for(item in myObject) {
    console.log(item);
}
// 输出:b、c

不变性

属性或者对象不可改变。

所有方法创建的对象或属性都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等),其他对象的内容不受影响,仍然是可变的。

1. 对象常量

结合 writable: false 和 configurable: false 可以创建一个真正的常量属性(不可修改、重定义或者删除)。

【示例】:

var myObject = {};

Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false,
    configurable: false
});
2. 禁止扩展

Object.preventExtensions() 禁止一个对象添加新属性并且保留已有属性。

【示例】:

var myObject = {
    a: 2
};

Object.preventExtensions(myObject);

myObject.b = 3;
console.log(myObject.b); // 输出:undefined
3. 密封

Object.seal() 会创建一个“密封”的对象,禁止添加新属性,同时不允许配置已有属性。

【实质】:在现有对象上调用 Object.preventExtensions() 并把所有现有属性标记为 configurable: false。

【结果】:密封之后的对象不能添加新属性,也不能重新配置或者删除任何已有的属性,只能修改属性的值。

4. 冻结

Object.freeze() 会创建一个冻结对象,禁止添加新属性,不允许配置已有属性且修改属性的值。

【实质】:在现有对象上调用 Object.seal() 并把所有“数据访问”属性标记为 writable: false。

【注意】:当你深度冻结一个对象时(在一个对象上调用 Object.freeze(),然后遍历它引用的所有对象,并在这些对象上调用 Object.freeze()),一定要小心,因为这样做有可能会在无意中冻结其他共享对象。

[[Get]]

属性访问,例如 myObject.a,实际上在 myObject 对象中实现了 [[Get]] 操作。

【过程】:对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果有就返回该属性的值。如果没有找到,则沿着原型链继续找。若还是没有,则返回值 undefined。

【注意】:属性访问和变量访问不同。如果在词法作用域中访问一个不存在的变量,则会抛出 ReferenceError 异常,而不会像属性访问那样返回 undefined。

[[Put]]

给对象属性赋值不会触发 [[Put]] 操作来设置或者创建这个属性。[[Put]] 操作触发取决于诸多因素。

【过程】:
判断对象中是否已经存在这个属性(最重要的因素)。如果已经存在:(暂时不讨论不存在的处理过程)

  1. 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

Getter 和 Setter

  • getter:隐藏函数,会在获取属性值时调用。
  • setter:隐藏函数,会在设置属性值时调用。

【注意】:在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。

【访问描述符】:给一个属性定义 getter、setter 或者两者都有时。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get 以及 configurable 和 enumerable 特性。

【示例】:

var myObject = {
    // 给 a 定义一个 getter
    get a() {
        return 2;
    }
};

Object.defineProperty(myObject, "b", {
    // 给 b 设置一个 getter
    get: function() {
        return this.a * 2;
    }
    enumerable: true
});

console.log(myObject.a); // 输出:2
console.log(myObject.b); // 输出:4

【解释】:不管是对象文字形式中的 get a() {...},还是 defineProperty() 中的显式定义,二者都会在对象中创建一个不包含 value 的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。

【示例】:

var myObject = {
    // 给 a 定义一个 getter
    get a() {
        return 2;
    }
};

myObject.a = 3;
console.log(myObject.a); // 输出:2

【解释】:由于只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。而且即便有合法的 setter,由于我们自定义的 getter 只会返回 2,所以 set 操作是没有意义的。为了让属性更合理,还应当定义 setter。但需要注意,setter 会覆盖单个属性默认的 [[Put]] 操作。

存在性

判断对象属性是否存在的两个方法,in 操作符以及 hasOwnProperty() 方法。

【示例】:

var myObject = {
    a: 2
};

console.log("a" in myObject); // 输出:true
console.log("b" in myObject); // 输出:false

console.log(myObject.hasOwnPreperty("a")); // 输出:true
console.log(myObject.hasOwnPreperty("b")); // 输出:false

【解释】:

  • in 操作符:检查属性是否在对象以及原型链中。
  • hasOwnProperty():检查属性是否在对象中,不会检查原型链。

【其他】:
所有的普通对象都可以调用 hasOwnProperty()(基于 Object.prototype 的委托),但有的对象可能没有连接到 Object.property,例如 this 中所讲的“非常空”对象 Object.create(null),在这种情况下,调用 hasOwnProperty() 方法就会失败。此时,可以使用更加强硬的方法来进行判断,Object.prototype.hasOwnProperty.call(myObject, "a")。

【注意】:
in 操作符检查的是对象、数组的属性名而非属性值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,348评论 0 5
  • 作者:clearbug原文地址:http://www.cnblogs.com/craftsman-gao/p/48...
    IT程序狮阅读 768评论 1 8
  • 什么是对象 可以使用两种形式来创建对象: 声明形式和构建形式。 js 数据类型 string number boo...
    hhooke阅读 277评论 0 0
  • 在前面我们看到过关于函数调用的位置不同this所绑定的对象也不同,但是对象是何物呢? 语法 对象的定义可以通过声明...
    FeRookie阅读 285评论 0 5
  • 为什么要读皮亚杰?我知道皮亚杰是瑞士著名的心理学家,是“发生认识论”的创始人,研究儿童认知发展,也是南明数学(我的...
    原晓丹阅读 6,428评论 1 9