JavaScript: String.indexOf(...) 允许使用正则表达式吗?

277
在JavaScript中,是否有一个等价于String.indexOf(...)的方法,它可以接受一个正则表达式作为第一个参数,同时还允许传入第二个参数?
我需要做类似的操作。
str.indexOf(/[abc]/ , i);

并且

str.lastIndexOf(/[abc]/ , i);

虽然String.search()接受一个正则表达式作为参数,但它不允许我指定第二个参数!
编辑:
这比我最初想象的要难,所以我编写了一个小的测试函数来测试所有提供的解决方案...它假设regexIndexOf和regexLastIndexOf已经添加到String对象中。
function test (str) {
    var i = str.length +2;
    while (i--) {
        if (str.indexOf('a',i) != str.regexIndexOf(/a/,i)) 
            alert (['failed regexIndexOf ' , str,i , str.indexOf('a',i) , str.regexIndexOf(/a/,i)]) ;
        if (str.lastIndexOf('a',i) != str.regexLastIndexOf(/a/,i) ) 
            alert (['failed regexLastIndexOf ' , str,i,str.lastIndexOf('a',i) , str.regexLastIndexOf(/a/,i)]) ;
    }
}

我正在进行以下测试,以确保至少对于一个字符的正则表达式,结果与使用indexOf相同。
//在一堆x中寻找a test('xxx'); test('axx'); test('xax'); test('xxa'); test('axa'); test('xaa'); test('aax'); test('aaa');

在编程中,[ ] 中的 | 表示字面字符 |。你可能意思是 [abc] - Markus Jarderot
是的,谢谢你,你说得对,我会修复它,但正则表达式本身并不重要... - Pat
我发现一个更简单有效的方法是只使用string.match(/ [A-Z] /)。如果没有匹配,该方法返回null,否则您将获得一个对象,您可以执行match(/ [A-Z] /)。index以获取第一个大写字母的索引。 - Syler
对于那些对各种解决方案的性能感兴趣的人,请访问以下链接:https://jsperf.app/reguqe/2 - undefined
22个回答

235

String构造函数的实例具有.search()方法,该方法接受一个正则表达式并返回第一个匹配项的索引。

要从特定位置开始搜索(模拟.indexOf()的第二个参数),可以使用slice切掉前面的i个字符:

str.slice(i).search(/re/)

但是这将获取较短字符串中的索引(在切掉第一部分后),因此如果返回的索引不是-1,您将需要添加被切去的部分长度(i)到返回的索引中。这将给出原始字符串中的索引:

function regexIndexOf(text, re, i) {
    var indexInSuffix = text.slice(i).search(re);
    return indexInSuffix < 0 ? indexInSuffix : indexInSuffix + i;
}

2
虽然String.search()接受一个正则表达式作为参数,但它不允许我指定第二个参数! - Pat
15
str.substr(i).search(/re/) - Glenn
7
这是一个很好的解决方案,但输出结果有点不同。indexOf会返回从开头算起的数字(无论偏移量为多少),而这个函数会返回从偏移量处开始的位置。因此,为了达到相同的效果,你需要使用类似下面这样的代码:function regexIndexOf(text, offset) { var initial = text.substr(offset).search(/re/); if(initial >= 0) { initial += offset; } return initial; } - gkoberger
如果i未定义(它是可选的),则此代码将无法工作。因此,更加防错的版本需要进行输入检查:regexIndexOf(text, re, i) { let idx = (i && i > 0) ? text.substr(i).search(re) : text.search(re); return idx < 0 ? idx : idx + (i?i:0); } - Stan Sokolov

157
结合已经提到的几种方法(indexOf显然相当简单),我认为以下这些函数可以解决问题:
function regexIndexOf(string, regex, startpos) {
    var indexOf = string.substring(startpos || 0).search(regex);
    return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
}

function regexLastIndexOf(string, regex, startpos) {
    regex = (regex.global) ? regex : new RegExp(regex.source, "g" + (regex.ignoreCase ? "i" : "") + (regex.multiLine ? "m" : ""));
    if(typeof (startpos) == "undefined") {
        startpos = string.length;
    } else if(startpos < 0) {
        startpos = 0;
    }
    var stringToWorkWith = string.substring(0, startpos + 1);
    var lastIndexOf = -1;
    var nextStop = 0;
    var result;
    while((result = regex.exec(stringToWorkWith)) != null) {
        lastIndexOf = result.index;
        regex.lastIndex = ++nextStop;
    }
    return lastIndexOf;
}

