JavaScript進階學習(一)—— 基於正則表達式的簡單js模板引擎實現

文章來源:小青年原創
發佈時間:2016-06-26
關鍵詞:JavaScript,正則表達式,js模板引擎
轉載需標註本文原始地址: http://zhaomenghuan.github.io...javascript

前言

這年頭MVC、MVVM框架氾濫,不少時候咱們只是用了這些框架,有時候想深刻去了解這些框架背後的原理實現時,閱讀源碼時發現無從下手,js基本功簡直渣渣,因此想利用業餘時間仍是要補補基礎。之前看JavaScript的一些書籍時老是把正則表達式這一章跳過了,遇到一些須要寫正則的時候而後都是各類copy,js要進階感受仍是要系統學習一下正則,雖然看起來像亂碼同樣的匹配規則,可是若是熟練使用會頗有用,那麼今天就先從正則開始吧!和大部分書籍同樣,本文前篇也會是講解基礎,本文的不少內容都是摘自網絡進行整理,有些內容須要各位本身進行實踐驗證。html

正則表達式

正則表達式(英語:Regular Expression,在代碼中常簡寫爲regex、regexp或RE)使用單個字符串來描述、匹配一系列符合某個句法規則的字符串搜索模式。搜索模式可用於文本搜索和文本替換。html5

基本語法

RegExp 構造函數可建立一個正則表達式對象,用特定的模式匹配文本。建立一個正則對象的兩種方法:字面量和構造函數。要表示字符串,字面量形式不使用引號,而傳遞給構造函數的參數使用引號。java

