JavaScript字符串连接在使用null或undefined值时的行为

104

在JavaScript中,您可能已经知道'' + null = "null"'' + undefined = "undefined"(在我可以测试的大多数浏览器中:Firefox,Chrome和IE)。 我想知道这种奇怪现象的起源是什么(Brendan Eich脑子里到底想了什么?!),是否有计划在未来的ECMA版本中进行更改。确实非常令人沮丧,需要使用'sthg' + (var || '')来连接包含变量的字符串,而使用第三方框架如Underscore等则犹如对着果冻钉钉子。 编辑: 为满足 StackOverflow 的要求并澄清我的问题,这是一个三重问题:
  • JS 将 nullundefined 转换为它们在 String 拼接时的字符串值背后的历史是什么?
  • 未来的 ECMAScript 版本中是否有机会改变这种行为?
  • 最漂亮的方法是将 String 与潜在的 nullundefined 对象连接起来,而不会遇到此问题(在字符串中获得一些"undefined""null")? 主观标准下的“漂亮”意味着:简短,干净,有效。 不用说'' + (obj ? obj : '')真的不太漂亮...

+1 同意,我也期望有同样的行为(现在这个行为让我的应用程序数据变慢了很多,需要很多 if 条件)。我有 R 的背景,在那里你会得到你所说的 paste0("a", NULL) == "a" - Michele
实际上,这种行为正是我所期望的。null 和 undefined 会被转换为它们的字符串表示形式,这与空字符串不同,但编写一个函数来检查其参数并在其为 null 或 undefined 时返回空字符串,并返回任何其他内容的 toString 就足够简单了。 - Mike Lippert
@MikeLipper 这很容易,但不是本地的,并且在这方面没有合并(JS中没有运算符重载,希望如此)。 合并在JS中对于紧凑的代码非常有用,而这种优势被这种行为破坏了。 您必须使用解决方法。 如果我提出问题,那么这种行为远远不符合我的期望。 - Doc Davluz
2
这是一种很好的方式,让您知道某个值没有被分配实际的字符串值和/或缺少 toStringvalueOf 方法。我想,在您希望它们显示为空字符串的情况下,您希望它们显示为 nullundefined 的情况同样多。幸运的是,我几乎从未遇到过这个问题,因为我经常使用 join 连接字符串(它解决了其他问题,如在使用三元(?)表达式时进行连接)。 - jongo45
7个回答

120

你可以使用Array.prototype.join来忽略undefinednull


['a', 'b', void 0, null, 6].join(''); // 'ab6'
根据规范

如果elementundefinednull,则让next成为空字符串;否则,让next为ToString(element)。


鉴于此,

  • JS将nullundefined转换为它们的字符串值在String连接中的奇怪行为背后的历史是什么?

    实际上,在某些情况下,当前的行为是有意义的。

    function showSum(a,b) {
        alert(a + ' + ' + b + ' = ' + (+a + +b));
    }
    例如,如果上面的函数没有参数被调用,则undefined + undefined = NaN可能比 += NaN 更好。
    一般来说,如果您想在字符串中插入某些变量,则显示undefinednull是有意义的。可能,Eich也这样想。
    当然,有些情况下忽略它们会更好,例如将字符串连接在一起时。但对于这些情况,您可以使用Array.prototype.join
    “在未来的ECMAScript版本中是否有更改此行为的机会?”
    很可能不会。
    由于已经有了Array.prototype.join,修改字符串连接的行为只会导致劣势而没有优势。此外,它会破坏旧代码,因此它不向后兼容。
    “将String与潜在的nullundefined进行连接的最漂亮的方法是什么?” Array.prototype.join似乎是最简单的方法。它是否最漂亮可能是基于个人观点的。

我认为这是对第三部分问题的最佳答案。我想点赞,但该问题还有另外两个部分。不过,我想这应该作为三个不同的问题发布。 - Keen
@Cory 确实,我已经完成了答案。 - Oriol
30
你可以使用 Array.prototype.join 方法来忽略 undefined 和 null 值。需要注意的是,只有当你将一个空字符串作为 join 参数时,此方法才会按照期望的方式运作。 - Madbreaks
2
.join() 后添加 .trim(),它将删除前导/尾随空格(如果使用空格连接)。同样,其他类型的字符也可以被删除。 - Fyodor Yemelyanenko
结合字符串插值用于URL构建:let url = \${AREA_ROOT}/Projects/${[projectId].join('')}`` - Chris Marisic

41

如何以最美观的方式将可能为null或undefined对象的字符串连接起来而不会出现问题?[...]

有几种方法,您部分地提到了它们。简而言之,我能想到的唯一干净的方法是使用一个函数:

const Strings = {};
Strings.orEmpty = function( entity ) {
    return entity || "";
};

// usage
const message = "This is a " + Strings.orEmpty( test );

当然,您可以(并且应该)更改实际的实现以适应您的需求。这也是我认为此方法优越的原因:它引入了封装。