更新:编辑了regexLastIndexOf(),现在它似乎模仿了lastIndexOf()。如果它仍然失败或在什么情况下失败,请告诉我。
更新:通过了在此页面评论中找到的所有测试,以及我自己的测试。当然,这并不意味着它是百分之百可靠的。欢迎任何反馈。

2
我认为使用regex.lastIndex = result.index + 1;regex.lastIndex = ++nextStop;更有效率。这样做可以更快地进行下一次匹配,而且不会丢失任何结果。 - Gedrox
@Gedrox 是的,我认为如果没有你的建议,它的时间复杂度是二次的,但如果RegExp足够短,则可以具有线性复杂度。 - user1537366
如果字符串包含多个JSON对象或多个部分符合正则表达式,该怎么办? - TeraTon
2
如果您愿意从npm中提取它,这两个实用程序函数现在可以在https://www.npmjs.com/package/index-of-regex中作为npm包使用。 - Capaj
这里的重点似乎是在regexLastIndexOf上,但是regexIndexOf也应该使用RegExp::lastIndex,否则像/^./这样的正则表达式将会匹配任何地方,并且回溯断言在边界上也无法工作。 - Codesmith
显示剩余8条评论

54

我有一个简短的版本给你。它对我很有效!

var match      = str.match(/[abc]/gi);
var firstIndex = str.indexOf(match[0]);
var lastIndex  = str.lastIndexOf(match[match.length-1]);

如果您需要原型版本:

String.prototype.indexOfRegex = function(regex){
  var match = this.match(regex);
  return match ? this.indexOf(match[0]) : -1;
}

String.prototype.lastIndexOfRegex = function(regex){
  var match = this.match(regex);
  return match ? this.lastIndexOf(match[match.length-1]) : -1;
}

编辑:如果您想为 fromIndex 添加支持

String.prototype.indexOfRegex = function(regex, fromIndex){
  var str = fromIndex ? this.substring(fromIndex) : this;
  var match = str.match(regex);
  return match ? str.indexOf(match[0]) + fromIndex : -1;
}

String.prototype.lastIndexOfRegex = function(regex, fromIndex){
  var str = fromIndex ? this.substring(0, fromIndex) : this;
  var match = str.match(regex);
  return match ? str.lastIndexOf(match[match.length-1]) : -1;
}

使用它就像这样简单:

var firstIndex = str.indexOfRegex(/[abc]/gi);
var lastIndex  = str.lastIndexOfRegex(/[abc]/gi);

这实际上是一个不错的技巧。如果您将其扩展为像indexOflastIndexOf一样接受startIndex参数,那就太棒了。 - Robert Koritnik
@RobertKoritnik - 我编辑了我的回答,以支持 startIndex(或 fromIndex)。希望能有所帮助! - pmrotule
2
你的算法在以下情况下会出现问题:"aRomeo Romeo".indexOfRegex(new RegExp("\\bromeo", 'gi')); 结果将是1,而不是7,因为indexOf将查找“romeo”第一次出现的位置,无论它是否出现在单词的开头。 - Coral Kashri
非常棒的技巧。为了处理CoralK所提到的情况,您可以将indexOfRegex的返回语句替换为:if(match){let list=this.split(regex);match.pop();list.pop();return match.join('').length+list.join('').length+(fromIndex||0);}else return -1; - yorg
lastIndexOfRegex 无法使用字符范围。 let s ='alpha beta(gamma)',p = lastIndexOfRegex(s,new RegExp('[()]')),correct =(p == s.length-1); 它查找提供的正则表达式的第一个匹配项的最后一个实例的索引。这与正则表达式的最后一个匹配项的索引不同。 - Cheeso
显示剩余3条评论

10

使用:

str.search(regex)

查看这里的文档。


21
@OZZIE说:不是很准确。这基本上是Glenn的答案(获得了约150个赞),只是完全 没有解释,不支持起始位置为非0,而且是发布于七年之后。 - ccjmne

7
你可以使用 substr 函数。
str.substr(i).match(/[abc]/);

