原文:You-Dont-Know-JSjavascript
对象能够经过两种形式定义:声明(文字)形式和构造形式java
声明(文字)形式:git
var myObj = {
key: value
// ...
};
复制代码
构造形式:github
var myObj = new Object();
myObj.key = value;
复制代码
这里书上说 JavaScript 有六种主要类型,ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。数组
关于 js 的类型 https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive数据结构
null
自己是基本类型:null 有时会被看成一种对象类型,可是这其实只是语言自己的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"学习
原理是这样的,不一样的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,天然前三位也是 0,因此执行 typeof 时会返回“object”。测试
JavaScript 中还有一些对象子类型,一般被称为内置对象
在 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]
复制代码
原始值 "I am a string" 并非一个对象,它只是一个字面量,而且是一个不可变的值。 若是要在这个字面量上执行一些操做,好比获取长度、访问其中某个字符等,那须要将其转换为 String 对象。在必要时语言会自动把字符串字面量转换成一个 String 对象。
Object.prototype.toString…
的用法有个小技巧:https://gist.github.com/Yunkou/67d5da9d05b922479d771d8bcde3308d 判断 js 类型核心的代码:
Object.prototype.toString.call(obj).slice(8, -1); 复制代码
基本类型值 "I am a string"
不是一个对象,它是一个不可变的基本字面值。为了对它进行操做,好比检查它的长度,访问它的各个独立字符内容等等,都须要一个 String
对象。
幸运的是,在必要的时候语言会自动地将 "string"
基本类型强制转换为 String
对象类型,这意味着你几乎从不须要明确地建立对象。JS 社区的绝大部分人都 强烈推荐 尽量地使用字面形式的值,而非使用构造的对象形式。
考虑下面的代码:
var strPrimitive = "I am a string";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
复制代码
在这两个例子中,咱们在字符串的基本类型上调用属性和方法,引擎会自动地将它强制转换为 String
对象,因此这些属性/方法的访问能够工做。
null
和 undefined
没有对象包装的形式,仅有它们的基本类型值。相比之下,Date
的值 仅能够 由它们的构造对象形式建立,由于它们没有对应的字面形式。
咱们须要使用 . 或 [ ] 操做符。
两种语法的主要区别在于,.
操做符后面须要一个 标识符(Identifier)
兼容的属性名,而 [".."]
语法基本能够接收任何兼容 UTF-8/unicode 的字符串做为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"]
语法访问,由于 Super-Fun!
不是一个合法的 Identifier
属性名。 [".."]
语法能够传变量。
在对象中,属性名 老是 字符串。若是你使用 string
之外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中经常使用于索引的数字,因此要当心不要将对象和数组使用的数字搞混了。
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
复制代码
若是你试图在一个数组上添加属性,可是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"
复制代码
在 ES5 以前,JavaScript 语言没有给出直接的方法,让你的代码能够考察或描述属性性质间的区别,好比属性是否为只读。
在 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(..)
来添加新属性,或使用指望的性质来修改既存的属性(若是它是 configurable
的!)。
writable
控制着你改变属性值的能力。
考虑这段代码:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3;
myObject.a; // 2
复制代码
如你所见,咱们对 value
的修改悄无声息地失败了。若是咱们在 strict mode
下进行尝试,会获得一个错误:
"use strict";
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3; // TypeError
复制代码
这个 TypeError
告诉咱们,咱们不能改变一个不可写属性。
注意: 咱们一下子就会讨论 getters/setters,可是简单地说,你能够观察到 writable:false
意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时须要扔出一个 TypeError
,来和 writable:false
保持一致。
只要属性当前是可配置的,咱们就可使用相同的 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,这与 strict mode
无关,若是你试图改变一个不可配置属性的描述符定义,就会发生 TypeError。要当心:如你所看到的,将 configurable
设置为 false
是 一个单向操做,不可撤销!
注意: 这里有一个须要注意的微小例外:即使属性已是 configurable:false
,writable
老是能够没有错误地从 true
改变为 false
,但若是已是 false
的话不能变回 true
。
configurable:false
阻止的另一个事情是使用 delete
操做符移除既存属性的能力。
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
调用(无声地)失败了,由于咱们将 a
属性设置成了不可配置。
delete
仅用于直接从目标对象移除该对象的(能够被移除的)属性。若是一个对象的属性是某个其余对象/函数的最后一个现存的引用,而你 delete
了它,那么这就移除了这个引用,因而如今那个没有被任何地方所引用的对象/函数就能够被做为垃圾回收。可是,将 delete
当作一个像其余语言(如 C/C++)中那样的释放内存工具是 不 恰当的。delete
仅仅是一个对象属性移除操做 —— 没有更多别的含义。
咱们将要在这里提到的最后一个描述符性质是 enumerable
(还有另外两个,咱们将在一下子讨论 getter/setters 时谈到)。
它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象-属性枚举操做中出现,好比 for..in
循环。设置为 false
将会阻止它出如今这样的枚举中,即便它依然彻底是能够访问的。设置为 true
会使它出现。
全部普通的用户定义属性都默认是可 enumerable
的,正如你一般但愿的那样。但若是你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为 enumerable:false
。
咱们能够查询一个对象是否拥有特定的属性,而 没必要 取得那个属性的值:
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]]
链。咱们会在第五章详细讲解 [[Prototype]]
时,回来讨论这个两个操做重要的不一样。
经过委托到 Object.prototype
,全部的普通对象均可以访问 hasOwnProperty(..)
(详见第五章)。可是建立一个不连接到 Object.prototype
的对象也是可能的(经过 Object.create(null)
—— 详见第五章)。这种状况下,像 myObject.hasOwnProperty(..)
这样的方法调用将会失败。
在这种场景下,一个进行这种检查的更健壮的方式是 Object.prototype.hasOwnProperty.call(myObject,"a")
,它借用基本的 hasOwnProperty(..)
方法并且使用 明确的 this 绑定(详见第二章)来对咱们的 myObject
实施这个方法。
注意: in
操做符看起来像是要检查一个值在容器中的存在性,可是它实际上检查的是属性名的存在性。在使用数组时注意这个区别十分重要,由于咱们会有很强的冲动来进行 4 in [2, 4, 6]
这样的检查,可是这老是不像咱们想象的那样工做。
先前,在学习 enumerable
属性描述符性质时,咱们简单地解释了"可枚举性(enumerability)"的含义。如今,让咱们来更加详细地从新讲解它。
var myObject = {};
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚举,如通常状况
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
"b" in myObject; // true
myObject.hasOwnProperty("b"); // true
// .......
for (var k in myObject) {
console.log(k, myObject[k]);
}
// "a" 2
复制代码
你会注意到,myObject.b
实际上 存在,并且拥有能够访问的值,可是它不出如今 for..in
循环中(然而使人诧异的是,它的 in
操做符的存在性检查经过了)。这是由于 “enumerable” 基本上意味着“若是对象的属性被迭代时会被包含在内”。
注意: 将 for..in
循环实施在数组上可能会给出意外的结果,由于枚举一个数组将不只包含全部的数字下标,还包含全部的可枚举属性。因此一个好主意是:将 for..in
循环 仅 用于对象,而为存储在数组中的值使用传统的 for
循环并用数字索引迭代。
另外一个能够区分可枚举和不可枚举属性的方法是:
var myObject = {};
Object.defineProperty(
myObject,
"a",
// 使 `a` 可枚举,如通常状况
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 使 `b` 不可枚举
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false
Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
复制代码
propertyIsEnumerable(..)
测试一个给定的属性名是否直 接存 在于对象上,而且是 enumerable:true
。
Object.keys(..)
返回一个全部可枚举属性的数组,
Object.getOwnPropertyNames(..)
返回一个 全部 属性的数组,不论能不能枚举。
in
和 hasOwnProperty(..)
区别于它们是否查询 [[Prototype]]
链,
Object.keys(..)
和 Object.getOwnPropertyNames(..)
都 只 考察直接给定的对象。
(当下)没有与 in
操做符的查询方式(在整个 [[Prototype]]
链上遍历全部的属性,如咱们在第五章解释的)等价的、内建的方法能够获得一个 全部属性 的列表。你能够近似地模拟一个这样的工具:递归地遍历一个对象的 [[Prototype]]
链,在每一层都从 Object.keys(..)
中取得一个列表——仅包含可枚举属性。
for..in
循环迭代一个对象上(包括它的 [[Prototype]]
链)全部的可迭代属性。但若是你想要迭代值呢?
在数字索引的数组中,典型的迭代全部的值的办法是使用标准的 for
循环,好比:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
// 1 2 3
复制代码
可是这并无迭代全部的值,而是迭代了全部的下标,而后由你使用索引来引用值,好比 myArray[i]
。
ES5 还为数组加入了几个迭代帮助方法,包括 forEach(..)
、every(..)
、和 some(..)
。这些帮助方法的每个都接收一个回调函数,这个函数将施用于数组中的每个元素,仅在如何响应回调的返回值上有所不一样。
forEach(..)
将会迭代数组中全部的值,而且忽略回调的返回值。every(..)
会一直迭代到最后,或者 当回调返回一个 false
(或“falsy”)值,而 some(..)
会一直迭代到最后,或者 当回调返回一个 true
(或“truthy”)值。
这些在 every(..)
和 some(..)
内部的特殊返回值有些像普通 for
循环中的 break
语句,它们能够在迭代执行到末尾以前将它结束掉。
若是你使用 for..in
循环在一个对象上进行迭代,你也只能间接地获得值,由于它实际上仅仅迭代对象的全部可枚举属性,让你本身手动地去访问属性来获得值。
注意: 与以有序数字的方式(for
循环或其余迭代器)迭代数组的下标比较起来,迭代对象属性的顺序是 不肯定 的,并且可能会因 JS 引擎的不一样而不一样。对于须要跨平台环境保持一致的问题,不要依赖 观察到的顺序,由于这个顺序是不可靠的。
可是若是你想直接迭代值,而不是数组下标(或对象属性)呢?ES6 加入了一个有用的 for..of
循环语法,用来迭代数组(和对象,若是这个对象有定义的迭代器):
var myArray = [1, 2, 3];
for (var v of myArray) {
console.log(v);
}
// 1
// 2
// 3
复制代码
for..of
循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫作 @@iterator
的默认内部函数那里获得),每次循环都调用一次这个迭代器对象的 next()
方法,循环迭代的内容就是这些连续的返回值。
数组拥有内建的 @@iterator
,因此正如展现的那样,for..of
对于它们很容易使用。可是让咱们使用内建的 @@iterator
来手动迭代一个数组,来看看它是怎么工做的:
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 }
复制代码
注意: 咱们使用一个 ES6 的 Symbol
:Symbol.iterator
来取得一个对象的 @@iterator
内部属性。咱们在本章中简单地提到过 Symbol
的语义(见“计算型属性名”),一样的原理也适用于这里。你老是但愿经过 Symbol
名称,而不是它可能持有的特殊的值,来引用这样特殊的属性。另外,尽管这个名称有这样的暗示,但 @@iterator
自己 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!
正如上面的代码段揭示的,迭代器的 next()
调用的返回值是一个 { value: .. , done: .. }
形式的对象,其中 value
是当前迭代的值,而 done
是一个 boolean
,表示是否还有更多内容能够迭代。
注意值 3
和 done:false
一块儿返回,猛地一看会有些奇怪。你不得不第四次调用 next()
(在前一个代码段的 for..of
循环会自动这样作)来获得 done:true
,以使本身知道迭代已经完成。这个怪异之处的缘由超出了咱们要在这里讨论的范围,可是它源自于 ES6 生成器(generator)函数的语义。
虽然数组能够在 for..of
循环中自动迭代,但普通的对象 没有内建的 @@iterator。这种故意省略的缘由要比咱们将在这里解释的更复杂,但通常来讲,为了将来的对象类型,最好不要加入那些可能最终被证实是麻烦的实现。
JS 中的对象拥有字面形式(好比 var a = { .. }
)和构造形式(好比 var a = new Array(..)
)。字面形式几乎老是首选,但在某些状况下,构造形式提供更多的构建选项。
许多人声称“Javascript 中的一切都是对象”,这是不对的。对象是六种(或七中,看你从哪一个方面说)基本类型之一。对象有子类型,包括 function
,还能够被行为特化,好比 [object Array]
做为内部的标签表示子类型数组。
对象是键/值对的集合。经过 .propName
或 ["propName"]
语法,值能够做为属性访问。无论属性何时被访问,引擎实际上会调用内部默认的 [[Get]]
操做(在设置值时调用 [[Put]]
操做),它不只直接在对象上查找属性,在没有找到时还会遍历 [[Prototype]]
链(见第五章)。
属性有一些能够经过属性描述符控制的特定性质,好比 writable
和 configurable
。另外,对象拥有它的不可变性(它们的属性也有),能够经过使用 Object.preventExtensions(..)
、Object.seal(..)
、和 Object.freeze(..)
来控制几种不一样等级的不可变性。
属性没必要非要包含值 —— 它们也能够是带有 getter/setter 的“访问器属性”。它们也能够是可枚举或不可枚举的,这控制它们是否会在 for..in
这样的循环迭代中出现。
你也可使用 ES6 的 for..of
语法,在数据结构(数组,对象等)中迭代 值,它寻找一个内建或自定义的 @@iterator
对象,这个对象由一个 next()
方法组成,经过这个 next()
方法每次迭代一个数据。