JavaScript对应于printf/String.Format的函数

2411

我正在寻找一个良好的JavaScript等价物来替代C/PHP中的printf()或对于C#/Java程序员,String.Format()(.NET使用 IFormatProvider)。

我的基本要求是数字的千位分隔符格式,但能处理许多组合(包括日期)的东西会更好。

我意识到微软的Ajax库提供了一个版本的String.Format(),但我们不想要整个框架的开销。


4
除了下面众多优秀的回答,您或许也想看看这个链接:https://dev59.com/pXNA5IYBdhLWcg3wPbSe#2648463,在我看来,这是解决此问题最高效的方法。 - Annie
3
我写了一个简单的程序,使用类C语言的printf语法。 - Braden Best
var search = [$scope.dog, "1"]; var url = vsprintf("http://earth/Services/dogSearch.svc/FindMe/%s/%s", search); ***对于Node,您可以通过“npm install sprintf-js”获取您的模块。 - Jenna Leaf
5
这里的大多数答案都令人失望。printf和String.Format不仅仅是简单的模板,而且问题特别提到了千位分隔符,这是简单模板解决方案都无法处理的。 - blm
除了“模板字符串”之外,人们可能还在寻找String.padStart。 (请参见https://dev59.com/jXE85IYBdhLWcg3wnU-p) - Nor.Z
显示剩余3条评论
61个回答

1616

现代JavaScript

从ES6开始,您可以使用模板字符串:

let soMany = 10;
console.log(`This is ${soMany} times easier!`);
// "This is 10 times easier!"

请查看Kim在下面的答案以获取详细信息。


旧回答

尝试使用JavaScript的sprintf()


如果您真的想自己编写一个简单的格式化方法,请不要逐个替换,而是同时进行替换。

因为大多数其他被提到的建议会在先前替换的替换字符串中也包含格式序列时失败,例如:

"{0}{1}".format("{1}", "{0}")

通常你期望的输出结果应该是{1}{0},但实际输出结果是{1}{1}。因此建议使用类似于fearphage的建议中的同时替换方法。


28
如果只需要简单的数字转字符串操作,num.toFixed() 方法就足够了! - heltonbiker
2
@MaksymilianMajer 看起来这是完全不同的东西。 - Evan Carroll
@Evan 这个链接没问题,可以正常访问。 - Octavia Togami
4
这个答案不再被接受。自从ES6以后,这已经被整合到了JavaScript语言中(在浏览器和NodeJS中都是如此)。请参考下面@Kim的回答。 Translated: 这个答案不再适用。自从ES6以后,这个功能已经被纳入JavaScript语言中,包括浏览器和NodeJS。请参阅下面@Kim的回答。 - Ryan Shillington
6
内插字符串不适合于那些在开发时无法确定的模板字符串。例如,您的页面可能会从Web服务器检索模板字符串,然后使用某些变量将其填充。 - Daniel
显示剩余9条评论

1499

在之前提出的解决方案基础上进行改进:

// First, checks if it isn't implemented yet.
if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) { 
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

"{0}已经过时,但是{1}仍然活着!{0} {2}".format("ASP", "ASP.NET")

输出结果

ASP已经过时,但是ASP.NET仍然活着!ASP {2}


如果您不想修改 String 的原型:

if (!String.format) {
  String.format = function(format) {
    var args = Array.prototype.slice.call(arguments, 1);
    return format.replace(/{(\d+)}/g, function(match, number) { 
      return typeof args[number] != 'undefined'
        ? args[number] 
        : match
      ;
    });
  };
}

这将给你更加熟悉的:

String.format('{0}已经死了,但{1}还活着!{0} {2}', 'ASP', 'ASP.NET');

得到相同的结果:

ASP已经死了,但ASP.NET还活着!ASP {2}


14
如果args[number]为0,则"||"技巧无效。 应该明确使用if()来检查(args[number] === undefined)。 - fserb
4
在简写的if语句中的else语句中,为什么不直接使用"match",而要用"'{' + number + '}'"呢? "match"应该等于那个字符串。 - mikeycgto
4
如果您有多个字符串相互附加(使用 + 运算符),请确保将完整的字符串放在括号中:("asd {0}"+"fas {1}").format("first", "second"); 否则,此函数仅会应用于最后一个被附加的字符串。 - Lukas Knuth
3
稍微细微地改变了结果。想象一下'foo {0}'.format(fnWithNoReturnValue()),当前它会返回foo {0}。通过你的修改,它将返回foo undefined - fearphage
4
我认为这比使用 sprintf() 更好,因为它基本上做的是相同的事情,并且非常小巧。(原文已经是英文) - user2233706
显示剩余17条评论

644

有趣的是,Stack Overflow实际上为String原型创建了自己的格式化函数,名为formatUnicorn。试试看吧!进入控制台并输入类似以下内容:

"Hello, {name}, are you feeling {adjective}?".formatUnicorn({name:"Gabriel", adjective: "OK"});

Firebug

你会得到这个输出:

你好,Gabriel,你感觉还好吗?

你可以使用对象、数组和字符串作为参数!我获取了它的代码并重新制作了一个新版本的 String.prototype.format

String.prototype.formatUnicorn = String.prototype.formatUnicorn ||
function () {
    "use strict";
    var str = this.toString();
    if (arguments.length) {
        var t = typeof arguments[0];
        var key;
        var args = ("string" === t || "number" === t) ?
            Array.prototype.slice.call(arguments)
            : arguments[0];

        for (key in args) {
            str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]);
        }
    }

    return str;
};

