【第766期】你不懂JS:对象

图片

前言前端

经过最近两章的阅读,你们会以为文章太长吗?看这种一篇文中,你大概会花多少时间呢?欢迎给早读君留言。今天继续由前端早读课专栏做者@HetfieldJoe带来连载《你不懂JS》的分享。ps:本文略长,耐点心看看正则表达式


正文从这开始~算法


你不懂JS:this与对象原型 第三章:对象设计模式


【第764期】你不懂JS:this是什么?【第765期】你不懂JS:this豁然开朗!中,咱们讲解了this绑定如何根据函数调用的调用点指向不一样的对象。但究竟什么是对象,为何咱们须要指向它们?这一章咱们就来详细探索一下对象。数组


语法安全

对象来自于两种形式:声明(字面)形式,和构造形式。数据结构


一个对象的字面语法看起来像这样:框架

图片


构造形式看起来像这样:ide

图片


构造形式和字面形式的结果是彻底同种类的对象。惟一真正的区别在于你能够向字面声明一次性添加一个或多个键/值对,而对于构造形式,你必须一个一个地添加属性。函数


注意: 像刚才展现的那样使用“构造形式”来建立对象是极其少见的。你颇有可能老是想使用字面语法形式。对大多数内建的对象也同样(后述)。


类型

对象是大多数JS工程依赖的基本构建块儿。它们是JS的6中主要类型(在语言规范中称为“语言类型”)中的一种。

  • string

  • number

  • boolean

  • null

  • undefined

  • object


注意 简单基本类型 (string,number,boolean,null,和undefined)自身 不是 object。null有时会被当成一个对象类型,可是这种误解源自与一个语言中的Bug,它使得typeof null错误地(使人困惑地)返回字符串"object"。实际上,null是它本身的基本类型


一个常见的错误论断是“JavaScript中的一切都是对象”。这明显是不对的。


对比来看,存在几种特殊的对象子类型,咱们能够称之为 复杂基本类型。


function是对象的一种子类型(技术上讲,叫作“可调用对象”)。函数在JS中被称为“头等(first class)”类型,就由于它们基本上就是普通的对象(附带有可调用的行为语义),并且它们能够像其余普通的对象那样被处理。


数组也是一种形式的对象,带有特别的行为。数组在内容的组织上要稍稍比通常的对象更加结构化。


内建对象

有几种其余的对象子类型,一般称为内建对象。对于其中的一些来讲,它们的名称看起来暗示着它们和它们对应的基本类型有着直接的联系,但事实上,它们的关系更复杂,咱们一下子就开始探索。

  • String

  • Number

  • Boolean

  • Object

  • Function

  • Array

  • Date

  • RegExp

  • Error


若是你依照和其余语言的类似性来看的话,好比Java语言的String类,这些内建类型有着实际类型的外观,甚至是类(class)的外观,


可是在JS中,它们实际上仅仅是内建的函数。这些内建函数的每个均可以被用做构造器(也就是,一个函数能够和new操做符一块儿调用——参照第二章),其结果是一个新 构建 的相应子类型的对象。好比:

图片


咱们会在本章稍后详细地看到Object.prototype.toString...究竟是如何工做的,但简单地说,咱们能够经过借用基本的默认toString()方法来考察子类型的内部,并且你能够看到它揭示了strObject其实是由String构造器建立的对象。


基本类型值"I am a string"不是一个对象,它是一个不可变的基本字面值。为了对它进行操做,好比检查它的长度,访问它的各个独立字符内容等等,都须要一个String对象。


幸运的是,在必要的时候语言会自动地将"string"基本类型转换为String对象类型,这意味着你几乎从不须要明确地建立对象。主流的JS社区都 强烈推荐 尽量地使用字面形式的值,而非使用构造的对象形式。


考虑下面的代码:

image.png


在这两个例子中,咱们在字符串的基本类型上调用属性和方法,引擎会自动地将它转换为String对象,因此这些属性/方法的访问能够工做。


