65.9K
CodeProject 正在变化。 阅读更多。
Home

Arrgh.js - 将 LINQ 带入 JavaScript

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

5.00/5 (68投票s)

2016 年 11 月 28 日

CPOL

43分钟阅读

viewsIcon

81937

downloadIcon

209

创建一个轻量级的 JavaScript 库,为 JavaScript 带来真正的 .NET 类集合和 LINQ。

目录

引言

在本文中,我想谈谈我创建并发布到 npm 和 NuGet 的一个 JavaScript 库。这个库被称为 arrgh.js,它为 JavaScript 带来了真正的 .NET 类集合和 LINQ。如果你熟悉 C# 中的 LINQ,那么你就已经知道 arrgh.js 的工作方式了!

在本文中,我们将了解 arrgh.js 是什么,它是如何创建的,如何进行测试和文档编写,以及如何发布到 npmNuGet。在本文撰写时,我已经将代码添加到了本文中。要获取最新版本的代码,您可以查看 GitHub 仓库。您还可以查看 完整的文档

我非常确定我们都曾或多或少地使用过 JavaScript 中的数组。
一个具有 forEach 函数的数组,但仅从 IE9 开始支持(当然,我的客户需要 IE8 支持)。
一个最近才添加了 contains 函数支持的数组,但将其命名为 includes(我最近读到他们选择 includes 而不是 contains,因为将 contains 添加到标准中会破坏一个流行的框架,真是的……)。
一个既是队列又是栈的数组,并且在某种程度上部分是一个列表,可以添加元素,但不能删除元素。
删除一个元素就像搜索要删除元素的索引一样繁琐,将数组在该点分割,跳过一个元素,然后将剩余的部分重新组合起来。手动操作。
您还记得是需要 splice 还是 slice 吗?

总而言之,我发现数组是一大令人沮丧(事实上,对于整个 JavaScript 来说都可以这么说)的东西。不用说,我开始寻找替代方案。基本上,我想要的是 C# 风格的集合,并支持 JavaScript 中的 LINQ。当然,这之前已经有人做过了,但我找到的库并不满足我所有的要求,不在旧浏览器中工作,文档不够充分,没有惰性求值,缺少诸如 Dictionary (HashMap) 之类的集合类型,或者没有按照我想要的方式实现它们。我找到的最好的库是 linq.js,但它想模仿 C# 的方式,将所有内容都写成 PascalCase,而 JavaScript 使用 camelCase(后来我发现我下载的是旧版本,因为最新版本确实使用了 camelCase)。

于是我决定自己构建 JavaScript 集合和 LINQ。也因为它真的很有趣。我称之为 arrgh.js,它是 array(数组)和 argh!(表示沮丧的感叹词)的组合,后者是我在使用 JavaScript,尤其是数组时发出的沮丧的尖叫。

如果您想跳过一切,直接开始工作,可以使用 npm 或 NuGet 进行安装。

npm install arrgh.js

Install-Package arrgh.js

代码

许多编程冒险都始于一个空白的文本文件,我的也是。我创建了一个名为 arrgh.js 的项目文件夹,然后在其中创建了一个名为 src 的源文件文件夹。在该文件夹中,我创建了 arrgh.js 文件并开始编写。我基本上有两个选择:扩展 JavaScript 数组类(这被认为是糟糕的做法,可能会在未来破坏 JavaScript 数组或我的实现),或者从头开始创建我自己的集合对象。我选择了后者。

arrgh.Enumerable

我的第一个实现只是一个数组包装器。简单而粗糙。

var Enumerable = function (arr) {
   this.arr = arr;
};

Enumerable.prototype.forEach = function (callback) {
    var i;
    for (i = 0; i < this.arr.length; i += 1) {
        callback(this.arr[i], i);
    }
};

// Usage.
var e = new Enumerable(["Hello", "Enumerable"]);
e.forEach(function (s) {
    console.log(s);
});

然后我以几乎相同的方式实现了 whereselect

Enumerable.prototype.where = function (predicate) {
    var filtered = [];
    this.forEach(function (e, i) {
        if (predicate(e, i)) {
            filtered.push(e);
        }
    });
    return filtered;
};

// Usage.
var e = new Enumerable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
var evens = e.where(function (n) {
    return n % 2 === 0;
});

尽管这可能很简单,但它无法胜任。想象一下,在后期,执行以下操作:

Enumerable.range(0, 1000000).where(isEven).take(10);

它现在将遍历一百万个元素,检查它们是否为偶数,然后取 10 个。如果我们想要 10 个,我们不需要遍历超过 20 个元素!所以,让我们编写代码,使其能够根据需要惰性求值元素。我们将实现 迭代器模式。这允许我们逐个遍历集合的元素,这意味着如果我们从一个理论上无限的集合中请求下一个 20 个元素,我们只需要计算前 20 个元素。