来自O'Reilly出版的著名JavaScript书籍:“substr未被ECMAScript标准化,因此已被弃用。”但我喜欢你所表达的基本思想。 - Jason Bunting
1
这不是问题。如果你真的很关心它,可以使用String.substring()代替——你只需要稍微做一些数学运算即可。此外,JavaScript不应该完全受制于其父语言。 - Peter Bailey
这并不是一个无关紧要的问题 - 如果你的代码在一个实现中运行,而该实现没有实现substr,因为他们想要遵守ECMAScript标准,那么你将会遇到问题。当然,用substring替换它并不难,但意识到这一点是很好的。 - Jason Bunting
1
当你遇到问题时,通常有非常简单的解决方案。我认为评论是明智的,但是负评却有点吹毛求疵。 - VoronoiPotato
请问您能否编辑您的答案,提供一个可运行的演示代码? - vsync

7
基于BaileyP的答案。主要区别在于这些方法如果无法匹配模式,则返回-1编辑:由于Jason Bunting的回答,我得到了一个想法。为什么不修改正则表达式的.lastIndex属性呢?但是这只适用于带有全局标志 (/g) 的模式。 编辑:更新以通过测试用例。
String.prototype.regexIndexOf = function(re, startPos) {
    startPos = startPos || 0;

    if (!re.global) {
        var flags = "g" + (re.multiline?"m":"") + (re.ignoreCase?"i":"");
        re = new RegExp(re.source, flags);
    }

    re.lastIndex = startPos;
    var match = re.exec(this);

    if (match) return match.index;
    else return -1;
}

String.prototype.regexLastIndexOf = function(re, startPos) {
    startPos = startPos === undefined ? this.length : startPos;

    if (!re.global) {
        var flags = "g" + (re.multiline?"m":"") + (re.ignoreCase?"i":"");
        re = new RegExp(re.source, flags);
    }

    var lastSuccess = -1;
    for (var pos = 0; pos <= startPos; pos++) {
        re.lastIndex = pos;

        var match = re.exec(this);
        if (!match) break;

        pos = match.index;
        if (pos <= startPos) lastSuccess = pos;
    }

    return lastSuccess;
}

到目前为止,这似乎是最有希望的(经过一些语法修复):-) 只在边缘情况下失败了一些测试。像'axx'.lastIndexOf('a',0)!='axx'.regexLastIndexOf(/ a /,0)之类的事情...我正在研究它,看看是否能够解决那些情况。 - Pat

6
RexExp 实例已经拥有一个名为 lastIndex 的属性(如果它们是全局的),所以我正在复制正则表达式,并稍微修改来适应我们的目的,在字符串上运行 exec 函数并查看 lastIndex。这比在字符串上循环要快得多。(你已经有足够的示例将其应用于字符串原型,对吧?)
function reIndexOf(reIn, str, startIndex) {
    var re = new RegExp(reIn.source, 'g' + (reIn.ignoreCase ? 'i' : '') + (reIn.multiLine ? 'm' : ''));
    re.lastIndex = startIndex || 0;
    var res = re.exec(str);
    if(!res) return -1;
    return re.lastIndex - res[0].length;
};

function reLastIndexOf(reIn, str, startIndex) {
    var src = /\$$/.test(reIn.source) && !/\\\$$/.test(reIn.source) ? reIn.source : reIn.source + '(?![\\S\\s]*' + reIn.source + ')';
    var re = new RegExp(src, 'g' + (reIn.ignoreCase ? 'i' : '') + (reIn.multiLine ? 'm' : ''));
    re.lastIndex = startIndex || 0;
    var res = re.exec(str);
    if(!res) return -1;
    return re.lastIndex - res[0].length;
};

reIndexOf(/[abc]/, "tommy can eat");  // Returns 6
reIndexOf(/[abc]/, "tommy can eat", 8);  // Returns 11
reLastIndexOf(/[abc]/, "tommy can eat"); // Returns 11

您可以将函数原型化到RegExp对象上:
RegExp.prototype.indexOf = function(str, startIndex) {
    var re = new RegExp(this.source, 'g' + (this.ignoreCase ? 'i' : '') + (this.multiLine ? 'm' : ''));
    re.lastIndex = startIndex || 0;
    var res = re.exec(str);
    if(!res) return -1;
    return re.lastIndex - res[0].length;
};

