前面已经涉及过很多关于对象的知识,那么对象到底是什么呢?
通俗来讲,对象就是一系列属性的方法的集合
前面已经涉及过,在对象,包装类里面讲过一种是字面量创建,一种是构造函数实例对象。唯一的区别就是在字面量创建时可以添加多个属性在里面,而构造函数则需要一个一个添加。我们绝大部分用的是文字创建
JavaScript有六种基本类型,这些类型本质上不是对象,JavaScript中有许多特殊的对象子类型,我们称之为
复杂基本类型,比如函数,数组等
JavaScript中还有一些对象子类型,通常被称为内置对象
基本包装类
-
String
-
Number
-
Boolean
引用类型
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置函数可以当做构造函数来使用,就是通过new操作符来创建实例
对于 Object、Array、Function 和 RegExp(正则表达式)来说,无论使用文字形式还是构 造形式,它们都是对象,不是字面量。
对象中的内容是一些存储在特定位置上的值组成的,在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度 来说就是引用)一样,指向这些值真正的存储位置。
var myObject = { a: 2 }; myObject.a; // 2 myObject["a"]; // 2
访问对象属性有俩种访问方式,.a 语法通常被称为“属性访问”,["a"] 语法通常被称为“键访问”。
这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [".."] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名。比如
[super-fun] .[super-fun] //error
这样不符合标识符规则的属性名只能通过键访问
在对象中,属性名永远都是字符串,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串
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"; var myObject = { [prefix + "bar"]:"hello", [prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world
可计算属性名最常用的场景应该是ES6的symbol,这里不做过多解释
我们常常将对象中的函数叫做方法,其实,从技术的角度来说,函数永远不会“属于”一个对象,当对象的属性中有方法是,属性访问返回的函数和其他函数没有任何的区别,况且,在this中,调用位置不同函数的执行也不同,不管从哪个方面看,函数都不可能属于对象,他只是对对象的一个引用。
数组也支持 [ ] 访问形式,不过就像我们之前提到过的,数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数
数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"
虽然给对象添加了属性,但是对象的长度仍然没有发生变化
如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
这个普通的对象属性对应的属性描述符不仅有value值,而且加入了writable(可写),enumerable(可枚举),configurable(可配置)
我们可以使用 Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置,举例来说
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
我们使用这个方法来添加了一个属性,同时添加了特性,当然我们一般给对象添加属性不会使用这种繁琐的方法,除非你想改变其特性
-
writable
writable决定是否可以修改属性的值
var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: false, // 不可写! configurable: true, enumerable: true } ); myObject.a = 3; myObject.a; // 2
这样,我们对属性的修改失败,如果在严格模式下还会出错(TypeError)
-
configurable
只要属性是可配置的,就可以通过defineProperty(..)方法来修改属性描述符
var myObject = { a:2 }; myObject.a = 3; myObject.a; // 3 Object.defineProperty( myObject, "a", { value: 4, writable: true, configurable: false, // 不可配置! enumerable: true } ); myObject.a; // 4 myObject.a = 5; myObject.a; // 5 Object.defineProperty( myObject, "a", { value: 6, writable: true, configurable: true, enumerable: true } ); // TypeError
不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。因此,把 configurable 修改成false 是单向操作,无法撤销!
注意:即便属性是 configurable:false,我们还是可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true,除了无法修改,configurable:false 还会禁止删除这个属性
-
enumberable
从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。
-
对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)
-
禁止扩展
如 果 你 想 禁 止 一 个 对 象 添 加 新 属 性 并 且 保 留 已 有 属 性, 可 以 使 用 Object.prevent Extensions(..):
var myObject = { a:2 }; Object.preventExtensions( myObject ); myObject.b = 3; myObject.b; // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错
-
密封
Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上隐式调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。(可以修改属性值)
-
冻结
Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。感觉这样的对象就是一个所谓的“活死人”了,不可操作,永生永世
yObject = { a: 2 }; myObject.a; // 2
一段简单的访问属性的代码,看起来是在对象里面寻找这个属性,其实他真的在对象里面寻找,不过有一个更详细的过程:
-
对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值
-
如果没有找到,遍历可能存在的 [[Prototype]] 链也就是原型链。
-
最终都没有找到[[Get]]操作符就会返回undefined
由于仅根据返回值无法判断出到底变量的值为 undefined 还是变量不存在,所以 [[Get]]操作返回了 undefined。不过稍后我们会介绍如何区分这两种情况。
[[Put]]相对比较容易理解,有得到就要有添加,而这又与描述符有不可分割的关系,[[Put]] 被触时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)
-
属性是否是访问描述符(参见 3.3.9 节)?如果是并且存在 setter 就调用 setter。
-
属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
-
如果都不是,将该值设置为属性的值。
如果对象中不存在这个属性,后续过程会更复杂,我们在后面详细介绍
用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。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 },
// 确保 b 会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是对象文字语法中的 get a() { .. },还是 defineProperty(..) 中的显式定义,二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数, 它的返回值会被当作属性访问的返回值
-
getter 是一种获得属性值的方法,setter是一种设置属性值的方法
-
getter负责查询值,它不带任何参数,setter则负责设置键值,值是以参数的形式传递,在他的函数体中,一切的return都是无效的
-
get/set访问器不是对象的属性,而是属性的特性,特性只有内部才用,因此在javaScript中不能直接访问他们,为了表示特性是内部值用两队中括号括起来表示如[[Value]]
-
对象属性分为访问器属性和对象属性
所以在使用getter时最好使用getter,如果不使用,可能会使getter无效
var myObject = { // 给 a 定义一个 getter get a() { return 2; } }; myObject.a = 3; myObject.a; // 2
无效
var myObject = { // 给 a 定义一个 getter get a() { return this._a_; }, // 给 a 定义一个 setter set a(val) { this._a_ = val * 2; } }; myObject.a = 2; myObject.a; // 4
有效
在前面说过,访问对象的属性返回undefined,可能是对象没有这个属性,也可能是属性没有值,那么怎样区分呢?
var myObject = { a:2 }; ("a" in myObject); // true ("b" in myObject); // false myObject.hasOwnProperty( "a" ); // true myObject.hasOwnProperty( "b" ); // false
in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。相比之下, hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。