因此,Enumerable 将实现一个 getIterator 方法(我想称之为 getEnumerator,就像 C# 中一样,但 Enumerator 在 JavaScript 中已经被占用了)。getIterator 将返回一个可以返回集合下一个元素的。当然,这也意味着我们必须重写 forEach 方法。

var ArrayIterator = function (arr) {
    var currentIndex = -1;
    this.moveNext = function () {
        currentIndex += 1;
        // Return whether more elements are available.
        return currentIndex < arr.length;
    }
    this.current = function () {
        return arr[currentIndex];
    };
};

var Enumerable = function (arr) {
    this.getIterator = function () {
        return new ArrayIterator(arr);
    }
};

Enumerable.prototype.forEach = function (callback) {
    var iterator = this.getIterator();
    var currentIndex = 0;
    while (iterator.moveNext()) {
        callback(iterator.current(), currentIndex);
        currentIndex += 1;
    }
};

// Usage.
var e = new Enumerable(["Hello", "Enumerable"]);
e.forEach(function (s) {
    console.log(s);
});

Iterator 支持两个函数:moveNextcurrent。这应该很熟悉,因为 .NET 的 IEnumerator 支持相同的功能。请注意,我没有实现 reset 方法,因为 .NET 只为 COM 互操作实现了它,而 JavaScript 不做。对于泛型 IEnumerator<T> 中的 dispose 也是如此。有趣的是,用法保持不变。

现在,如果我们查看 forEach 方法,会发现它首先通过 getIterator 请求 Iterator。然后它通过调用 moveNextcurrent 来遍历集合。当没有更多元素可用时,moveNext 返回 falseforEach 停止循环并返回给调用者。现在有一些规则是 每个 Iterator 都应该考虑的。首先,moveNext 可以被调用任意次数,但一旦它返回 false,每次后续调用都应该返回 false。此外,每当 moveNext 返回 false 时,current 都应返回 undefined

我只想对 forEach 方法做一点小调整。它应该实现一种中断功能。您不希望每次调用 forEach 时都被迫遍历整个集合。因此,让回调返回 false 或任何假值(不包括 undefinednull)将中断循环。

function isNull(obj) {
    return obj === undefined || obj === null;
}

Enumerable.prototype.forEach = function (callback) {
    var iterator = this.getIterator();
    var cont;
    var currentIndex = 0;
    while ((isNull(cont) || cont) && iterator.moveNext()) {
        cont = callback(iterator.current(), currentIndex);
        currentIndex += 1;
    }
};

// Usage.
var e = new Enumerable(["Hello", "Enumerable"]);
e.forEach(function (s) {
    console.log(s);
    // Break after the first element.
    // "Enumerable" will never log.
    return false;
});

如您所见,现在可以通过返回假值来跳出循环。我决定不包括 undefinednull,因为 undefined 是任何函数的默认返回值,我不想强迫用户始终显式返回 true(或任何真值)。为简单起见,我选择将 undefinednull 视为相同的值(即,没有值)。这实际上是 forEach 在 arrgh.js 的已发布版本中的实现方式。

使用此设计,我们必须彻底重新思考像 where 这样的函数是如何工作的。在前面的示例中,它返回了一个 array,这是不可取的。如果我们返回一个 Enumerable,我们可以链式调用函数。但是,Enumerable 期望一个 array 作为输入,这在 where 中也是不适用的。关于整个 Iterator 的重点是,我们不希望立即求值结果,而是希望返回一个 Iterator 供后续函数使用。这听起来异常困难,但我相信代码示例比一千个单词更有说服力。

var isArray = function (obj) {
    return Object.prototype.toString.call(obj) === "[object Array]";
};

var Enumerable = function (enumerable) {
    var getIterator;
    if (isArray(enumerable)) {
        getIterator = function () {
            return new ArrayIterator(enumerable);
        };
    } else if (typeof enumerable=== "function") {
        getIterator = enumerable;
    } else {
        throw new Error("Invalid input parameter.");
    }
    this.getIterator = getIterator;
};

var WhereIterator = function (source, predicate) {
    var iterator = source.getIterator();
    var index = -1;
    var current;
    this.moveNext = function () {
        while (iterator.moveNext()) {
            index += 1;
            current = iterator.current();
            if (predicate(current, index)) {
                return true;
            }
        }
        current = undefined;
        return false;
    };
    this.current = function () {
        return current;
    };
};

Enumerable.prototype.where = function (predicate) {
    var self = this;
    return new Enumerable(function () {
        return new WhereIterator(self, predicate);
    });
};

// Create an alias.
Enumerable.prototype.filter = Enumerable.prototype.where;

// Usage.
var e = new Enumerable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
var evens = e.where(function (n) {
    return n % 2 === 0;
});

首先是 isArray 函数。这太疯狂了,但这是判断一个 object 是否为 JavaScript 中 array 的唯一 100% 可靠的方法。实际上有一个 npm 包专门用于这一行代码,这也太疯狂了。较新的浏览器默认实现了此函数,但我想让此库兼容 IE8。我还希望它轻量级,这意味着不依赖其他库。所以,这就是 isArray

接下来,您可以看到,我们让 Enumerable 接受一个参数,该参数可以是 arrayfunction(假定为 getIterator 函数)。这允许我们创建各种 IteratorsEnumerablesIterator 重载由 where 函数使用,该函数将一个创建 WhereIterator 的函数传递给它。

现在,WhereIterator 乍一看,看起来像一个丑陋的野兽(尽管与我们最终会遇到的其他一些 Iterators 相比,它就像个小狗)。where 函数始终在 Enumerable 上调用,该 Enumerable 将是过滤的源。我们获取源的 Iterator,然后简单地遍历它。当一个元素满足条件时,我们返回给调用者并指示可能有更多值。当源没有更多元素时,我们将 current 设置为 undefined 并返回给调用者,指示没有找到更多元素。再次强调,where 的用法保持不变。

最后但同样重要的是,我们创建了一个 where 的别名。我认为这很好,因为 JavaScript 和其他语言一样,使用 filter 而不是 where 这个名称。

因为 where 现在返回另一个 Enumerable,所以调试这段代码变得非常困难,毕竟,知道 Enumerable 中有什么的唯一方法就是通过 forEach 进行枚举。所以,让我们快速创建另一个函数 toArray。使用 toArray,我们可以轻松地将 Enumerable 转换为常规的 JavaScript array,并像往常一样继续我们的业务。

Enumerable.prototype.toArray = function () {
    var arr = [];
    this.forEach(function (elem) {
        arr.push(elem);
    });
    return arr;
};

// Usage.
var e = new Enumerable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
var evens = e.where(function (n) {
    return n % 2 === 0;
}).toArray();

另一个有用的,而且非常简单的函数,用于使任何集合只读,是 asEnumerable,现在我们已经能够实现它了,因为 Enumerable 接受一个函数作为输入。

Enumerable.prototype.asEnumerable = function () {
    return new Enumerable(this.getIterator);
};

arrgh.Iterator

现在,如果您在调试器中测试之前的代码,您会注意到 getIterator 返回一个 ArrayIterator 或一个 WhereIterator。在最终库中,大约有 20 个 Iterators。如果 getIterator 始终返回 Iterator,那不是很好吗?这允许我们检查一个 object 是否是任何 Iterator。所以,至少我们需要一个 Iterator 的基类。

所以,有两个选择。要么创建一个基类 Iterator,然后继承 ArrayIteratorWhereIterator,要么创建一个类 Iterator 并将 moveNextcurrent 函数传递给它,将内部实现保留在各自的函数中。我选择了后者。让我们看看这对我们的代码意味着什么。

var Iterator = function (moveNext, current) {
    this.moveNext = moveNext;
    this.current = current;
};

var getArrayIterator = function (arr) {
    var len = arr.length,
    index = -1;
    return new Iterator(function () {
        if (arr.length !== len) {
            throw new Error("Collection was modified, enumeration operation may not execute.");
        }
        index += 1;
        return index < len;
    }, function () {
        return arr[index];
    });
};

var Enumerable = function (enumerable) {
    var getIterator;
    if (isArray(enumerable)) {
        getIterator = function () {
            return getArrayIterator(enumerable);
        };
    } else if (typeof enumerable=== "function") {
        getIterator = enumerable;
    } else {
        throw new Error("Invalid input parameter.");
    }
    this.getIterator = getIterator;
};

Enumerable.prototype.where = function (predicate) {
    var self = this;
    return new Enumerable(function () {
        var iterator = self.getIterator();
        var index = -1;
        var current;
        return new Iterator(function () {
            while (iterator.moveNext()) {
                index += 1;
                current = iterator.current();
                if (predicate(current, index)) {
                    return true;
                }
            }
            current = undefined;
            return false;
        }, function () {
            return current;
        });
    });
};

如您所见,ArrayIteratorWhereIterator 已消失,只剩下一个 Iterator 构造函数。这种方法有优缺点。优点是,显然,只有一个 Iterator 类现在可以根据构造函数改变其行为。另一个(可以说微不足道的)优点是,这种语法稍短,在压缩版本中节省了大约 2 KB(在 20 KB 中)。缺点是,我们的代码现在大量使用了闭包(这不一定是坏事),并且实现类稍微难读一些。我将数组的 Iterator 隐藏在一个 getArrayIterator 函数中,因为 List 类稍后也会使用这个 Iterator(因此我也添加了对 length 是否未更改的检查)。

到目前为止,我们已经看到了 Enumerables 和 arrgh.js 的基础知识。所有其他方法,如 alldistinctanyorderByselect,都使用与 where 相同的方法实现。只需返回一个带有自定义 IteratorEnumerable

当前发布的 Enumerable 版本要复杂一些,因为它还接受其他 Enumerablesstrings 作为输入,以及多个输入参数(如 C# 的 params)。这只是多几个 if 语句,所以您应该能够轻松理解。

顺便说一句,这里是 rangetake 的实现,所以您可以测试那个小代码片段 Enumerable.range(0, 1000000).where(n => n % 2 === 0).take(10); 并看到它确实只求值了 18 个值。

Enumerable.range = function (start, count) {
    if (!isNull(count)) {
        if (count < 0) {
            throw new Error("Count cannot be lower than 0.");
        }
        if (start + (count - 1) > Number.MAX_SAFE_INTEGER) {
            throw new Error("Start and count can not exceed " + MAX_SAFE_INTEGER + ".");
        }
    }
    return new Enumerable(function () {
        if (isNull(count)) {
            var moved = false;
            return new Iterator(function () {
                if (!moved) {
                    moved = true;
                } else {
                    start += 1;
                }
                return start <= Number.MAX_SAFE_INTEGER;
            }, function () {
                if (!moved || start > Number.MAX_SAFE_INTEGER) {
                    return undefined;
                }
                return start;
            });
        } else {
            var index = -1;
            return new Iterator(function () {
                index += 1;
                return index < count;
            }, function () {
                if (index === -1 || index >= count) {
                    return undefined;
                }
                return start + index;
            });
        }
    });
};

Enumerable.prototype.take = function (count) {
    var self = this;
    return new Enumerable(function () {
        var iterator = self.getIterator(),
        index = -1;
        return new Iterator(function () {
            index += 1;
            return index < count && iterator.moveNext();
        }, function () {
            if (index === -1 || index >= count) {
                return undefined;
            }
            return iterator.current();
        });
    });
};

迭代 Enumerable.range(0) 将实际上一直进行到 Number.MAX_SAFE_INTEGER(在已发布版本中为了兼容浏览器而注入),即 9007199254740991,并且很可能会崩溃您的浏览器,除非您使用 anysometaketakeWhilefirstfirstOrDefault 来限制结果。

arrgh.List

有了 Enumerable,我们可以继续我们的下一个类 List。为了创建 List,我们将继承自 Enumerable。同样,有一个包可以做到这一点,但它非常小,我不想有任何依赖项,所以我自己创建了一个辅助方法(不要问 inheritTemp,这只是 JavaScript 的另一种荒谬之处)。

var Temp = function () {
    // This will shut up JSLint :-)
    // Minify will remove 'return' so no precious bytes are lost.
    return;
};

function inherit(inheritor, inherited) {
    Temp.prototype = inherited.prototype;
    inheritor.prototype = new Temp();
    Temp.prototype = null;
    inheritor.prototype.constructor = inheritor;
}

var List = function (arr) {
    var self = this;

    Enumerable.call(this, function () {
        return getArrayIterator(self);
    });

    arr = arr || [];
    if (isArray(arr)) {
        var i;
        for (i = 0; i < arr.length; i += 1) {
            this[i] = arr[i];
        }
    } else {
        throw new Error("Invalid input parameter.");
    }
    this.length = arr.length;
};
inherit(List, Enumerable);

// Usage.
var l = new List(["Hello", "List"]);
console.log(l[0]);
console.log(l[1]);

List 构造函数接受一个 array 作为输入参数,并将 array 的内容添加到自身。这似乎是个好主意,因为 Listarray 在只读场景下现在可以互换使用,但我遇到了实现 addremove 方法的许多问题。例如,当 List 的用户手动添加下一个索引时该怎么办?在 array 上添加索引时,length 会相应调整,但这不是我们自己能做的。同样,当用户更改 array 上的 length 时,索引会相应添加或删除,这也不是我们自己能做的。最终,我决定保持 length 属性不变(尽管存在用户意外更改它并破坏 List 的风险),并放弃了类数组的方法。

下一个方法是封装一个 array,就像 C# 的 List<T> 类一样。可惜,JavaScript 没有私有成员,但约定似乎是用下划线作为私有成员的前缀。我个人更喜欢创建一个名为 _(下划线)的对象,其中包含所有私有成员。

var List = function (arr) {
    var self = this;
    // Copy the original array so
    // manipulating the original array
    // will not affect the List in any way
    arr = arr ? arr.slice() : [];

    Enumerable.call(this, function () {
        return getArrayIterator(self._.arr);
    });

    this._ = {
        arr: arr
    };
    this.length = arr.length;
};
inherit(List, Enumerable);

List.prototype.get = function (index) {
    if (index < 0 || index >= this.length) {
        throw new Error("Index was out of range. Must be non-negative and less than the size of the collection.");
    }
    return this._.arr[index];
};

// Usage.
var l = new List(["Hello", "List"]);
console.log(l.get(0));
console.log(l.get(1));
// Throw error.
console.log(l.get(2));

同样,当前发布的 List 接受更多输入参数,例如其他 Enumerablesstrings,但基础仍然是一个隐式私有 array 和一个隐式只读 length 属性。还请注意如何使用 getArrayIterator

那么,我们来看看 addremove 是如何实现的。

List.prototype.add = function (item) {
    this._.arr.push(item);
    this.length += 1;
};

List.prototype.remove = function (item) {
    // indexOf inherited from Enumerable.
    var index = this.indexOf(item);

    if (index >= 0) {
        this._.arr.splice(index, 1);
        this.length -= 1;
        return true;
    }
    return false;
};

如您所见,仍然是数组操作让您抓狂,但至少它被很好地封装在 List 类中,该类具有许多有用的函数,并且在浏览器之间保持一致。

indexOf 函数是从 Enumerable 继承的,我将不深入介绍,因为它只是众多函数之一。但是,值得一提的是 List 类实际上会覆盖它。由于 List 知道自身的长度以及其中内容的索引,而 Enumerable 不知道,我们可以优化 List 上的一些函数,例如 indexOf。我将展示如何做到这一点,但将使用 count 函数来展示。

Enumerable.prototype.count = function (predicate) {
    var count = 0;
    predicate = predicate || alwaysTrue;

    this.forEach(function (elem) {
        if (predicate(elem)) {
            count += 1;
        }
    });
    return count;
};

List.prototype.count = function (predicate) {
    if (!predicate) {
        return this.length;
    } else {
        return Enumerable.prototype.count.call(this, predicate);
    }
};

predicate 未指定时,List 可以简单地返回其 length 属性,而 Enumerable 必须先求值所有元素。

在 arrgh.js 的已发布版本中,List 构造函数允许将 Enumerables 作为输入传递,因此 Enumerable.toList 函数非常简单。当然,List 将不得不枚举集合以填充内部数组。

Enumerable.prototype.toList = function () {
    return new List(this);
};

总而言之,List 类并不复杂。它拥有的其他函数有 addRangeclearinsertsetsort。毫无疑问,sort 是最复杂的,但我们稍后会讲到(它重用了 orderBy 功能)。

arrgh.Dictionary

Dictionary,与 List 不同,是一个相当复杂的野兽!这也是我在其他 LINQ 库中没有看到令人满意的实现的唯一类。如果您对 JavaScript 内部机制有所了解,您就会知道每个 JavaScript 对象实际上都是作为哈希映射实现的(Dictionary 本质上就是这样的)。相信我,我做了一些谷歌搜索来让它工作,但互联网上的人们通常会提到使用 JavaScript 的 object。好吧,我使用 object 作为 Dictionary 有几个反对意见。首先,它只允许 strings 作为键。其次,它不容易遍历,您需要使用 for 循环并检查 hasOwnProperty,然后 JSLint 会抱怨您实际上应该使用 Object.keys(当然,这在旧浏览器中不支持)。此外,对象缺少许多很好的功能,例如 Enumerable 提供的所有功能。object 本身根本不是一个集合。

真正的问题在于实现一个真正的 Dictionary。如前所述,它也称为哈希映射,但我们将从哪里获得哈希?在 .NET 中,每个对象都有一个 GetHashCode 函数,该函数实际上在某个 COM 对象中实现,可能直接与您的硬件交互(我不知道这是否属实,但我知道对于我们普通人来说,实现它几乎是不可能的)。JavaScript 没有这些,所以我们必须自己实现。正如我所说,这不可能,所以我查看了下一个最佳选项,那就是……使用 object 作为哈希映射。

所以这是第一个问题,对象只使用字符串作为键,但在 .NET 中我们可以使用任何对象作为键,而不仅仅是字符串。所以,我们要做的是使用对象的 toString 实现,它可以被覆盖。但是,由于 toString 经常用于调试目的或用于屏幕显示,因此我们将允许一个额外的自定义 getHash 方法。即使这样还不够,我们也允许使用相等比较器来获取哈希并比较键。相等比较器也解决了另一个问题:哈希冲突。

为此,我将向您展示一个默认的相等比较器以及 add 函数,它们用于确定对象的哈希。

function isActualNaN (obj) {
    return obj !== obj;
}

var defaultEqComparer = {
    equals: function (x, y) {
        return x === y || (isActualNaN(x) && isActualNaN(y)); // NaN edge case.
    },
    getHash: function (obj) {
        var hash;
        if (obj === null) {
            hash = "null";
        } else if (obj === undefined) {
            hash = "undefined";
        } else if (isActualNaN(obj)) {
            hash = "NaN";
        } else {
            hash = typeof obj.getHash === "function" ?
            obj.getHash() :
            typeof obj.toString === "function" ? obj.toString() : Object.prototype.toString.call(obj);
        }
        return hash;
    }
};

var Dictionary = function (eqComparer) {
    var self = this;

    Enumerable.call(self, function () {
        var iterator = self._.entries.getIterator();
        return new Iterator(function () {
            return iterator.moveNext();
        }, function () {
            var current = iterator.current();
            if (current) {
                return { key: current.key, value: current.value };
            }
            return undefined;
        });
    });

    this.length = 0;
    this._ = {
        eqComparer: ensureEqComparer(eqComparer),
        keys: {},
        entries: new List()
    };
};
inherit(Dictionary, Enumerable);

function dictionaryContainsKey (dictionary, hash, key) {
    if (dictionary._.keys.hasOwnProperty(hash)) {
        return dictionary._.keys[hash].contains(key, function (x, y) {
            return dictionary._.eqComparer.equals(x.key, y);
        });
    }
    return false;
}

Dictionary.prototype.add = function (key, value) {
    var hash = this._.eqComparer.getHash(key);
    if (dictionaryContainsKey(this, hash, key)) {
        throw new Error("Key [" + key + "] is already present in the dictionary.");
    }

    if (!this._.keys[hash]) {
        this._.keys[hash] = new List();
    }
    var pair = { key: key, value: value };
    this._.keys[hash].add(pair);
    this._.entries.add(pair);

    this.length += 1;
};

幸运的是,这并不像看起来那么困难。关键在于 defaultEqComparer。我想指出,undefinednullNaN 是有效的键,并且 NaN 被检查是否等于 NaN(通常,NaN === NaN 结果为 false)。相等比较器有一个 getHash 函数和一个 equals 函数。getHash 函数获取 objects 的哈希,在我们的例子中,这实际上只是一个 string。当两个 objects 产生相同的哈希时,将使用 equals 函数来检查对象是否相等(这并非总是如此)。一个例子将澄清。

var d = new Dictionary({
    equals: function (x, y) {
        return x === y;
    },
    getHash: function (obj) {
        return obj.firstName;
    }
});

d.add({
    firstName: "Bill",
    lastName: "Gates"
});
d.add({
    firstName: "Bill",
    lastName: "Clinton"
});

由于这两个对象都具有相同的 firstName“Bill”,它被用作哈希,因此存在哈希冲突(两个对象产生相同的哈希)。但是,由于 equals 函数认为这两个对象不相等,因此这两个对象都被添加为 Dictionary 的键(而不是抛出错误说键已存在)。

add 实现中,您可以看到计算了哈希,并且当它不存在时,将其添加到 keys 对象中。哈希映射到一个 List 对象,该对象用于保存具有该特定哈希的所有值。具有相同哈希的元素越多,查找具有该哈希的键的速度就越慢。这很重要,因为哈希映射通常具有 O(1) 的查找时间,但这更像是 O(1 左右)。任何 JavaScript 对象的默认哈希是 "[object Object]",所以一定要覆盖 toString,实现 getHash 或使用自定义相等比较器,否则您的查找时间将与 List 一样。

key = {
    Bill: [
        Bill Clinton,
        Bill Gates
    ],
    AnotherHash: [ value ],
    ["[object Object]"]: [ objectsWithDefaultToString ]
};

现在,对于 Dictionary Iterator。您会注意到 Dictionary 中有一个条目列表 List。这在 Iterator 中使用。使用 hashes 对象,我们丢失了元素的顺序,因此我们将所有元素也保存在 entries 中。这使得迭代非常容易,因为我们只需要遍历 List。请注意,在迭代期间复制了键值对。这是因为键值对对客户端是只读的(当然,客户端可以通过直接修改 _.entries 来搞乱)。我应该注意到 .NET 在内部使用链表,而我们的 List 在内部使用数组。两者都有优点和缺点,例如更新速度(链表获胜)和内存使用(数组获胜)。

这是 remove 函数,它使用给定的键删除一个键值对。getPairByKey 查找哈希,然后在映射的 List 中查找键。它通过 firstOrDefault 来实现,该函数返回 List 中项目的第一个实例或在未找到项目时返回默认值(自我提醒:可以使用 singleOrDefault 而不是,因为一个键不能被添加两次到 Dictionary)。

function getPairByKey (dict, hash, key, whenNotExists) {
    var elem;
    if (!dict._.keys.hasOwnProperty(hash)) {
        whenNotExists();
    } else {
        var def = {};
        elem = dict._.keys[hash].firstOrDefault(function (kvp) {
            return dict._.eqComparer.equals(kvp.key, key);
        }, def);
        if (elem === def) {
            whenNotExists();
        }
    }
    return elem;
}

Dictionary.prototype.remove = function (key) {
    var hash = this._.eqComparer.getHash(key);
    var notFound;
    var pair;

    pair = getPairByKey(this, hash, key, function () {
        notFound = true;
    });
    if (notFound) {
        return false;
    }

    var keys = this._.keys[hash];
    keys.remove(pair);
    this._.entries.remove(pair);
    if (!keys.any()) {
        delete this._.keys[hash];
    }
    this.length -= 1;
    return true;
};

这里是检查字典中是否存在键以及获取特定键值的函数。

Dictionary.prototype.containsKey = function (key) {
    var hash = this._.eqComparer.getHash(key);
    return dictionaryContainsKey(this, hash, key);
};

Dictionary.prototype.get = function (key) {
    var hash = this._.eqComparer.getHash(key);
    return getPairByKey(this, hash, key, function () {
        throw new Error("Key [" + key + "] was not found in the dictionary.");
    }).value;
};

这是另一个很棒的函数 tryGet(.NET 中的 TryGetValue)。此函数尝试使用指定的键获取值。通常,当您尝试使用不存在的键获取项目时会抛出错误。但是,使用 tryGet 时不会出错,而是返回一个 boolean 来指示是否找到键,如果找到,还会返回该值。在 .NET 中,您会在 out 参数中获取该值,但 JavaScript 没有这个概念。相反,我返回一个包含 success 布尔值和 value 对象的对象。当 successtrue 时,value 包含该键的值(可能为 undefined);当 successfalse 时,value 始终为 undefined。这个函数实际上是我唯一需要绕过 .NET out 参数的地方。

Dictionary.prototype.tryGet = function (key) {
    var hash = this._.eqComparer.getHash(key),
    notFound,
    pair = getPairByKey(this, hash, key, function () {
        notFound = true;
    });
    if (notFound) {
        return {
            success: false,
            value: undefined
        };
    }
    return {
        success: true,
        value: pair.value
    };
};

幸运的是,Dictionary 的用法非常简单。

var d = new Dictionary();

var billGates = {
    firstName: "Bill",
    lastName: "Gates"
};
var billClinton = {
    firstName: "Bill",
    lastName: "Clinton"
};

d.add(billGates, "Richest man in the world.");
d.add(billClinton, "Was president of the USA.");

console.log(d.containsKey(billGates));
// Logs "true"

console.log(d.get(billClinton));
// Logs "Was president of the USA."

d.remove(billClinton);
console.log(d.containsKey(billClinton));
// Logs "false"

要实际运行此示例,您需要 arrgh.js 的完整实现,而不仅仅是我到目前为止展示的片段。

我们现在也可以实现 Enumerable.toDictionary

function identity(x) {
    return x;
}

Enumerable.prototype.toDictionary = function (keySelector, elementSelector, eqComparer) {
    if (typeof arguments[1] === "function") {
        elementSelector = arguments[1];
        eqComparer = arguments[2];
    } else {
        eqComparer = arguments[1];
    }
    elementSelector = elementSelector || identity;
    eqComparer = ensureEqComparer(eqComparer);

    var d = new Dictionary(eqComparer);
    this.forEach(function (elem) {
        d.add(keySelector(elem), elementSelector(elem));
    });
    return d;
};

// Usage.
var names = new Enumerable(["John", "Annie", "Bill", "Sander"]);

// Names by first letter (throws if first letter is not unique).
var d = names.toDictionary(n => n[0]);

// Names as uppercase by first letter.
d = names.toDictionary(n => n[0], n => n.toUpperCase());

// Names using first letter as a key.
d = names.toDictionary(n => n, {
    equals: function (x, y) {
        return x === y;
    },
    getHash: function (obj) {
        return obj[0];
    }
});

// Names using first letter as a key and uppercased as value.
d = names.toDictionary(n => n, n => n.toUpperCase(), {
    equals: function (x, y) {
        return x === y;
    },
    getHash: function (obj) {
        return obj[0];
    }
});

toDictionary 函数有几个重载,elementSelectoreqComparer 都是可选的。因此,如果 toDictionary 的第二个参数是函数,则它是 elementSelector,如果是对象,则它是 eqComparer

arrgh.OrderedEnumerable

您认为 Dictionary 很复杂?那么,就进入痛苦的世界吧,OrderedEnumerable 的世界。您是否注意到,您可以在 .NET 中使用 someCollection.OrderBy(...).ThenBy(...).ThenByDescending(...).ToList() 来对集合进行排序?OrderBy 返回一个 IEnumerable,但不是任何 IEnumerable,而是一个特殊的 IOrderedEnumerable,它具有 ThenByThenByDescending 扩展方法。在 .NET 中,您在外部看不到它,但 IOrderedEnumerable 实际上会保留一些内部变量,例如调用 OrderByThenBy 的集合以及排序是升序还是降序。棘手之处在于,最终,您将枚举 ThenByDescending 返回的集合,但它需要了解其父对象,因为 ThenByDescending 需要应用额外的排序,而不能覆盖其父对象的排序。这实际上是 LINQ 中唯一一个不枚举其父对象,而是使用父对象来调整自身枚举的集合。

首先,我将向您展示 EnumerableOrderedEnumerable 上的方法以及一个比较器函数。比较器函数比较两个键,当第一个键大于第二个时返回正数,当第一个键小于第二个时返回负数,当两个键相等时返回 0。在 .NET 中,我发现 null 比任何东西都小,然后是 NaN(对于 double?),然后是正常的排序,就像您期望的那样。在我的实现中,我将 undefined 视为比 null 小。这是与 JavaScript 数组 sort 函数的一个基本区别,该函数会忽略 undefined 并始终将其放在数组的末尾(您可以使用自定义比较器,但 undefined 仍然被忽略)。因此,在我的实现中,undefined 不会被忽略,如果您传入自定义比较器,您仍然可以将 undefined 移到集合的后面。

function defaultCompare(x, y) {
    if (isNull(x) || isNull(y)) {
        // Treat undefined as smaller than null
        // and both as smaller than anything else.
        var noVal = function (a, b, val) {
            if (a === b) {
                return 0;
            }
            if (a === val && b !== val) {
                return -1;
            }
            if (a !== val && b === val) {
                return 1;
            }
        };
        var eq = noVal(x, y, undefined);

        if (eq === undefined) {
            return noVal(x, y, null);
        }
        return eq;
    }

    // Treat NaN as smaller than anything else
    // except undefined and null.
    if (isActualNaN(x) && isActualNaN(y)) {
        return 0;
    }
    if (isActualNaN(x)) {
        return -1;
    }
    if (isActualNaN(y)) {
        return 1;
    }

    if (x > y) {
        return 1;
    }
    if (x < y) {
        return -1;
    }
    return 0;
}

var OrderedEnumerable = function (source, keySelector, compare, descending) {
    compare = compare || defaultCompare;
    descending = descending ? -1 : 1;
    // ...
};
inherit(OrderedEnumerable, Enumerable);

Enumerable.prototype.orderBy = function (keySelector, compare) {
    return new OrderedEnumerable(this, keySelector, compare, false);
};

Enumerable.prototype.orderByDescending = function (keySelector, compare) {
    return new OrderedEnumerable(this, keySelector, compare, true);
};

OrderedEnumerable.prototype.thenBy = function (keySelector, compare) {
    return new OrderedEnumerable(this, keySelector, compare, false);
};

OrderedEnumerable.prototype.thenByDescending = function (keySelector, compare) {
    return new OrderedEnumerable(this, keySelector, compare, true);
};

仅使用这段相当简单的代码(除了那个庞大的 defaultCompare 函数)我们就拥有了进行实际排序所需的一切。prototype 函数是用户获取 OrderedEnumerable 引用的唯一方法,因为构造函数并未暴露。排序使用 快速排序算法。这不是一篇关于算法的文章,但让我给您一些基础知识。在集合中,我们取一个所谓的枢轴值,最好是中间值。现在我们取两个计数器,一个从 0 开始,一个从集合的最后一个索引(length - 1)开始。我们现在使用比较函数将第 0 个索引元素与枢轴进行比较,如果第 0 个元素大于或等于枢轴,我们则停留在该索引上并继续到下一个循环(索引为 length - 1);如果元素小于枢轴,我们继续到下一个元素并执行相同的操作。当我们到达下一个循环时,我们执行相同的操作,只是这次我们检查元素是否小于枢轴,如果是,我们就停止。之后,我们交换值。一旦我们到达枢轴,我们将递归地对枢轴左侧的所有值和右侧的所有值执行相同的操作。此外,快速排序是一种原地算法,意味着它会修改当前集合,而不是创建一个新集合并保留输入不变。这是这些步骤的一个略微尴尬的视觉表示。

2, 5, 3, 4, 1
^     p     -

2, 5, 3, 4, 1
   ^  p     -

2, 5, 3, 4, 1
   -  p     ^

2, 1, 3, 4, 5
   *        *

2, 1, 3, 4, 5
p  ^

1, 2, 3, 4, 5
*

1, 2, 3, 4, 5
         p  ^

1, 2, 3, 4, 5

sorted.

为了使其更复杂,快速排序算法有一个缺点,它不是稳定的。这意味着如果两个元素相等,它们仍可能被交换,从而丢失它们在集合中的相对顺序。所以,如果输入集合包含 Bill ClintonBill Gates(按此顺序),然后我们按名字排序,那么输出集合可能会将它们的顺序交换为 Bill GatesBill Clinton。在许多情况下这不是问题,但 .NET 实现 OrderByThenBy 时使用的是稳定的快速排序。幸运的是,我们可以相对容易地解决这个问题。我们不比较集合的实际元素,而是对索引列表进行排序,每个索引都映射到一个元素,如果两个索引的元素相等,我们则比较索引本身。这是稳定快速排序的实现。

function stableQuicksort(map, startIndex, endIndex, compare) {
    var low = startIndex,
    high = endIndex,
    pindex = Math.floor((low + high) / 2),
    pivot = map[pindex],
    lindex,
    hindex,
    result,
    temp;

    while (low <= high) {
        lindex = map[low];
        result = compare(lindex, pivot);
        // First loop, going from start to pivot.
        while (result < 0 || (result === 0 && lindex < pivot)) {
            low += 1;
            lindex = map[low];
            result = compare(lindex, pivot);
        }

        hindex = map[high];
        result = compare(hindex, pivot);
        // Second loop, going from end to pivot.
        while (result > 0 || (result === 0 && hindex > pivot)) {
            high -= 1;
            hindex = map[high];
            result = compare(hindex, pivot);
        }

        // Swap elements.
        if (low <= high) {
            temp = map[low];
            map[low] = map[high];
            map[high] = temp;
            low += 1;
            high -= 1;
        }
    }

    // Recursively sort collection left and right of the pivot.
    if (low < endIndex) {
        stableQuicksort(map, low, endIndex, compare);
    }
    if (high > startIndex) {
        stableQuicksort(map, startIndex, high, compare);
    }
}

现在,您一直在等待的时刻,OrderedEnumerable 的实现。

var OrderedEnumerable = function (source, keySelector, compare, descending) {
    var self = this;
    var keys;
    var compare = compare || defaultCompare;
    var descending = descending ? -1 : 1;

    self.getSource = function () {
        if (source.getSource) {
            return source.getSource();
        }
        return source;
    };

    self.computeKeys = function (elements, count) {
        var arr = new Array(count);
        var i;
        for (i = 0; i < count; i += 1) {
            arr[i] = keySelector(elements[i]);
        }
        keys = arr;
        if (source.computeKeys) {
            source.computeKeys(elements, count);
        }
    };
    self.compareKeys = function (i, j) {
        var result = 0;
        if (source.compareKeys) {
            result = source.compareKeys(i, j);
        }
        if (result === 0) {
            result = compare(keys[i], keys[j]) * descending;
        }
        return result;
    };
    Enumerable.call(this, function () {
        var sourceArr = self.getSource().toArray();
        var count = sourceArr.length;
        var map = new Array(count);
        var index;
        self.computeKeys(sourceArr, count);
        for (index = 0; index < count; index += 1) {
            map[index] = index;
        }
        stableQuicksort(map, 0, count - 1, self.compareKeys);
        index = -1;
        return new Iterator(function () {
            index += 1;
            return index < count;
        }, function () {
            return sourceArr[map[index]];
        });
    });
};

我将立即承认,这花了我几次尝试和大量的时间。所以,让我们一步一步地进行。由于我们需要在枚举之前对整个源集合进行排序,因此 getIterator 函数首先对所有内容进行排序,然后返回一个相当小的 Iterator

首先,getIterator 使用 getSource 来求值需要排序的集合(这可能是 whereselect 等的结果),并将其转换为 arraygetSource 函数返回第一个不是 OrderedEnumerablesource(通过检查 getSource 函数的存在来测试)。所以 someCollection.where(...).orderBy(...).thenBy(...).thenByDescending().getIterator() 将排序 where 函数的结果。

接下来,我们将计算键,即需要排序的值。我们只执行一次(并且总是执行一次,即使我们从未需要它们)。所以,假设我们需要按 firstName 对一组人进行排序,那么 keys 现在是一个包含“John”、“Bill”、“Steve”等的 array

然后我们创建 map,即我们将排序的索引。请记住,我们需要索引来执行稳定排序。然后我们将 map、集合的整个范围(0 到 length - 1)以及 compareKeys 函数传递给 stableQuicksort 函数,该函数发挥其魔力。

compareKeys 函数执行实际的比较并返回一个正整数、一个负整数或 0。好处是,如果 source 包含一个 compareKeys 函数,它就会使用该函数。只有当源的 compareKeys 返回 0 时,当前函数才会比较其键。因此,在 someCollection.orderBy(p => p.firstName).thenBy(p => p.lastName).toArray(); 的情况下,只有当两个元素的 firstName 相等时,才比较它们的 lastName。请记住,我们正在比较索引,因此我们需要从 keys 数组中获取实际值。

stableQuicksort 根据键映射对索引进行重新排列。这意味着 map 被排序了,但 source 没有。所以,在 Iterator 中,使用当前索引,我们可以通过 map 中的索引来获取 sourceArr 中元素的索引。这是一个小例子。

var sourceArr = [2, 5, 3, 4, 1];
var sourceArrComparer = function (x, y) {
    return defaultCompare(sourceArr[x], sourceArr[y]);
};
var map = [0, 1, 2, 3, 4];
stableQuicksort(map, 0, 4, sourceArrComparer);
console.log(map); // [4, 0, 2, 3, 1]
console.log(sourceArr[map[0]]); // 1
console.log(sourceArr[map[1]]); // 2
// etc.

使用这个 stableQuicksort 函数,我们也可以编写 List 上的 sort 函数(这是一个原地排序)。List sort 允许对整个 ListList 的一部分进行排序,但我们都已经处理好了。所以您可以自行研究这段代码。

arrgh.Lookup

Lookup 并不复杂,但也不漂亮(我决定将所有内容放在一个大函数中,这样它就没有额外的 prototype 函数)。查找基本上是一个集合的集合,其中每个集合都有一个用于分组的键。内部它使用一个 Dictionary(事实上,它本身也基本上实现为一个查找)。

var Lookup = function (source, keySelector, elementSelector, eqComparer) {
    var d;
    Enumerable.call(this, function () {
        var iterator = d.getIterator();
        return new Iterator(iterator.moveNext, function () {
            var current = iterator.current();
            if (isNull(current)) {
                return current;
            }
            var group = current.value.asEnumerable();
            group.key = current.key;
            return group;
        });
    });

    // The elementSelector is optional,
    // putting the eqComparer at argument[2].
    if (typeof elementSelector !== "function") {
        eqComparer = elementSelector;
        elementSelector = null;
    }
    elementSelector = elementSelector || identity;

    d = new Dictionary(eqComparer);
    source.forEach(function (elem) {
        var key = keySelector(elem);
        var element = elementSelector(elem);
        if (d.containsKey(key)) {
            d.get(key).add(element);
        } else {
            d.add(key, new List([element]));
        }
    });

    this.length = d.length;
    this.get = function (key) {
        var group;
        if (d.containsKey(key)) {
            group = d.get(key).asEnumerable();
            group.key = key;
        } else {
            group = new Enumerable();
            group.key = key;
        }
        return group;
    };
};
inherit(Lookup, Enumerable);

如您所见,源被迭代,并且具有特定键的每个值都被添加到与该键关联的 List 中。这基本上是 Dictionary 中的 keys 对象,只是这里的键显式不是哈希。在 get 函数中,我们看到具有指定键的 List 被返回为 Enumerable(使其只读),并且 key 被添加到 Enumerable。当键不存在时,将返回一个带有指定 key 的空 Enumerable。在枚举时,我们也做同样的事情。

OrderedEnumerable 一样,用户获取 Lookup 的唯一方法是使用 Enumerable 上的 toLookup 函数。

Enumerable.prototype.toLookup = function (keySelector, elementSelector, eqComparer) {
    if (typeof arguments[1] === "function") {
        elementSelector = arguments[1];
        eqComparer = arguments[2];
    } else {
        eqComparer = arguments[1];
    }
    elementSelector = elementSelector || identity;
    eqComparer = ensureEqComparer(eqComparer);

    return new Lookup(this, keySelector, elementSelector, eqComparer);
};

// Usage.
var names = new Enumerable(["Bianca", "John", "Bill", "Annie", "Barney"]);

// Names by first letter (throws if first letter is not unique).
var l = names.toLookup(n => n[0]);

// Names as uppercase by first letter.
l = names.toLookup(n => n[0], n => n.toUpperCase());

// Names using first letter as a key.
l = names.toLookup(n => n, {
    equals: function (x, y) {
        return x === y;
    },
    getHash: function (obj) {
        return obj[0];
    }
});

// Names using first letter as a key and uppercased as value.
l = names.toLookup(n => n, n => n.toUpperCase(), {
    equals: function (x, y) {
        return x[0] === y[0];
    },
    getHash: function (obj) {
        return obj[0];
    }
});

arrgh

不幸的是,我无法在一篇文章中展示 arrgh.js 的所有内容。Enumerable 类已经有 55 个函数,其中大多数至少有一个重载。然而,我已经向您展示了 arrgh.js 中的所有类以及一些函数。 完整的文档 应该能帮助您上手。您可能想在 .NET 中查找某个函数,arrgh.js 很可能工作方式相同。

所以,有几点说明。在大多数情况下,我不会检查参数类型。一切都按文档工作,但如果您传递了一个字符串而不是一个预期的对象,谁知道会发生什么。这种方法的优点是,我可以省略 100 多个类型检查,这有助于保持代码小巧快速。缺点当然是,函数可能会产生错误或更糟,不正确的结果,而您可能永远不会知道(当然,您会知道,因为您测试了您的代码)。

另外,我认为提及 arrgh.js 暴露的内容可能很有用。

var arrgh = (function (undefined, MAX_SAFE_INTEGER) {
    "use strict";

    // Lots of code here...

    return {
        Enumerable: Enumerable,
        Dictionary: Dictionary,
        Iterator: Iterator,
        List: List
    };
}(undefined, Number.MAX_SAFE_INTEGER || 9007199254740991));

Number.MAX_SAFE_INTEGER 对于 Enumerable.range 是必需的,其上限是,您猜对了,Number.MAX_SAFE_INTEGER(这在旧浏览器中不支持,因此使用了字面值)。

使用 Jasmine 进行测试

arrgh.js 是一个相当经过充分测试的库,如果我自己说的话。它目前有 779 个测试,确保在 IE8、IE11、Firefox 和 Chrome 中都能获得正确一致的结果(我使用的是 Win7 机器,所以没有 Edge 或 Safari,但我看不出它们为什么不工作)。带您了解所有这些测试是愚蠢的,但展示它们如何工作以及如何使它们为您工作非常有用。在本文的剩余部分,我们将需要 Node.js 和 npm,所以请访问 他们的网站,下载并安装它。npm 是 Node.js 的包管理器。

安装完成后,打开命令提示符并导航到 arrgh.js 的根文件夹。在那里,创建 package.json,手动创建(创建一个名为 package.json 的文件并在其中放入 {})或在命令提示符中键入 npm init。一旦有了 package.json,就使用 npm install jasmine --save-dev 安装 Jasmine,首选的测试框架。

cd C:\arrgh.js
npm install jasmine --save-dev

npm 将创建一个 node_modules 文件夹并安装 Jasmine 运行所需的所有内容。此外,--save-dev 将在您的 package.json 中创建一个开发依赖项。这意味着您不必将包提交到源代码管理,但可以使用 npm install 轻松恢复所有依赖项。

现在让我们编写一些测试!在您的根目录中创建一个名为 test 的文件夹。我们现在需要两样东西:测试,以及一个显示测试结果的页面。让我们先创建测试结果页面,因为它非常简单。在 test 文件夹中创建一个 html 文件,我将其命名为 index.browser.html(我的项目中还有一个 index.html 文件,但它是用于自动化的 Jenkins 构建环境,本文不涵盖)。在 html 文件中粘贴以下 HTML。

<!doctype html>
<html>
<head>
  <title>arrgh.js tests</title>
  <link rel="shortcut icon" type="image/png" href="../node_modules/jasmine-core/images/jasmine_favicon.png">
  <link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
</head>
<body>
  <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
  <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
  <script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>

  <script src="../src/arrgh.js"></script> <!-- Path to your arrgh.js file -->

  <script src="spec/tests.js"></script>
</body>
</html>

在此示例中,我将使用单个 tests.js 文件,但我实际上已将我的测试分成单独的文件。最终,tests.js 将运行所有这些文件。

现在,在 test 文件夹中,创建一个名为 spec 的文件夹。这就是您实际的测试将要存放的地方。在该文件夹中创建 tests.js 文件。编写测试现在非常简单。Jasmine 是一个行为驱动开发 (BDD) 框架,这意味着我们将描述测试应该做什么,然后去做。

describe("arrgh.Enumerable", function () {
    describe("toArray", function () {
        it("should produce an array containing some elements", function () {
            var e = new arrgh.Enumerable([1, 2, 3, 4, 5]);
            expect(e.toArray()).toEqual([1, 2, 3, 4, 5]);
        });

        it("should produce an empty array", function () {
            var e = new arrgh.Enumerable();
            expect(e.toArray()).toEqual([]);
        });
    });

    describe("count", function () {
        it("should have the count of the initial array", function () {
            var e = new arrgh.Enumerable([1, 2, 3, 4, 5]);
            expect(e.count()).toBe(5);
        });
    });
});

describe 的作用正如其名,描述即将发生的事情。您可以随意嵌套它。在您的 describes 中,您会有一个 it。在 it 中,您编写测试并使用 expecttoEqual 来比较您的预期值与实际结果。toEqual 比较引用类型(如对象和数组)的相等性(无需它们具有相同的引用)。toBe 比较值类型(如整数和布尔值)。您可以使用 toEqual 而不是 toBe,但不能反之。

如果您打开我们之前创建的 html 页面,您现在应该会看到测试结果。尝试使一个测试失败,看看会发生什么。

正如我所说,我已经将我的测试分成了不同的文件。在 tests.js 中,我声明了一些将在我的其他测试中使用的全局变量(实际上,Jasmine 有更好的解决方案,但不知何故它在不同浏览器中不稳定,所以我选择了全局变量)。分离基于集合的类型、迭代器以及一些分组良好的测试,例如所有 join 操作。

所以,简而言之,这是 tests.js。

var p0 = {
    first: "Sander",
    last: "Rossel",
    age: 28,
    hobbies: ["Programming" ,"Gaming" ,"Music"]
};

var p1 = {
    first: "Bill",
    last: "Murray",
    age: 58,
    hobbies: ["Hiking", "Travelling"]
};

// Other globals...

(function () {
    "use strict";
    describe("arrgh.js tests", function () {
        testEnumerable();
        // Other tests...
    });
}());

然后是 test-Enumerable.js。

var testEnumerable = function () {
    "use strict";

    describe("Enumerable", function () {
        // A whole lot of other tests...

        describe("contains", function () {
            it("should return true when the collection contains the object", function () {
                var e = new arrgh.Enumerable(people);
                expect(e.contains(p3)).toBe(true);
            });
            // A whole lot of other tests...
        });
        // Even more tests...
    });
});

我应该提到,我并没有检查测试集中(著名)人物的实际年龄和爱好。

使用 JSDoc 进行文档编写

接下来,我们将使用 JSDoc 生成一些文档。您可以使用 npm 安装 JSDoc。除了在当前项目中安装它之外,我们还将全局安装它,以便我们可以使用 CLI(命令行界面)。

npm install jsdoc --save-dev
npm install jsdoc -g

JSDoc 使用注释中的注解。您可以将注释直接放在任何文档中,通过 JSDoc 运行它,然后获得一些漂亮的文档。注释的类型必须是 /**/,因为 // 会被忽略。注解以 @ 开头。我选择直接在我的源代码中注释函数。不用担心文件大小,因为我们将在稍后最小化源时删除所有注释(除一个)。

我们将从记录全局命名空间开始。

/**
 * Contains all collection classes used by arrgh.js.
 * @namespace arrgh
 */
var arrgh = (function (undefined, MAX_SAFE_INTEGER) {

然后我们可以记录该命名空间中的类型。

/**
 * Represents a list of objects that can be accessed by index. Provides methods to manipulate the list.
 * @memberof arrgh
 * @constructor
 * @extends arrgh.Enumerable
 * @param {(Array|String|arrgh.Enumerable|params)} [enumerable=[]] - An array, string or enumerable whose elements are copied to the new list.
 */
var List = function (enumerable) {

@memberof@constructor@extends 基本上是不言自明的。@param 的默认格式是 @param {type} name - description。对于可选参数,将名称放在方括号中,例如 [name]。对于默认值,添加 =default,例如 [enumerable=[]](默认值是空数组)。当一个函数接受多种类型的参数时,您可以像示例中那样简单地列出它们,{(type1, type2, etc.)}。星号表示接受任何类型。

现在我们有了构造函数,我们可以记录它的函数。

/**
 * Gets the item at the specified index.
 * @function get
 * @memberof arrgh.List
 * @instance
 * @param {Number} index - The index at which the item should be retrieved.
 * @returns {*} - Returns the item at the specified index.
 * @throws Throws an error when the index is smaller than zero or equal or greater than the length of the collection.
 */
List.prototype.get = function (index) {

也可以记录全局类型,例如输入函数、回调函数或相等比较器之类的对象。

/**
 * A function that tests if two elements are equal.
 * @callback equals
 * @param {*} x - The element to test for equality.
 * @param {*} y - The element to test on.
 * @returns {Boolean} - Return whether the elements are equal.
 */

/**
 * Returns a hash code for the specified object.
 * @callback getHash
 * @param {*} obj - The object for which a hash code is to be returned.
 * @returns {String} - A hash code for the specified object.
 */

/**
 * Defines methods to support the comparison of objects for equality.
 * @name equalityComparer
 * @type {Object}
 * @property {equals} [equals=(===)] - A function that tests if two elements are equal.
 * @property {getHash} [getHash=getHash() || toString()] - A function that computes an element's hash code.
 */

/**
 * Represents a collection of keys each mapped to one or more values.
 * @memberof arrgh
 * @private
 * // ...
 * @param {equalityComparer} [eqComparer=(===)] - An object that tests if two keys are equal.
 */
var Lookup = function (source, keySelector, elementSelector, eqComparer) {

让我们生成一些文档。打开命令提示符并键入 jsdoc myFile.js。JSDoc 应该会创建一个 out 文件夹,其中包含生成的文档。文档现在应该看起来像这样。

您可以通过 CLI 参数设置输出样式并将其写入特定文件夹,但我们稍后将使用 Gulp 来完成。

使用 Gulp 进行自动化

接下来,我们将自动化一些事情。每次我保存文件(源文件或测试文件)时,我都想对 JavaScript 进行 linting,运行测试,生成文档,最小化代码,以及任何我想要的东西。如果我能自动化,我也想用一个命令运行所有这些。输入 Gulp

Gulp 是一个构建自动化工具。使用 Gulp,我们获取一些输入,通过一些任务运行它,然后将输出作为输入传递给下一个任务。最后,我们将最终输出写入某个目标,例如文件或控制台。

我们将把 Gulp 安装到我们的项目中以及全局安装,以便我们可以轻松地再次使用 CLI。

npm install gulp --save-dev
npm install gulp -g

接下来,我们将在项目根文件夹中创建一个名为 gulpfile.js 的文件。Gulp 本身不做什么。我们将需要一些插件。让我们先对我们的源文件进行 linting。为此,我们首先需要 gulp-minify 插件。

npm install gulp-minify --save-dev.

现在我们有了 Gulp 和 minify 插件,我们实际上可以在 gulpfile 中放入一些有用的代码。

var gulp = require('gulp');
var minify = require('gulp-minify');

gulp.task('minify', function () {
    return gulp.src('src/*.js')
    .pipe(minify({
        ext: {
            src: '.debug.js',
            min: '.js'
        },
        preserveComments: 'some'
    }))
    .pipe(gulp.dest('dist'));
});

gulp.task('default', function () {
    gulp.start('minify');
});

如果您之前做过一些 Node.js 工作,这看起来会很熟悉。我们首先需要 gulpgulp-minify。这两行代码将加载我们的运行时包(来自 node_modules 文件夹)。接下来,我们在 gulp 中创建一个名为 minify 的任务。我们使用 gulp.src 获取源文件,然后将其管道传输到 minify 模块。我们可以向 minify 模块传递一个配置对象,指定源文件和最小化文件的扩展名。最小化文件将命名为 arrgh.js,源文件命名为 arrgh.debug.js。我们保留了一些注释(即顶部的许可证)。结果再次被管道传输并写入 dist 文件夹。

在默认任务中,我们运行 minify 任务。

打开命令提示符,导航到您的项目文件夹,然后只需运行 gulp。这将运行默认任务。您也可以通过指定名称来运行特定任务。

gulp
gulp minify

以上任何一种操作都会运行 gulpfile 并最小化我们的源文件。

对于后续运行,我们希望删除所有之前的构建文件并重新开始。我们还希望对 JavaScript 进行 linting。我们将需要更多插件。

npm install gulp-clean --save-dev
npm install jshint --save-dev
npm install gulp-jshint --save-dev

gulpfile 现在看起来像这样。

var gulp = require('gulp');
var minify = require('gulp-minify');
var clean = require('gulp-clean');
var jshint = require('gulp-jshint');

gulp.task('clean', function () {
    return gulp.src([
        'dist/'
    ], { read: false })
    .pipe(clean());
})
.task('minify', ['clean'], function () {
    return gulp.src('src/*.js')
    .pipe(minify({
        ext: {
            src: '.debug.js',
            min: '.js'
        },
        preserveComments: 'some'
    }))
    .pipe(gulp.dest('dist'));
})
.task('lint', function () {
    return gulp.src('src/*.js')
    .pipe(jshint('jshint.conf.json'))
    .pipe(jshint.reporter('default'));
});

gulp.task('default', function () {
    gulp.start(['minify', 'lint']);
});

minify 任务现在依赖于 clean 任务。在清理完旧文件之前,我们无法进行 minify。默认任务现在将运行 minify(它将运行 clean)和 lint。lint 任务使用 jshint,它可以接受一个 json 文件作为参数。这非常酷,因为我们现在可以在外部文件中配置 jshint,并保持我们的 gulpfile 清洁。所以,在您的项目文件夹中创建一个 jshint.conf.json 文件。您可以在 jshint 文档 中找到各种配置选项。这是我的配置文件。

{
    "bitwise": true,
    "curly": true,
    "eqeqeq": true,
    "esversion": 3,
    "forin": true,
    "freeze": true,
    "futurehostile": true,
    "latedef": true,
    "nocomma": true,
    "nonbsp": true,
    "nonew": true,
    "notypeof": true,
    "strict": true,
    "undef": true,
    "unused": true
}

接下来是 JSDoc 任务。对于 arrgh.js,我还安装了另一个模板,因为我不太喜欢默认的。该模板名为 jaguarjs

npm install gulp-jsdoc3 --save-dev
npm install jaguarjs-jsdoc --save-dev

gulp-jsdoc3 也使用外部配置文件。它告诉 JSDoc 写入的位置,使用哪个模板,如果您喜欢的模板支持,您还可以配置该模板。在发布的版本中,我还有一个 README.md(GitHub 也使用它),我已将其包含在此配置文件中,并将其写入首页。我们这里没有,所以省略了它。

{
    "tags": {
        "allowUnknownTags": true,
        "dictionaries": ["jsdoc"]
    },
    "templates": {
        "applicationName": "arrgh.js",
        "meta": {
            "title": "arrgh.js",
            "description": "A lightweight JavaScript library that brings proper .NET-like collections and LINQ to the browser.",
            "keyword": "JavaScript, LINQ, collections, Array"
        }
    },
    "opts": {
        "destination": "docs",
        "private": true,
        "template": "node_modules/jaguarjs-jsdoc"
    }
}

JSDoc 插件的有趣之处在于,它在 Gulp 中有点反模式。它不管道任何东西,它只是获取输入,写入文档,然后将输入管道传输到下一个作业。无论如何,我们仍然可以创建一个任务来生成我们的文档。

var gulp = require('gulp');
var minify = require('gulp-minify');
var clean = require('gulp-clean');
var jshint = require('gulp-jshint');
var jsdoc = require('gulp-jsdoc3');

gulp.task('clean', function () {
    return gulp.src([
        'dist/',
        'docs/'
    ], { read: false })
    .pipe(clean());
})
.task('minify', ['clean'], function () {
    return gulp.src('src/*.js')
    .pipe(minify({
        ext: {
            src: '.debug.js',
            min: '.js'
        },
        preserveComments: 'some'
    }))
    .pipe(gulp.dest('dist'));
})
.task('lint', function () {
    return gulp.src('src/*.js')
    .pipe(jshint('jshint.conf.json'))
    .pipe(jshint.reporter('default'));
})
.task('jsdoc', ['clean'], function () {
    return gulp.src('src/*.js')
    .pipe(jsdoc(require('./jsdoc.conf.json')))
});

gulp.task('default', function () {
    gulp.start(['minify', 'lint', 'jsdoc']);
});

接下来,我们希望在 arrgh.js 发生更改时自动执行所有这些任务。我们可以使用 gulp.watch 来实现。

gulp.watch(['src/*.js', '*.conf.json'], ['minify', 'lint', 'jsdoc']);

gulp.task('default', function () {
    gulp.start(['minify', 'lint', 'jsdoc']);
});

现在,每当我们的源文件或配置文件发生更改时,Gulp 都会运行 minifylintjsdoc 任务。这次,当您运行 gulp 时,您会注意到它不会像以前那样终止。这是因为它现在正在监视您的文件。要在命令提示符中终止批处理作业,请使用 ctrl+c。

我的 gulpfile 目前看起来不像那样(尽管它曾经是),但您现在应该知道 Gulp 的工作原理以及如何创建和运行任务。

Karma

我们还缺少一项:自动化测试!不幸的是,仅使用 Jasmine,我们无法自动化任何内容。我们需要一个测试框架。市面上有几个,但我选择了 Karma。我们将从安装 Karma 开始。我们将需要很多插件(再次),所以准备好安装 Karma、Jasmine 插件和一些浏览器启动器(安装适用于您的浏览器启动器)。

npm install karma --save-dev
npm install karma-jasmine --save-dev
npm install karma-chrome-launcher --save-dev
npm install karma-firefox-launcher --save-dev
npm install karma-ie-launcher --save-dev

这是 gulpfile 中的 Karma 部分。

var karma = require('karma').Server;
// Code...
.task('test', function (done) {
    new karma({
        configFile: __dirname + '/karma.conf.js',
    }, function (err) {
        if (err > 0) {
            return done(err);
        }
        return done();
    }).start();
});

不幸的是,当 err > 0 时,Karma(或 Node.js)会在控制台中显示一个非常丑陋且无用的堆栈跟踪。可以使用 gulp-util 来帮助解决这个问题,但我不会在本文中详述。再次,我们有一个外部配置文件。

module.exports = function(config) {
    config.set({
        frameworks: ['jasmine'],
        files: [
            'src/*.js',
            'test/spec/*.js'
        ],
        reporters: ['progress'],
        port: 9876,
        autoWatch: true,
        browsers: ['Chrome'], // And/or Firefox or IE...
        singleRun: true
    });
};

如果您希望在每次文件更改时都进行测试,请将 singleRun 设置为 false

现在,当您从控制台运行 gulp test 时,您应该会看到一个浏览器启动,进行一些 Karma 操作,然后关闭,并在您的控制台中显示结果。

让我们添加一些代码覆盖率,因为测试如果没有知道它覆盖了什么,有什么用呢?

npm install karma-coverage --save-dev

您只需要更改配置文件。

module.exports = function(config) {
    config.set({
        frameworks: ['jasmine'],
        files: [
            'src/*.js',
            'test/spec/*.js'
        ],
        preprocessors: {
            'src/*.js': ['coverage']
        },
        reporters: ['progress', 'coverage'],
        port: 9876,
        autoWatch: true,
        browsers: ['Chrome'],
        singleRun: true,
        coverageReporter: {
            reporters: [
                { type : 'html', subdir: 'html' }
            ],
            dir : 'test/coverage/',
            check: {
                global: {
                    statements: 95,
                    branches: 95,
                    functions: 95,
                    lines: 95
                },
            }
        }
    });
};

有了这个设置,您将在 /test/coverage/html 中获得一个漂亮而详细的 HTML 报告。您还可以设置一些全局阈值,因此当未达到最低覆盖率时,任务将失败。

不在本文的范围内,但值得一提的是,您可以为 Jenkins 和 Travis 等 CI 系统安装额外的插件。例如,Jenkins 使用 JUnit 报告,因此您可以安装 karma-junit-reporter。Jenkins 还使用 cobertura 格式进行代码覆盖率,该格式已包含在 karma-coverage 插件中,但应进行配置(在 coverageReporter.reporters 中添加另一个报告器)。

所以现在我们已经完成了所有内容的 linting、测试、最小化和文档编写(别忘了也要测试您的最小化文件)。是时候发布这个宝贝了!

发布 arrgh.js

现在我们已经准备就绪,让我们看看如何将 arrgh.js 发布到 npm 和 NuGet。显然,这是我的包,所以您实际上无法发布它。名称已被占用,并且是我的。您可以想出一个新的名称并发布它,但这对您来说有点不好。

npm

首先,我们将介绍如何发布到 npm。首先,您需要一个 npmjs.com 上的帐户。您可以在网站上注册,或在控制台中创建一个用户,键入 npm adduser(尽管我从未尝试过)。创建帐户后,您就可以使用控制台登录 npm。

npm login

控制台将要求您输入用户名和密码。现在您已登录,理论上,您只需使用 npm publish 即可发布。

cd myProject
npm publish

npm 然后会查看您的 package.json 文件,并使用它来创建您的包的页面。我们尚未讨论 package.json,只知道您需要它来保存您的开发依赖项。package.json 文件包含有关您项目的信息,例如名称、描述、作者、许可证、依赖项等。据我所知,名称、版本和描述是必需的,其他都是可选的。这是我 package.json 文件中最重要的部分。

{
  "name": "arrgh",
  "version": "0.9.2",
  "description": "A lightweight JavaScript library that brings proper .NET-like collections and LINQ to the browser.",
  "main": "arrgh.js",
  "files": [
    "arrgh.js",
    "arrgh.debug.js"
  ],
  "scripts": {
    "prepublish": "gulp && xcopy .\\dist\\arrgh.js .\\arrgh.js* && xcopy .\\dist\\arrgh.debug.js .\\arrgh.debug.js*"
  }
  //...
}

每次发布时,名称和版本都需要是唯一的。所以您不能发布任何版本的 arrgh,因为 arrgh 这个名称属于我。我当前已发布的版本是 0.9.2,所以不能再次发布。

默认情况下,npm 会发布您的整个文件夹及其子文件夹,因此您可以指定要定位的特定文件。文件将按原样发布,包括它们当前的路径。所以,如果您发布 folder/subfolder/my-script.js,发布的包也将包含 folder/subfolder/my-script.js。您还可以创建一个 npmignore 文件,其功能与 gitignore 文件相同(npm 将忽略 gitignore 和 npmignore 中包含的文件和模式,但 npmignore 会覆盖 gitignore)。一些文件,如 package.json,无论您的设置如何都会被发布。您可以在 npm 开发者文档 中找到这些信息。

这是一个有趣的故事。我个人讨厌安装一个我只想使用的包,然后我得到一堆我永远不会使用的文件,比如一个 gulpfile,因为作为 Node.js 包的消费者,我为什么要需要一个 gulpfile!?所以,我只想发布必需的 package.json、readme.md 和 dist 文件夹的内容,但不是文件夹本身。所以我将 dist/arrgh.jsdist/arrgh.debug.js 放在 package.json 的 files 字段中,并且 dist 文件夹被分发了。在版本 1.0.0 下。没问题,我想。我将取消发布它,然后再次正确发布它。所以,我取消了发布,以为一切都会消失(甚至我看不见它),但它实际上还在那里,现在我永远无法再次发布 1.0.0。所以,我选择了 0.9.0 作为测试……

当然,我在这里是为了让您不必犯这个错误。您可以使用 npm pack 来查看实际会发布什么。这将创建一个包含将要发布的文件的 tarball 文件。不幸的是,无法从 dist 中抓取文件并将其放入根目录,所以我使用 package.json scripts 对象在发布之前将 dist 的内容复制到根文件夹。我也在运行 Gulp,以防万一。如果 Gulp 构建或测试失败,npm 将不会发布。您还可以指定其他脚本,例如 postpublishpreinstallinstall 等。

在更新时,您可以手动递增版本,或者在命令中使用 npm version major/minor/patch。npm 在内部使用 semvar

这就是我拥有自己的 npm 包的方法!

NuGet

除了 npm,我还想将我的包发布到 NuGet,因为我通常用 C# 编写。在开始之前,您需要 NuGet 的包版本,称为 nuspec。就像 package.json 一样,nuspec 文件包含有关您的包的信息。它是一个 XML 文件,包含 id、version、title 和 description 等字段。它并不算大,所以这是我 0.9.2 版本的完整 nuspec 文件。

<?xml version="1.0"?>
<package>
    <metadata>
        <id>arrgh.js</id>
        <version>0.9.2</version>
        <title>arrgh.js</title>
        <description>A lightweight JavaScript library that brings proper .NET-like collections and LINQ to the browser.</description>
        <authors>Sander Rossel</authors>
        <owners>Sander Rossel</owners>
        <language>JavaScript</language>
        <licenseUrl>https://spdx.org/licenses/MIT</licenseUrl>
        <projectUrl>https://sanderrossel.github.io/arrgh.js/</projectUrl>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <releaseNotes>Fixed Dictionary implementation.</releaseNotes>
        <copyright>Copyright 2016</copyright>
        <tags>JavaScript LINQ collections array List HashMap Dictionary</tags>
    </metadata>
    <files>
        <file src="dist\*.js" target="content\Scripts" />
    </files>
</package>

请注意,我多么神奇地可以告诉 NuGet 要包含哪些文件以及将它们放在哪里!?真棒!目标必须以 libcontenttools 开头,所以我选择了 content。您应该阅读完整的 Nuspec 文档 以获取更多信息。

要将包发布到 NuGet,您可能会使用 Visual Studio,但我在这方面根本没有使用过 VS,所以现在为什么开始呢?相反,我们可以从 NuGet 下载 中下载 Windows x86 命令行工具。我实际上也将其包含在 GitHub 中。要构建包,只需启动命令并使用 nuget pack

cd folder_of_project_including_nuget_cli
nuget pack

这将创建一个 nupkg 文件,您可以使用它来进行私有存储库(您可以在 Visual Studio 中创建它们,只需前往 NuGet 设置,创建一个新存储库,然后定位计算机上的某个文件夹,现在 NuGet 将自动查找该文件夹中的 nupkg 文件),或者您可以 手动上传到 NuGet。当然,您也需要在 NuGet 上拥有一个帐户。

可以直接从命令行推送包,但我实际上并未尝试过。您可以在 文档 中找到如何操作。手动上传非常简单,以至于我甚至没有 bother。当然,如果您有一个 CI 服务器,如 Jenkins、TFS 或 Travis,这会很棒(也很必要)。

这就是我拥有自己的 NuGet 包的方法!

添加对 AMD/RequireJS 和 CommonJS/Node.js 的支持

对 AMD/RequireJS 和 CommonJS/Node.js 的支持已在 1.1.0 版本中添加。您可以在 GitHub 上找到这些更改。

现在在浏览器中一切都运行良好,但这是 JavaScript 和 Web,事情总是有多种实现方式。加载 JavaScript 文件也是如此。在浏览器中,您可以简单地在 HTML 中引用一个脚本,它就可以工作(您的包会作为全局变量公开),但这只是公开脚本的一种方式。另一种公开脚本的方法是通过 AMD(异步模块定义),由 RequireJS 支持。

require(['node_modules/arrgh/arrgh.js'], function (arrgh) {
	// Use arrgh here...
});

这很简单,因为您不必在 HTML 文件中按正确的顺序引用所有脚本。

还有另一种方法,由 Node.js(后端 JavaScript 解决方案)使用,那就是 CommonJS。CommonJS 允许您在需要时 require 文件,并具有与 RequireJS 相同的优点,但语法更简单。

var arrgh = require('arrgh');
// Use arrgh here...

不幸的是,支持这两种方法之一需要我们更改 JavaScript,并将阻止我们“简单地”在 HTML 中加载脚本。幸运的是,我们可以同时支持这三种。

var arrgh = function () { ... };

// Support for AMD...
define(arrgh);

// Support for CommonJS...
module.exports = arrgh();

// Support for "simple" referencing...
window.arrgh = arrgh();

所以,基本上,归根结底是检查是否加载了 RequireJS,如果是,则使用 define;如果加载了 CommonJS,则使用 module.exports;否则,我们将 arrgh 附加到 window 对象。

这是相当简单的代码,也很容易在 Google 上找到(并且可能已经有一个 npm 包了)。

(function (name, definition) {
    "use strict";
    if (typeof module !== "undefined") {
        module.exports = definition();
    }
    else if (typeof define === "function" && typeof define.amd === "object") {
        define(definition);
    }
    else {
        window[name] = definition();
    }
}("arrgh", function () { ... });

这段代码确保我们使用正确的加载脚本的方法。请注意,这些方法是互斥的,CommonJS 优先于 AMD,AMD 优先于简单加载。

当然,我们仍然需要测试我们的东西。我添加了一个简单的 AMD 测试,我手动执行了它。

describe("arrgh.js tests", function () {
    it("should load using require.js", function (done) {
        require(['../src/arrgh.js'], function (arrgh) {
            expect(arrgh.Enumerable).not.toBeNull();
            done();
        });
    });
});

Node.js 测试有点复杂,因为我们将在一个全新的平台上运行。我复制了 tests.js 文件并创建了一个名为 node-spec.js 的文件(命名对于我们将使用的 jasmine-node 库很重要)。该文件与复制的文件完全相同,只是 arrgh.js 的定义和加载其他脚本不同。

var arrgh = require('../../src/arrgh.js');
/* jshint ignore:start */
var fs = require('fs');
eval(fs.readFileSync('./test/spec/test-Enumerable_overridden.js','utf-8'));
eval(fs.readFileSync('./test/spec/test-Iterators.js','utf-8'));
// [...]
/* jshint ignore:end */

这不是我最好的代码,但如果它看起来很傻并且有效,那它就不傻。我们现在可以使用 npm 安装 jasmine-nodegulp-jasmine-node。在我们的 gulpfile 中,我们可以添加以下任务。

var jasmineNode = require('gulp-jasmine-node');

[...]

.task('test-node', ['test'], function () {
    return gulp.src('test/spec/node-spec.js')
    .pipe(jasmineNode({ reporter: [ new jasmine.TerminalReporter({ color: true }) ] }));
})

不幸的是,我们的测试在 Node.js 中运行方式略有不同,[NaN] 不再等于 [NaN],因此包含 NaN 的测试需要稍作更改。

it("should add NaN to the list", function () {
    var l = new arrgh.List();
    l.add(NaN);
    var arr = l.toArray();
    expect(arr.length).toBe(1);
    expect(arr[0]).toBeNaN();
    // This doesn't work anymore.
    //expect(l.toArray()).toEqual([NaN]);
});

还有一件事,要使 require('arrgh') 在 Node.js 中正常工作,我们的 arrgh.js 文件需要命名为 index.js。所以,在我们的 package.json 中,我们需要添加一个额外的 xcopy 命令“xcopy .\\dist\\arrgh.js .\\index.js*”,并且我们还需要在 package.json 的 files 节点中添加 index.js。

经过一些测试、更改文档和玩弄后,我们准备发布了!我已将此更改发布为 1.1.0,因为 0.9 实际上已经是 1.0 版本了……无论如何,请享用 arrgh.js 的这些新功能!

结论

所以,这就是我创建自己的 JavaScript LINQ 库并将其发布到 npm 和 NuGet 的旅程。我在此过程中学到了很多东西,我希望我将其中一些知识传授给了您,读者。我目前在工作中为项目使用 arrgh,它使一些事情变得非常简单和简洁。

如果您也决定使用 arrgh,请告诉我,我很想听听您的反馈!任何建议、改进、错误或缺失的功能都可以报告在此处或 GitHub issues 上。

祝您编码愉快!

© . All rights reserved.