请注意巧妙的 Array.prototype.slice.call(arguments) 调用 - 这意味着如果你输入的参数是字符串或数字,而不是单个JSON格式的对象,你几乎可以得到C#的 String.Format 行为。
"a{0}bcd{1}ef".formatUnicorn("FOO", "BAR"); // yields "aFOObcdBARef"

那是因为Arrayslice会强制将arguments中的任何内容转换为Array,无论它最初的形式如何,而key将是每个数组元素的索引(0、1、2......)被强制转换为一个字符串(例如,"0",所以你的第一个正则表达式模式为"\\{0\\}")。很不错。

538
使用stackoverflow上的代码回答问题感觉非常棒,加个+1。 - Sneakyness
7
正则表达式允许使用全局标志(g),可以替换多个相同的关键字。在上面的例子中,您可以在同一句子中多次使用 {name} 并将它们全部替换。 - KrekkieD
3
老实说,这似乎非常脆弱。比如,如果 name"blah {adjective} blah",会发生什么? - sam hocevar
8
“稍微夸张”?将用户数据误解为格式字符串进行解释的代码是一种漏洞类别。98.44%远远低于平均水平。 - sam hocevar
4
@samhocevar,我无法相信你这么对我,小Bob把我放在了“待定名单”上。 ;) 如果你在没有任何安全检查的情况下在数据库服务器上运行客户端JavaScript处理的文本,那我们就完了。 ;^) 你知道,从客户端(例如Postman)发送到服务器的任何东西都不应该能够绕过服务器的安全性检查。并且你应该假设任何危险的东西都可能从客户端发送过来。也就是说,如果你需要从始终可由用户编辑的客户端JavaScript代码中获得100%的安全性,并且你认为该函数可能会带来安全风险,那么你选择的游戏是错误的。 - ruffin
显示剩余19条评论

357

JavaScript中的数字格式化

我来到这个问题页面,希望找到如何在JavaScript中格式化数字,而不引入另一个库。这是我找到的内容:

浮点数四舍五入

在JavaScript中,相当于sprintf("%.2f", num)的方法似乎是num.toFixed(2),它将num格式化为两位小数,并进行四舍五入(但请查看@ars265的有关Math.round的评论)。

(12.345).toFixed(2); // returns "12.35" (rounding!)
(12.3).toFixed(2); // returns "12.30" (zero padding)

指数形式

sprintf("%.2e", num)的等效写法是num.toExponential(2)

(33333).toExponential(2); // "3.33e+4"

十六进制和其他进位制

要以进位制B打印数字,请尝试使用num.toString(B)。JavaScript支持自动转换为和从2到36进制(此外,一些浏览器有有限的base64编码支持)。

(3735928559).toString(16); // to base 16: "deadbeef"
parseInt("deadbeef", 16); // from base 16: 3735928559

