【问题标题】:Implementing a yield iterator of object properties in JavaScript在 JavaScript 中实现对象属性的 yield 迭代器
【发布时间】:2013-01-19 16:54:37
【问题描述】:

我需要一个 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 一次。

EDIT2:我不是在寻找“一种遍历对象属性的方法”。我正在寻找对象属性的iterator这是一个巨大的差异。这个问题并不像乍一看那么微不足道。

【问题讨论】:

  • “def”怎么了?为什么在“jkl”之前输出“ghi”?
  • 你有什么尝试吗?看起来你只是想让 SO 为你制作代码......
  • 顺便说一句:属性顺序未在 JS 中认证。您的要求无法满足。
  • JS什么时候输for( var key in obj )??
  • 你们中的大多数人都没有抓住问题的重点,不是吗。他想要 JS 中的“屈服”语义。他想要迭代一个类似“树”的结构并将控制权返回给找到“键”的调用者,然后继续迭代。

标签: javascript continuations


【解决方案1】:

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

在 ECMAScript 6 中,我们有一个 yield 语句。但是,假设您很疯狂,并且想现在使用此功能。使用 traceur-compiler 编译到您的普通旧 JavaScript,我们得到 following

输入:

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;
})();

【讨论】:

    【解决方案2】:

    虽然 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/。问题是每次更新 needle 时都必须加载 values 数组:

    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();
    

    【讨论】:

    • 不错的一个。它也适用于 Firefox 和 Opera。不幸的是,无法在 IE 上进行测试...但是,您在每次迭代时都在扫描 objToSearch。不是 OP 想要的。
    • 我不是为了表演,但你绝对正确
    • @Nick - 只是一个想法,但我不打算实现这个,你可以采用我的第一种方法并添加一个已经遍历对象的列表,你可以跳过它进行下一次扫描(与删除不完全相同)。这样你就不需要提前“复制”整个 obj
    • @JustinBicknell 我认为这可能是迄今为止两全其美的。一种“快速转发”到最后一个属性。您需要保留这些链,因为您正在进行深度搜索,但仍然可行。我需要衡量的是 for(k in o) 到我真正感兴趣的第一个键的成本。我怀疑复制对象无论如何都会更昂贵......
    • 好吧,聊了聊,确实是最高效的解决方案。不过,如果我的解决方案成为瓶颈,我会选择它,因为设置起来很麻烦。
    【解决方案3】:

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

    您的要求因此无法满足。

    如果您只想要一个迭代器而不介意顺序,您可以看看这个问题/答案:How to simulate JavaScript yield?

    这是the spec says关于属性顺序的内容:

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

    然而,实际上,您可以期望大多数浏览器的特定顺序:Elements order in a "for (… in …)" loop

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

    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),我要睡觉了。但是你可以看到它的发展方向以及你如何制作一些东西。

    【讨论】:

    • 你赢得了比赛。恭喜!
    • 很好的答案。这在规范的 12.6.4 中指定
    • 这是迄今为止唯一有效的答案,为什么我应该对一个完全有效的问题投反对票,我不知道。
    • 唯一半相关的答案,看来这里很多人根本不知道存在发电机这种东西。
    • 顺便说一句,我在问问题之前查看了规格并得出了相同的结论。我还查看了 Object.keys 和朋友。所以我问我是否错过了 JS 的一些角落和缝隙。嗯。
    【解决方案4】:

    假设我对您的理解正确,并记住这不是“真正的收益”,并将所有代码放在您似乎想要的地方,

    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();
        }
    };
    

    【讨论】:

    • 这个问题是它不是一个迭代器。它预先扫描整个对象,然后使用这些值。迭代器只会扫描到下一个匹配项并保留状态。
    • 是的,我已经说过了。您也可以只递归结构并传入回调。但是在没有任何解释为什么这是不可接受的情况下......
    • 这是不可接受的原因是因为它比迭代器的计算成本更高。 Justin Bicknell 已经在此处发布了您正在采用的方法的示例(请参阅他的第二个 sn-p)。这没有回答问题。
    • 是的,这与 Justin Bicknell 的解决方案完全相同,但是您将结果转移到数组中。检查我的cmets他的答案。对象可能非常大,迭代器的全部意义在于不预先支付搜索价格。
    • 然后在完成后递归并抛出异常。 (是的;当我写我的答案时,我还没有看到他的第二个答案。)
    猜你喜欢
    • 2016-02-26
    • 2011-06-18
    • 2010-11-08
    • 2013-07-10
    • 1970-01-01
    • 2015-08-29
    • 2023-03-24
    • 2015-09-22
    • 2013-04-04
    相关资源
    最近更新 更多