如果您没有封装,那么您只需要问最“漂亮”的方法是什么。您会问自己这个问题,因为您已经知道,您将会陷入一个无法再更改实现的地方,所以您希望它一开始就完美无缺。但是事实上变化是不可避免的,要求、观点甚至环境都在不断发展。那么为什么不允许自己进行实现上的修改,并且只需微调一行或者两行代码就能够轻松完成呢?

您可能会认为这是作弊,因为它并没有真正回答如何实现实际逻辑。但是这正是我的观点:这并不重要。好吧,也许有一点点重要。但是实际上,不需要担心,因为修改起来非常简单。而且由于它不是内联的,所以它看起来更加漂亮——无论您是按照这种方式还是用更复杂的方式实现它。

如果您在代码中反复重复使用||内联,那么您会遇到两个问题:

  • 您会重复代码。
  • 由于您重复了代码,未来对其进行维护和更改将变得困难。

这些都是在高质量软件开发方面公认的反模式。

有些人会说这太耗费性能;他们会谈论性能问题。这是无稽之谈。首先,这几乎不会增加额外的开销。如果您担心这一点,那么您选择的语言可能不正确。即使是jQuery也使用函数。人们需要克服微观优化的困扰。

另一件事是:您可以使用代码“编译器”=缩小器。在该领域中,好的工具会尝试检测编译步骤中应内联哪些语句。这样,您就可以保持代码整洁和可维护性,并且仍然可以获得最后一点性能提升,如果您仍然相信它或者确实有一个环境,那么这很重要。

最后,请相信浏览器。他们会优化代码,并且现在他们做得非常出色。


你的答案非常完美,因为它似乎是处理这个问题更有效和优雅的方式。即使它没有回答前两个困难的点,我也接受它。@Oriol 也值得一些赞誉,因为他也回答了那些缺失的点。 - Doc Davluz
讽刺的是,你在警告重复代码的同时,提出了一种会导致重复代码的方法。 - a better oliver
@zeroflagL,你可以在用例抽象层面上进行更多的封装。但我不知道涉及哪些用例。我只是提出了一个想法。 - Ingo Bürk

16

ECMA规范

为了更好地解释为什么它会按照这种方式运作,需要参考自版本一以来一直存在的规范。在5.1版中,定义与此相同,以下是5.1版的定义。

第11.6.1节:加法运算符(+)

加法运算符可以执行字符串连接或数字加法。
产生式 AdditiveExpression : AdditiveExpression + MultiplicativeExpression 的计算如下:
1. 计算 AdditiveExpression 的结果并将其赋值给 lref。 2. 获取 lref 的值并将其赋值给 lval。 3. 计算 MultiplicativeExpression 的结果并将其赋值给 rref。 4. 获取 rref 的值并将其赋值给 rval。 5. 将 lval 转换为基本类型的值并将其赋值给 lprim。 6. 将 rval 转换为基本类型的值并将其赋值给 rprim。 7. 如果 Type(lprim) 为 String 或 Type(rprim) 为 String,则返回将 ToString(lprim) 和 ToString(rprim) 连接起来的字符串。 8. 返回将 ToNumber(lprim) 和 ToNumber(rprim) 应用于加法操作的结果。请参阅注释 11.6.3。
因此,如果其中一个值最终是String,则在两个参数(第7行)上使用ToString,并将它们连接起来(第7a行)。ToPrimitive返回所有非对象值不变,因此nullundefined不受影响: 第9.1节ToPrimitive 引用块: ToPrimitive抽象操作接受一个输入参数和一个可选的PreferredType参数。 ToPrimitive抽象操作将其输入参数转换为非对象类型...按照表10进行转换:
对于所有非Object类型,包括Null和Undefined,结果等于输入参数(无转换)。 因此,在这里ToPrimitive什么也不做。

最后,第9.8节ToString

抽象操作ToString根据表13将其参数转换为String类型的值:

表13对于Undefined类型给出"undefined",对于Null类型给出"null"

它会改变吗?这甚至算得上是一个“怪异”现象吗?

正如其他人指出的那样,这很不可能改变,因为它会破坏向后兼容性(并且带来没有真正好处),尤其是考虑到自1997年规范版本以来这种行为一直相同。我也不认为这是一个奇怪的现象。

如果要更改这种行为,您会更改nullundefinedToString定义,还是会特殊处理这些值的加法运算符?ToString在规范中被广泛使用,"null"似乎是表示null的一个无可争议的选择。举几个例子,在Java中,"" + null是字符串"null",而在Python中,str(None)是字符串"None"

解决方法

其他人提供了很好的解决方法,但我想补充一点,我怀疑你不想使用entity || ""作为你的策略,因为它将true解析为"true",但将false解析为""这个答案中的数组连接具有更符合预期的行为,或者你可以更改这个答案的实现来检查entity == nullnull == nullundefined == null都为真)。