当使用如42.359.toFixed(2)这样的方法时,一样的转换也发生在数字基本字面量42和包装对象new Nubmer(42)之间。一样的还有Boolean对象和"boolean"基本类型。


null和undefined没有对象包装的形式,仅有它们的基本类型值。相比之下,Date的值 仅能够 由它们的构造对象形式建立,由于它们没有对应的字面形式。


不管使用字面仍是构造形式,Object,Array,Function,和RegExp(正则表达式)都是对象。在某些状况下,构造形式确实会比对应的字面形式提供更多的建立选项。由于对象能够被任意一种方式建立,更简单的字面形式几乎是全部人的首选。仅仅在你须要使用额外的选项时使用构建形式。


Error对象不多在代码中明示地被建立,它们一般在抛出异常时自动地被建立。它们能够由new Error(..)构造形式建立,但一般是没必要要的。


内容

正如刚才提到的,对象的内容由存储在特定命名的 位置 上的(任意类型的)值组成,咱们称这些值为属性。


有一个重要的事情须要注意:当咱们说“内容”时,彷佛暗示这这些值 实际上 存储在对象内部,但那只不过是表面现象。引擎会根据本身的实现来存储这些值,并且一般都不是把它们存储在容器对象 内部。在容器内存储的是这些属性的名称,它们像指针(技术上讲,叫 引用(reference))同样指向值存储的地方。


考虑下面的代码:

image.png


为了访问在myObject在 位置 a的值,咱们须要使用.或[ ]操做符。.a语法一般称为“属性(property)”访问,而["a"]语法一般称为“键(key)”访问。在现实中,它们俩都访问相同的 位置,并且会拿出相同的值,2,因此这些术语能够互换使用。从如今起,咱们将使用最多见的术语——“属性访问”。


两种语法的主要区别在于,.操做符后面须要一个标识符(Identifier)兼容的属性名,而[".."]语法基本能够接收任何兼容UTF-8/unicode的字符串做为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用["Super-Fun!"]语法访问,由于Super-Fun!不是一个合法的Identifier属性名。


并且,因为[".."]语法使用字符串的 值 来指定位置,这意味着程序能够动态地组建字符串的值。好比:

image.png


在对象中,属性名 老是 字符串。若是你使用字符串之外(基本)类型的值,它会首先被转换为字符串。这甚至包括在数组中经常使用于索引的数字,因此要当心不要将对象和数组使用的数字搞混了。

image.png


计算型属性名

若是你须要将一个计算表达式 做为 一个键名称,那么咱们刚刚描述的myObject[..]属性访问语法是十分有用的,好比myObject[prefix + name]。可是当使用字面对象语法声明对象时则没有什么帮助。


ES6加入了 计算型属性名,在一个字面对象声明的键名称位置,你能够指定一个表达式,用[ ]括起来:

image.png


计算型属性名 的最多见用法,多是用于ES6的Symbol,咱们将不会在本书中涵盖关于它的细节。简单地说,它们是新的基本数据类型,拥有一个不透明不可知的值(技术上讲是一个string值)。你将会被强烈地不鼓励使用一个Symbol的 实际值 (这个值理论上会因JS引擎的不一样而不一样),因此Symbol的名称,好比Symbol.Something(这是个瞎编的名称!),才是你会使用的:

image.png


属性(Property) vs. 方法(Method)

有些开发者喜欢在讨论对一个对象的属性访问时作一个区别,若是这个被访问的值刚好是一个函数的话。由于这诱令人们认为函数 属于 这个对象,并且在其余语言中,属于对象(也就是“类”)的函数被称做“方法”,因此相对于“属性访问”,咱们常能听到“方法访问”。


有趣的是,语言规范也作出了一样的区别。


从技术上讲,函数毫不会“属于”对象,因此,说一个对象的引用上恰好被访问的函数自动是一个“方法”,看起来有些像是延伸了语义。


