在JavaScript中实现对象属性的yield迭代器

Implementing a yield iterator of object properties in JavaScript

本文关键字:yield 迭代器 属性 对象 JavaScript 实现      更新时间:2023-09-26

我需要一个true迭代器,它的工作方式如下:

var haystackObj = {
        'needle': 'abc',
        'prop2': {
                'prop1': 'def',
                'prop2': {
                        'needle': 'ghi',
                },
                'needle': 'jkl',
        },
};
var needleKey = 'needle';
var iterator = {
    next: function () {
            /* 
             * WHAT CODE GOES HERE? 
             * 
             * Should return the next property, recursively, with the name 
             * equal to needleKey, of haystackObj.
             *
             */
    }
};
var value = iterator.next();
console.log(value); // -> 'abc'
value = iterator.next();
console.log(value); // -> 'ghi'
value = iterator.next();
console.log(value); // -> 'jkl'

我认为这对于for(k in o)和一流的延续来说是微不足道的,但JS没有这些。

编辑:我只能扫描haystackObj一次。

编辑2:我不是在寻找"迭代对象属性的方法"。我在寻找对象属性的迭代器这是一个巨大的区别这个问题并不像乍一看那么微不足道。

JS中不保证属性顺序。不同的发动机表现不同。(一些引擎基于字母顺序,另一些基于最后添加的顺序。)

因此,您的要求无法满足。

如果你只是想要一个迭代器而不在乎顺序,你可以看看这个问题/答案:如何模拟JavaScript产量?

这就是规范中关于属性顺序的说明:

没有指定枚举属性的机制和顺序(第一个算法中的步骤6.a,第二个算法的步骤7.a)。枚举过程中可能会删除正在枚举的对象的属性。如果在枚举期间尚未访问的属性被删除,则不会访问该属性。如果在枚举期间向要枚举的对象添加了新属性,则不能保证在活动枚举中访问新添加的属性。在任何枚举中,属性名称的访问次数不得超过一次。

然而,在现实中,你可以从大多数浏览器中期待一个特定的顺序:元素顺序在一个";对于(…in…)";环路

我认为实现伪生成器的唯一方法(根据实际订单适合您的事实)是复制您的对象,并在需要时删除副本的扫描属性。这意味着你不会对相同的属性进行两次重新扫描。一些代码示例:

var Iterator = function() {
    var copy = $.extend(haystackObj, true);
    // ^ using jQuery's extend for a quick function, but use w/e you want.
    // Anyway keep it in a closure. This copy will have its properties deleted
    // after each iteration.
    return {
        next: function next() {
            var found = false,
                needle;
            for (var prop in copy) {
                if (typeof copy[prop] === 'object') {
                    // Since next() doesn't take any argument...
                    // That's a bad solution. You should use an inner function
                    // to recurse. But I'm going to bed right now!
                    var copyCopy = $.extend(copy, true);
                    copy = copy[prop];
                    found = next();
                    copy = copyCopy;
                }
                else {
                    if (prop === needleKey) {
                        found = true;
                    }
                }
                if (found) {
                    needle = copy[prop];
                }
                // Delete the current property to simulate a real generator.
                delete copy[prop];
                if (found) {
                    return needle;
                }
            }
        }
    };
};
// Usage:
var iterator = Iterator();
iterator.next(); // "abc"

这段代码不起作用(请参阅jsfiddle),我要睡觉了。但你可以看到它的发展方向,以及你如何做出一些东西。

假设我正确理解你,并记住这不是"真正的收益",并将所有代码放在你想要的地方,

var iterator = {
    next: function () {
        /* 
        * WHAT CODE GOES HERE? 
        * 
        * Should return the next property, recursively, with the name 
        * equal to needleKey, of haystackObj.
        *
        */
        var values=[], findneedles;
        findneedles = function(o){
            var k;
            for(k in o){
                if(k === needleKey){
                    values.push(o[k]);
                }else if(typeof o[k] === 'object'){
                    findneedles(o[k]);
                }
            }
        };
        findneedles(haystackObj);
        this.next = function(){
            return values.shift();
        };
        return values.shift();
    }
};

尽管Florian Margaine的回答指出属性的顺序取决于js引擎,但该解决方案在chrome中有效。我花了一点时间,但现在是了http://jsfiddle.net/6zCkJ/3/:编辑(这个解决方案是在OP说树只能处理一次之前完成的)

