语法
对象来自于两种形式:声明(字面)形式,和构造形式。
var myObj = { --> 字面形式key: value// ...};var myObj = new Object(); --> 构造形式myObj.key = value;
类型
JS 的六种主要类型(在语言规范中称为“语言类型”)中的一种:
stringnumberbooleannullundefinedobject
简单基本类型 (string、number、boolean、null、和 undefined)自身 不是 object。null有时会被当成一个对象类型,但是这种误解源自于一个语言中的 Bug,它使得 typeof null 错误地(而且令人困惑地)返回字符串 "object"。实际上,null 是它自己的基本类型
一个常见的错误论断是“JavaScript中的一切都是对象”。这明显是不对的
**
function是对象的一种子类型(技术上讲,叫做“可调用对象”)- 数组也是一种形式的对象,带有特别的行为
内建对象
StringNumberBooleanObjectFunctionArrayDateRegExpError
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
JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。因为必要时候基本类型会被强制转换为对应的内建对象
内容
对象的内容由存储在特定命名的 位置 上的(任意类型的)值组成,我们称这些值为属性
当我们说“内容”时,似乎暗示着这些值 实际上 存储在对象内部,但那只不过是表面现象。引擎会根据自己的实现来存储这些值,而且通常都不是把它们存储在容器对象 内部。在容器内存储的是这些属性的名称,它们像指针(技术上讲,叫 引用(reference))一样指向值存储的地方
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
为了访问 myObject 在 位置 a 的值,我们需要使用 . 或 [ ] 操作符。.a 语法通常称为“属性(property)”访问,而 ["a"] 语法通常称为“键(key)”访问。都是属于“属性访问”
两种语法的主要区别在于,. 操作符后面需要一个 标识符(Identifier) 兼容的属性名,而 [".."] 语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。比如引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"] 语法访问,因为 Super-Fun! 不是一个合法的 Identifier 属性名;还有个好处就是程序可以动态地组建字符串的值来作为属性名。
在对象中,属性名 总是 字符串。如果你使用 string 以外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中常用于索引的数字,所以要小心不要将对象和数组使用的数字搞混了
var myObject = { };
myObject[true] = "foo"; --> 属性名字为 “true”
myObject[3] = "bar"; --> 属性名为 '3'
myObject[myObject] = "baz"; -->属性名为 '[object Object]'
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](https://www.yuque.com/tuyong/skill/zcrgia#35850a95)
属性(Property) vs. 方法(Method)
每次你访问一个对象的属性都是一个 属性访问,无论你得到什么类型的值。如果你 恰好 从属性访问中得到一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐含的 this 绑定处通过最终函数调用点已经解释,this又运行时决定而非编写时)
就算声明一个函数表达式作为字面对象的一部分,那个函数都不会地 属于 这个对象 —— 仍然仅仅是同一个函数对象的多个引用罢了
var myObject = {
foo: function foo() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
数组
数组也使用 [ ] 访问形式,在存储值的方式和位置上它们的组织更加结构化(虽然仍然在存储值的 类型 上没有限制)。数组采用 数字索引,这意味着值被存储的位置,通常称为 下标,是一个非负整数
但数组也是对象,所以虽然每个索引都是正整数,你还可以在数组上添加属性:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz"; // --> 对这个数组对象新增加一个属性“bar”
myArray.length; // 3 --> 新增加的属性不会影响数组长度!!!
myArray.baz; // "baz"
如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz" --> 上面的3这个被转换成了一个数字索引,因为改变了数组的长度,并且设置了
索引为3的数组的值
复制对象
var myObject = {
a: 2,
b: anotherObject, // 引用,不是拷贝!
c: anotherArray, // 又一个引用!
d: anotherFunction
};
一个 浅拷贝(shallow copy) 会得到一个新对象,它的 a 是值 2 的拷贝,但 b、c 和 d 属性仅仅是引用,它们指向被拷贝对象中引用的相同位置。一个 深拷贝(deep copy) 将不仅复制 myObject,还会复制 anotherObject 和 anotherArray
JSON 安全的对象(也就是,可以被序列化为一个 JSON 字符串,之后还可以被重新解析为拥有相同的结构和值的对象)可以简单地这样 复制:
var newObj = JSON.parse( JSON.stringify( someObj ) );
(浅拷贝)ES6 为此任务已经定义了 Object.assign(..)。Object.assign(..) 接收 目标 对象作为第一个参数,然后是一个或多个 源 对象作为后续参数
var newObj = Object.assign( {}, myObject );
然而在 Object.assign(..) 中发生的复制是单纯的 = 式赋值,所以任何在源对象属性的特殊性质(比如 writable)在目标对象上 都不会保留 。
属性描述符(Property Descriptors)
在 ES5 中,所有的属性都用 属性描述符(Property Descriptors) 来描述。
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
我们普通的对象属性 a 的属性描述符的内容要比 value 为 2 多得多。它还包含另外三个性质:writable、enumerable、和 configurable
可以通过如下代码动态定义某个属性的这三个性质
Object.defineProperty( myObject, "a", {
value: 2,
writable: false,
configurable: true,
enumerable: true
} );
Writable 可写性
当值被设置成false时,任意的赋值操作都无效;并且下 strict mode 下会抛出TypeError 错误; writable:false 意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个 TypeError,来和 writable:false 保持一致
myObject.a = 3; --> 赋值无效
myObject.a; // 2
可配置性(Configurable)
将 configurable 设置为 false 是 一个单向操作,不可撤销!configurable:false 阻止的另外一个事情是使用 delete 操作符移除既存属性的能力
可枚举性(Enumerable)
这个性质控制着一个属性是否能在特定的对象-属性枚举操作中出现,比如 for..in 循环。设置为 false 将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为 true会使它出现
所有普通的用户定义属性都默认是可 enumerable 的
propertyIsEnumerable(..)测试一个给定的属性名是否直 接存 在于对象上,并且是enumerable:true。Object.keys(..)返回一个所有可枚举属性的数组Object.getOwnPropertyNames(..)返回一个 所有 属性的数组,不论能不能枚举。
不可变性(Immutability)
将属性或对象(有意或无意地)设置为不可改变的
所有 这些方法创建的都是浅不可变性(内部对象引用不受影响)。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的 内容 不会受影响,任然保持可变
对象常量(Object Constant)
通过将 writable:false 与 configurable:false 组合,你可以实质上创建了一个作为对象属性的 常量(不能被改变,重定义或删除),比如:
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
防止扩展(Prevent Extensions)
如果你想防止一个对象被添加新的属性,但另一方面保留其他既存的对象属性,可以调用 Object.preventExtensions(..):
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
封印(Seal)
Object.seal(..) 创建一个“封印”的对象,这意味着它实质上在当前的对象上调用 Object.preventExtensions(..),同时也将它所有的既存属性标记为 configurable:false。
所以,你既不能添加更多的属性,也不能重新配置或删除既存属性(虽然你依然 可以 修改它们的值)。
冻结(Freeze)
Object.freeze(..) 创建一个冻结的对象,这意味着它实质上在当前的对象上调用 Object.seal(..),同时也将它所有的“数据访问”属性设置为 writable:false,所以它们的值不可改变
[[Get]]属性访问
对一个对象进行默认的内建 [[Get]] 操作,会 首先 检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。
然而,如果按照被请求的名称 没能 找到属性,[[Get]] 的算法定义了另一个重要的行为。我们会在第五章来解释 接下来 会发生什么(遍历 [[Prototype]] 链,如果有的话)
通过任何方法都不能找到被请求的属性的值,那么它会返回 undefined。
[[Put]]
调用 [[Put]] 时,它根据几个因素表现不同的行为,包括(影响最大的)属性是否已经在对象中存在了。
如果属性存在,[[Put]] 算法将会大致检查:
- 这个属性是访问器描述符吗(见下一节”Getters 与 Setters”)?如果是,而且是 setter,就调用 setter。
- 这个属性是
writable为false数据(属性)描述符吗?如果是,在非strict mode下无声地失败,或者在strict mode下抛出TypeError。 - 否则,像平常一样设置既存属性的值。
Getters 与 Setters
ES5 引入了一个方法来覆盖这些默认操作的一部分,但不是在对象级别而是针对每个属性,就是通过 getters 和 setters。Getter 是实际上调用一个隐藏函数来取得值的属性。Setter 是实际上调用一个隐藏函数来设置值的属性。
当你将一个属性定义为拥有 getter 或 setter 或两者兼备,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对)。对于访问器描述符,它的 value 和 writable 性质因没有意义而被忽略,取而代之的是 JS 将会考虑属性的 set 和 get 性质(还有 configurable 和 enumerable)
var myObject = {
// 为 `a` 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 为 `b` 定义 getter
get: function(){ return this.a * 2 }, //this的调用点是在myObject内部,因此可访问 a
// 确保 `b` 作为对象属性出现
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
setter的操作
var myObject = {
// 为 `a` 定义 getter
get a() { --> 这里a 就表示属性a名称,且必须是 属性的名称,要么无法重定义属性的set/get成功
return this._a_; // _a_只是一个代名词而已,可以用任意的名字替换 如 value
},
// 为 `a` 定义 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
存在性(Existence)
我们可以查询一个对象是否拥有特定的属性,而 不必 取得那个属性的值:
var myObject = {
a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in 操作 和 hasOwnProperty 操作的不同
in操作符会检查属性是否存在于对象 中,或者是否存在于[[Prototype]]链对象遍历的更高层中hasOwnProperty(..)仅仅 检查myObject是否拥有属性,但 不会 查询[[Prototype]]链
通过委托到 Object.prototype,所有的普通对象都可以访问 hasOwnProperty(..)。但是创建一个不链接到 Object.prototype 的对象也是可能的(通过 Object.create(null) )。这种情况下,像 myObject.hasOwnProperty(..) 这样的方法调用将会失败。
在这种场景下,一个进行这种检查的更健壮的方式是 Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基本的 hasOwnProperty(..) 方法而且使用 明确的 this 绑定 来对我们的 myObject 实施调用这个方法
迭代(Iteration)
ES5 还为数组加入了几个迭代帮助方法,forEach(..) 将会迭代数组中所有的值,并且忽略回调的返回值。every(..) 会一直迭代到最后,或者 当回调返回一个 false(或“falsy”)值,而 some(..) 会一直迭代到最后,或者 当回调返回一个 true(或“truthy”)值。
ES6 加入了一个有用的 for..of 循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器),不关心下标!
for..of 循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator 的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next() 方法,循环迭代的内容就是这些连续的返回值。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
通过 Symbol 名称,而不是它可能持有的特殊的值,来引用这样特殊的属性(属性名就是Symbol``.``iterator)。另外,尽管这个名称有这样的暗示,但 @@iterator 本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!
这章与generator有很大的牵扯,需要再次看看
