JavaScript字符串操作的偏方怪法

字符串数组在程序编写过程中是十分常用的类型,因此程序语言都会将String和Array作为基本类型,并提供许多字符串和数组的方法来简化对字符串的操作。JavaScript里面也提供了String类型Array类型,并且有很多基本的String方法Array方法来方便地对字符串进行合并、查找、替换、截取等处理。

JavaScript作为一个脚本语言,又提供了一种动态解析运行的机制,而这特性,又让使得在String操作的时候出现一些结合使用Array的有趣方法。这些方法可能有些偏门有点奇怪,但有时在效率、可读性、复用性上表现得却更好。

重复字符串

常常我们想要把字符串多次打印出来(比如想要个分割线),我们就需要将一个字符串重复多次, 可惜JavaScript并没有提供类似repeat这样的方法。当然我们可以用循环来拼接出来,但是我们可以利用JavaScript中Array的join方法来实现repeat

  1. function repeat(str, n) {
  2. var arr = new Array(n+1);
  3. return arr.join(str);
  4. }
  5.  
  6. document.write(repeat('-', 7));
  7.  
  8. //output:
  9. //-------

利用n+1个Array元素产生的n个间隙,再以目标字符串来拼接,我们就能得到字符串重复的功能。

扩展String的prototype使方法应用于所有字符串

JavaScript的对象继承和方法寻找是基于原型链(prototype chain),所有使用着的字符串都可以说是继承于String的对象,我们可以为String对象的prototype添加方法或者属性,这样该方法就可以应用到所有我们使用的对象上了。比如上边的repeat方法,就可以改成:

String.prototype.repeat = function(n) {
    var arr = new Array(n+1);
    return arr.join(this);
};
 
document.write('-'.repeat(21));
 
//output:
//---------------------

然后,直接通过字符串调用repeat方法,就可以得到跟上边一样的结果。

这可以让我们实现对字符串方法的扩充,简洁对字符串的操作,但是这会“污染”了JavaScript的String,当代码被转到其他文件但是那个文件下并没有得到这段扩充,就可能会造成找不到该方法;另外,调用prototype扩展方法比直接调用方法要稍微“慢”一些,因为JavaScript会先去在字符串对象自身的方法中尝试寻找,再找到String的prototype的方法;再者也许在将来我们扩充的方法(比如repeat)变成了标准方法了,再使用这代码就会覆盖了标准方法,得到不一致的结果。

但是忽略这些考虑,扩充JavaScript标准类型的prototype还是会给编程带来许多的遍历。

用Array作StringBuilder

在很多高级语言中,加号(+)在字符串的操作中被赋予了更多的意义:作为字符串拼接的操作符。不过在Java和C#中,我们也知道如何频繁进行字符串拼接的操作,使用加号(+)就会产生效率问题,因此在这种情况下就会推荐使用StringBuilder

JavaScript也支持使用加号(+)来进行字符串拼接,那么也会有存在效率问题呢。可是JavaScript并没有提供StringBuilder这样的类。

其实在Java,C#中使用StringBuilder时,我们多数也是用append方法,而很少会用insert。好在JavaScript的Array是不限大小自动增长的,所以我们就可以利用Array来做StringBuilder,最后再join空字符串来拼接出目标字符串。

  1. var sb = [];
  2. for(var i = 0; i <=21; i++) {
  3. sb.push(i);
  4. }
  5.  
  6. document.write(sb.join(''));
  7.  
  8. //output:
  9. //0123456789101112131415161718192021

 

到底是用Array做StringBuilder还是直接字符串拼接,jsPerf上有过很多testcases比较两者的效率,但是因为初始值、环境、字符串长度等原因,所以结果不一。其实字符串内容不是很大,或者可以使用多个加号(+)组合在一起,那么字符串拼接还是可以的;若是在代码不同地方对同一字符串变量进行追加,那么可能使用Array配合join会更好。

用split替代字符串的子串查找和替换

在字符串的操作中,很常出现的就是想要从字符串中查找一个子字符串是否存在,然后截取出该字符串,抑或是将该子字符串替换成其它字符串。

比如给一个文件名,希望根据点(.)分割获取基本名和后缀名。先来看看使用标准String方法实现的这些操作:

  1. function getBaseName(str) {
  2. var pos = str.lastIndexOf('.');
  3. if(pos < 0)return str;
  4. return str.substring(0, pos);
  5. }
  6.  
  7. function getExtension(str) {
  8. var pos = str.lastIndexOf('.');
  9. if(pos < 0)return '';
  10. return str.substr(pos+1);
  11. }
  12.  
  13. var fileName = 'hello_world.js';
  14.  
  15. document.write(getBaseName(fileName));
  16. document.write('<br />');
  17. document.write(getExtension(fileName));
  18.  
  19. //output:
  20. //hello_world
  21. //js

(除了substrsubstring外,JavaScript还有slice都可以用来获取字符串的子串,但也正是因为选择太多,常常让我在出现选择恐慌,还有位置是该不该+1,对负数是如何处理也让我揪心。)

之前看到可以通过join把数组变成字符串,也可以利用String的split的方法把字符串变成数组。对于上边取文件名及扩展名的问题,我们就可以根据“.”把文件名分裂成数组各个部分,那么假如得到的数字大于1(后缀名存在),则所得数字的最后一个元素就是文件的扩展名了:

  1. function getBaseName(str) {
  2. var segs = str.split('.');
  3. if(segs.length > 1) segs.pop();
  4. return segs.join('.');
  5. }
  6.  
  7. function getExtension(str) {
  8. var segs = str.split('.');
  9. if(segs.length <= 1)return '';
  10. return segs.pop();
  11. }

 