var needleKey = 'needle';
var currIndex = 0;
var runningIndex = 0;
var getValueByIndex = function (obj) {
    var objToSearch = obj || haystackObj;
    for (var x in objToSearch) {
        if (x == needleKey) {
            if (runningIndex == currIndex)  {
                currIndex += 1;
                return objToSearch[x];
            }
            runningIndex += 1;
        } else if (typeof objToSearch[x] == 'object') {
            var found = getValueByIndex(objToSearch[x]);
            if (found) return found;
        }
    }
}
var iterator = {
    next: function () {
        runningIndex = 0;
        return getValueByIndex(0);
    }
};

另一种只遍历树一次的方法如下http://jsfiddle.net/6zCkJ/6/.问题是,每当指针更新时,都必须加载值数组:

var currIndex = 0;
var valuesArray = [];
var loadValues = function (obj) {
    var objToSearch = obj || haystackObj;
    for (var x in objToSearch) {
        if (x == needleKey) {
            valuesArray.push(objToSearch[x])
        } else if (typeof objToSearch[x] == 'object') {
            loadValues(objToSearch[x]);
        }
    }
}
loadValues();
console.log(valuesArray);
var iterator = {
    next: function () {
        return valuesArray[currIndex++];
    }
};

编辑:到目前为止,这里发布的所有答案都涉及到必须至少在整个树上导航一次或多次,这不是OP想要的,包括必须复制对象并在遍历时删除属性。不过,有一种解决方案涉及在对象遍历时用元数据标记对象,这允许您在下次遇到对象时跳过它们。使用我的第一种方法,添加这些优化并有望完成OP的请求将是相当琐碎的。

好吧,所以我忍不住想让它发挥作用。以下是我的做法http://jsfiddle.net/6zCkJ/12/。你可以看到,我将找到的对象存储在foundObjects对象中,其中的键由该对象的路径组成,所以你可以快速查找,看看它是否已经被递归。numFound用于正确地递增正在运行的索引。我还没有对此进行大量测试,但这应该是一个良好的开端:

var Iterator = function () {
    var needleKey = 'needle';
    var currIndex = 0;
    var runningIndex = 0;
    var foundObjects = {};
    var getValueByIndex = function (obj,currentPath) {
        var objToSearch = obj || haystackObj;
        for (var x in objToSearch) {
            currentPath += x + '_';
            if (x == needleKey) {
                if (runningIndex == currIndex) {
                    currIndex += 1;
                    if (!foundObjects[currentPath]) {
                        foundObjects[currentPath] = {
                            numFound: 0,
                            finished: false
                        };
                    }
                    foundObjects[currentPath].numFound += 1;
                    return objToSearch[x];
                }
                runningIndex += 1;
            } else if (typeof objToSearch[x] == 'object') {
                if (foundObjects[currentPath] && foundObjects[currentPath].finished) {
                    runningIndex += foundObjects[currentPath].numFound;
                } else {
                    var found = getValueByIndex(objToSearch[x],currentPath);
                    if (found) {
                        return found;
                    }
                }
            }
            if (!foundObjects[currentPath]) {
                foundObjects[currentPath] = {
                    numFound: 0,
                    finished: true
                };
            }
            foundObjects[currentPath].finished = true;
        }
    }
    this.next = function () {
        runningIndex = 0;
        return getValueByIndex(0,'');
    }
};
var iterator = new Iterator();
var value = iterator.next();

我将为子孙后代回答这个问题。

在ECMAScript 6中,我们有一个yield语句。但假设你疯了,你想现在就使用这个功能。使用traceur编译器编译到普通的旧JavaScript,我们得到以下内容。

输入:

var iterator = function* (object) {
        for(var key in object) {
                yield key;
                for( k of iterator(object[key]) ) {
                        yield k;
                }
        }
};
var o = {
        a: 10,
        b: 11,
        c: {
                ca: 12,
                cb: 13,
        },
        d: 14,
};
var res = [];
for( key of iterator(o) ) {
        res.push(key);
}
res;

输出

