JavaScript延迟执行

3
为了挑战自己,我正在用JavaScript实现LINQ(实际上是一组带有类似LINQ功能的函数)。但是,就目前而言,这些函数会立即处理数据;对于某些函数(如Sum或Aggregate),这是正确的行为,但对于其他函数(如Select或While)则是不正确的。
我很好奇是否有一种在JavaScript中可以获得与.Net相同行为的结构,在这种结构下,直到枚举集合或使用具有立即执行功能的函数之前,不会发生任何实际处理。
注:我相信这项任务(在JS中实现LINQ)已经完成。这不是重点。这是一个来自自己的挑战,这可能有助于增加我对LINQ(以及巧合的JS)的理解。除了个人启示外,我很快就要在工作中使用LINQ,根据各个项目的需要 可能会使用JS,并且我在工作之外使用JS进行一些事情。
编辑:看来我吸引了一些不熟悉LINQ的人,所以我想我应该对此进行一些解释。LINQ是.NET中的Language-INtegrated查询。LINQ允许对许多数据源(包括实际的SQL关系数据库)进行类似于SQL的查询,例如LINQ to Objects,这是我试图实现的内容之一。
LINQ的一个特性是对许多方法进行延迟执行。如果我有一个集合`customers`并调用`var query = customers.Where(c => c.Age > 40);`(或在JS中可能会变成`var query = customers.Where(function (c) { return c.Age > 40; });`),返回值是一个接口类型,集合的实际处理(仅返回包含年龄大于40岁的客户的子集)还没有发生。当我使用没有延迟执行的方法时(例如`query.First()`或`query.ToArray()`),那么所有延迟处理都会发生。这可以是链,例如`customers.Where(...).Skip(5).Select(...).OrderBy(...)`(每个“...”都是一个函数)。
简而言之,代码如下:
var collection = [1, 2, 3, 4, 5];
var query = collection.Where(function (n) { return n % 2 == 0; });
collection.push(6);
alert(query.Max());

这将导致结果为“6”。


此外,我目前正在通过在对象和数组上创建原型来实现此项目,迭代this的元素,并跳过任何函数元素。类似创建可枚举类可能更好(如果需要返回函数或匿名对象之类的内容,则实际上可能是必需的),但这就是我目前拥有的。我的函数通常显示为以下内容:

Object.prototype.Distinct = Array.prototype.Distinct = function (comparer) {
    comparer = comparer || function (a, b) { return a == b; };

    var result = [];
    for (var idx in this) {
        var item = this[idx];
        if (typeof item == "function") continue;
        if (!result.Contains(item, comparer)) result.push(item);
    }
    return result;
};

它已经实现了:LINQ for javascript。为LINQ欢呼..;) - Anirudha
2
你能举个你想支持的语法的例子吗?假设你已经实现了延迟执行的API,使用它大致会是什么样子? - Mike Edwards
@Anirudh,虽然它在JS中实现了类似LINQ的功能,但它没有LINQ的延迟执行。而且,正如我所说,我知道这已经被做过了,但这是一个个人挑战。看另一个孩子写字并不能教会孩子好的书写技巧。 - Brian S
1
延迟执行是由C#中的yield关键字引起的。实际上,这是一种语法糖,编译器将其转换为switch语句。你应该能够创建一个类似于JavaScript的模拟: http://startbigthinksmall.wordpress.com/2008/06/09/behind-the-scenes-of-the-c-yield-keyword/ - Mister Epic
@MikeEdwards,请考虑以下示例代码:var query = myCollection.Where(...).Skip(5).Select(...);(其中“...”是函数)。通过从我的函数返回数组,完全可以实现这一点,因为我的函数是原型化到Array上的,但在.Net中,除非例如我调用了query.First();,否则_没有_任何处理会发生。 - Brian S
2个回答

2

从根本上讲,您需要做的是从函数中返回对象而不是执行操作。您返回的对象将包含未来执行操作所需的代码。考虑一个示例用例:

var myCollection = [];
for(var i = 0; i < 100; i++) { myCollection.push(i); }

var query = Iter(myCollection).Where(function(v) { return v % 2 === 0; })
    .Skip(5).Select(function(v) { return v*2; });

var v;
while(v = query.Next()) {
    console.log(v);
}

我们期望的输出结果是:
20
24
28
...
188
192
196