有些函数确实拥有this引用,并且 有时 这些this引用指向调用点的对象引用。但这个用法真的没有使这个函数比其余函数更像“方法”,由于this是在运行时在调用点动态绑定的,这使得它与这个对象的关系至可能是间接的。


每次你访问一个对象的属性都是一个 属性访问,不管你获得什么类型的值。若是你 刚好 从属性访问中获得一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐式this绑定以外的可能性在刚才已经解释过了)。


举个例子:

image.png


someFoo和myObject.someFoo只不过是同一个函数的两个分离的引用,它们中的任何一个都不意味着这个函数很特别或被其余对象所“拥有”。若是上面的foo()定义里面拥有一个this引用,那么myObject.someFoo的 隐式绑定 将会是这个两个引用间 惟一 能够观察到的不一样。它们中的任何一个都没有称为“方法”的道理。


也许有人会争辩,函数 变成了方法,不是在定义期间,而是在调用的执行期间,根据它是如何在调用点被调用的(是否带有一个环境对象引用 —— 细节见第二章)。甚至这种解读也有些牵强。


可能最安全的结论是,在JavaScript中,“函数”和“方法”是能够互换使用的。


注意: ES6加入了super引用,它一般是和class(见附录A)一块儿使用的。super的行为方式(静态绑定,而非动态绑定),给了这种说法更多的权重:一个super绑定到某处的函数比起“函数”更像一个“方法”。可是一样地,这仅仅是微妙的语义上的(和机制上的)细微区别。


就算你声明一个函数表达式做为字面对象的一部分,那个函数都不会魔法般地 属于 这个对象——仍然仅仅是同一个函数对象的多个引用罢了。

image.png


注意: 在第六章中,咱们会为字面对象的foo: function foo(){ .. }声明语法介绍一种ES6的简化语法。


数组

数组也使用[ ]访问形式,但正如上面提到的,在存储值的方式和位置上它们的组织更加结构化(虽然仍然在存储值的类型上没有限制)。数组采用 数字索引,这意味着值被存储的位置,一般称为 下标,是一个非负整数,好比0和42。

image.png


数组也是对象,因此即使每一个索引都是正整数,你还能够在数组上添加属性:

image.png


注意,添加命名属性(不管是使用.仍是[ ]操做符语法)不会改变数组的length所报告的值。


你 能够 把一个数组当作普通的键/值对象使用,而且从不添加任何数字下标,但这不是好主意,由于数组对它原本的用途有特定的行为和优化,正如普通对象那样。使用对象来存储键/值对,而用数组在数字下标上存储值。


当心: 若是你试图在一个数组上添加属性,可是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):

image.png


复制对象

当开发者们初次拿起Javascript语言时,最常须要的特性就是如何复制一个对象。看起来应该有一个内建的copy()方法,对吧?可是事情实际上比这复杂一些,由于在默认状况下,复制的算法应当是什么,并不明确。


好比,考虑这个对象:

image.png


一个myObject的 拷贝 究竟应该怎么表现?


首先,咱们应该回答它是一个 浅(shallow) 仍是一个 深(deep) 拷贝?一个 浅拷贝(shallow copy) 会获得一个新对象,它的a是值2的拷贝,但b,c和d属性仅仅是引用,它们指向被拷贝对象中引用的相同位置。一个 深拷贝(deep copy) 将不只复制myObject,还会复制anotherObject和anotherArray。但以后咱们让anotherArray拥有anotherObject和myObject的引用,因此 那些 也应当被复制而不是仅保留引用。如今因为循环引用,咱们获得了一个无限循环复制的问题。


咱们应当检测循环引用并打破循环遍历吗(无论位于深处的,没有彻底复制的元素)?咱们应当报错退出吗?或者介于二者之间?


