Javascript拼接HTML字符串的方法列舉及思路

Javascript拼接HTML字符串的方法列舉及思路

拼接字符串的方法介紹

字符串拼接基本上在任何編程語言中都是非常普通而常用的功能,Javascript裏也是如此。其中對HTML字符串的拼接算是比較難的,我就經常被大量的屬性及引號的嵌套搞得苦不堪言。

常規但很不好用的方法

下面是一個最常用的拼HTML字符串的寫法:

1
var li = '<li class="li '+dd.class+'" id="li+'+i+'"><span>'+dd.text'</span></li>';

這種方法是最好理解的(同時也可以說是執行效率最高的),但缺點也很明顯:繁瑣,非常容易出錯,維護起來麻煩,而且代碼幾乎沒有重用性,即使下次碰到長得差不多的還是得重寫拼的語句。

這種方法還有一毛病,就是如果你+的某個值是nullundefined,拼接時他並不會把他們轉成空字符串,而是輸出一個’null’或’undefined’字符串拼到你要的結果裏,導致結果大相徑庭。

方便易用的方法

現在有非常好用的“模板”庫可以大大簡化上面的工作。他們一般是這麼個用法:

1,先創建一個作爲“模板”的字符串,如:’My name is ${name},I\’m ${age}.’

2,傳一個對象進去,其中包含了你要填進模板的值,如:{name:’LIX’,age:11}

3,最後你就能得到你想要的字符串了,如:My name is LIX,I’m 11.

我也想實現這樣的功能!但我不想去找模板庫,或者說起碼我要弄懂原理,自己會寫了,再去用別人的寫得很好的模板庫。

自我實現

開始之前,我把原字符串裏${name}這樣的子串,稱作“字段標籤”,以便後續講解。下面就是幾個實現這種效果的代碼及思路

方法一

今天恰好在司徒正美的《JavaScript框架設計》中看到拼字符串的方法介紹,裏面提到了一個初級但好用的方法。其原理也非常簡單,首先也是建一個“模板”字符串,然後用字符串的replace,結合正則表達式,把字符串中的一個個模板key替換成對應的字面值。
下面是簡化後的代碼:

1
2
3
4
5
6
function mix (str,group) {
        str = str.replace(/\$\{([^{}]+)\}/gm,function (m,n) {
            return (group[n] != undefined) ? group[n] : '';
        })
        return str;
    }

其中function裏面的m,n值得講一下。他們是從哪兒傳的值呢?就是正則表達式。string的replace方法,如果第2個參數是個函數的話,那函數的第1個參數值肯定就是“當前匹配到的字符串”。

但這裏的函數有了兩個參數,第2個參數n,是什麼?他就是你正則中的分組的第1組(被第1組()包起來的部分)——也就是說,如果你願意,還可以有很多組,然後replace的函數就可以有很多個參數了。

replace接受一個處理函數,其第一個參數是當前匹配到的子字符串,後面的參數就依次是正則匹配到的的第1組,第2組…

而函數中的return則是重中之重,如果沒有返回,那麼替換就不會發生。replace正是用return回來的子串替換掉之前匹配到的子串的(就是參數m).

使用方法:

1
mix('My name is "${name}",I\'m "${age}".',{name:'LIX',age:11})

也可以這樣用:

1
mix('My name is "${0}",I\'m "${1}".',['LIX',11])

這個方法原理簡單易懂,代碼也少,但有個問題,我測試的時候發現這個比使用普通的+=串聯字符串慢了10倍不止!!太讓人心寒了啊

而我對這種方便又好用的拼字符串的方法非常眼熱,所以我只能考慮如何去提高其效率了。

方法二

既然replace+正則表達式效率不高,我就打算試試不用replace的方法。而查找字段標籤(即${name}這樣的)還是用正則來做,找到之後,我們把字符串在此標籤之前的部分,以及之後的部分都截取出來——恰好去掉${name}這一截,然後用+直接連上此標籤對應的值(例子裏是LIX),如此循環。

