【书名】:你不知道的JavaScript(上卷)
【作者】:Kyle Simpson
【本书总页码】:213
【已读页码】:130
1. 定义方式
声明形式:var myObj = {key: value, ...};
构造形式:var myObj = new Object(); myObj.key = value; ...
2. 6种基本类型
string, number, boolean, null, undefined, object
简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型。
实际上,JavaScript 中有许多特殊的对象子类型,可以称之为复杂基本类型。
函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数(比如当作另一个函数的参数)。
数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂一些。
3. 内置对象
JavaScript 中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。
String,Number,Boolean,Object,Function,Array,Date,RegExp,Error
在 JavaScript 中,它们只是一些内置函数。这些内置函数可以当作构造函数(由 new 产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
var strPrimitive = "I am a string"; typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" ); typeof strObject; //"object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
简单来说,可以认为子类型在内部借用了 Object 中的 toString() 方法。从代码中可以看到,strObject 是由 String 构造函数创建的一个对象。
原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。幸好,在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说并不需要显式创建一个对象。
null 和 undefined 没有对应的构造形式,它们只有文字形式。相反,Date 只有构造,没有文字形式。
对于 Object、Array、Function 和 RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。在某些情况下,相比用文字形式创建对象,构造形式可以提供一些额外选项。由于这两种形式都可以创建对象,所以首选更简单的文字形式。建议只在需要那些额外选项时使用构造形式。
Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 newError(..) 这种构造形式来创建,不过一般来说用不着。
4. 对象内容属性
var myObject = {a: 2};
myObject.a; // 2
myObject["a"]; // 2
如果要访问 myObject 中 a 位置上的值,需要使用 . 操作符或者 [] 操作符。.a 语法通常被称为“属性访问”,["a"] 语法通常被称为“键访问”。实际上它们访问的是同一个位置,并且会返回相同的值 2。
这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [".."] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名。举例来说,如果要引用名称为 "Super-Fun!" 的属性,那就必须使用 ["Super-Fun!"] 语法访问,因为 Super-Fun! 并不是一个有效的标识符属性名。
此外,由于 [".."] 语法使用字符串来访问属性,所以可以在程序中构造这个字符串,比如说:
var myObject = {a:2};
var idx;
if (wantA) {idx = "a";}
//之后
console.log( myObject[idx] ); // 2
在对象中,属性名永远都是字符串。如果使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
4.1 可计算属性名
如果需要通过表达式来计算属性名,那么刚刚讲到的 myObject[..]这种属性访问语法就可以派上用场了,如可以使用 myObject[prefix + name]。但是使用文字形式来声明对象时这样做是不行的。
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
可计算属性名最常用的场景可能是 ES6 的符号(Symbol)。不过简单来说,它们是一种新的基础数据类型,包含一个不透明且无法预测的值(从技术角度来说就是一个字符串)。一般来说不会用到符号的实际值,所以通常接触到的是符号的名称,比如 Symbol.Something:
var myObject = {
[Symbol.Something]: "hello world"
}
4.2 属性与方法
如果访问的对象属性是一个函数,由于函数很容易被认为是属于某个对象,因此把“属性访问”说成是“方法访问”也就不奇怪了。
确实,有些函数具有 this 引用,有时候这些 this 确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把一个函数变成一个“方法”,因为 this 是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。
4.3 数组
数组也支持 [] 访问形式,不过就像之前提到过的,数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数。
数组也是对象,所以虽然每个下标都是整数,仍然可以给数组添加属性:
var myArray = [ "foo", 42, "bar" ];
myArray.baz ="baz";
myArray.length; // 3
myArray.baz; // "baz"
可以看到虽然添加了命名属性(无论是通过 . 语法还是 [] 语法),数组的 length 值并未发生变化。
注意:如果试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性):
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
4.4 复制对象
function anotherFunction() { /*..*/ }
var anotherObject = {c: true};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是复本!
c: anotherArray, // 另一个引用!
d: anotherFunction
};
anotherArray.push( anotherObject, myObject );
如何准确地表示 myObject 的复制呢?
首先,应该判断它是浅复制还是深复制。对于浅拷贝来说,复制出的新对象中 a 的值会复制旧对象中 a 的值,也就是 2,但是新对象中 b、c、d 三个属性其实只是三个引用,它们和旧对象中 b、c、d 引用的对象是一样的。对于深复制来说,除了复制 myObject 以外还会复制 anotherObject 和 anotherArray。这时问题就来了,anotherArray 引用了 anotherObject 和myObject,所以又需要复制 myObject,这样就会由于循环引用导致死循环。
对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
当然,这种方法需要保证对象是 JSON 安全的,所以只适用于部分情况。
相比深复制,浅复制非常易懂并且问题要少得多,所以 ES6 定义了 Object.assign(..) 方法来实现浅复制。Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owned key)并把它们复制(使用 = 操作符赋值)到目标对象,最后返回目标对象,就像这样:
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
注意:由于 Object.assign(..) 就是使用 = 操作符来赋值,所以源对象属性的一些特性(比如 writable)不会被复制到目标对象。
4.5 属性描述符
var myObject = {a:2};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
这个普通的对象属性对应的属性描述符(也被称为“数据描述符”,因为它只保存一个数据值)可不仅仅只是一个 2。它还包含另外三个特性:writable(可写)、enumerable(可枚举)和 configurable(可配置)。
在创建普通属性时属性描述符会使用默认值,我们也可以使用Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
使用 defineProperty(..) 给 myObject 添加了一个普通的属性并显式指定了一些特性。
1. Writable
writable 决定是否可以修改属性的值。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
对于属性值的修改静默失败(silently failed)了。如果在严格模式下,这种方法会出错:
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
TypeError 错误表示我们无法修改一个不可写的属性。
2. 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
最后一个 defineProperty(..) 会产生一个 TypeError 错误,不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。
注意: 把 configurable 修改成false 是单向操作,无法撤销! 有一个小小的例外:即便属性是 configurable:false,还是可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。
除了无法修改,configurable:false 还会禁止删除这个属性:
var myObject = {a:2};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
最后一个 delete 语句(静默)失败了,因为属性是不可配置的。
delete 只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象 / 函数就可以被垃圾回收。
3. Enumerable
从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in 循环。如果把 enumerable 设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。
用户定义的所有的普通属性默认都是 enumerable。但是如果不希望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false。
4.6 不变性
所有的方法创建的都是浅不变形,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的:
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]
假设代码中的 myImmutableObject 已经被创建而且是不可变的,但是为了保护它的内容myImmutableObject.foo,还需要使用下面的方法让 foo 也不可变。
1. 对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
2. 禁止扩展
如果想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..):
var myObject = {a:2};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
3. 密封
Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
4. 冻结
Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。
这个方法是可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像之前说过的,这个对象引用的其他对象是不受影响的)。
可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。