文章来源:小青年原创
发布时间: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]%
表示两个百分号中不该该出现字母)。为了逐字表达,你必须在^.$()|*+?{\
这些字符前加上转移字符''。在方括号中,不须要转义字符。
属性 | 含义 |
---|---|
$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属性
方法 | 含义 |
---|---|
compile | 正则表达式比较 |
exec | 执行查找 |
test | 进行匹配 |
toSource | 返回特定对象的定义(literal representing),其值可用来建立一个新的对象。重载Object.toSource方法获得的。 |
toString | 返回特定对象的串。重载Object.toString方法获得的。 |
valueOf | 返回特定对象的原始值。重载Object.valueOf方法获得 |
详情你们能够查看这里:MDN JavaScript 标准库 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
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}$/
^ 表示打头的字符要匹配紧跟^后面的规则
$ 表示打头的字符要匹配紧靠$前面的规则
[ ] 中的内容是可选字符集
[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}://.+$/ 或 /^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>
前面咱们花了很长的篇幅讲解正则表达式是为了你们看这部分是时候可以有所理解,若是前面的内容一会儿没有看懂也没有关系,你们能够先看看这部分的内容回过头去查看刚刚的内容。
咱们首先会想到写一个模板,咱们常见的是写成这样:
<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
。
哈哈,是否是懵逼了?反正我常常是晕的,不行,仍是要对比一下搞清楚。
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()
方法咱们须要子字符串的内容或者符合的正则表达式。很明显咱们的思路就出来了,接下来咱们用代码实现。
咱们这里参考前面的内容写一个基本函数,设置一个变量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>
经过上面的代码咱们已经实现了将模板字符串分隔成子字符串。
使用字符串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字符串。可是与前面不一样的是咱们分隔的子字符串中有三种类型:
含普通html标签的子字符串(如:<p>name:
)
含js对象值的子字符串(如:this.name
)
含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源代码