参考页面

JS数字格式化快速教程

Mozilla toFixed()参考页面(包含链接到toPrecision(),toExponential(),toLocaleString()等)


23
把数字文字用括号括起来,而不是留下奇怪的空格,这样不是更好吗? - rmobis
7
那样看起来可能更好,没错。但我在那里的目标只是为了指出语法错误陷阱。 - rescdsk
4
顺便提一句,如果您正在使用较旧的浏览器,或支持较旧的浏览器,则某些浏览器对toFixed的实现有误。使用Math.round替代toFixed是更好的解决方案。 - ars265
7
@Raphael_和@rescdsk:..也可以使用:33333..toExponential(2); - Peter Jaric
Or (33333).toExponential(2) - Jonathan
显示剩余2条评论

311

从 ES6 开始,您可以使用模板字符串

let soMany = 10;
console.log(`This is ${soMany} times easier!`);
// "This is 10 times easier!"

请注意,模板字符串使用反引号 ` 而不是(单)引号括起来。
请注意,一旦定义了字符串,它就会立即展开。
更多信息请参见:

https://developers.google.com/web/updates/2015/01/ES6-Template-Strings

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings

注意: 请查看Mozilla网站以获取支持的浏览器列表。


104
模板字符串的问题在于它们似乎会立即执行,这使得它们作为类似 i18n 字符串表的用途完全无用。我无法提前定义字符串,并在以后和/或重复使用时提供参数。 - Tustin2121
5
你说得对,这些字符串本来就不是用来赋值给变量的,这可能有点难以理解,但只需要把这些带有模版的字符串放进一个函数里,就可以很容易地使用它们的即时执行特性。请参见https://jsfiddle.net/zvcm70pa/ - inanutshellus
17
@Tustin2121,使用模板字符串或旧式字符串连接没有区别,它们都是同一件事的语法糖。你只需要在一个简单的函数中包装一个旧式字符串生成器,使用字符串模板也可以完成相同的任务。const compile = (x, y) => \我可以随时调用这个模板字符串.. x=${x}, y=${y}`...compile(30, 20)` - cchamberlain
13
此解决方案无法适用于传递给变量(例如来自服务器的)的格式字符串。 - user993954
7
模板字符串不具备格式化功能,因此在这方面它们是无法比较的。例如,不能指定参数宽度,也不能确保参数具有精确的精度。 - Dragas
显示剩余5条评论

182

jsxt,Zippo

这个选项更加合适。

String.prototype.format = function() {
    var formatted = this;
    for (var i = 0; i < arguments.length; i++) {
        var regexp = new RegExp('\\{'+i+'\\}', 'gi');
        formatted = formatted.replace(regexp, arguments[i]);
    }
    return formatted;
};

通过此选项,我可以替换这样的字符串:

'The {0} is dead. Don\'t code {0}. Code {1} that is open source!'.format('ASP', 'PHP');

使用您的代码,第二个{0}将不会被替换。 ;)

3
我用这种方法更新了你的示例。它有许多好处,包括节省本地实现(如果存在)、字符串化等。我尝试删除正则表达式,但是似乎需要进行全局替换。 :-/ - tbranyen
7
jsxt 是遗憾地采用 GPL 许可证。 - AndiDog
非常低效的方法。在不需要时使用正则表达式,多次查找整个字符串进行搜索。 - Artem Balianytsia

123

对于 Node.js 用户,可以使用类似printf的功能的util.format

util.format("%s world", "Hello")

2
截至Node v0.10.26,此功能不支持%x。 - Max Krohn
2
也不支持宽度和对齐修饰符(例如 %-20s %5.2f)。 - FGM
5
我不得不滑到页面底部才能看到这个有用的答案。 - Daniel Viglione
1
util.format node documentation中得知:"util.format()是一种同步方法,旨在作为调试工具。某些输入值可能会产生显着的性能开销,从而阻塞事件循环。请谨慎使用此函数,永远不要在热代码路径中使用。" - r.pedrosa

120

我使用这个简单的函数:

String.prototype.format = function() {
    var formatted = this;
    for( var arg in arguments ) {
        formatted = formatted.replace("{" + arg + "}", arguments[arg]);
    }
    return formatted;
};
这与string.format非常相似。
"{0} is dead, but {1} is alive!".format("ASP", "ASP.NET")

2
我认为这段代码仍然不正确。 正确的应该像 Filipiz 发布的那样。 - wenqiang
3
参考资料,for...in在某些浏览器中无法按照此代码所期望的方式工作。它将循环遍历所有可枚举属性,在某些浏览器中包括arguments.length,而在其他浏览器中甚至不包括参数本身。无论如何,如果添加了Object.prototype,任何添加都可能被包含在其中。该代码应该使用标准的for循环,而不是 for...in - cHao
3
如果先前的替换中也包含格式字符串,则此方法会失败:"{0} is dead, but {1} is alive!".format("{1}", "ASP.NET") === "ASP.NET is dead, but ASP.NET is alive!" - Gumbo
6
变量arg是全局的。你需要使用以下代码替换:for (var arg in arguments) { - Pauan
1
该函数不安全,因为它有时会解释传递给它的字符串中的 {n}'{0}'.format('you should only see this {1}', 'you should not see this') - Marian
显示剩余6条评论

68

我很惊讶没有人使用reduce,这是一个原生并且简洁、强大的JavaScript函数。

ES6 (EcmaScript2015)

String.prototype.format = function() {
  return [...arguments].reduce((p,c) => p.replace(/%s/,c), this);
};

console.log('Is that a %s or a %s?... No, it\'s %s!'.format('plane', 'bird', 'SOman'));

< ES6

function interpolate(theString, argumentArray) {
    var regex = /%s/;
    var _r=function(p,c){return p.replace(regex,c);}
    return argumentArray.reduce(_r, theString);
}

interpolate("%s, %s and %s", ["Me", "myself", "I"]); // "Me, myself and I"

工作原理:

reduce函数将一个函数应用于累加器和数组中的每个元素(从左到右),将其减少为单个值。

var _r= function(p,c){return p.replace(/%s/,c)};

console.log(
  ["a", "b", "c"].reduce(_r, "[%s], [%s] and [%s]") + '\n',
  [1, 2, 3].reduce(_r, "%s+%s=%s") + '\n',
  ["cool", 1337, "stuff"].reduce(_r, "%s %s %s")
);


5
这里有一个使用这种方法创建简化版 printf 函数的版本:https://jsfiddle.net/11szrbx9 - Dem Pilafian
1
这里是另一个使用ES6的例子,一行代码:(...a) => {return a.reduce((p: string, c: any) => p.replace(/%s/, c)); - dtasev
1
ES6中不需要使用String.prototype.format((a,b,c)=>\${a}, ${b}和${c}`)(...['我', '自己', '我自己'])`(请注意,这有点冗余,以更好地适应您的示例) - Tino
你需要为printf的每个类型说明符实现替换函数,并包含填充前缀的逻辑。以明智的方式迭代格式字符串似乎是次要的挑战,我认为这是一个整洁的解决方案,如果你只需要字符串替换的话。 - collapsar

54

这是JavaScript中sprintf函数的最小实现:它只支持 "%s" 和 "%d",但我留出了扩展空间。对于OP来说是无用的,但其他从Google搜索进入此线程的人可能会受益。

function sprintf() {
    var args = arguments,
    string = args[0],
    i = 1;
    return string.replace(/%((%)|s|d)/g, function (m) {
        // m is the matched format, e.g. %s, %d
        var val = null;
        if (m[2]) {
            val = m[2];
        } else {
            val = args[i];
            // A switch statement so that the formatter can be extended. Default is %s
            switch (m) {
                case '%d':
                    val = parseFloat(val);
                    if (isNaN(val)) {
                        val = 0;
                    }
                    break;
            }
            i++;
        }
        return val;
    });
}

例子:

alert(sprintf('Latitude: %s, Longitude: %s, Count: %d', 41.847, -87.661, 'two'));
// Expected output: Latitude: 41.847, Longitude: -87.661, Count: 0

与先前回复中类似的解决方案相比,这个解决方案会一次性完成所有替换操作,因此不会替换先前已经被替换过的值的部分。


有什么想法可以适用于“%02d”吗? - Dongdong Kong

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