正则表达式编程

本文摘抄自javascript正则表达式迷你书

正则表达式是匹配模式,要么匹配字符,要么匹配位置

1. 正则表达式的四种操作

正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”

有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换

进行任何相关操作,也需要宿主引擎相关 API 的配合使用。当然,在 JavaScript中,相关 API也不多

1.1 验证

验证是正则表达式最直接的应用,比如表单验证

在说验证之前,先要说清楚匹配是什么概念

所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,“匹配”的本质就是“查找”

有没有匹配,是不是匹配上,判断是否的操作,即称为“验证”

这里举一个例子,来看看如何使用相关 API 进行验证操作的

比如,判断一个字符串中是否有数字

使用 search

var regex = /\d/;
var string = "abc123";
console.log( !!~string.search(regex) );
// => true

使用 test

var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) );
// => true

使用 match

var regex = /\d/;
var string = "abc123";
console.log( !!string.match(regex) );
// => true

使用 exec

var regex = /\d/;
var string = "abc123";
console.log( !!regex.exec(string) );
// => true

其中,最常用的是 test

1.2 切分

匹配上了,我们就可以进行一些操作,比如切分。

所谓“切分”,就是把目标字符串,切成一段一段的。在 JavaScript中使用的是 split

比如,目标字符串是 "html,css,javascript",按逗号来切分:

var regex = /,/;
var string = "html,css,javascript";
console.log( string.split(regex) );
// => ["html", "css", "javascript"]

又比如,如下的日期格式:

2017/06/26
2017.06.26
2017-06-26

可以使用 split“切出”年月日:

var regex = /\D/;
console.log( "2017/06/26".split(regex) );
console.log( "2017.06.26".split(regex) );
console.log( "2017-06-26".split(regex) );
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]

1.3 提取

虽然整体匹配上了,但有时需要提取部分匹配的数据。

此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关 API。

这里,还是以日期为例,提取出年月日。注意下面正则中的括号:

使用 match

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( string.match(regex) );
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

使用 exec

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( regex.exec(string) );
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

使用 test

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
regex.test(string);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

使用 search

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
string.search(regex);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
// => "2017" "06" "26"

使用 replace

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
var date = [];
string.replace(regex, function (match, year, month, day) {
  date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]

其中,最常用的是 match

1.4 替换

找,往往不是目的,通常下一步是为了替换。在 JavaScript中,使用 replace进行替换。

比如把日期格式,从 yyyy-mm-dd 替换成 yyyy/mm/dd

var string = "2017-06-26";
var today = new Date( string.replace(/-/g, "/") );
console.log( today );
// => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)

2. 相关 API 注意要点

从上面可以看出用于正则操作的方法,共有 6 个,字符串实例 4 个,正则实例 2 个:

String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec

2.1 searchmatch的参数问题

我们知道字符串实例的那 4 个方法参数都支持正则和字符串。

searchmatch,会把字符串转换为正则的

var string = "2017.06.27";
console.log( string.search(".") );
// => 0
//需要修改成下列形式之一
console.log( string.search("\\.") );
console.log( string.search(/\./) );
// => 4
// => 4
console.log( string.match(".") );
// => ["2", index: 0, input: "2017.06.27"]
//需要修改成下列形式之一
console.log( string.match("\\.") );
console.log( string.match(/\./) );
// => [".", index: 4, input: "2017.06.27"]
// => [".", index: 4, input: "2017.06.27"]
console.log( string.split(".") );
// => ["2017", "06", "27"]
console.log( string.replace(".", "/") );
// => "2017/06.27"

2.2 match返回结果的格式问题

match返回结果的格式,与正则对象是否有修饰符 g 有关

var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;
console.log( string.match(regex1) );
console.log( string.match(regex2) );
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => ["2017", "06", "27"]

没有 g,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。

g,返回的是所有匹配的内容

当没有匹配时,不管有无 g,都返回 null

2.3 execmatch更强大

当正则没有 g 时,使用 match返回的信息比较多。但是有 g 后,就没有关键的信息 index

exec方法就能解决这个问题,它能接着上一次匹配后继续匹配:

var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => 4
// => ["06", "06", index: 5, input: "2017.06.27"]
// => 7
// => ["27", "27", index: 8, input: "2017.06.27"]
// => 10
// => null
// => 0

其中正则实例 lastIndex属性,表示下一次匹配开始的位置。

比如第一次匹配了 "2017",开始下标是 0,共 4 个字符,因此这次匹配结束的位置是 3,下一次开始匹配的位置是 4。

从上述代码看出,在使用 exec时,经常需要配合使用 while循环:

var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
var result;
while ( result = regex2.exec(string) ) {
  console.log( result, regex2.lastIndex );
}
// => ["2017", "2017", index: 0, input: "2017.06.27"] 4
// => ["06", "06", index: 5, input: "2017.06.27"] 7
// => ["27", "27", index: 8, input: "2017.06.27"] 10

2.4 修饰符 g,对 exextest的影响

上面提到了正则实例的 lastIndex属性,表示尝试匹配时,从字符串的 lastIndex位开始去匹配

字符串的四个方法,每次匹配时,都是从 0 开始的,即 lastIndex属性始终不变

而正则实例的两个方法 exectest,当正则是全局匹配时,每一次匹配完成后,都会修改 lastIndex。下面让我们以 test为例,看看你是否会迷糊:

var regex = /a/g;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex );
console.log( regex.test("ababc"), regex.lastIndex );
// => true 1
// => true 3
// => false 0