16

使用合并运算符 - 语法为??

以下是示例,可以在浏览器控制台中测试:

  • "Hello " + (null ?? "")
  • "Hello " + (undefined ?? "")

两者都将得到结果:'Hello '


我建议在回答中不要使用修辞性问题。它们可能会被误解为根本不是答案。你是在尝试回答这个页面顶部的问题,对吗?否则请删除此帖子。 - Yunnosch
虽然这段代码可能解决了问题,但是包括解释它如何以及为什么解决了问题将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请[编辑]您的答案以添加解释并指出适用的限制和假设。 - Yunnosch
3
@Yunnosch,我正在回答这个问题:“如何在不陷入问题的情况下,以最美观的方式连接具有潜在空值或未定义对象的字符串。”我的答案不包含修辞性问题——??就是字面上的语法... - Don Cheadle
2
请查看 https://stackoverflow.com/editing-help,它可能会帮助您更清楚地区分(夸张的)散文标点和代码语法。 - Yunnosch

5
为什么不先过滤出真值再联接呢?
const concatObject = (obj, separator) =>
    Object.values(obj)
        .filter((val) => val)
        .join(separator);


let myAddress = {
    street1: '123 Happy St',
    street2: undefined,
    city: null,
    state: 'DC',
    zip: '20003'
}

concatObject(myAddress, ', ')

> "123 Happy St, DC, 20003"

4
有没有可能在未来的ECMAScript版本中改变这种行为?
我认为机会非常小。原因有几个:
我们已经知道ES5和ES6长什么样了
未来的ES版本已经完成或正在草案中,据我所知,没有一个版本会改变这种行为。需要记住的是,要使这些标准在浏览器中得以建立,并且您可以使用这些标准编写应用程序而不依赖于编译为实际Javascript的代理工具,需要几年的时间。
试着估计一下持续时间。甚至大多数浏览器都不完全支持ES5,这可能需要再过几年。ES6甚至没有完全指定。从蓝色的天空出现,我们至少再看五年。
浏览器自己做他们自己的事情
浏览器已经决定了某些问题。您不知道所有浏览器是否会以完全相同的方式完全支持此功能。当然,一旦成为标准的一部分,您就会知道,但是目前即使宣布它成为ES7的一部分,也只能是最好的猜测。
并且,浏览器可能会在这里做出自己的决定,特别是因为:
这种变化很破坏
标准的最大特点之一是它通常会尝试向后兼容。这在Web上尤其正确,因为相同的代码必须在各种环境中运行。
如果标准引入了新功能,并且旧浏览器不支持该功能,则需要告知客户更新其浏览器以使用该网站。但是,如果您更新浏览器,突然间有一半的互联网破裂了,那就是一个错误,嗯,不是。
当然,这种特定的更改不太可能破坏大量脚本。但通常这是一个很差的参数,因为标准是普遍的,并且必须考虑每个机会。只需考虑
"use strict";

作为切换到严格模式的指令,这表明了标准所做出的努力,试图使所有内容兼容,因为他们本可以将严格模式作为默认模式(甚至是唯一模式)。但是通过这个巧妙的指令,您可以让旧代码在不进行更改的情况下运行,并仍然可以利用新的、更严格的模式。
另一个向后兼容的例子是“===”操作符。 “==”有根本性的缺陷(尽管有些人不同意),它本可以改变其含义。相反,“===”被引入,允许旧代码继续运行而不会破坏;同时允许使用更严格的检查编写新程序。
对于标准来说,要打破兼容性,必须有非常好的理由。这就带我们来到
没有好的理由
是的,它困扰着你。这是可以理解的。但是最终,这是一件可以非常容易地解决的事情。使用“||”,编写函数 - 都可以。您可以以几乎零成本使其正常工作。那么投入所有时间和精力分析这种我们知道会破坏的更改真正的好处是什么呢?我只是看不到点。
Javascript在设计上有几个弱点。随着该语言变得越来越重要和强大,这已经成为一个越来越大的问题。但是虽然有很多很好的理由改变它的设计,但其他事情并不意味着要更改
免责声明:本答案部分基于个人观点。

2
为了添加 null 和 '',它们需要满足最小的通用类型标准,这种情况下是字符串类型。
因此,null 被转换为 "null",由于它们都是字符串,所以两者被连接起来。
数字也是同样的情况:
4 + '' = '4'
因为其中有一个字符串无法转换为任何数字,所以 4 将被转换为字符串。

5
我了解JS中的合并运算符,但这还不足以证明它的正确性。一个null或undefined对象在一个空字符串中进行合并是可以的,我认为这没有问题。你觉得有问题吗? - Doc Davluz
我认为你的意思是“标准”(源自希腊语,表示一种标准或判断),而不是“criterium”(拉丁语的等效词,但通常只用于英语中的一种自行车比赛类型,实际上取自法语“critérium”,表示“竞争”)。 - Rob Gilliam

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