另外,“复制”一个函数意味着什么,也不是很清楚。有一些技巧,好比提取一个函数源代码的toString()序列化表达(这个源代码会因实现不一样而不一样,并且根据被考察的函数的类型,其结果甚至在全部引擎上都不可靠)。


那么咱们如何解决全部这些刁钻的问题?不一样的JS框架都各自挑选本身的解释而且作出本身的选择。可是哪种(若是有的话)才是JS应看成为标准采用的呢?长久以来,没有明确答案。


一个解决方案是,JSON安全的对象(也就是,能够被序列化为一个JSON字符串,以后还能够被从新变换为拥有相同的结构和值的对象)能够简单地这样 复制:

image.png


固然,这要求你保证你的对象是JSON安全的。对于某些状况,这没什么大不了的。而对另外一些状况,这还不够。


同时,浅拷贝至关易懂,并且没有那么多问题,因此ES6为此任务已经定义了Object.assign(..)。Object.assign(..)接收 目标 对象做为第一个参数,而后是一个或多个 源 对象做为后续参数。它会在 源 对象上迭代全部的 可枚举(enumerable),owned keys(直接拥有的键),并把它们拷贝到 目标 对象上(仅经过=赋值)。它还会很方便地返回 目标 对象,正以下面你能够看到的:

image.png


注意: 在下一部分中,咱们将讨论“属性描述符(property descriptors)”并展现Object.defineProperty(..)的使用。然而在Object.assign(..)中发生的复制是单纯的=式赋值,因此任何在源对象属性的特殊性质(好比writable)在目标对象上 都不会保留 。


属性描述符(Property Descriptors)

在ES5以前,JavaScript语言没有给出直接的方法,让你的代码能够考察或描述属性的性质间的区别,好比属性是否为只读。


在ES5中,全部的属性都用 属性描述符(Property Descriptors) 来描述。


考虑这段代码:

image.png


正如你所见,咱们普通的对象属性a的属性描述符(称为“数据描述符”,由于它仅持有一个数据值)的内容要比value为2多得多。它还包含另外3个性质:writable,enumerable,和configurable。


当咱们建立一个普通属性时,能够看到属性描述符的各类性质的默认值,咱们能够用Object.defineProperty(..)来添加新属性,或使用指望的性质来修改既存的属性(若是它是configurable的!)。


举例来讲:

image.png


使用defineProperty(..),咱们手动明确地在myObject上添加了一个直白的,普通的a属性。然而,你一般不会使用这种手动方法,除非你想要把描述符的某个性质修改成不一样的值。


可写性(Writable)

writable控制着你改变属性值的能力。


考虑这段代码:

image.png

如你所见,咱们对value的修改悄无声息地失败了。若是咱们在strict mode下进行尝试,会获得一个错误:

image.png


这个TypeError告诉咱们,咱们不能改变一个不可写属性。


注意: 咱们一下子就会讨论getters/setters,可是简单地说,你能够观察到writable:false意味着值不可改变,和你定义一个空的setter是有些等价的。实际上,你的空setter在被调用时须要扔出一个TypeError,来和writable:false保持一致。


可配置性(Configurable)

只要属性当前是可配置的,咱们就可使用一样的defineProperty(..)工具,修改它的描述符定义。

image.png


最后的defineProperty(..)调用致使了一个TypeError,这与strict mode无关,若是你试图改变一个不可配置属性的描述符定义,就会发生TypeError。要当心:如你所看到的,将configurable设置为false是 一个单向操做,不可撤销!


注意: 这里有一个须要注意的微小例外:即使属性已是configurable:false,writable老是能够没有错误地从true改变为false,但若是已是false的话不能变回true。


configurable:false阻止的另一个事情是使用delete操做符移除既存属性的能力。

image.png


如你所见,最后的delete调用失败了(无声地),由于咱们将a属性设置成了不可配置。