注意上面代码中的第三次调用 test,因为这一次尝试匹配,开始从下标 lastIndex,即 3 位置处开始查
找,自然就找不到了。

如果没有 g,自然都是从字符串第 0 个字符处开始尝试匹配:

var regex = /a/;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex );
console.log( regex.test("ababc"), regex.lastIndex );
// => true 0
// => true 0
// => true 0

2.5 test整体匹配时需要使用 ^$

这个相对容易理解,因为 test是看目标字符串中是否有子串匹配正则,即有部分匹配即可

如果,要整体匹配,正则前后需要添加开头和结尾:

console.log( /123/.test("a123b") );
// => true
console.log( /^123$/.test("a123b") );
// => false
console.log( /^123$/.test("123") );
// => true

2.6 split相关注意事项

split方法看起来不起眼,但要注意的地方有两个的。

第一,它可以有第二个参数,表示结果数组的最大长度:

var string = "html,css,javascript";
console.log( string.split(/,/, 2) );
// =>["html", "css"]

第二,正则使用分组时,结果数组中是包含分隔符的:

var string = "html,css,javascript";
console.log( string.split(/(,)/) );
// =>["html", ",", "css", ",", "javascript"]

2.7 replace是很强大的

replace有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数

当第二个参数是字符串时,如下的字符有特殊的含义:

属性 描述
1,2,…,$99 匹配第 1-99 个 分组里捕获的文本
$& 匹配到的子串文本
$` 匹配到的子串的左边文本
$' 匹配到的子串的右边文本
$$ 美元符号

例如,把 "2,3,5",变成 "5=2+3":

var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2");
console.log(result);
// => "5=2+3"

又例如,把 "2,3,5",变成 "222,333,555":

var result = "2,3,5".replace(/(\d+)/g, "$&$&$&");
console.log(result);
// => "222,333,555

再例如,把 "2+3=5",变成 "2+3=2+3=5=5":

var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
console.log(result);
// => "2+3=2+3=5=5"

我们对最后这个进行一下说明。要把 "2+3=5",变成 "2+3=2+3=5=5",其实就是想办法把 = 替换成=2+3=5=,其中,& 匹配的是 =, 匹配的是 2+3,' 匹配的是 5。因此使用 "&`&'&" 便达成了目的

当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:

"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
  console.log([match, $1, $2, index, input]);
});
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]

此时我们可以看到 replace 拿到的信息,并不比 exec

2.8 使用构造函数需要注意的问题

一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多 \

var string = "2017-06-27 2017.06.27 2017/06/27";
var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]
regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]

2.9 修饰符

ES5 中修饰符,共 3 个:

修饰符 描述
g 全局匹配,即找到所有匹配的,单词是 global
m 多行匹配,只影响 ^$,二者变成行的概念,即行开头和行结尾。单词是 multiline
i 忽略字母大小写,单词是 ingoreCase

当然正则对象也有相应的只读属性:

var regex = /\w/img;
console.log( regex.global );
console.log( regex.ignoreCase );
console.log( regex.multiline );
// => true
// => true
// => true

2.10 source属性

正则实例对象属性,除了 globalingnoreCasemultilinelastIndex属性之外,还有一个 source属性。

它什么时候有用呢?

比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:

var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log( regex.source )
// => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

2.11 构造函数属性

构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是 1,…,9 之外,还有几个不太常用的
属性(有兼容性问题):

静态属性 描述 简写形式
RegExp.input 最近一次目标字符串 RegExp["$_"]
RegExp.lastMatch 最近一次匹配的文本 RegExp["$&"]
RegExp.lastParen 最近一次捕获的文本 RegExp["$+"]
RegExp.leftContext 目标字符串中lastMatch之前的文本 RegExp["$`"]
RegExp.rightContext 目标字符串中lastMatch之后的文本 RegExp["$'"]

测试代码如下:

var regex = /([abc])(\d)/g;
var string = "a1b2c3d4e5";
string.match(regex);
console.log( RegExp.input );
console.log( RegExp["$_"]);
// => "a1b2c3d4e5"
console.log( RegExp.lastMatch );
console.log( RegExp["$&"] );
// => "c3"
console.log( RegExp.lastParen );
console.log( RegExp["$+"] );
// => "3"
console.log( RegExp.leftContext );
console.log( RegExp["$`"] );
// => "a1b2"
console.log( RegExp.rightContext );
console.log( RegExp["$'"] );
// => "d4e5"