var $__generatorWrap = function(generator) {
        return $traceurRuntime.addIterator({
                next: function(x) {
                        switch (generator.GState) {
                                case 1:
                                        throw new Error('"next" on executing generator');
                                case 3:
                                        throw new Error('"next" on closed generator');
                                case 0:
                                        if (x !== undefined) {
                                        throw new TypeError('Sent value to newborn generator');
                                }
                                case 2:
                                        generator.GState = 1;
                                if (generator.moveNext(x, 0)) {
                                        generator.GState = 2;
                                        return {
                                                value: generator.current,
                                                done: false
                                        };
                                }
                                generator.GState = 3;
                                return {
                                        value: generator.yieldReturn,
                                        done: true
                                };
                        }
                },
                'throw': function(x) {
                        switch (generator.GState) {
                                case 1:
                                        throw new Error('"throw" on executing generator');
                                case 3:
                                        throw new Error('"throw" on closed generator');
                                case 0:
                                        generator.GState = 3;
                                throw x;
                                case 2:
                                        generator.GState = 1;
                                if (generator.moveNext(x, 1)) {
                                        generator.GState = 2;
                                        return {
                                                value: generator.current,
                                                done: false
                                        };
                                }
                                generator.GState = 3;
                                return {
                                        value: generator.yieldReturn,
                                        done: true
                                };
                        }
                }
        });
};
var iterator = function(object) {
        var $that = this;
        var $arguments = arguments;
        var $state = 20;
        var $storedException;
        var $finallyFallThrough;
        var $__0;
        var $__1;
        var $__2;
        var $__3;
        var $__4;
        var $__5;
        var key;
        var $G = {
                GState: 0,
                current: undefined,
                yieldReturn: undefined,
                innerFunction: function($yieldSent, $yieldAction) {
                        while (true) switch ($state) {
                                case 20:
                                        $__2 = [];
                                $state = 21;
                                break;
                                case 21:
                                        $__3 = object;
                                $state = 23;
                                break;
                                case 23:
                                        for (var $__4 in $__3) $__2.push($__4);
                                $state = 25;
                                break;
                                case 25:
                                        $__5 = 0;
                                $state = 17;
                                break;
                                case 17:
                                        if ($__5 < $__2.length) {
                                        $state = 12;
                                        break;
                                } else {
                                        $state = 19;
                                        break;
                                }
                                case 11:
                                        $__5++;
                                $state = 17;
                                break;
                                case 12:
                                        key = $__2[$__5];
                                $state = 13;
                                break;
                                case 13:
                                        if (!(key in $__3)) {
                                        $state = 11;
                                        break;
                                } else {
                                        $state = 15;
                                        break;
                                }
                                case 15:
                                        this.current = key;
                                $state = 1;
                                return true;
                                case 1:
                                        if ($yieldAction == 1) {
                                        $yieldAction = 0;
                                        throw $yieldSent;
                                }
                                $state = 3;
                                break;
                                case 3:
                                        $__0 = $traceurRuntime.getIterator(iterator(object[key]));
                                $state = 7;
                                break;
                                case 7:
                                        if (!($__1 = $__0.next()).done) {
                                        $state = 8;
                                        break;
                                } else {
                                        $state = 11;
                                        break;
                                }
                                case 8:
                                        k = $__1.value;
                                $state = 9;
                                break;
                                case 9:
                                        this.current = k;
                                $state = 5;
                                return true;
                                case 5:
                                        if ($yieldAction == 1) {
                                        $yieldAction = 0;
                                        throw $yieldSent;
                                }
                                $state = 7;
                                break;
                                case 19:
                                        $state = -2;
                                case -2:
                                        return false;
                                case -3:
                                        throw $storedException;
                                default:
                                        throw "traceur compiler bug: invalid state in state machine: " + $state;
                        }
                },
                moveNext: function($yieldSent, $yieldAction) {
                        while (true) try {
                                return this.innerFunction($yieldSent, $yieldAction);
                        } catch ($caughtException) {
                                $storedException = $caughtException;
                                switch ($state) {
                                        default:
                                                this.GState = 3;
                                        $state = -2;
                                        throw $storedException;
                                }
                        }
                }
        };
        return $__generatorWrap($G);
};
var o = {
        a: 10,
        b: 11,
        c: {
                ca: 12,
                cb: 13
        },
        d: 14
};
var res = [];
for (var $__1 = $traceurRuntime.getIterator(iterator(o)), $__0; !($__0 = $__1.next()).done;) {
        key = $__0.value;
        {
                res.push(key);
        }
}
res;

所以yield语句在JavaScript中是可能的,但非常不切实际。

我最终实际使用了什么

用法示例:

var object = {...};
var callback = function (key, value) {
        // Do stuff...
        return traverse.CONTINUE; 
        // or return traverse.STOP if you want the iteration to stop
};
traverse(object, callback);

实施:

var traverse = (function () {
        var _traverse = function (object, callback) {
                var key, value, command;
                for( key in object ) {
                        value = object[key];
                        command = callback(key, value);
                        if( command === _traverse.STOP ) {
                                return _traverse.STOP;
                        }
                        command = _traverse(value, callback);
                        if( command === _traverse.STOP ) {
                                return _traverse.STOP;
                        }
                }
        };
        _traverse.CONTINUE = 1;
        _traverse.STOP = 2;
        return _traverse;
})();