delete仅用于直接从目标对象移除该对象的属性(能够被移除的属性)。若是一个对象的属性是某个其余对象/函数的最后一个现存的引用,而你delete了它,那么这就移除了这个引用,因而如今那个没有被任何地方引用的对象/函数就能够被做为垃圾回收。可是,将delete当作一个像其余语言(如C/C++)中那样的释放内存工具是不正确的。delete仅仅是一个对象属性移除操做——没有更多别的含义。


可枚举性(Enumerable)

咱们将要在这里提到的最后一个描述符性质是enumerable(还有另外两个,咱们将在一下子讨论getter/setters时谈到)。


它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象属性枚举操做中出现,好比for..in循环。设置为false将会阻止它出如今这样的枚举中,即便它依然彻底是能够访问的。设置为true会使它出现。


全部普通的用户定义属性都默认是可enumerable的,正如你一般但愿的那样。但若是你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为enumerable:false。


咱们一下子就更加详细地演示可枚举性,因此在大脑中给这个话题上打一个书签。


不可变性(Immutability)

有时咱们但愿将属性或对象(有意或无心地)设置为不可改变的。ES5用几种不一样的微妙方式,加入了对此功能的支持。


一个重要的注意点是:全部 这些方法都建立的是浅不可变性。也就是,它们仅影响对象和它的直属属性的性质。若是对象拥有对其余对象(数组,对象,函数等)的引用,那个对象的 内容 不会受影响,任然保持可变。

image.png


在这段代码中,咱们假想myImmutableObject已经被建立,并且被保护为不可变。可是,为了保护myImmutableObject.foo的内容(也是一个对象——数组),你将须要使用下面的一个或多个方法将foo设置为不可变。


注意: 在JS程序中建立彻底不可动摇的对象是不那么常见的。有些特殊状况固然须要,但做为一个普通的设计模式,若是你发现本身想要 封印(seal) 或 冻结(freeze) 你全部的对象,那么你可能想要退一步来从新考虑你的程序设计,让它对对象值的潜在变化更加健壮。


对象常量(Object Constant)

经过将writable:false与configurable:false组合,你能够实质上建立了一个做为对象属性的 常量(不能被改变,重定义或删除),好比:

image.png


防止扩展(Prevent Extensions)

若是你想防止一个对象被添加新的属性,但另外一方面保留其余既存的对象属性,调用Object.preventExtensions(..):

image.png


在非-strict mode模式下,b的建立会无声地失败。在strict mode下,它会抛出TypeError。


封印(Seal)

Object.seal(..)建立一个“封印”的对象,这意味着它实质上在当前的对象上调用Object.preventExtensions(..),同时也将它全部的既存属性标记为configurable:false。


因此,你既不能添加更多的属性,也不能从新配置或删除既存属性(虽然你依然 能够 修改它们的值)。


冻结(Freeze)

Object.freeze(..)建立一个冻结的对象,这意味着它实质上在当前的对象上调用Object.seal(..),同时也将它全部的“数据访问”属性设置为writable:false,因此他们的值不可改变。


这种方法是你能够从对象自身得到的最高级别的不可变性,由于它阻止任何对对象或对象的直属属性的改变(虽然,如上面提到的,任何被引用的对象的内容不受影响)。


你能够“深度冻结”一个对象:在这个对象上调用Object.freeze(..),而后递归地迭代全部它引用的对象(目前尚未受过影响的),而后在它们上也调用Object.freeze(..)。可是要当心,这可能会影响其余(共享的)你并不打算影响的对象。


[[Get]]

关于属性访问如何工做有一个重要的细节。


考虑下面的代码:

image.png


myObject.a是一个属性访问,可是它并非看起来那样,仅仅在myObject中寻找一个名为a的属性。


根据语言规范,上面的代码实际上在myObject上执行了一个[[Get]]操做(有些像[[Get]]()函数调用)。对一个对象进行默认的内建[[Get]]操做,会 首先 检查对象,寻找一个拥有被请求的名称的属性,若是找到,就返回相应的值。