3. 真实案例

3.1 使用构造函数生成正则表达式

我们知道要优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。模拟 getElementsByClassName方法,就是很能说明该问题的一个例子

这里 getElementsByClassName函数的实现思路是:

  • 比如要获取 className为 "high" 的 dom 元素;

  • 首先生成一个正则:/(^|\s)high(\s|$)/

  • 然后再用其逐一验证页面上的所有dom元素的类名,拿到满足匹配的元素即可

代码如下(可以直接复制到本地查看运行效果):

<p class="high">1111</p>
<p class="high">2222</p>
<p>3333</p>
<script>
function getElementsByClassName (className) {
  var elements = document.getElementsByTagName("*");
  var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
  var result = [];
  for (var i = 0; i < elements.length; i++) {
  var element = elements[i];
  if (regex.test(element.className)) {
  result.push(element)
  }
  }
  return result;
}
var highs = getElementsByClassName('high');
highs.forEach(function (item) {
  item.style.color = 'red';
});
</script>

3.2 使用字符串保存数据

般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串

使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:

var utils = {};
"Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(fun
ction (item) {
  utils["is" + item] = function (obj) {
  return {}.toString.call(obj) == "[object " + item + "]";
  };
});
console.log( utils.isArray([1, 2, 3]) );
// => true

3.3 if 语句中使用正则替代 &&

比如,模拟 ready函数,即加载完毕后再执行回调(不兼容 IE 的):

var readyRE = /complete|loaded|interactive/;

function ready(callback) {
  if (readyRE.test(document.readyState) && document.body) {
    callback()
  } else {
    document.addEventListener(
      'DOMContentLoaded',
      function () {
        callback()
      },
      false
    );
  }
};
ready(function () {
  alert("加载完毕!")
});

3.4 使用强大的 replace

因为 replace方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息来做文章

这里以查询字符串(querystring)压缩技术为例,注意下面 replace 方法中,回调函数根本没有返回任何
东西

function compress(source) {
  var keys = {};
  source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
    keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
  });
  var result = [];
  for (var key in keys) {
    result.push(key + '=' + keys[key]);
  }
  return result.join('&');
}
console.log(compress("a=1&b=2&a=3&b=4"));
// => "a=1,3&b=2,4"

3.5 综合运用

最后这里再做个简单实用的正则测试器

具体效果如下:

运行效果如下:

代码,直接贴了,相信你能看得懂(代码改编于《JavaScript Regular Expressions》):

<section>
  <div id="err"></div>
  <input id="regex" placeholder="请输入正则表达式">
  <input id="text" placeholder="请输入测试文本">
  <button id="run">测试一下</button>
  <div id="result"></div>
</section>
<style>
  section {
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    height: 300px;
    padding: 0 200px;
  }

  section * {
    min-height: 30px;
  }

  #err {
    color: red;
  }

  #result {
    line-height: 30px;
  }

  .info {
    background: #00c5ff;
    padding: 2px;
    margin: 2px;
    display: inline-block;
  }
</style>

<script>
  (function () {
    // 获取相应dom元素
    var regexInput = document.getElementById("regex");
    var textInput = document.getElementById("text");
    var runBtn = document.getElementById("run");
    var errBox = document.getElementById("err");
    var resultBox = document.getElementById("result");
    // 绑定点击事件
    runBtn.onclick = function () {
      // 清除错误和结果
      errBox.innerHTML = "";
      resultBox.innerHTML = "";
      // 获取正则和文本
      var text = textInput.value;
      var regex = regexInput.value;
      if (regex == "") {
        errBox.innerHTML = "请输入正则表达式";
      } else if (text == "") {
        errBox.innerHTML = "请输入测试文本";
      } else {
        regex = createRegex(regex);
        if (!regex) return;
        var result, results = [];
        // 没有修饰符g的话,会死循环
        if (regex.global) {
          while (result = regex.exec(text)) {
            results.push(result);
          }
        } else {
          results.push(regex.exec(text));
        }
        if (results[0] == null) {
          resultBox.innerHTML = "匹配到0个结果";
          return;
        }
        // 倒序是有必要的
        for (var i = results.length - 1; i >= 0; i--) {
          var result = results[i];
          var match = result[0];
          var prefix = text.substr(0, result.index);
          var suffix = text.substr(result.index + match.length);
          text = prefix +
            '<span class="info">' +
            match +
            '</span>' +
            suffix;
        }
        resultBox.innerHTML = "匹配到" + results.length + "个结果:<br>" + text;
      }
    };
    // 生成正则表达式,核心函数
    function createRegex(regex) {
      try {
        if (regex[0] == "/") {
          regex = regex.split("/");
          regex.shift();
          var flags = regex.pop();
          regex = regex.join("/");
          regex = new RegExp(regex, flags);
        } else {
          regex = new RegExp(regex, "g");
        }
        return regex;
      } catch (e) {
        errBox.innerHTML = "无效的正则表达式";
        return false;
      }
    }
  })();
</script>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容