为了实现这一点,我们定义了 .Where()、.Skip() 和 .Select() 方法,返回具有重写版本的 .Next() 方法的类实例。支持此功能的工作代码:(将跟踪设置为 true 以观察执行顺序是惰性的)
var trace = false;

function extend(target, src) {
    for(var k in src) {
        target[k] = src[k];
    }
    return target;
}

function Iter(wrapThis) {
    if(wrapThis.Next) {
        return wrapThis;
    } else {
        return new ArrayIter(wrapThis);
    }
}

Iter.prototype = {
    constructor: Iter,
    Where:  function(fn) { return new WhereIter(this, fn); },
    Skip:   function(count) { return new SkipIter(this, count); },
    Select: function(fn) { return new SelectIter(this, fn); }
};

function ArrayIter(arr) {
    this.arr = arr.slice();
    this.idx = 0;
}

ArrayIter.prototype = extend(Object.create(Iter.prototype),
{
    constructor: ArrayIter,
    Next: function() {
        if(this.idx >= this.arr.length) {
            return null;
        } else {
            return this.arr[this.idx++];
        }
    }
});

function WhereIter(src, filter) {
    this.src = src; this.filter = filter;
}

WhereIter.prototype = extend(Object.create(Iter.prototype), {
    constructor: WhereIter,
    Next: function() {
        var v;
        while(true) {
            v = this.src.Next();
            trace && console.log('Where processing: ' + v);
            if(v === null || this.filter.call(this, v)) { break; }
        }
        return v;
    }
});

function SkipIter(src, count) {
    this.src = src; this.count = count;
    this.skipped = 0;
}

SkipIter.prototype = extend(Object.create(Iter.prototype), {
    constructor: SkipIter,
    Next: function() {
        var v;
        while(this.count > this.skipped++) {
            v = this.src.Next();
            trace && console.log('Skip processing: ' + v);
            if(v === null) { return v; }
        }
        return this.src.Next();
    }
});

function SelectIter(src, fn) {
    this.src = src; this.fn = fn;
}

SelectIter.prototype = extend(Object.create(Iter.prototype), {
    constructor: SelectIter,
    Next: function() {
        var v = this.src.Next();
        trace && console.log('Select processing: ' + v);
        if(v === null) { return null; }
        return this.fn.call(this, v);
    }
});

var myCollection = [];
for(var i = 0; i < 100; i++) {
    myCollection.push(i);
}

var query = Iter(myCollection).Where(function(v) { return v % 2 === 0; })
    .Skip(5).Select(function(v) { return v*2; });

var v;
while(v = query.Next()) {
    console.log(v);

}

您也可以考虑使用“字符串lambda”来使您的查询更加易读。这样,您可以使用"v*2"而不是function(v) { return v*2; }

这大致符合我对解决方案的期望,尽管需要我花一点时间来完全消化它。关于字符串lambda,太棒了!而且也非常简短。220行代码配有详细文档和额外的健壮性。我想我会跳过functional.js(它与我试图自学的内容有些重叠),但to-function.js让事情变得非常漂亮! - Brian S
这个解决方案似乎正是我正在寻找的,标记为已回答。现在要弄清楚在这个方法中所有的函数! - Brian S

0

我不是完全清楚你想做什么,但我认为你应该研究的是defineProperty方法。你可能希望重新定义.length属性,并在读取它后执行代码。或者,如果你只想在读取属性本身时执行一次,则可以在那时执行。我不确定LINQ如何工作,甚至不知道它是什么,所以我有点含糊。无论如何,使用defineProperty,你可以做出类似以下的操作:

Object.defineProperty(o, "a", { get : function(){return 1;});

只有在访问属性时才允许执行操作(而且您还可以做更多的事情)。


这很有趣!我不确定它是否解决了我的核心问题,但如果没有别的作用,这可以让我构建一个可枚举的类,并避免对集合内容进行类型检查以查看它们是否是方法--这可能会导致解决我的延迟执行问题。然而,我有一个问题:你所说的“你可能希望做的就是重新定义[.length][2]属性,并在读取它之后才执行代码。”是什么意思? - Brian S
啊,原来这对你的情况不相关。然而,这确实使得有一个result/records属性成为可能,该属性将被动态加载和枚举。你只需要定义一个空记录属性即可。 - David Mulder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接