然而,若是按照被请求的名称 没能 找到属性,[[Get]]的算法定义了另外一个重要的行为。咱们会在第五章来解释 接下来 会发生什么(遍历[[Prototype]]链,若是有的话)。


但[[Get]]操做的一个重要结果是,若是它经过任何方法都不能找到被请求的属性的值,那么它会返回undefined。

image.png


这个行为和你经过标识符名称来引用 变量 不一样。若是你引用了一个在可用的词法做用域内没法解析的变量,其结果不是像对象属性那样返回undefined,而是抛出ReferenceError。

image.png


从 值 的角度来讲,这两个引用没有区别——它们的结果都是undefined。然而,在[[Get]]操做的底层,虽然不明显,可是比起处理引用myObject.a,处理myObject.b的操做要多作一些潜在的工做。


若是仅仅考察结果的值,你没法分辨一个属性是存在并持有一个undefined值,仍是由于属性根本 不 存在因此[[Get]]没法返回某个特定值而返回默认的undefined。可是,你很快就能看到你其实 能够 分辨这两种场景。


[[Put]]

既然为了从一个属性中取得值而存在一个内部定义的[[Get]]操做,那么很明显应该也存在一个默认的[[Put]]操做。


这很容易让人认为,给一个对象的属性赋值,将会在这个对象上调用[[Put]]来设置或建立这个属性。可是实际状况却有一些微妙的不一样。


调用[[Put]]时,它根据几个因素表现不一样的行为,包括(影响最大的)属性是否已经在对象中存在了。


若是属性存在,[[Put]]算法将会大体检查:

  • 这个属性是访问器描述符吗(见下一节"Getters 与 Setters")?若是是,并且是setter,就调用setter。

  • 这个属性是writable为false数据描述符吗?若是是,在非strict mode下无声地失败,或者在strict mode下抛出TypeError。

  • 不然,像日常同样设置既存属性的值。


若是属性在当前的对象中不存在,[[Put]]操做会变得更微妙和复杂。咱们将在第五章讨论[[Prototype]]时再次回到这个场景,更清楚地解释它。


Getters 与 Setters

对象默认的[[Put]]和[[Get]]操做分别彻底控制着如何设置既存或新属性的值,和如何取得既存属性。


注意: 使用较先进的语言特性,覆盖整个对象(不只是每一个属性)的默认[[Put]]和[[Get]]操做是可能的。这超出了咱们要在这本书中讨论的范围,但咱们会在后面的“你不懂JS”系列中涵盖此内容。


ES5引入了一个方法来覆盖这些默认操做的一部分,但不是在对象级别而是针对每一个属性,就是经过getters和setters。Getter是实际上调用一个隐藏函数来取得值的属性。Setter是实际上调用一个隐藏函数来设置值的属性。


当你将一个属性定义为拥有getter或setter或二者兼备,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对)。对于访问器描述符,它的value和writable性质没有意义而被忽略,取而代之的是JS将会考虑属性的set和get性质(还有configurable和enumerable)。


考虑下面的代码:

image.png


无论是经过在字面对象语法中使用get a() { .. },仍是经过使用defineProperty(..)明肯定义,咱们都在对象上建立了一个没有实际持有值的属性,访问它们将会自动地对getter函数进行隐藏的函数调用,其返回的任何值就是属性访问的结果。

image.png


由于咱们仅为a定义了一个getter,若是以后咱们试着设置a的值,赋值操做并不会抛出错误而是无声地将赋值废弃。就算这里有一个合法的setter,咱们的自定义getter将返回值硬编码为仅返回2,因此赋值操做是没有意义的。


为了使这个场景更合理,正如你可能指望的那样,每一个属性还应当被定义一个覆盖默认[[Put]]操做(也就是赋值)的setter。几乎可肯定,你将老是想要同时声明getter和setter(仅有它们中的一个常常会致使之外的行为):

image.png