考虑到文件名中可能包含多个“.”,所以我们还是需要用“.”把除了最后一部分外的各个部分join回来。

看到可以对字符串先splitjoin,就可以想到,我们可以想到对于这两个方法的参数可以传入不同的字符串,这样就起到了代替String的replace方法进行子串替换的功能了,而且还是全局替换。

比如希望把所有的下划线(_)替换成横杠(-):

  1. var str = 'hello_from_ider_to_world'.split('_').join('-');
  2. document.write(str);
  3.  
  4. //Output:
  5. // hello-from-ider-to-world

相对于String的replace方法,该方法的有点在于:可以实现全局替换;而若要让replace能够全局替换,则需要传入正则表达式对象(RegExp)而不能是字符串作为第一参数。

replace可接受RegExp、Function作为参数

很多人知道String的replace方法是用来替换字符串子串的,也可能知道它可以接受正则表达式作为第一参数,而且如何要替换所有出现的地方,就必须要用RegExp并包含global标记。

比如之前的替换操作,用replace就应该是:

  1. var str = 'hello_from_ider_to_world'.replace(/_/g, '-');
  2. document.write(str);

 

再比如很常用的trim方法,虽然JavaScript并没有提供我们也可以自己很快的实现:

  1. String.prototype.trim = function() {
  2. return this.replace(/^\s+|\s+$/g, '');
  3. };

我们知道正则表达式一个很强大的功能就是向后引用(Back Reference),实际上JavaScript的replace不仅在第一个参数内做向后引用,而且在替换字符串上,也可以进行向后引用,只是很多地方可能用反斜杠(\)加数字作为标示而JavaScript则是用美刀($)加数字作为标示。

  1. var friends = 'friends of Ider, friend of Angie';
  2. var result  = friends.replace(/(friends?) of (\w+)/g, "$2's $1");
  3.  
  4. document.write(result);
  5. //output:
  6. //Ider's friends, Angie's friend

通过在替换字符串里面进行向后引用,我们很快就把“朋友 of 谁谁谁”变成了“谁谁谁的朋友”。如果还要更复杂点怎么办呢?没有关系,replace还能接受Function作为参数作为回调函数,其中函数的第一个参数是整个匹配中的字符串,之后每一个代表个个向后引用匹配的,函数的返回值则是作为替换的字符串。所以很多使用,函数参数都会用$0, $1, $2来表示。来看个例子:

  1. var friends ="friends of mine, friend of her and friends of his";
  2. var result = friends.replace(/(friends?) of (\w+)/g,
  3. function($0, $1, $2) {
  4. if($2 == 'mine') $2 = 'my';
  5. return $2 + ' ' + $1;
  6. });
  7.  
  8. document.write(result);
  9.  
  10. //output:
  11. //my friends, her friend and his friends

通过回调函数就可以实现很多很负责的字符串匹配了。至于效率,就先不考虑了。

在switch语句的case从句使用表达式

跟很多语言一样,JavaScript的分支语句中也包括switch。不过一般的switch语句的case从句中,只能是整型常数或者常量表达式(constant-expression),好一些的也支持字符串,不过也只能是字符串常量。JavaScript算是“好一些”之一,但是它更好,支持任何的表达式。

根据ECMAScript 5.1中对switch语句的描述:

SwitchStatement :
    switch ( Expression ) CaseBlock
CaseBlock :
    { CaseClausesopt }
    { CaseClausesoptDefaultClause CaseClausesopt }
CaseClauses :
    CaseClause
    CaseClauses CaseClause
CaseClause :
    case Expression : StatementListopt
DefaultClause :
    default : StatementListopt

case下仅仅约束为 Expression而非const,所以我们就拥有了类似下边的代码:

  1. function whereIsIder(s) {
  2. var result = '';
  3. switch(true) {
  4. case s == 'Ider':
  5. result = ' is Ider';
  6. break;
  7. case /^Ider/.test(s):
  8. result = ' starts with Ider';
  9. break;
  10. case /Ider$/.test(s):
  11. result = ' ends with Ider';
  12. break;
  13. case /Ider/.test(s):
  14. result = ' contains Ider';
  15. break;
  16. default:
  17. result = ' does not have Ider';
  18. break;
  19. }
  20.  
  21. document.write('{'+s +'}' + result + '');
  22. }
  23.  
  24. whereIsIder('Ider');
  25. whereIsIder('Ider Blog');
  26. whereIsIder('Hello, Ider');
  27. whereIsIder('Welcome, Ider Blog');
  28.  
  29. // {Ider} is Ider
  30. // {Ider Blog} starts with Ider
  31. // {Hello, Ider} ends with Ider
  32. // {Welcome, Ider Blog} contains Ider

其实这个语句就是个变相的if…else if…语句,而且要注意的是switch的内容是true常量,而不是一般形式时用的变量。switch语句的语意还是一样的:根据switch内的语句值,将第一个与语句值相同的case作为入口执行其下代码。并不会是执行case表达式值为true的部分。

 

代码运行结果:http://jsfiddle.net/WwTYH/

References:
  1. String prototype object – Mozilla Developer Network
  2. String Object (JavaScript) – Internet Explorer Developer Center
  3. JavaScript String Object – w3schools.com
  4. Faster JavaScript Trim – Flagrant Badassery
Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInEmail this to someone