我怎样才能确保全局范围JS范围(窗口)保持清洁?

时间:2015-05-21 15:49:54

标签: javascript testing

提供第三方库很危险,因为它所使用的环境可能会有很大差异。谈论JS在浏览器中运行时,不小心覆盖了可能被另一个库使用的全局(即window)属性,可能会破坏严重依赖脚本的站点。

为避免这种情况,我想自动测试是否以任何非预期的方式修改了全局范围,例如:

function foo() {
  myVar = 'this becomes global because of a missing var keyword';
}

window.jQuery = 'this is not jQuery at all, breaking everything that uses it';

(我错过了另一种变体吗?)

一个典型的例子

我的库打包了一个特定版本的jQuery。使用var myJQuery = jQuery.noConflict(true);,我可以恢复预先存在的$jQuery。如果我忘记提供true作为参数,则仅恢复$,并且我的jQuery版本仍保留在window.jQuery中,可能会破坏需要不同版本的其他库。

我希望有一个可以自动检测这种情况的测试。

到目前为止我尝试了什么

使用JSLint / JSHint进行静态分析

据我所知,由于缺少var关键字,这仅涵盖偶然的全局变量。显然,无法检测到“故意”window属性分配。

有JSHint的global配置选项,它允许我指定是否可以写入全局属性,但在这种情况下,我不必列出所有可能的(或现实的)偶然全局变量将来相遇?

实施自定义检查以查找window.something = ...作业是个好主意吗?

将Jasmine规范之后的window与规范之前的window快照进行比较

由于我有一套广泛的Jasmine测试,在每个规范之前制作window快照,并在执行规范后将当前window与该快照进行比较似乎是一个好主意。最大的优点是它涵盖了许多执行路径。由于与某些遗留代码相关的各种原因,如果没有重大的重构,这将无法解决,这将花费比我更多的时间。

与上述相同,但手动

这有点奏效,但它几乎不涉及任何执行路径,可能会留下一些未被发现的泄漏全局变量。

注意在开发期间不修改全局范围

我几乎找不到有关自动测试这种情况的资源,所以看起来大多数人都小心谨慎地始终使用var,范围内的一切,如果绝对必要,请使用尽可能少的全局变量非常独特的名字。也许他们也有策略让失败并在用户开始抱怨后迅速回滚。

这种方法并不令人满意,因为它缺乏严格的自动测试。

tl; dr我想知道什么

  • 是否存在自动测试全球范围污染的现有方法?
  • 如果没有,那么在彻底性和所需的实施工作方面,最好的方法是什么?

2 个答案:

答案 0 :(得分:2)

您可以使用Object.freeze()

  

Object.freeze()方法冻结一个对象:即阻止新对象   属性被添加到它;防止现有属性   被删除;并防止现有的属性,或他们的   可更改性,可配置性或可写性。在   本质上,对象是有效不可变的。

因此,这行代码将阻止将来修改window

Object.freeze(window);

请注意,这是非常核心,会打破很多代码。使用它之后,所有试图修改window的代码都会抛出。

答案 1 :(得分:0)

毕竟我们决定利用Jasmine测试的覆盖范围。要做到这一点(在Jasmine 1.x中),您需要覆盖jasmine.Spec.prototype.addBeforesAndAftersToQueue,如:

var originalFunc = jasmine.Spec.prototype.addBeforesAndAftersToQueue;
jasmine.Spec.prototype.addBeforesAndAftersToQueue = function myReplacement() {
    this.queue.addBefore(new jasmine.Block(this.env, saveWindowSnapshot));

    // Call the original.
    originalFunc.apply(this, myReplacement.arguments);

    // Check for changes and expect to find no changes.
    this.queue.add(new jasmine.Block(this.env, function () {
        var changedProperties = compareSnapshotWithCurrent(...);
        expect(changedProperties.length).toEqual(0);
    }, this);
};

compareSnapshotWithCurrent()最好以递归方式实现,因此会注意到对现有全局对象属性的修改。由于window下树的大小,4-5的递归深度限制对于性能和彻底性的良好平衡是个好主意。

由于window包含循环引用,因此跟踪访问对象也不是一个坏主意。如果没有设置递归深度限制,则可以避免无限循环。

window的某些属性在运行脚本时会自然更改,例如window.location。取决于执行环境,例如,浏览器与PhantomJS相比,可能存在其他或缺少的window属性。因此,对于compareSnapshotWithCurrent,忽略列入白名单的属性是有意义的。

我的实现大致如下:

// recursionLevel starts at 0 and is incremented on each descent to limit depth.
// propertyPrefix starts with 'window' and is a dot-separated "path" in the object
// tree under window.
// obj and snapshot are initially the window object and its snapshot, and on later
// recursions its children.
function compareSnapshotWithCurrent(recursionLevel, propertyPrefix, obj, snapshot) {
    var changedProperties = [];
    $.each(Object.keys(obj), function (index, propertyName) {
        var prefixedPropertyName = propertyPrefix + '.' + propertyName;
        var property = obj[propertyName];

        if (!isWhitelisted(prefixedPropertyName)) {
            // If snapshot doesn't have the property:
            changedProperties.push(prefixedPropertyName);
            // If the depth limit has not been exceeded yet, and the current
            // property is either function or object, and it has not been
            // visited before:
            // Track this property as visited.
            Array.push.apply(changedProperties, compareWindowObjSnapshotWithReality(
                recursionLevel + 1,
                prefixedPropertyName,
                property,
                snapshot[propertyName]);
            // Finally, if the property differs entirely from its snapshot
            // counterpart:
            changedProperties.push(prefixedPropertyName);
        }
    });

    return changedProperties;
}

提供伪代码解决方案的道歉 - 由于IP原因,我只能分享这个想法,而不是来源。