代碼如下:

1
2
3
4
5
6
7
function loopMix0 (str,group) {
        var reg = /\$\{([^{}]+)\}/gm, res;
        while(res = reg.exec(str)) {
            str = str.substr(0,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '')+str.substr(res.index+res[0].length);
        }
        return str;
    }

正則的exec方法是個比較奇特的方法,因爲他不會一次把所有符合匹配條件的子串都返回,而是每次只返回當前匹配到的1個子串,詳細格式如此:

[當前匹配到的子串,(如果正則有分組,那麼這裏就是依次按分組匹配到的值,組1,組2...),index(這是當前匹配到的子串的index)]

如果要靠exec把所有能匹配的都給匹配了,那只有循環了。exec每次匹配後,都會改變他自己的lastIndex屬性,以便下次exec的時候不會又把以前匹配過的再匹配一次。當exec沒有返回結果的時候,就表示全部匹配完成了。

這樣就沒有用replace,而是用了字符串的原生方法,效率應該有提高吧?

現實是殘酷的,此方法和方法1的效率幾乎沒提高。這個方法的缺點很明顯,就是和replace一樣,每次循環中還是對整個字符串做操作(不停的賦予新值,然後用新值代入下次循環),效率當然不能提高。

方法三

明白了方法2的缺點,要做改進就很簡單了。我先新建一個空字符串,然後還是按上面的循環,只是每次都依次把字段標籤前的部分,字段標籤對應值,字段標籤後頭的部分,連接到這個空字符串上。這樣,雖然這個空字符串越來越長了,但我們再也沒有每次都對原始字符串進行修改了——原始字符串纔是最長的好吧!!

代碼如下:

1
2
3
4
5
6
7
8
9
function loopMix1 (str,group) {
        var reg = /\$\{([^{}]+)\}/gm, res,returnString = '',start = 0;
        while(res = reg.exec(str)) {
            returnString += str.substring(start,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '');
            start = res.index+res[0].length;
        }
        returnString += str.substr(start);
        return returnString;
    }

其中有個變量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變短了,反正前半截本來也是再也不要了的嘛。代碼如下:

1
2
3
4
5
6
7
8
9
10
11
function loopMix2 (str,group) {
        var reg = /\$\{([^{}]+)\}/gm, res,returnString = '',start = 0;
        while(res = reg.exec(str)) {            
            returnString += str.substring(0,res.index)+((group[res[1]] != undefined) ? group[res[1]] : '');
            start = res.index+res[0].length;
            str = str.substr(start);
            reg.lastIndex = 0;
        }
        returnString += str;
        return returnString;
    }

代碼中不只是把str變短了,還重置了reg的查詢下標,以防萬一。

這樣是不是比上個方法更進一步?答案是否定的,此方法比方法3慢,原因還是因爲在循環裏操作過多,導致效率不增反降。不過比方法1,2要快就是了。

方法五

由於我們的字段標籤${name}是比較容易識別的,在不故意把str弄錯的情況下,我們可以用string的原生方法:indexOf來將字段標籤提取出來,然後拼接。

思路是先找到’${‘,再按照得到的index,找到緊鄰的’}',然後取中間的值,也就得到了字段標籤的key值,然後從group中得到對應值,拼進結果字符串中。代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function loopMix3 (str,group) {
        var index=0,close=0,returnString = '',name = '';
        while((index = str.indexOf('${',index)) !== -1) {   
            returnString += str.substring(close,index);
            close = str.indexOf('}',index);
            name = str.substring(index+2,close);
            returnString += (group[name] != undefined) ? group[name] : ''
            index = close;
            close +=1;
        }
        returnString += str.substr(close);
        return returnString;
    }

要點:其中要特別注意的是要隨時改變indexOf查找的起始位置(index),以及substring開始截取的位置(close)。