字面量: /pattern/flags
構造函數: RegExp(pattern [, flags])
  • pattern 正則表達式文本android

  • flags 該參數能夠是下面單個值或者幾個值的任意組合:git

    • g 全局匹配github

    • i 忽略大小寫web

    • gi或ig 全局匹配、忽略大小寫正則表達式

    • m 多行查找,讓開始和結束字符(^ 和 $)工做在多行模式(也就是,^ 和 $ 能夠匹配字符串中每一行的開始和結束(行是由 n 或 r 分割的),而不僅是整個輸入字符串的最開始和最末尾處。json

    • u Unicode。將模式視爲Unicode碼位(code points)序列。

    • y sticky。使用隱式的^錨點把正則錨定在了lastIndex所指定的偏移位置處,具體能夠看看這篇文章:JavaScript:正則表達式的/y標識

具體例子:

/ab+c/i;
new RegExp('ab+c', 'i');
new RegExp(/ab+c/, 'i');

當表達式被賦值時,字面量形式提供正則表達式的編譯(compilation)狀態,當正則表達式保持爲常量時使用字面量。例如當你在循環中使用字面量構造一個正則表達式時,正則表達式不會在每一次迭代中都被從新編譯(recompiled)。

而正則表達式對象的構造函數,如 new RegExp('ab+c') 提供了正則表達式運行時編譯(runtime compilation)。若是你知道正則表達式模式將會改變,或者你事先不知道什麼模式,而是從另外一個來源獲取,如用戶輸入,這些狀況均可以使用構造函數。

從ECMAScript 6開始,當第一個參數爲正則表達式而第二個標誌參數存在時,new RegExp(/ab+c/, 'i')再也不拋出TypeError (「當從其餘正則表達式進行構造時不支持標誌」)的異常,取而代之,將使用這些參數建立一個新的正則表達式。

當使用構造函數創造正則對象時,須要常規的字符轉義規則(在前面加反斜槓 )。好比,如下是等價的:

var re = new RegExp("\\w+");
var re = /\w+/;

正則表達式中的特殊字符

^$ —— 做用是分別指出一個字符串的開始和結束。

  • "^The":表示全部以"The"開始的字符串"There","The cat"等;

  • "of despair$":表示因此以"of despair"結尾的字符串;

  • "^abc$":表示開始和結尾都是"abc"的字符串——呵呵,只有"abc"本身了;

  • "notice":表示任何包含"notice"的字符串。

最後那個例子,若是你不使用兩個特殊字符,你就在表示要查找的串在被查找串的任意部分——你並不把它定位在某一個頂端。

*+?{} —— 表示一個或一序列字符重複出現的次數。

分別表示「沒有或更多」,「一次或更多」,「沒有或一次」和指定重複次數的範圍。{}必須指定範圍的下限,如:"{0,2}"而不是"{,2}"。*、+和?至關於{0,}、{1,}和{0,1}。

  • "ab*":表示一個字符串有一個a後面跟着零個或若干個b。("a", "ab", "abbb",……);

  • "ab+":表示一個字符串有一個a後面跟着至少一個b或者更多;

  • "ab?":表示一個字符串有一個a後面跟着零個或者一個b;

  • "a?b+$":表示在字符串的末尾有零個或一個a跟着一個或幾個b。

  • "ab{2}":表示一個字符串有一個a跟着2個b("abb");

  • "ab{2,}":表示一個字符串有一個a跟着至少2個b;

  • "ab{3,5}":表示一個字符串有一個a跟着3到5個b。

| —— 表示「或」操做

  • "hi|hello":表示一個字符串裏有"hi"或者"hello";

  • "(b|cd)ef":表示"bef"或"cdef";

  • "(a|b)*c":表示一串"a""b"混合的字符串後面跟一個"c";

. —— 能夠替代任何字符

  • "a.[0-9]":表示一個字符串有一個"a"後面跟着一個任意字符和一個數字;

  • "^.{3}$":表示有任意三個字符的字符串(長度爲3個字符);

[] —— 表示某些字符容許在一個字符串中的某一特定位置出現

  • "[ab]":表示一個字符串有一個"a"或"b"(至關於"a|b");

  • "[a-d]":表示一個字符串包含小寫的'a'到'd'中的一個(至關於"a|b|c|d"或者"[abcd]");

  • "^[a-zA-Z]":表示一個以字母開頭的字符串;

  • "[0-9]%":表示一個百分號前有一位的數字;

  • ",[a-zA-Z0-9]$":表示一個字符串以一個逗號後面跟着一個字母或數字結束。

能夠在方括號裏用'^'表示不但願出現的字符,'^'應在方括號裏的第一位。(如:%[^a-zA-Z]%表示兩個百分號中不該該出現字母)。爲了逐字表達,你必須在^.$()|*+?{\這些字符前加上轉移字符''。在方括號中,不須要轉義字符。

RegExp對象的屬性

屬性 含義
$1-$9 若是它(們)存在,是匹配到的子串
$_ 參見input
$* 參見multiline
$& 參見lastMatch
$+ 參見lastParen
$` 參見leftContext
$''         參見rightContext
constructor    建立一個對象的一個特殊的函數原型
global       是否在整個串中匹配(bool型)
ignoreCase     匹配時是否忽略大小寫(bool型)
input        被匹配的串
lastIndex     最後一次匹配的索引
lastParen     最後一個括號括起來的子串
leftContext    最近一次匹配以左的子串
multiline     是否進行多行匹配(bool型)
prototype     容許附加屬性給對象
rightContext    最近一次匹配以右的子串
source       正則表達式模式
lastIndex     最後一次匹配的索引

詳情你們能夠查看這裏:MDN JavaScript 標準庫 RegExp屬性

RegExp對象的方法

方法 含義
compile    正則表達式比較
exec        執行查找
test        進行匹配
toSource     返回特定對象的定義(literal representing),其值可用來建立一個新的對象。重載Object.toSource方法獲得的。
toString       返回特定對象的串。重載Object.toString方法獲得的。
valueOf     返回特定對象的原始值。重載Object.valueOf方法獲得

詳情你們能夠查看這裏:MDN JavaScript 標準庫 RegExp方法

不過在這裏咱們仍是須要說明一下的是咱們用得比較多的方法主要分爲兩類:

RegExp 對象方法

RegExp.prototype.compile() ——編譯正則表達式

用法:regexObj.compile(pattern, flags)

功能說明:compile() 方法用於在腳本執行過程當中編譯正則表達式,也可用於改變和從新編譯正則表達式。該方法能夠編譯指定的正則表達式,編譯以後的正則表達式執行速度將會提升,若是正則表達式屢次被調用,那麼調用compile方法能夠有效的提升代碼的執行速度,若是該正則表達式只能被使用一次,則不會有明顯的效果。

var str="Every man in the world! Every woman on earth!";
var patt=/man/g;
var str2=str.replace(patt,"person");
document.write(str2+"<br>");
patt=/(wo)?man/g;
patt.compile(patt); 
str2=str.replace(patt,"person");
document.write(str2);
結果:
Every person in the world! Every woperson on earth!
Every person in the world! Every person on earth!

RegExp.prototype.exec() —— 檢索字符串中指定的值。返回找到的值,並肯定其位置。

用法:regexObj.exec(str)

功能說明:exec() 方法若是成功匹配,exec 方法返回一個數組,而且更新正則表達式對象的屬性。返回的數組包括匹配的字符串做爲第一個元素,緊接着一個元素對應一個成功匹配被捕獲的字符串的捕獲括號(capturing parenthesis)。(one item for each capturing parenthesis that matched containing the text that was captured.)若是匹配失敗,exec 方法將返回 null。

var str="Hello world,hello zhaomenghuan!";
// look for "Hello"或"hello"
var patt=/hello/gi;
while((result = patt.exec(str))!== null){
    document.write("result:" + result +"的位置爲"+ result.index + "<br />"); 
}
結果:
result:Hello的位置爲0
result:hello的位置爲12

RegExp.prototype.test() —— 檢索字符串中指定的值。返回 true 或 false。

用法:regexObj.test(str)

功能說明:test() 方法用於檢測一個字符串是否匹配某個模式,若是字符串中有匹配的值返回 true ,不然返回 false。

var result = /hello/.test('This is a hello world!');
document.write(result);
結果:
true

支持正則表達式的 String 對象的方法

search —— 檢索與正則表達式相匹配的值

用法:string.search(searchvalue)

searchvalue 必須。查找的字符串或者正則表達式。

功能說明:search()方法用於檢索字符串中指定的子字符串,或檢索與正則表達式相匹配的子字符串。若是沒有找到任何匹配的子串,則返回 -1。

var str="Mr. Blue has a blue house";
document.write(str.search(/blue/i));
結果:
4

match —— 找到一個或多個正則表達式的匹配。

用法:string.match(regexp)

regexp 必需。規定要匹配的模式的 RegExp 對象。若是該參數不是 RegExp 對象,則須要首先把它傳遞給 RegExp 構造函數,將其轉換爲 RegExp 對象。返回值類型爲Array。

功能說明:match() 方法可在字符串內檢索指定的值,或找到一個或多個正則表達式的匹配。match() 方法將檢索字符串 String Object,以找到一個或多個與 regexp 匹配的文本。這個方法的行爲在很大程度上有賴於 regexp 是否具備標誌 g。若是 regexp 沒有標誌 g,那麼 match() 方法就只能在 stringObject 中執行一次匹配。若是沒有找到任何匹配的文本, match() 將返回 null。不然,它將返回一個數組,其中存放了與它找到的匹配文本有關的信息。

var str = "The rain in SPAIN stays mainly in the plain"; 
var match=str.match(/ain/gi);
document.getElementById("demo").innerHTML=match;
結果:
ain,AIN,ain,ain

replace 替換與正則表達式匹配的子串。

用法:string.replace(searchvalue,newvalue)

searchvalue 必須。規定子字符串或要替換的模式的 RegExp 對象。
請注意,若是該值是一個字符串,則將它做爲要檢索的直接量文本模式,而不是首先被轉換爲 RegExp 對象。
newvalue 必需。一個字符串值。規定了替換文本或生成替換文本的函數。
返回值爲String類型,一個新的字符串,是用 replacement 替換了 regexp 的第一次匹配或全部匹配以後獲得的。

split 把字符串分割爲字符串數組。

用法:string.split(separator,limit)

separator 可選。字符串或正則表達式,從該參數指定的地方分割 string Object。
limit 可選。該參數可指定返回的數組的最大長度。若是設置了該參數,返回的子串不會多於這個參數指定的數組。若是沒有設置該參數,整個字符串都會被分割,不考慮它的長度。
返回值爲Array類型,一個字符串數組。該數組是經過在 separator 指定的邊界處將字符串 string Object 分割成子串建立的。返回的數組中的字串不包括 separator 自身。

var str="How are you doing today?";
var match = str.split(/ /);
document.write(match);
結果:
How,are,you,doing,today?

正則表達式的應用實例說明

校驗是否全由數字組成 —— /^[0-9]{1,20}$/

^ 表示打頭的字符要匹配緊跟^後面的規則
&dollar; 表示打頭的字符要匹配緊靠$前面的規則
[ ] 中的內容是可選字符集
[0-9] 表示要求字符範圍在0-9之間
{1,20}表示數字字符串長度合法爲1到20,即爲[0-9]中的字符出現次數的範圍是1到20次。

/^ 和 $/成對使用應該是表示要求整個字符串徹底匹配定義的規則,而不是隻匹配字符串中的一個子串。

校驗登陸名:只能輸入5-20個以字母開頭、可帶數字、「_」、「.」的字串—— /^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4,19}$/

^[a-zA-Z]{1} 表示第一個字符要求是字母。
([a-zA-Z0-9]|[._]){4,19} 表示從第二位開始(由於它緊跟在上個表達式後面)的一個長度爲4到9位的字符串,它要求是由大小寫字母、數字或者特殊字符集[._]組成。

校驗密碼:只能輸入6-20個字母、數字、下劃線——/^(w){6,20}$/

w:用於匹配字母,數字或下劃線字符

校驗普通電話、傳真號碼:能夠「+」或數字開頭,可含有「-」 和 「 」——/^[+]{0,1}(d){1,3}[ ]?([-]?((d)|[ ]){1,12})+$/

d:用於匹配從0到9的數字;
「?」元字符規定其前導對象必須在目標對象中連續出現零次或一次
能夠匹配的字符串如:+123 -999 999 ; +123-999 999 ;123 999 999 ;+123 999999等

校驗URL——/^http[s]{0,1}://.+&dollar;/ 或 /^http[s]{0,1}://.{1,n}$/ (表示url串的長度爲length(「https://」) + n )

/ :表示字符「/」。
. 表示全部字符的集
+ 等同於{1,},就是1到正無窮吧。

校驗純中文字符——/^[u4E00-u9FA5]+$/

[u4E00-u9FA5] :中文字符集的範圍

以上表達式均在下面的javascript中測試經過:

<html>
<head>
    <meta charset="utf-8"/>
    <title></title>
</head>
<body>
    <form>
        規則表達式 : (填寫/ /之間的表達式)<br />
        <input type="input" name="regxStr" value="" ><br />
        校驗字符串 :<br />
        <input type="input" name="str" value="" >
        <input type="button" name="match" value="匹配" onClick="alert(regx(regxStr.value,str.value));">
    </form>
    <script type="text/javascript">
        function regx(r,s){
            if (r == null || r == ""){
               return;
            }
            var patrn= new RegExp(r);
            return patrn.test(s)
        }
    </script>
</body>
</html>

js模板引擎實現原理

前面咱們花了很長的篇幅講解正則表達式是爲了你們看這部分是時候可以有所理解,若是前面的內容一會兒沒有看懂也沒有關係,你們能夠先看看這部分的內容回過頭去查看剛剛的內容。

咱們首先會想到寫一個模板,咱們常見的是寫成這樣:

<script type="text/html" id="template">
    <p>name: {{name}}</p>
    <p>age: {{profile.age}}</p>
</script>

固然也可使用<template></template>標籤,並且這個也是如今的流行趨勢,擁抱模塊化,不過本文不是講這個標籤和模塊化,若是你們感興趣能夠看看這兩篇文章:

你們也能夠看下面這個基礎例子:

<div class="content"></div>    

<template id="template">
    <p>name: 小青年</p>
    <p>age: 22</p>
</template>    

<script type="text/javascript">    
    var isSupport='content' in document.createElement('template');
    if (isSupport) {    
        var tpl = document.querySelector('#template');
        var content = document.querySelector(".content");
        var clone = document.importNode(tpl.content, true);
        content.appendChild(clone); 
    } else {
        alert("is not support template");
    }
</script>

迴歸正題,咱們繼續說上面的模板,開始寫模板引擎。

基礎模板引擎原理講解

咱們使用js模板引擎,能夠認爲是在作一個MVC結構的系統,模型(model)-視圖(view)-控制器(controller)。控制器(controller)做爲中間部分,首先要拿到模型,這裏咱們須要拿到模板裏面與視圖相關的內容,如上面的例子中{{ }}中的內容,首先用正則查找:

var re = /{{(.+?)}}/g;
while((match = re.exec(tpl))!==null) {
     console.log(match);
}
結果:
"{{name}},name"
"{{age}},age"

/{{(.+?)}}/g的意思是查找開頭爲{{和結尾爲}}的子字符串。經過RegExp 對象exec()方法搜索匹配獲得的是一個數組,咱們能夠經過match[0]表示匹配的原字符串,match[1]表示匹配的目標字符串,咱們經過執行字符串替換方法就能夠獲得目標字符串。

<div id="content"></div>
<!--HTML模板(相似MVC中的view)-->
<script type="text/html" id="template">
    <p>name: {{name}}</p>
    <p>age: {{age}}</p>
</script>
        
<script type="text/javascript">
    // 模板引擎函數(相似MVC中的controller)
    var mtpl = function(tpl, data) {
        var re = /{{(.+?)}}/g;
        while((match = re.exec(tpl))!==null) {
            if(match[1]){
                tpl = tpl.replace(match[0], data[match[1]])
            }else{
                tpl = tpl.replace(match[0], '')
            }    
        }
        return tpl
    }
    
    // 模板數據(相似MVC中的model)
    var tpl = document.getElementById("template").innerHTML;
    document.getElementById("content").innerHTML = mtpl(tpl,{
        name: "zhaomenghuan",
        age: 22
    });
</script>

這裏咱們經過data['key']的形式取值而後替換模板中的{{...}}的內容實現了一個內容的替換。上述代碼很簡單,基本實現了一個字符替換而已,咱們上面是經過字符串替換實現了模板和數據的匹配,可是若是咱們上面那個json數據是這樣的:

var data = {
    base: {
        name: 'zhaomenghuan',
        age: 22    
    },
    skills: ['html5','javascript','android']
}

咱們直接經過data[match[1]]進行顯然會有問題,咱們雖然能夠經過data.base['name']獲取,可是對於模板引擎函數封裝來講是不夠完善的,並且也不能執行JavaScript,好像並無相似於一些有名的js模板引擎庫中的語法功能,因此略顯low。下面咱們在這個基礎上進行改造。

下面咱們說一下一種最原始的方法,經過Function構造器實現,根據字符串建立一個函數。在一篇文章中看到說這種方法執行JavaScript性能低下,可是對於初學者來講,學習一下實現思路我以爲也是有意義的,畢竟對於新手來講談性能是件奢侈的事。咱們首先看個例子:

var fn = new Function("arg", "console.log(arg + 1);"); 
fn(2); // outputs 3

fn但是一個貨真價實的函數,它接受一個參數,函數體是console.log(arg + 1);,上面那個例子等同於:

var fn = function(arg) {
    console.log(arg + 1);
}
fn(2);

咱們經過new Function能夠將字符串轉成JavaScript執行,看起來是否是很すごい(厲害,'sigoyi',好像還有個單詞是‘daijobu’,女友每次問我,我每次都是回答‘daijo’,女友喜歡看動漫,今天她放假先回家了,捨不得,想一想她平時萌萌噠的樣子,越是想念,學幾個簡單單詞,下次見面說說,哈哈。)

接着說,咱們有時候參數是多個,咱們雖然能夠輸入多個參數,可是這顯然不是最好的辦法,咱們可使用apply,這樣咱們沒必要顯式地傳參數給這個函數。好比咱們前面的例子:

var data = {
    name: 'zhaomenghuan',
    age: 22
}
new Function("console.log(this.name + ' is ' + this.age +' years old.');").apply(data);
結果:
"zhaomenghuan is 22 years old."

apply()會自動設定函數執行的上下文,這就是爲何咱們能在函數裏面使用this.name,這裏this指向data對象。這裏咱們獲得什麼啓示呢?咱們考驗經過給new Function傳入模板字符串和數據生成咱們的內容。

咱們能夠經過數組push()或者+=拼接方式將分隔的字符串鏈接起來,有文章中稱,「在現代瀏覽器使用+=會比數組push方法快,在v8引擎中使用+=方法會比數組拼接快4.7倍,而在IE6-8下push比+=拼接快」。至於兩者效率比較不在本文範圍內,你們能夠自行探究,可是咱們爲了簡化問題,不考慮效率問題,咱們能夠將分隔的字符串用下列方法push拼接:

var r=[];
r.push("<p>");
r.push(this.name);
r.push("</p><p>");
r.push(this.age);
r.push("</p>");
return r.join("");

咱們若是直接拼接成數組而後轉成對象也能夠,可是須要將<>轉義,爲了方便,咱們有時候能夠這樣處理:

var data = {
    name: 'zhaomenghuan',
    age: 22
}    
var code = 'var r=[];\n';
code += 'r.push("<p>");\n';
code += 'r.push(this.name);\n'
code += 'r.push("</p><p>");\n';
code += 'r.push(this.age);\n';
code += 'r.push("</p>");\n';
code += 'return r.join("");';
console.log(code)
var fn = new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
console.log(fn)
結果:
"<p>zhaomenghuan</p><p>22</p>"

寫到這裏我相信聰明的人應該知道我接下來要作的事情了,主要是兩個:如何根據咱們自定義的分隔字符分隔模板字符串,而後就是動態生成字符串。不過咱們能夠看出來這裏咱們還有個沒有提到的是讓模板可以嵌入JavaScript的語法關鍵詞,好比if,for等,這個處理方法和上面的略有不一樣,須要加以判斷,不過咱們能夠劃分爲解析html和js兩大類。

自定義分隔字符分隔模板字符

在講如何分割字符串前咱們先看三個函數:slice|splice|split

哈哈,是否是懵逼了?反正我常常是暈的,不行,仍是要對比一下搞清楚。

經常使用函數 slice | splice | split 對比

String.prototype.slice() —— 從一個字符串中提取字符串並返回新字符串

語法:str.slice(beginSlice[, endSlice])

參數:

  • beginSlice:從該索引(以 0 爲基數)處開始提取原字符串中的字符。若是值爲負數,會被當作 sourceLength + beginSlice 看待,這裏的sourceLength 是字符串的長度 (例如, 若是beginSlice 是 -3 則看做是: sourceLength - 3)

  • endSlice:可選。在該索引(以 0 爲基數)處結束提取字符串。若是省略該參數,slice會一直提取到字符串末尾。若是該參數爲負數,則被看做是 sourceLength + endSlice,這裏的 sourceLength 就是字符串的長度(例如,若是 endSlice 是 -3,則是, sourceLength - 3)。

Array.prototype.slice()—— 把數組中一部分的淺複製(shallow copy)存入一個新的數組對象中,並返回這個新的數組。

語法:arr.slice([begin[, end]])

參數:

  • begin:從該索引處開始提取原數組中的元素(從0開始)。
    若是該參數爲負數,則表示從原數組中的倒數第幾個元素開始提取,slice(-2)表示提取原數組中的倒數第二個元素到最後一個元素(包含最後一個元素)。若是省略 begin,則 slice 從索引 0 開始。

  • end:在該索引處結束提取原數組元素(從0開始)。slice會提取原數組中索引從 begin 到 end 的全部元素(包含begin,但不包含end)。
    slice(1,4) 提取原數組中的第二個元素開始直到第四個元素的全部元素 (索引爲 1, 2, 3的元素)。若是該參數爲負數, 則它表示在原數組中的倒數第幾個元素結束抽取。 slice(-2,-1)表示抽取了原數組中的倒數第二個元素到最後一個元素(不包含最後一個元素,也就是隻有倒數第二個元素)。若是 end 被省略,則slice 會一直提取到原數組末尾。

描述:
slice 不修改原數組,只會返回一個包含了原數組中提取的部分元素的一個新數組。原數組的元素會按照下述規則拷貝("一級深拷貝"[one level deep]規則):

  • 若是該元素是個對象引用 (不是實際的對象),slice 會拷貝這個對象引用到新的數組裏。兩個對象引用都引用了同一個對象。若是被引用的對象發生改變,則改變將反應到新的和原來的數組中。

  • 對於字符串和數字來講(不是 String 和 Number 對象),slice 會拷貝字符串和數字到新的數組裏。在一個數組裏修改這些字符串或數字,不會影響另外一個數組。

若是向兩個數組任一中添加了新元素,則另外一個不會受到影響。

Array.prototype.splice() —— 用新元素替換舊元素,以此修改數組的內容

語法:array.splice(start, deleteCount[, item1[, item2[, ...]]])

參數:

  • start​
    從數組的哪一位開始修改內容。若是超出了數組的長度,則從數組末尾開始添加內容;若是是負值,則表示從數組末位開始的第幾位。

  • deleteCount
    整數,表示要移除的數組元素的個數。若是 deleteCount 是 0,則不移除元素。這種狀況下,至少應添加一個新元素。若是 deleteCount 大於start 以後的元素的總數,則從 start 後面的元素都將被刪除(含第 start 位)。

  • itemN
    要添加進數組的元素。若是不指定,則 splice() 只刪除數組元素。

返回值:
由被刪除的元素組成的一個數組。若是隻刪除了一個元素,則返回只包含一個元素的數組。若是沒有刪除元素,則返回空數組。
描述:
若是添加進數組的元素個數不等於被刪除的元素個數,數組的長度會發生相應的改變。請注意,splice() 方法與 slice() 方法的做用是不一樣的,splice() 方法會直接對數組進行修改。

String.prototype.split() —— 經過把字符串分割成子字符串來把一個 String 對象分割成一個字符串數組

語法:string.split(separator,limit)

咱們在前面講解【支持正則表達式的 String 對象的方法】時講到這個方法了,這裏再也不贅述,列出來只爲方便你們對比學習。

這裏列出的方法中對於咱們分隔字符串有用的是String.prototype.slice()String.prototype.split(),另個方法的區別在於使用slice()方法們須要知道子字符串的索引值index,使用split()方法咱們須要子字符串的內容或者符合的正則表達式。很明顯咱們的思路就出來了,接下來咱們用代碼實現。

基於String.prototype.slice()方法分隔字符串

咱們這裏參考前面的內容寫一個基本函數,設置一個變量cursor做爲索引值指針,每次執行完一個匹配操做,咱們將指針移動一下。咱們前面講RegExp.prototype.exec()返回值時重點說到了三個參數。
若match = re.exec(str),則有:
match.index:匹配的對象起始位置
match[0]:表示匹配的原字符串
match[1]:表示匹配的目標字符串
若咱們明白了思路咱們就能夠寫下面的代碼:

<script type="text/html" id="template">
    <p>name: {{this.name}}</p>
    <p>age: {{this.age}}</p>
</script>

<script type="text/javascript">    
    var mtpl = function(tpl,data) {
        var re = /{{(.+?)}}/g,cursor = 0;
                            
        while((match = re.exec(tpl))!== null) {
            // 開始標籤  {{ 前的內容和結束標籤 }} 後的內容
            console.log(tpl.slice(cursor, match.index))
            // 開始標籤  {{ 和 結束標籤 }} 之間的內容
            console.log(match[1])
            // 每一次匹配完成移動指針
            cursor = match.index + match[0].length;
        }
        // 最後一次匹配完的內容
        console.log(tpl.substr(cursor, tpl.length - cursor))
    }
    
    // 調用
    var tpl = document.getElementById("template").innerHTML;
    mtpl(tpl,null);
</script>

經過上面的代碼咱們已經實現了將模板字符串分隔成子字符串。

基於String.prototype.split()方法分隔字符串

使用字符串split()方法,下面咱們不使用正則做爲分隔符號,這裏就使用自定義符號做爲分隔標準,如:

var sTag = '{%';//開始標籤
var eTag = '%}';//結束標籤

而後咱們以sTag爲分隔符執行split方法:

var matchs = tpl.split(sTag);

返回值matchs 爲一個子字符串數組,而後對數組每個子字符串以eTag爲分隔符執行split方法,進一步獲得的子字符串數組分爲兩種類型,一種是於咱們匹配子字符串參數有關的子串數組,一種是與匹配參數無關的子串數組(這種數組長度爲1)。之因此要分得這麼細是爲了後面字符串鏈接時更方便的時候合適的方法鏈接。

<script type="text/html" id="template">
    <p>name: {%this.name%}</p>
    <p>age: {%this.age%}</p>
</script>

<script type="text/javascript">
    var mtpl = function(tpl, data) {
        var sTag = '{%';//開始標籤
        var eTag = '%}';//結束標籤
        var matchs = tpl.split(sTag);
        for (var i = 0, len = matchs.length; i < len; i++) {
            var match = matchs[i].split(eTag);
            if (match.length === 1) {
                console.log(match[0]);
            } else {
                if(match[0]){
                    console.log(match[0]);
                }   
                if (match[1]) {
                    console.log(match[1]);
                }
            }
        }
    }
    
    // 調用
    var tpl = document.getElementById("template").innerHTML;
    mtpl(tpl,null);
</script>

動態鏈接字符串

咱們上面使用了字符串分隔函數把字符串進行了分隔,而且提取了關鍵子字符串,下面咱們講解如何將這些字符串鏈接起來。
咱們這裏定義一個這樣的模板:

<script type="text/tpl" id="template">
    <p>name: {{this.name}}</p>
    <p>age: {{this.profile.age}}</p>
    {{if (this.sex) {}}
        <p>sex: {{this.sex}}</p>
    {{}}}
    <ul>
        {{for(var i in this.skills){}}
        <li>{{this.skills[i]}}</li>
        {{}}}
    </ul>
</script>

很明顯咱們這個模板就相對前面的複雜得多了,可是基本思路是同樣的,無非是提取{{...}}的內容,而後結合數據從新組合成新的html字符串。可是與前面不一樣的是咱們分隔的子字符串中有三種類型:

  1. 含普通html標籤的子字符串(如:<p>name:

  2. 含js對象值的子字符串(如:this.name

  3. 含javascript關鍵字的代碼片斷(如:if (this.sex) {

咱們剛剛前面一直只提到了第一、2兩種,這兩種直接可使用數組push方法就能夠鏈接起來,可是第3中不能使用數組push,而是應該直接鏈接。
因此這裏咱們分兩種狀況:

var reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,;

var code = 'var r=[];\n';
// 解析html
function parsehtml(line) {
    // 單雙引號轉義,換行符替換爲空格,去掉先後的空格
    line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");
    code +='r.push("' + line + '");\n';
}

// 解析js代碼        
function parsejs(line) {   
    // 去掉先後的空格
    line = line.replace(/(^\s+)|(\s+$)/g,"");
    code += line.match(reExp)? line + '\n' : 'r.push(' + line + ');\n';
}

當咱們寫完這兩個函數的時候,咱們直接替換上面咱們分隔字符串時候獲得的字字符串時候打印的函數便可。固然咱們會看到不少文章爲了壓縮代碼,將這兩個函數合併成一個函數,其實對於咱們理解這個問題,還原問題本質並無實際意義,這裏建議仍是很開寫更清晰。

完整代碼以下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <div id="content"></div>        
    <script type="text/tpl" id="template">
        <p>name: {{this.name}}</p>
        <p>age: {{this.profile.age}}</p>
        {{if (this.sex) {}}
            <p>sex: {{this.sex}}</p>
        {{}}}
        <ul>
            {{for(var i in this.skills){}}
            <li>{{this.skills[i]}}</li>
            {{}}}
        </ul>
    </script>
        
    <script type="text/javascript">    
        var mtpl = function(tpl, data) {
            var re = /{{(.+?)}}/g, 
                cursor = 0
                reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,    
                code = 'var r=[];\n';

            // 解析html
            function parsehtml(line) {
                // 單雙引號轉義,換行符替換爲空格,去掉先後的空格
                line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");
                code +='r.push("' + line + '");\n';
            }
            
            // 解析js代碼        
            function parsejs(line) {   
                // 去掉先後的空格
                line = line.replace(/(^\s+)|(\s+$)/g,"");
                code += line.match(reExp)? line + '\n' : 'r.push(' + line + ');\n';
            }    
            
            while((match = re.exec(tpl))!== null) {
                // 開始標籤  {{ 前的內容和結束標籤 }} 後的內容
                parsehtml(tpl.slice(cursor, match.index))
                // 開始標籤  {{ 和 結束標籤 }} 之間的內容
                parsejs(match[1])
                // 每一次匹配完成移動指針
                cursor = match.index + match[0].length;
            }
            // 最後一次匹配完的內容
            parsehtml(tpl.substr(cursor, tpl.length - cursor));
            code += 'return r.join("");';
            return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
        }
        
        var tpl = document.getElementById("template").innerHTML.toString();
        document.getElementById("content").innerHTML = mtpl(tpl,{
            name: "zhaomenghuan",
            profile: { 
                age: 22 
            },
            sex: 'man',
            skills: ['html5','javascript','android']
        });
    </script>
</body>
</html>

另一個自定義標籤的和這個代碼相似,你們能夠本身試試,或者看本文全部的代碼演示完整工程。

至此咱們完成了一個基於正則表達式的簡單js模板引擎,本文目的不在於造一個輪子,也不是爲了重複製造文章,只是把相關問題進行梳理,在本身知識體系中造成一個更加清晰的思路,經過這個實際例子將正則表達式、數組和字符串相關知識點串起來,方面後面本身查閱,也方便初學者能夠學習借鑑。

參考文章

MDN JavaScript 標準庫 RegExp
js正則表達式基本語法(精粹)
只有20行Javascript代碼!手把手教你寫一個頁面模板引擎
TemplateEngine.js源代碼
template.js源代碼

clipboard.png

相關文章
相關標籤/搜索