注意: 在这个例子中,咱们实际上将赋值操做([[Put]]操做)指定的值2存储到了另外一个变量_a_中。_a_这个名称只是用在这个例子中的单纯的惯例,并不意味着它的行为有什么特别之处——它和其余普通属性没有区别。


存在性(Existence)

咱们早先看到,像myObject.a这样的属性访问可能会获得一个undefined值,不管是它明确存储着undefined仍是属性a根本就不存在。那么,若是这两种状况的值相同,咱们还怎么区别它们呢?


咱们能够查询一个对象是否拥有特定的属性,而没必要取得那个属性的值:

image.png


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]这样的检查,可是这老是不像咱们想象的那样工做。


枚举(Enumeration)

先前,在学习enumerable属性描述符性质时,咱们简单地解释了"可枚举性(enumerability)"的含义。如今,让咱们来更加详细地从新审视它。

image.png


你会注意到,myObject.b实际上 存在,并且拥有能够访问的值,可是它不出如今for..in循环中(然而使人诧异的是,它的in操做符的存在性检查经过了)。这是由于“enumerable”基本上意味着“若是对象的属性被迭代时会被包含在内”。


注意: 将for..in循环实施在数组上可能会给出意外的结果,由于枚举一个数组将不只包含全部的数字下标,还包含全部的可枚举属性。因此一个好主意是:将for..in循环 仅 用于对象,而为存储在数组中的值使用传统的for循环并用数字索引迭代。


意外一个能够区分可枚举和不可枚举属性的方法是:

image.png


propertyIsEnumerable(..)测试一个给定的属性名是否直 接存 在于对象上,而且是enumerable:true。


Object.keys(..)返回一个全部可枚举属性的数组,而Object.getOwnPropertyNames(..)返回一个 全部 属性的数组,不论能不能枚举。


in和hasOwnProperty(..)区别于它们是否查询[[Prototype]]链,而Object.keys(..)和Object.getOwnPropertyNames(..)都 只 考察直接给定的对象。


(当下)没有与in操做符的查询方式(在整个[[Prototype]]链上遍历全部的属性,如咱们在第五章解释的)等价的,内建的方法能够获得一个 全部属性 的列表。你能够近似地模拟一个这样的工具:递归地遍历一个对象的[[Prototype]]链,在每一层都从Object.keys(..)中取得一个列表——仅包含可枚举属性。


迭代(Iteration)

for..in循环迭代一个对象上(包括它的[[Prototype]]链)全部的可迭代属性。但若是你想要迭代值呢?


在数字索引的数组中,典型的迭代全部的值的办法是使用标准的for循环,好比:

image.png


可是这并无迭代全部的值,而是迭代了全部的下标,而后由你使用索引来引用值,好比myArray[i]。


ES5还为数组加入了几个迭代帮助方法,包括forEach(..),every(..),和some(..)。这些帮助方法的每个都接收一个回调函数,这个函数将施用于数组中的每个元素,仅在如何响应回调的返回值上有所不一样。


forEach(..)将会迭代数组中全部的值,而且忽略回调的返回值。every(..)会一直迭代到最后,或者 当回调返回一个false(或“falsy”)值,而some(..)会一直迭代到最后,或者 当回调返回一个true(或“truthy”)值。


这些在every(..)和some(..)内部的特殊返回值有些像普通for循环中的break语句,它们能够在迭代执行到末尾以前将它结束掉。


若是你使用for..in循环在一个对象上进行迭代,你也只能间接地获得值,由于它实际上仅仅迭代对象的全部可枚举属性,让你本身手动地去访问属性来获得值。


注意: 与以有序数字的方式(for循环或其余迭代器)迭代数组的下标比较起来,迭代对象属性的顺序是 不肯定 的,并且可能会因JS引擎的不一样而不一样。对于须要跨平台环境保持一致性的问题,不要依赖 观察到的顺序,由于这个顺序是不可靠的。


