ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 2.3 对象 ### 2.3.1 语法 定义对象方法: * 文字声明形式(常用) ~~~ var myObj = { key: value // ... }; ~~~ * 构造形式 ~~~ var myObj = new Object(); myObj.key = value; ~~~ 构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键/ 值对,但是在构造形式中你必须逐个添加属性。 ### 2.3.2 类型 对象是JavaScript 的基础。在JavaScript 中一共有六种主要类型(术语是“**语言类型**”): ~~~ • string • number • boolean • null • undefined • object ~~~ 注意,**简单基本类型(string、boolean、number、null 和undefined)本身并不是对象。**null 有时会被当作一种对象类型,但是这其实只是语言本身的一个bug,即对null 执行typeof null 时会返回字符串"object"。实际上,null 本身是基本类型。 #### 内置对象 JavaScript 中还有一些对象子类型,通常被称为**内置对象**。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。 ~~~ • String • Number • Boolean • Object • Function • Array • Date • RegExp • Error ~~~ 这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如Java中的String 类。 但是在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] ~~~ ### 2.3.3 内容 对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,称之为**属性**。 在引擎内部,属性的值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。 ~~~ var myObject = { a: 2 }; myObject.a; // 2 myObject["a"]; // 2 ~~~ 如果要访问myObject 中a 位置上的值,我们需要使用` . `操作符或者` []` 操作符。`.a` 语法通常被称为“**属性访问**”,`["a"]` 语法通常被称为“**键访问**”。 这两种语法的主要区别在于`. `操作符要求属性名满足标识符的命名规范,而`[".."] `语法可以接受任意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" ~~~ **1. 可计算属性名** ES6 增加了可计算属性名,可以在文字形式中使用[] 包裹一个表达式来当作属性名: ~~~ var prefix = "foo"; var myObject = { [prefix + "bar"]:"hello", [prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world ~~~ 可计算属性名最常用的场景可能是ES6 的符号(Symbol),它们是一种新的基础数据类型,包含一个不透明且无法预测的值(从技术角度来说就是一个字符串)。 **2. 属性与方法** 由于函数很容易被认为是属于某个对象,在其他语言中,属于对象(也被称为“类”)的函数通常被称为“方法”,因此把“属性访问”说成是“方法访问”。 JavaScript 的语法规范也做出了同样的区分。从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”不妥。 无论返回值是什么类型,每次访问对象的属性就是**属性访问**。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定this)。 ~~~ function foo() { console.log( "foo" ); } var someFoo = foo; // 对foo 的变量引用 var myObject = { someFoo: foo }; foo; // function foo(){..} someFoo; // function foo(){..} myObject.someFoo; // function foo(){..} ~~~ `someFoo`和`myObject.someFoo `只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。如果foo() 定义时在内部有一个this 引用,那这两个函数引用的唯一区别就是myObject.someFoo 中的this 会被隐式绑定到一个对象。无论哪种引用形式都不能称之为“方法”。 即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用。 ~~~ var myObject = { foo: function() { console.log( "foo" ); } }; var someFoo = myObject.foo; someFoo; // function foo(){..} myObject.foo; // function foo(){..} ~~~ **3. 数组** 数组也支持[] 访问形式,数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数。 ~~~ var myArray = [ "foo", 42, "bar" ]; myArray.length; // 3 myArray[0]; // "foo" myArray[2]; // "bar" ~~~ 数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性: ~~~ 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. 复制对象** ~~~ 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)不会被复制到目标对象。 **5. 属性描述符** 从ES5 开始,所有的属性都具备了属性描述符,用于检测属性特性。 ~~~ 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 添加了一个普通的属性并显式指定了一些特性。然而,一般不会使用这种方式,除非你想修改属性描述符。 * 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 错误表示无法修改一个不可写的属性。 * 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 操作之后,这个未引用的对象/ 函数就可以被垃圾回收。但是,不要把delete 看作一个释放内存的工具(就像C/C++ 中那样),它就是一个删除对象属性的操作,仅此而已。 * Enumerable : 控制属性是否会出现在对象的属性枚举中 比如说for..in 循环。如果把enumerable 设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成true 就会让它出现在枚举中。 用户定义的所有的普通属性默认都是enumerable,这通常就是你想要的。但是如果你不希望某些特殊属性出现在枚举中,那就把它设置成enumerable:false。 **6. 不变性** 有时候你会希望**属性或者对象是不可改变**(无论有意还是无意)的,在ES5 中可以通过很多种方法来实现。 所有的方法创建的都是浅不变形,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的: ~~~ myImmutableObject.foo; // [1,2,3] myImmutableObject.foo.push( 4 ); myImmutableObject.foo; // [1,2,3,4] ~~~ 假设代码中的myImmutableObject 已经被创建而且是不可变的,但是为了保护它的内容myImmutableObject.foo,你还需要使用下面的方法让foo 也不可变。 * 对象常量 结合`writable:false` 和`configurable:false` 就可以创建一个真正的常量属性(不可修改、重定义或者删除): ~~~ var myObject = {}; Object.defineProperty( myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false } ); ~~~ * 禁止扩展 如果你想禁止一个对象添加新属性并且保留已有属性, 可以使用`Object.preventExtensions(..)`: ~~~ 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`,这样就无法修改它们的值。 这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(但这个对象引用的其他对象是不受影响的)。 你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用Object.freeze(..)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。 **7. [[get]]** 属性访问在实现时有一个微妙却非常重要的细节,思考下面的代码: ~~~ var myObject = { a: 2 }; myObject.a; // 2 ~~~ 在语言规范中,myObject.a 在myObject 上实际上是实现了`[[Get]]` 操作(有点像函数调用:`[[Get]]()`)。对象默认的内置[[Get]] 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。 然而,如果没有找到名称相同的属性,按照[[Get]] 算法的定义会执行另外一种非常重要的行为。(其实就是遍历可能存在的[[Prototype]] 链,也就是原型链)。 如果无论如何都没有找到名称相同的属性,那[[Get]] 操作会返回值undefined: ~~~ var myObject = { a:2 }; myObject.b; // undefined ~~~ 注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出一个ReferenceError 异常: ~~~ var myObject = { a: undefined }; myObject.a; // undefined myObject.b; // undefined ~~~ 从返回值的角度来说,这两个引用没有区别——它们都返回了undefined。然而,尽管乍看之下没什么区别,实际上底层的[[Get]] 操作对`myObject.b` 进行了更复杂的处理。 由于仅根据返回值无法判断出到底变量的值为undefined 还是变量不存在,所以[[Get]]操作返回了undefined。 **8. [[put]]** [[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。 如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容。 * 属性是否是访问描述符(下面第9点)?如果是并且存在setter 就调用setter。 * 属性的数据描述符中writable 是否是false ?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError 异常。 * 如果都不是,将该值设置为属性的值。 **9. Getter和Setter** 对象默认的[[Put]] 和[[Get]] 操作分别可以控制属性值的设置和获取。 在ES5 中可以使用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(..) 中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值: ~~~ var myObject = { // 给 a 定义一个getter get a() { return 2; } }; myObject.a = 3; myObject.a; // 2 ~~~ 由于只定义了a 的getter,所以对a 的值进行设置时set 操作会忽略赋值操作,不会抛出错误。而且即便有合法的setter,由于自定义的getter 只会返回2,所以set 操作是没有意义的。 为了让属性更合理,还应当定义setter,setter 会覆盖单个属性默认的[[Put]](也被称为赋值)操作。通常来说getter 和setter 是成对出现的(只定义一个的话通常会产生意料之外的行为): ~~~ var myObject = { // 给 a 定义一个getter get a() { return this._a_; }, // 给 a 定义一个setter set a(val) { this._a_ = val * 2; } }; myObject.a = 2; myObject.a; // 4 ~~~ **10. 存在性** 前面说过,如myObject.a 的属性访问返回值可能是undefined,但是这个值有可能是属性中存储的undefined,也可能是因为属性不存在所以返回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]] 链。 所有的普通对象都可以通过对于Object.prototype 的委托 来访问hasOwnProperty(..), 但是有的对象可能没有连接到Object.prototype( 通过Object.create(null) 来创建)。在这种情况下,形如myObejct.hasOwnProperty(..)就会失败。 这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基础的hasOwnProperty(..) 方法并把它显式绑定到myObject 上。 * 枚举 ~~~ 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 操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。 ~~~ 在数组上应用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(..) 得到的属性列表——只包含可枚举属性。 ### 2.3.4 遍历 对于数值索引的数组来说,可以使用标准的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(或者“假”值),`some(..) `会一直运行直到回调函数返回true(或者“真”值)。 every(..) 和some(..) 中特殊的返回值和普通for 循环中的break 语句类似,它们会提前终止遍历。 使用for..in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。 那么如何直接遍历值而不是数组下标(或者对象属性)呢?ES6 增加了一种用来遍历数组的`for..of `循环语法(如果对象本身定义了迭代器的话也可以遍历对象): ~~~ var myArray = [ 1, 2, 3 ]; for (var v of myArray) { console.log( v ); } // 1 // 2 // 3 ~~~ `for..of` 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next() 方法来遍历所有返回值。 `for..of`会寻找内置或者自定义的@@iterator 对象并调用它的next() 方法来遍历数据值。 我们使用内置的@@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.iterator` 来获取对象的`@@iterator` 内部属性。`@@iterator` 本身并不是一个迭代器对象,而是一个返回迭代器对象的函数。 和数组不同,普通的对象没有内置的`@@iterator`,所以无法自动完成for..of 遍历。 当然,你可以给任何想遍历的对象定义`@@iterator`,举例来说: ~~~ var myObject = { a: 2, b: 3 }; Object.defineProperty( myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this; var idx = 0; var ks = Object.keys( o ); return { next: function() { return { value: o[ks[idx++]], done: (idx > ks.length) }; } }; } } ); // 手动遍历myObject var it = myObject[Symbol.iterator](); it.next(); // { value:2, done:false } it.next(); // { value:3, done:false } it.next(); // { value:undefined, done:true } // 用for..of 遍历myObject for (var v of myObject) { console.log( v ); } // 2 // 3 ~~~