這個方法完全沒用正則,但效率還是沒有提高,完全比不上方法3,難道也是循環中操作太多?

PS:此方法的代碼有bug,比如字符串如下:’My name is “${name}”,this is a half ${name .{$name}’,這也就是我說的“故意”把字符串弄錯的情況,不過這個bug也是很好修復的,只要在找到一個${後,在查找}之前,再次繼續查找${,如果有結果,則continue下次循環。不過如此一來,又多了一個判斷,效率就更差了。

方法六

方法經常是寫着這段代碼,忽然就想起了另一種思路。比如此方法。

string有個自帶方法split,可以把字符串按某個分隔符拆分成數組,而且split支持正則表達式!也就是說我可以把我的原始字符串按${name}這樣的字段標籤折成數組!

然後呢,雖然把字符串折開了,但我們並沒有得到所有的字段標籤啊?string有個match方法,他能返回所有匹配參數的子串,而且他也接受正則,返回的也是個數組!

所以我現在拿這個正則做了兩個操作,一是將其作爲分隔符把原字符串拆了,二是用它將原字符串裏所有的字段標籤提取出來。

現在我們有了兩個數組,如果把這兩個數組從頭至尾拼合起來,恰好可以得到原始字符串!當然,我們肯定不能按原樣拼。。。

現在我們要循環數組並拼接了。這之前先問大家一個問題:同一個字符串split與match同一個正則操作後,返回的數組哪個長?

再問一個:’${name}${name}${name}${name}${name}${name}${name}${name}’.split(‘${name}’)返回的數組是哪樣的?

問這兩個問題是很重要的,與此功能函數的實現密不可分。

很容易就能發現,match返回的數組永遠比split返回的數組length少1!所以呢,抱着循環儘量要短的宗旨,我們要對match返回的數組做循環而不是對split.

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
2
3
4
5
6
7
8
9
10
11
12
13
//原
function mix (str,group) {
        return str.replace(/\$\{([^{}]+)\}/gm,function (m,n) {
            return (group[n] != void 1) ? group[n] : '';
        })
    }
//slice版
    function mix1 (str,group) {
        return str.replace(/\$\{[^{}]+\}/gm,function (m,n) {
            n = m.slice(2,-1);
            return (group[n] != void 1) ? group[n] : '';
        })
    }

單純用此兩者對比,效率孰高孰低?經測試,在所有瀏覽器下,mix1的效率都有略微提高

由此看來,正則表達式的效率實在是有待改進。

下面是一些相關測試:

mix與mix1對比

此改進方法同樣適合於後續思路。

總結

除了現成的方法1,後面的方法可以說都是我現想出來的,而且結果差強人意,並沒有如我所願效率越來越優的情況,只能說鍛鍊了一下思路吧。

如果這個結果不算打擊,那我再告訴大家一個“振奮人心”的消息吧:IE9下,效率最高的是方法1,即原始replace+regexp的方法!後續所有方法都算白瞎了,哈哈!

不過IE9下最快的replace方法,也沒有chrome下最慢的replace方法執行的次數多。

說到這裏,我要說一下:我是用jsperf.com測試的。測試地址

jsperf不但能對比,且每個測試都有執行次數,IE9下replace雖然效率最高,但執行次數還是趕不上chrome下replace的執行次數。

測試地址裏面已經有6個版本,原因嘛是因爲我測試着突然又想出了新思路,而jsperf里加新測試代碼就要新開版本。其中版本6是方法最全的。

經過反覆在各瀏覽器裏做測試,我發現:

1,chrome的效率是最快的,但測試結果非!常!不!穩!定!經常這次運行和下次運行完全是兩個結果
2,firefox的效率比chrome差些,但穩定,測試結果也與chrome結論一致
3,IE9效率最差!結論也很奇葩!

其他測試

3個效率最高的方法,方法一,方法三,方法六大比拼

轉自:http://jo2.org/javascript-join-html-string/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章