可是若是你想直接迭代值,而不是数组下标(或对象属性)呢?ES6加入了一个有用的for..of循环语法,用来迭代数组(和对象,若是这个对象有定义的迭代器):

image.png


for..of循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫作@@iterator的默认内部函数那里获得),每次循环都调用一次这个迭代器对象的next()方法,循环迭代的内容就是这些连续的返回值。


数组拥有内建的@@iterator,因此正如展现的那样,for..of对于它们很容易使用。可是让咱们使用内建的@@iterator来手动迭代一个数组,来看看它是怎么工做的:

图片


注意: 咱们使用一个ES6的Symbol:Symbol.iterator来取得一个对象的@@iterator 内部属性。咱们在本章中简单地提到过Symbol的语义(见“计算型属性名”),一样的原理适用这里。你老是但愿经过Symbol名称引用,而不是它可能持有的特殊的值,来引用这样特殊的属性。同时,与这个名称的含义无关,@@iterator自己 不是迭代器对象, 而是一个返回迭代器对象的 方法 ——一个重要的细节!


正如上面的代码段揭示的,迭代器的next()调用的返回值是一个{ value: .. , done: .. }形式的对象,其中value是当前迭代的值,而done是一个boolean,表示是否还有更多内容能够迭代。


注意值3和done:false一块儿返回,猛地一看会有些奇怪。你不得不第四次调用next()(在前一个代码段的for..of循环会自动这样作)来获得done:true,而使本身知道迭代已经完成。这个特别之处的缘由超出了咱们要在这里讨论的范围,可是它来自于ES6生成器函数的语义。


虽然数组能够在for..of循环中自动迭代,但普通的对象 没有内建的@@iterator。这种故意省略的缘由要比咱们将在这里解释的更复杂,但通常来讲,为了将来的对象类型,最好不要加入那些可能最终被证实是麻烦的实现。


可是 能够 为你想要迭代的对象定义你本身的默认@@iterator。好比:

图片


注意: 咱们使用了Object.defineProperty(..)来自定义咱们的@@iterator(很大程度上是由于咱们能够将它指定为不可枚举的),可是经过将Symbol做为一个 计算型属性名(在本章前面的部分讨论过),咱们也能够直接声明它,好比var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }。


每次for..of循环在myObject的迭代器对象上调用next()时,迭代器内部的指针将会向前移动并返回对象属性列表的下一个值(关于对象属性/值迭代顺序,参照前面的注意事项)。


咱们刚刚演示的迭代,是一个简单的一个值一个值的迭代,固然你能够为你的自定义数据结构定义任意复杂的迭代方法,只要你以为合适。对于操做用自户定义对象来讲,自定义迭代器与ES6的for..of循环相组合,是一个新的强大的语法工具。


举个例子,一个Pixel(像素)对象列表(拥有x和y的坐标值)能够根据距离原点(0,0)的直线距离决定它的迭代顺序,或者过滤掉那些“太远”的点,等等。只要你的迭代器从next()调用返回指望的{ value: .. }返回值,并在迭代结束后返回一个{ done: true }值,ES6的for..of循环就能够迭代它。


其实,你甚至能够生成一个永远不会“结束”,而且总会返回一个新值(好比随机数,递增值,惟一的识别符等等)的“无穷”迭代器,虽然你可能不会将这样的迭代器用于一个没有边界的for..of循环,由于它永远不会结束,并且会阻塞你的程序。

图片

这个迭代器会“永远”生成随机数,因此咱们当心地仅从中取出100个值,以使咱们的程序不被阻塞。


复习

JS中的对象拥有字面形式(好比var a = { .. }),和构造形式(好比var a = new Array(..))。字面形式几乎老是首选,但在某些状况下,构造形式提供更多的构建选项。


许多人错误地声称“Javascript中的一切都是对象”,这是不对的。对象是6种(或7中,看你从哪一个方面说)基本类型之一。对象有子类型,包括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()方法每次迭代一个数据。

相关文章
相关标签/搜索