Javascript拼接HTML字符串的⽅法列举及思路
转载过来,去掉⼀些废话吧。
⽬标:
⽅便的拼接字符串,不使⽤让⼈眼晕的+=。使⽤过程如下:
1,先创建⼀个作为“模板”的字符串,如:’My name is ${name},I\’m ${age}.’
2,传⼀个对象进去,其中包含了你要填进模板的值,如:{name:’LIX’,age:11}
3,最后你就能得到你想要的字符串了,如:My name is LIX,I’m 11.
调⽤⽅法:
mix('My name is "${name}",I\'m "${age}".',{name:'LIX',age:11})
或
mix('My name is "${0}",I\'m "${1}".',['LIX',11])
⼏种实现:
1,replace+regexp
在司徒正美的⽂章⾥看到的。下⾯是简化后的代码:
1function mix (str,group) {
2 str = place(/\$\{([^{}]+)\}/gm,function (m,n) {
3return (group[n] != undefined) ? group[n] : '';
4 })
5return str;
6 }
其中function⾥⾯的m,n值得讲⼀下。他们是从哪⼉传的值呢?就是正则表达式。string的replace⽅法,如果第2个参数是个函数的话,那函数的第1个参数值肯定就是“当前匹配到的字符串”。
但这⾥的函数有了两个参数,第2个参数n,是什么?他就是你正则中的分组的第1组(被第1组()包起来的部分)——也就是说,如果你愿意,还可以有很多组,然后replace的函数就可以有很多个参数了。
replace接受⼀个处理函数,其第⼀个参数是当前匹配到的⼦字符串,后⾯的参数就依次是正则匹配到的的第1组,第2组…
⽽函数中的return则是重中之重,如果没有返回,那么替换就不会发⽣。replace正是⽤return回来的⼦串替换掉之前匹配到的⼦串的(就是参数m).
这个⽅法原理简单易懂,代码也少,但有个问题,我测试的时候发现这个⽐使⽤普通的+=串联字符串慢了10倍不⽌!!太让⼈⼼寒了啊
⽽我对这种⽅便⼜好⽤的拼字符串的⽅法⾮常眼热,所以我只能考虑如何去提⾼其效率了。
⽅法⼆
既然replace+正则表达式效率不⾼,我就打算试试不⽤replace的⽅法。⽽查找字段标签(即${name}这样的)还是⽤正则来做,找到之后,我们把字符串在此标签之前的部分,以及之后的部分都截取出来——恰好去掉${name}这⼀截,然后⽤+直接连上此标签对应的值(例⼦⾥是LIX),如此循环。
代码如下:
1function loopMix0 (str,group) {
2var reg = /\$\{([^{}]+)\}/gm, res;
3while(res = (str)) {
4 str = str.substr(0,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '')+str.substr(res.index+res[0].length);
5 }
6return str;
7 }
正则的exec⽅法是个⽐较奇特的⽅法,因为他不会⼀次把所有符合匹配条件的⼦串都返回,⽽是每次只返回当前匹配到的1个⼦串,详细格式如此:
[当前匹配到的⼦串,(如果正则有分组,那么这⾥就是依次按分组匹配到的值,组1,组2...),index(这是
当前匹配到的⼦串的index)]
如果要靠exec把所有能匹配的都给匹配了,那只有循环了。exec每次匹配后,都会改变他⾃⼰的lastIndex属性,以便下次exec的时候不会⼜把以前匹配过的再匹配⼀次。当exec没有返回结果的时候,就表⽰全部匹配完成了。
这样就没有⽤replace,⽽是⽤了字符串的原⽣⽅法,效率应该有提⾼吧?
现实是残酷的,此⽅法和⽅法1的效率⼏乎没提⾼。这个⽅法的缺点很明显,就是和replace⼀样,每次循环中还是对整个字符串做操作(不停的赋予新值,然后⽤新值代⼊下次循环),效率当然不能提⾼。
⽅法三
明⽩了⽅法2的缺点,要做改进就很简单了。我先新建⼀个空字符串,然后还是按上⾯的循环,只是每次都依次把字段标签前的部分,字段标签对应值,字段标签后头的部分,连接到这个空字符串上。这样,虽然这个空字符串越来越长了,但我们再也没有每次都对原始字符串进⾏修改了——原始字符串才是最长的好吧!!
1function loopMix1 (str,group) {
2var reg = /\$\{([^{}]+)\}/gm, res,returnString = '',start = 0;
3while(res = (str)) {
4 returnString += str.substring(start,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '');
5 start = res.index+res[0].length;
6 }
7 returnString += str.substr(start);
8return returnString;
9 }
其中有个变量start,保存着下⼀次str开始截取的起始位置,很重要。
PS:循环结束后还要在returnString上加上原始字符串的最后⼀截哟,不然你就得不到你“预期中的那么长”了。
这代码有个变化就是不再是⽤的substr了,⽽是⽤的substring。因为substr的第2个参数是length,不再适合这⾥。
此⽅法⽐⽅法2快1倍有余!
说起substr和substring,就不得不提⼀个“万⼈迷”(迷惑不清的迷):substr和substring的第2个参数各是什么意思?如何才能不混淆?新婚节
其实很简单:substr⽐substring短得多,所以它迫切地需要“长度”,所以他的第2个参数是length.
⽅法四
⽅法3已经不错了,但我是个精益求精的⼈。⽅法3在理论上还有个缺点,就是原始字符串str始终没有改变,每次循环的时候都⼀样长,会不会拖累正则以及substring的效率呢?
所以我就每次循环都把str变短了,反正前半截本来也是再也不要了的嘛。代码如下:
1function loopMix2 (str,group) {
2var reg = /\$\{([^{}]+)\}/gm, res,returnString = '',start = 0;
昆虫日记
3while(res = (str)) {
4 returnString += str.substring(0,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '');
5 start = res.index+res[0].length;
6 str = str.substr(start);
7 reg.lastIndex = 0;
8 }
9 returnString += str;
散步的意思10return returnString;
11 }
代码中不只是把str变短了,还重置了reg的查询下标,以防万⼀。
这样是不是⽐上个⽅法更进⼀步?答案是否定的,此⽅法⽐⽅法3慢,原因还是因为在循环⾥操作过多,导致效率不增反降。不过⽐⽅法1,2要快就是了。
⽅法五
由于我们的字段标签${name}是⽐较容易识别的,在不故意把str弄错的情况下,我们可以⽤string的原⽣⽅法:indexOf来将字段标签提取出来,然后拼接。
思路是先找到’${‘,再按照得到的index,找到紧邻的’}',然后取中间的值,也就得到了字段标签的key值,然后从group中得到对应值,拼进结果字符串中。代码如下:
1function loopMix3 (str,group) {
2var index=0,clo=0,returnString = '',name = '';
3while((index = str.indexOf('${',index)) !== -1) {
4 returnString += str.substring(clo,index);收藏新闻
5 clo = str.indexOf('}',index);
6 name = str.substring(index+2,clo);
7 returnString += (group[name] != undefined) ? group[name] : ''
8 index = clo;
9 clo +=1;
10 }
11 returnString += str.substr(clo);冬虫夏草炖瘦肉
12return returnString;
13 }
要点:其中要特别注意的是要随时改变indexOf查找的起始位置(index),以及substring开始截取的位置(clo)。
这个⽅法完全没⽤正则,但效率还是没有提⾼,完全⽐不上⽅法3,难道也是循环中操作太多?
PS:此⽅法的代码有bug,⽐如字符串如下:’My name is “${name}”,this is a half ${name .{$name}’,这也就是我说的“故意”把字符串弄错的情况,不过这个bug也是很好修复的,只要在找到⼀个${后,在查找}之前,再次继续查找${,如果有结果,则continue下次循环。不过如此⼀来,⼜多了⼀个判断,效率就更差了。
⽅法六
经常是写着这段代码,忽然就想起了另⼀种思路。⽐如此⽅法。
string有个⾃带⽅法split,可以把字符串按某个分隔符拆分成数组,⽽且split⽀持正则表达式!也就是说我可以把我的原始字符串按${name}这样的字段标签折成数组!
然后呢,虽然把字符串折开了,但我们并没有得到所有的字段标签啊?string有个match⽅法,他能返回所有匹配参数的⼦串,⽽且他也接受正则,返回的也是
所以我现在拿这个正则做了两个操作,⼀是将其作为分隔符把原字符串拆了,⼆是⽤它将原字符串⾥所有的字段标签提取出来。
现在我们有了两个数组,如果把这两个数组从头⾄尾拼合起来,恰好可以得到原始字符串!当然,我们肯定不能按原样拼。。。
现在我们要循环数组并拼接了。在这之前先问⼤家两个问题:
1.同⼀个字符串split与match同⼀个正则操作后,返回的数组哪个长?
2.'${name}${name}${name}${name}${name}${name}${name}${name}'.split('${name}')返回的数组是哪样的?
问这两个问题是很重要的,与此功能函数的实现密不可分。
很容易就能发现,match返回的数组永远⽐split返回的数组length少1!所以呢,抱着循环尽量要短的宗旨,我们要对match返回的数组做循环⽽不是对split.
代码如下:
function matchMix(str, group) {
var reg = /\$\{[^{}]+\}/gm;
var strArr = str.split(reg);
var labelArr = str.match(reg);
var returnString = '',
i = 0,
label, len = labelArr.length;
for (; i < len; i++) {
label = labelArr[i].slice(2, -1);
returnString += strArr[i] + (group[label] != null ? group[label] : '');
}
return returnString + strArr[i];
}
PS:注意循环结束后还要为结果字符串加上split数组的最后⼀项啊!切记!
此⽅法⽐⽅法3要稍快⼀点,不过差距很⼩。我猜想在字符串⽐较长的情况下应该是此⽅法占优。
思路之外的优化
拿原始⽅法mix(replace+regexp)来说,他的效率还有没有办法提⾼呢?答案是有!
上⾯所有的思路,⼤家可以看到我⽤的是同⼀个regexp,即/\$\{([^{}]+)\}/gm,他是分了组的。⽽我们这⾥需要的匹配是很简单的,其实可以不分组!因为我们只需要得到${name},就能很⽅便的得到name:⽤slice截断⼀下就⾏了!
所以更改后的mix如下:
1//原
2function mix (str,group) {
place(/\$\{([^{}]+)\}/gm,function (m,n) {
4return (group[n] != void 1) ? group[n] : '';
5 })
6 }
7//slice版
胡基8function mix1 (str,group) {
place(/\$\{[^{}]+\}/gm,function (m,n) {
10 n = m.slice(2,-1);
11return (group[n] != void 1) ? group[n] : '';
抛物线顶点坐标12 })
13 }
单纯⽤此两者对⽐,效率孰⾼孰低?经测试,在所有浏览器下,mix1的效率都有略微提⾼
由此看来,正则表达式的效率实在是有待改进。
下⾯是⼀些相关测试:
此改进⽅法同样适合于其他思路。
总结
除了现成的⽅法1,后⾯的⽅法可以说都是我现想出来的,但是结果差强⼈意,并没有如我所愿效率越来越优的情况,只能说锻炼了⼀下思路吧。
如果这个结果不算打击,那我再告诉⼤家⼀个“振奋⼈⼼”的消息吧:IE9下,效率最⾼的是⽅法1,即原始replace+regexp的⽅法!后续所有⽅法都算⽩瞎了,哈哈!
不过IE9下最快的replace⽅法,也没有chrome下最慢的replace⽅法执⾏的次数多。
说到这⾥,我要说⼀下:我是⽤测试的。
jsperf不但能对⽐,且每个测试都有执⾏次数,IE9下replace虽然效率最⾼,但执⾏次数还是赶不上chrome下replace的执⾏次数。
系统英文测试地址⾥⾯已经有6个版本,原因嘛是因为我测试着突然⼜想出了新思路,⽽jsperf⾥加新测试代码就要新开版本。其中是⽅法最全的。
经过反复在各浏览器⾥做测试,我发现:
2,firefox的效率⽐chrome差些,但稳定,测试结果也与chrome结论⼀致3,IE9效率最差!结论也很奇葩!
其他测试
原⽂: