第52题 实现一个对象被for of遍历

for…of的工作原理

for…of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。

数组可以直接使用for…of遍历是因为数组内置了迭代器

让对象支持for…of

让对象支持for…of的办法就是手动给对象添加迭代器

    var myObject = { a: 1, b: 2, c: 3 };
    
    //写法一:简单写法
    myObject[Symbol.iterator] = function(){
        const _this = this
        //也可使用: keys = Object.getOwnPropertyNames(this)
        const keys = Object.keys(this)
        let index = 0
        return {
          next(){
            return {
              value: _this[keys[index++]],
              done: index>keys.length
            }
          }
        }
    }
    //写法二:标准写法,可以指定属性描述符
    Object.defineProperty( myObject, Symbol.iterator, {
    	enumerable: false,
    	writable: false,
    	configurable: true,
    	value: function() {
    		const _this = this
        //也可使用: keys = Object.getOwnPropertyNames(this)
        const keys = Object.keys(this)
        let index = 0
        return {
          next(){
            return {
              value: _this[keys[index++]],
              done: index>keys.length
            }
          }
        }
    	}
    });
    
    // 手动遍历 myObject
    var it = myObject[Symbol.iterator]();
    it.next(); // { value:1, done:false }
    it.next(); // { value:2, done:false }
    it.next(); // { value:3, done:false }
    it.next(); // { value:undefined, done:true }
    
    // 用 for..of 遍历 myObject
    //不要指望遍历结果总是(1,2,3),因为Object.keys()的无序性
    for (var v of myObject) {
    	console.log( v );
    }
    // 1
    // 2
    // 3

拥有迭代器的对象我们叫做iterable (就像上面的myObject),而迭代器叫做iterator,这是两个不同的概念

从上面的编码可以看出,给一个对象定义迭代器的步骤如下:

  1. 给对象添加一个名称为Symbol.iterator的属性方法
  2. 这个方法必须返回一个迭代器对象,它的结构必须如下:
    {
    	next: function() {
    		return {
    			value: any, //每次迭代的结果
    			done: boolean //迭代结束标识
    		}
    	}
    }
  • donetrue时候遍历结束
  • Symbol.iterator是一个内置符号

可复用的对象迭代器添加(通过原型委托)

想一想,如果有很多对象(但不是所有对象都需要)都想要使用for…of怎么办?你可以把前面介绍的为对象添加迭代器的代码封装成函数来复用,没有任何问题,不过下面要介绍的是通过原型委托来复用的写法:

    //首先创建一个基于对象原型扩展的iterable,并给它添加一个迭代器
    const iterable = Object.create(Object.prototype,{
    	[Symbol.iterator]:  {
            enumerable: false,
            writable: false,
            configurable: true,
            value: function() {
                const _this = this
                //也可使用: keys = Object.getOwnPropertyNames(this)
                const keys = Object.keys(this)
                let index = 0
                return {
                    next(){
                        return {
                            value: _this[keys[index++]],
                            done: index>keys.length
                        }
                    }
                }
    		}
    	}
    })
    
    //使用:
    var myObject = { a: 1, b: 2, c: 3 };
    var myObject2 = { x: "x", y: "y", z: "z" }
    
    //替换myObject的原型, 使myObject可迭代
    //为了不丢失对象myObject原有的原型中的东西
    //iterable在创建时将原型设为了Object.prototype
    Object.setPrototypeOf(myObject,iterable)
    
    myObject.d = 4
    
    for(let item of myObject){
      console.log(item)
    }
    //1
    //2
    //3
    //4
    
    //使myObject2可迭代
    Object.setPrototypeOf(myObject2,iterable) 
    for(let item of myObject2){
      console.log(item)
    }
    //x
    //y
    //z

上面的做法有一个问题,就是如果你的myObject已经修改过原型了再调用Object.setPrototypeOf(myObject2,iterable) ,这意味着原来的原型会丢失,下面介绍解决办法:

    //定义一个函数用于给obj添加迭代器
    function iterable(obj){
        if(Object.prototype.toString.call(obj) !== "[object Object]"){
        	return //非对象,不处理
    	}
    	if(obj[Symbol.iterator]){
    		return //避免重复添加
    	}
    	const it = Object.create(Object.getPrototypeOf(obj), {
    		[Symbol.iterator]:  {
    	        enumerable: false,
    	        writable: false,
    	        configurable: true,
    	        value: function() {
    	            const _this = this
    	            //也可使用: keys = Object.getOwnPropertyNames(this)
    	            const keys = Object.keys(this)
    	            let index = 0
    	            return {
    	                next(){
    	                    return {
    	                        value: _this[keys[index++]],
    	                        done: index>keys.length
    	                    }
    	                }
    	            }
    			}
    		}
    	})
    	Object.setPrototypeOf(obj, it)
    }
    
    //使用:
    var myObject = { a: 1, b: 2, c: 3 };
    
    iterable(myObject)// 让myObject可迭代
    
    myObject.d = 4
    
    for(let item of myObject){
      console.log(item)
    }
    //1
    //2
    //3
    //4

因为创建it时将it的原型指定为了obj的原型( Object.getPrototypeOf(obj) ),然后又将obj的原型指定为了it (Object.setPrototypeOf(obj, it)), 所以obj通过原型链可以找到原来的原型,丢失的问题也就解决了

让所有对象支持for…of

如果你想所有对象都支持for…of,给每个对象都去添加迭代器是比较繁琐的(即使你像上面那样实现了添加的复用),有一个办法就是直接给对象的原型添加迭代器,要指出的是这样做可能会有一些副作用,Object.prototype位于各种类型的原型链顶端,影响面会非常广,ES6本可以这样做,但是它却没这样做(肯定是有原因的),所以建议按需添加会比较好

    //在对象的原型上直接添加迭代器
    Object.prototype[Symbol.iterator] = function(){
        const _this = this
        const keys = Object.keys(this)
        let index = 0
    	return {
    		next(){
            	return {
    	        	value: _this[keys[index++]],
    	        	done: index>keys.length
            	}
            }
        }
    }
    
    //使用:
    var myObject = { a: 1, b: 2, c: 3 };
    for(let item of myObject){//这就像myObject本来就支持for...of一样
      console.log(item)
    }
    //1
    //2
    //3

for…of原理模拟

针对添加过迭代器的myObject,下面代码模拟了for…of的内部原理:

    //while版本模拟:
    //获得一个myObject的迭代器对象
    let it1 = myObject[Symbol.iterator]() 
    let item1
    while(!(item1 = it1.next()).done){
    	console.log(item1.value)
    }
    
    //for版本模拟:
    //获得一个myObject的迭代器对象(新的)
    let it2 = myObject[Symbol.iterator]() 
    let item2 = it2.next()
    for(; !item2.done; item2 = it2.next()){
    	console.log(item2.value)
    }
Last Updated:
Contributors: leeguooooo