在JavaScript中检查字符串是否为空白(即只包含空格)的最高效方法

The most performant way to check if a string is blank (i.e. only contains whitespace) in JavaScript?

本文关键字:空格 包含 方法 高效 检查 JavaScript 字符串 是否 空白      更新时间:2023-09-26

我需要写一个函数来测试,如果给定的字符串在某种意义上是"空白",它只包含空白字符。空格字符如下:

''u0009',
''u000A',
''u000B',
''u000C',
''u000D',
' ',
''u0085',
''u00A0',
''u1680',
''u180E',
''u2000',
''u2001',
''u2002',
''u2003',
''u2004',
''u2005',
''u2006',
''u2007',
''u2008',
''u2009',
''u200A',
''u2028',
''u2029',
''u202F',
''u205F',
''u3000'

函数将被调用很多次,所以它必须非常非常高效。但不应该占用太多内存(比如将数组中的每个字符映射为true/false)。到目前为止我尝试过的事情:

  • regexp -性能不太好
  • 修剪并检查长度是否为0 -性能不太好,还使用额外的内存来保存修剪过的字符串
  • 根据包含空白字符(if (!whitespaceCharactersMap[str[index]]) ...)的散列集检查每个字符串字符-工作得足够好
  • 我当前的解决方案使用硬编码比较:

    function(str) {
        var length = str.length;
        if (!length) {
            return true;
        }
        for (var index = 0; index < length; index++)
        {
            var c = str[index];
            if (c === ' ')
            {
                // skip
            }
            else if (c > ''u000D' && c < ''u0085')
            {
                return false;
            }
            else if (c < ''u00A0')
            {
                if (c < ''u0009')
                {
                    return false;
                }
                else if (c > ''u0085')
                {
                    return false;
                }
            }
            else if (c > ''u00A0')
            {
                if (c < ''u2028')
                {
                    if (c < ''u180E')
                    {
                        if (c < ''u1680')
                        {
                            return false;
                        }
                        else if(c > ''u1680')
                        {
                            return false;
                        }
                    }
                    else if (c > ''u180E')
                    {
                        if (c < ''u2000')
                        {
                            return false;
                        }
                        else if (c > ''u200A')
                        {
                            return false;
                        }
                    }
                }
                else if (c > ''u2029')
                {
                    if (c < ''u205F')
                    {
                        if (c < ''u202F')
                        {
                            return false;
                        }
                        else if (c > ''u202F')
                        {
                            return false;
                        }
                    }
                    else if (c > ''u205F')
                    {
                        if (c < ''u3000')
                        {
                            return false;
                        }
                        else if (c > ''u3000')
                        {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }
    

这似乎比哈希集(在Chrome上测试)快50-100%。

有人看到或知道进一步的选择吗?

更新1

我将在这里回答一些评论:

  • 它不只是检查用户输入是否为空。我必须解析某些数据格式,其中空格必须单独处理。
  • 值得优化。我以前对代码进行过分析。检查空白字符串似乎是一个问题。而且,正如我们所看到的,不同方法之间的性能差异可能高达10倍,这绝对值得努力。
  • 一般来说,我发现这种"哈希集vs.正则表达式vs.开关vs.分支"的挑战非常有教育意义。
  • 我需要浏览器以及node.js的相同功能。

现在这是我对性能测试的看法:

http://jsperf.com/hash-with-comparisons/6

如果你们能多做几次这些测试我会很感激的。

初步结论:

  • branchlessTest (a^9*a^10*a^11...)在Chrome和Firefox中非常快,但在Safari中不是。从性能的角度来看,这可能是Node.js的最佳选择。
  • switchTest在chrome和Firefox上也相当快,但是,令人惊讶的是在Safari和Opera中最慢
  • regexp with re.test(str)在任何地方都表现良好,在Opera中甚至最快。
  • 哈希和分支几乎在任何地方都显示出几乎相同的糟糕结果。比较也类似,往往表现最差(这可能是由于实现,检查' '应该是第一个)。

总而言之,对于我的情况,我将选择以下regexp版本:

var re = /[^'s]/;
return !re.test(str);

原因:

  • 无分支版本在Chrome和Firefox中很酷,但不太便携
  • 在Safari中切换太慢
  • regexp似乎在任何地方都表现良好,它们在代码中也非常紧凑

硬编码的解决方案似乎是最好的,但我认为switch应该更快。这取决于JavaScript解释器处理这些的方式(大多数编译器都非常有效地做到这一点),所以它可能是特定于浏览器的(即,在某些地方快,在其他地方慢)。另外,我不确定JavaScript处理utf字符串有多快,所以您可以尝试在比较值之前将字符转换为整数代码。

for (var index = 0; index < length; index++)
{
    var c = str.charCodeAt(index);
    switch (c) {
        case 0x0009: case 0x000A: case 0x000B: case 0x000C: case 0x000D: case 0x0020:
        case 0x0085: case 0x00A0: case 0x1680: case 0x180E: case 0x2000: case 0x2001:
        case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007:
        case 0x2008: case 0x2009: case 0x200A: case 0x2028: case 0x2029: case 0x202F:
        case 0x205F: case 0x3000: continue;
    }
    return false;
}

另一件要考虑的事情是改变for:

for (var index in str)
{
    ...
}

编辑

您的jsPerf测试得到了一些修订,当前的版本在这里。我的代码在Chrome 26和27以及IE10中要快得多,但在Firefox 18中也是最慢的。

我在64位Linux上的Firefox 20.0上运行了相同的测试(我不知道如何使jsPerf保存这些),结果证明它是两个最快的之一(与trimTest并列,都是大约11.8M ops/sec)。我还在WinXP上测试了Firefox 20.0.1,但在VirtualBox(仍然在64位Linux下,这里可能会有很大的不同)下,switchTest的速度为10M ops/sec, trimTest的速度为7.3M ops/sec。

所以,我猜性能取决于浏览器版本和/或甚至可能取决于底层操作系统/硬件(我想上面的FF18测试是在Win上进行的)。无论如何,要制作真正的最佳版本,你必须制作许多版本,在所有浏览器、操作系统、架构上测试每个版本……你可以获取,然后在你的页面中包含最适合访问者浏览器、操作系统、架构的版本……但是,我不确定哪种代码值得这么麻烦。

由于分支比大多数其他操作要昂贵得多,因此您希望将分支保持在最低限度。因此,if/else语句的序列可能不是很高效。一种主要使用数学的方法会快得多。例如:

执行相等性检查而不使用任何分支的一种方法是使用位操作。一个例子是,检查a == b:

a ^ b == 0

由于两个相似位(即1 ^ 1或0 ^ 0)的xor为0,因此两个相等的值的xor为0。这很有用,因为它允许我们将0视为"真"值,并进行更多的数学运算。假设我们有一堆布尔变量以这种方式表示:非零的数字为假,零表示真。如果我们想问,"这些都是真的吗?"我们只需要把它们相乘。如果其中任何一个为真(等于0),则整个结果将为0。

因此,例如,代码看起来像这样:
function(str) {
    for (var i = 0; i < str.length; i++) {
        var c = str[i];
        if ((c ^ ''u0009') * (c ^ ''u000A') * (c ^ ''u000B') ... == 0)
            continue;
        return false;
    }
    return true;
}

这样做比简单地做一些事情更高效的主要原因:

if ((c == ''u0009') || (c == ''u000A') || (c == ''u0008') ...)

是JavaScript有短路布尔运算符,这意味着每次使用||运算符时,它不仅执行或操作,而且还检查它是否可以证明该语句到目前为止必须为真,这是一个分支操作,这是昂贵的。另一方面,数学方法不涉及分支,除了if语句本身,因此应该要快得多。

这将对字符串的字符创建并使用'散列'查找,如果检测到非空白则返回false:

var wsList=[''u0009',''u000A',''u000B',''u000C',''u000D',' ',''u0085',''u00A0',''u1680',''u180E',''u2000',''u2001',''u2002',''u2003',''u2004',''u2005',''u2006',''u2007',''u2008',''u2009',''u200A',''u2028',''u2029',''u202F',''u205F',''u3000'];
var ws=Object.create(null);
wsList.forEach(function(char){ws[char]=true});
function isWhitespace(txt){
    for(var i=0, l=txt.length; i<l; ++i){
        if(!ws[txt[i]])return false;
    }
    return true;
}
var test1=" 'u1680 'u000B 'u2002 'u2004";
isWhitespace(test1);
/*
true
*/
var test2=" _ . a ";
isWhitespace(test2);
/*
false
*/

不确定它的性能(yet)。在jsperf上进行快速测试后,与使用/^'s*$/的RegExp相比,它非常慢。


编辑:

看来,您应该使用的解决方案可能取决于您正在处理的数据的性质:数据主要是空白还是主要是非空白?也主要是ascii范围的文本?对于一般的测试用例,您可以通过对常见的非空白字符范围使用范围检查(通过if),对最常见的空白使用switch,然后对其他所有内容使用散列查找来加快速度。如果被测试的大多数数据由最常见的字符(在0x0—0x7F之间)组成,这可能会提高测试的平均性能。

也许像这样的东西(if/switch/hash的混合)可以工作:

/*same setup as above with variable ws being a hash lookup*/
function isWhitespaceHybrid(txt){
    for(var i=0, l=txt.length; i<l; ++i){
        var cc=txt.charCodeAt(i)
        //above space, below DEL
        if(cc>0x20 && cc<0x7F)return false;
        //switch only the most common whitespace
        switch(cc){
            case 0x20:
            case 0x9:
            case 0xA:
            case 0xD:
            continue;
        }
        //everything else use a somewhat slow hash lookup (execute for non-ascii range text)
        if(!ws[txt[i]])return false;
    }
    return true;
}