RegExp.prototype.lastIndexOf = function(str, startIndex) {
    var src = /\$$/.test(this.source) && !/\\\$$/.test(this.source) ? this.source : this.source + '(?![\\S\\s]*' + this.source + ')';
    var re = new RegExp(src, 'g' + (this.ignoreCase ? 'i' : '') + (this.multiLine ? 'm' : ''));
    re.lastIndex = startIndex || 0;
    var res = re.exec(str);
    if(!res) return -1;
    return re.lastIndex - res[0].length;
};


/[abc]/.indexOf("tommy can eat");  // Returns 6
/[abc]/.indexOf("tommy can eat", 8);  // Returns 11
/[abc]/.lastIndexOf("tommy can eat"); // Returns 11

我正在修改RegExp的方式的简要说明:对于indexOf,我只需要确保全局标志被设置。对于lastIndexOf,我使用负向先行断言来查找最后一个出现的位置,除非RegExp已经匹配到了字符串的末尾。

在开头有一个"RexExp"的拼写错误。 - Nirvana

4

它本身不具备此功能,但您可以添加这个功能。

<script type="text/javascript">

String.prototype.regexIndexOf = function( pattern, startIndex )
{
    startIndex = startIndex || 0;
    var searchResult = this.substr( startIndex ).search( pattern );
    return ( -1 === searchResult ) ? -1 : searchResult + startIndex;
}

String.prototype.regexLastIndexOf = function( pattern, startIndex )
{
    startIndex = startIndex === undefined ? this.length : startIndex;
    var searchResult = this.substr( 0, startIndex ).reverse().regexIndexOf( pattern, 0 );
    return ( -1 === searchResult ) ? -1 : this.length - ++searchResult;
}

String.prototype.reverse = function()
{
    return this.split('').reverse().join('');
}

// Indexes 0123456789
var str = 'caabbccdda';

alert( [
        str.regexIndexOf( /[cd]/, 4 )
    ,   str.regexLastIndexOf( /[cd]/, 4 )
    ,   str.regexIndexOf( /[yz]/, 4 )
    ,   str.regexLastIndexOf( /[yz]/, 4 )
    ,   str.lastIndexOf( 'd', 4 )
    ,   str.regexLastIndexOf( /d/, 4 )
    ,   str.lastIndexOf( 'd' )
    ,   str.regexLastIndexOf( /d/ )
    ]
);

</script>

我没有完全测试这些方法,但到目前为止它们似乎是有效的。


已更新以处理这些情况。 - Peter Bailey
每次我准备接受这个答案时,我都会发现一个新的情况!这些情况给出不同的结果! 警告([str.lastIndexOf(/[d]/, 4), str.regexLastIndexOf(/[d]/, 4)]); - Pat
当然了 - str.lastIndexOf会对模式进行类型转换,将其转换为字符串。字符串“/[d]/”肯定不会在输入中找到,因此返回的-1实际上是准确的。 - Peter Bailey
明白了。在阅读了String.lastIndexOf()的规范之后,我误解了该参数的工作方式。这个新版本应该可以处理它。 - Peter Bailey
我刚刚在问题中添加了测试函数...这个测试失败了(其中之一)'axx'.lastIndexOf('a',1) != 'axx'.regexLastIndexOf(/a/,1) - Pat
显示剩余2条评论

3

我需要一个适用于数组的regexIndexOf函数,所以我自己编写了一个。但是我怀疑它是否经过了优化,不过我猜它应该能正常工作。

Array.prototype.regexIndexOf = function (regex, startpos = 0) {
    len = this.length;
    for(x = startpos; x < len; x++){
        if(typeof this[x] != 'undefined' && (''+this[x]).match(regex)){
            return x;
        }
    }
    return -1;
}

arr = [];
arr.push(null);
arr.push(NaN);
arr[3] = 7;
arr.push('asdf');
arr.push('qwer');
arr.push(9);
arr.push('...');
console.log(arr);
arr.regexIndexOf(/\d/, 4);

2
Jason Bunting的regexIndexOf函数可以更简单地进行反转,并且仍然支持UTF8字符,只需按照以下方式操作:
function regexLastIndexOf(string, regex, startpos=0) {
    return text.length - regexIndexOf([...text].reverse().join(""), regex, startpos) - 1;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接