如何检查一个字符串是否包含一个子字符串

在这里插入图片描述

在现代 Web 前端开发中,字符串处理是日常编码中最频繁的操作之一。无论是表单验证、搜索功能实现、日志分析还是动态内容渲染,判断一个字符串是否包含特定子字符串(Substring)都是不可或缺的基础能力。作为资深前端开发专家,掌握多种字符串包含性检测方法的底层机制、性能特征、兼容性差异以及最佳实践,对于构建高效、健壮且可维护的应用至关重要。本文将系统性地剖析 JavaScript 中判断子字符串存在的各种技术路径,结合实际开发场景,深入探讨其原理、实现细节与工程化应用。


基本概念与核心原理

在 JavaScript 中,字符串(String)是不可变的原始类型或 String 对象。判断子字符串存在性的操作本质上是在目标字符串中搜索指定模式(Pattern)的匹配过程。该操作的时间复杂度通常为 O(n*m),其中 n 是主字符串长度,m 是子字符串长度,尽管现代 JavaScript 引擎通过优化算法(如 Boyer-Moore 或其变种)显著提升了性能。

ECMAScript 标准为字符串提供了多种内置方法来执行包含性检查,每种方法在语义、返回值、兼容性和使用场景上都有所不同。开发者应根据具体需求选择最合适的方案,避免因误用导致逻辑错误或性能瓶颈。


示例一:使用 includes() 方法进行精确匹配

String.prototype.includes() 是 ES2015(ES6)引入的现代方法,用于判断一个字符串是否包含指定的子字符串,返回布尔值 truefalse。它是目前最推荐、语义最清晰的包含性检查方式。

// scripts/string-includes.js

/**
 * 检查字符串是否包含子串(区分大小写)
 * @param {string} str - 主字符串
 * @param {string} searchValue - 要搜索的子字符串
 * @param {number} [position=0] - 搜索起始位置
 * @returns {boolean} - 是否包含
 */
function containsSubstring(str, searchValue, position = 0) {
  // 输入验证
  if (typeof str !== 'string' || typeof searchValue !== 'string') {
    console.warn('Both arguments must be strings');
    return false;
  }

  // 使用 includes 方法
  return str.includes(searchValue, position);
}

// 基本用法示例
const sentence = 'The quick brown fox jumps over the lazy dog';
const searchTerm = 'fox';

console.log(containsSubstring(sentence, searchTerm)); // true
console.log(containsSubstring(sentence, 'Fox')); // false (区分大小写)
console.log(containsSubstring(sentence, 'the')); // true (出现在 "the lazy dog")
console.log(containsSubstring(sentence, 'jumps', 20)); // true (从索引 20 开始搜索)

// 实际应用场景:搜索过滤
class SearchFilter {
  constructor(items) {
    this.items = items; // 数组,元素为对象或字符串
  }

  /**
   * 根据关键词过滤项目
   * @param {string} keyword - 搜索关键词
   * @param {string} [field='name'] - 对象中的字段名(如果是对象数组)
   * @returns {Array} - 匹配的项目
   */
  filterByKeyword(keyword) {
    if (!keyword) return this.items;

    return this.items.filter(item => {
      const text = typeof item === 'string' ? item : item.name;
      return text.includes(keyword);
    });
  }

  /**
   * 高亮匹配文本(返回 HTML 字符串)
   * @param {string} keyword
   * @param {string} text
   * @returns {string} - 带高亮标签的 HTML
   */
  highlightMatch(keyword, text) {
    if (!keyword || !text.includes(keyword)) return text;

    const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 转义正则特殊字符
    const regex = new RegExp(`(${escapedKeyword})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
  }
}

// 使用示例
const products = [
  { id: 1, name: 'Laptop Pro', category: 'Electronics' },
  { id: 2, name: 'Wireless Mouse', category: 'Accessories' },
  { id: 3, name: 'Mechanical Keyboard', category: 'Accessories' }
];

const filter = new SearchFilter(products);
const results = filter.filterByKeyword('Wireless');
console.log(results); // [{ id: 2, name: 'Wireless Mouse', ... }]

const highlighted = filter.highlightMatch('Wireless', 'Find the Wireless Mouse here');
console.log(highlighted); // Find the <mark>Wireless</mark> Mouse here

示例二:使用 indexOf() 方法进行位置索引判断

String.prototype.indexOf() 是一个更早的 ECMAScript 方法,它返回子字符串在主字符串中首次出现的索引位置,如果未找到则返回 -1。通过检查返回值是否不等于 -1,可以判断子字符串是否存在。该方法兼容性极佳,适用于需要支持老旧浏览器的项目。

// scripts/string-indexof.js

/**
 * 使用 indexOf 判断子字符串存在性
 * @param {string} str
 * @param {string} searchValue
 * @param {number} [fromIndex=0]
 * @returns {boolean}
 */
function hasSubstring(str, searchValue, fromIndex = 0) {
  if (typeof str !== 'string' || typeof searchValue !== 'string') {
    throw new TypeError('Arguments must be strings');
  }

  return str.indexOf(searchValue, fromIndex) !== -1;
}

// 性能对比测试
function performanceTest() {
  const longString = 'a'.repeat(10000) + 'xyz' + 'b'.repeat(10000);
  const searchValue = 'xyz';
  const iterations = 10000;

  console.time('includes() performance');
  for (let i = 0; i < iterations; i++) {
    longString.includes(searchValue);
  }
  console.timeEnd('includes() performance');

  console.time('indexOf() !== -1 performance');
  for (let i = 0; i < iterations; i++) {
    longString.indexOf(searchValue) !== -1;
  }
  console.timeEnd('indexOf() !== -1 performance');
}

// performanceTest(); // Uncomment to run performance test

// 复杂应用场景:多关键词匹配
function multiKeywordSearch(text, keywords, matchAll = false) {
  if (!Array.isArray(keywords) || keywords.length === 0) {
    return false;
  }

  if (matchAll) {
    // 必须包含所有关键词
    return keywords.every(keyword => hasSubstring(text, keyword));
  } else {
    // 包含任意一个关键词
    return keywords.some(keyword => hasSubstring(text, keyword));
  }
}

// 使用示例
const article = 'JavaScript is a versatile programming language used for web development';
const techKeywords = ['JavaScript', 'web', 'Python'];

console.log(multiKeywordSearch(article, techKeywords, false)); // true (包含 JavaScript 和 web)
console.log(multiKeywordSearch(article, techKeywords, true)); // false (不包含 Python)

// 查找所有匹配位置
function findAllOccurrences(str, searchValue) {
  const indices = [];
  let index = str.indexOf(searchValue);

  while (index !== -1) {
    indices.push(index);
    index = str.indexOf(searchValue, index + 1); // 从下一个位置继续搜索
  }

  return indices;
}

const text = 'She sells seashells by the seashore';
const positions = findAllOccurrences(text, 'se');
console.log(positions); // [4, 9, 17, 29, 33]

示例三:使用正则表达式 RegExp.test() 进行模式匹配

当需要更复杂的匹配逻辑(如忽略大小写、匹配多个变体、使用通配符)时,正则表达式(Regular Expression)提供了强大的解决方案。RegExp.prototype.test() 方法执行搜索并返回布尔值,非常适合包含性检查。

// scripts/string-regex.js

/**
 * 使用正则表达式检查子字符串(支持忽略大小写等选项)
 * @param {string} str - 主字符串
 * @param {string|RegExp} pattern - 搜索模式
 * @param {Object} [options={}] - 配置选项
 * @param {boolean} [options.caseSensitive=true] - 是否区分大小写
 * @param {boolean} [options.wholeWord=false] - 是否匹配完整单词
 * @returns {boolean} - 是否匹配
 */
function matchesPattern(str, pattern, options = {}) {
  const { caseSensitive = true, wholeWord = false } = options;

  // 构建正则表达式
  let flags = caseSensitive ? 'g' : 'gi'; // i 标志表示忽略大小写
  let source;

  if (pattern instanceof RegExp) {
    source = pattern.source;
    // 合并 flags(注意:不能直接修改 RegExp 实例)
    flags = (caseSensitive ? '' : 'i') + (pattern.flags.includes('g') ? 'g' : '');
  } else {
    // 转义特殊字符
    source = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    if (wholeWord) {
      source = `\\b${source}\\b`; // \b 表示单词边界
    }
  }

  const regex = new RegExp(source, flags);
  return regex.test(str);
}

// 基本用法
const sentence = 'The QUICK Brown fox jumps quickly';
console.log(matchesPattern(sentence, 'quick')); // false (区分大小写)
console.log(matchesPattern(sentence, 'quick', { caseSensitive: false })); // true
console.log(matchesPattern(sentence, 'quick', { caseSensitive: false, wholeWord: true })); // false ("quickly" 包含 "quick" 但不是完整单词)
console.log(matchesPattern(sentence, 'fox', { wholeWord: true })); // true

// 复杂模式匹配
function advancedSearch(text, query) {
  // 支持简单的通配符:* 匹配任意字符序列,? 匹配单个字符
  const escapedQuery = query.replace(/[.+^${}()|[\]\\]/g, '\\$&');
  const regexSource = '^' + escapedQuery.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
  const regex = new RegExp(regexSource, 'i');

  return regex.test(text);
}

console.log(advancedSearch('filename.txt', '*.txt')); // true
console.log(advancedSearch('data1.csv', 'data?.csv')); // true
console.log(advancedSearch('data12.csv', 'data?.csv')); // false

// 使用预编译正则表达式提升性能(适用于高频调用)
const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const DOMAIN_PATTERN = /(?:^|\s)(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.[a-zA-Z]{2,})(?:$|\s)/gi;

function containsValidEmail(str) {
  return EMAIL_PATTERN.test(str);
}

function extractDomains(str) {
  const matches = [];
  let match;
  while ((match = DOMAIN_PATTERN.exec(str)) !== null) {
    matches.push(match[3]); // 提取域名部分
  }
  return matches;
}

console.log(containsValidEmail('contact@example.com')); // true
console.log(extractDomains('Visit https://www.google.com or http://github.io')); // ['google.com', 'github.io']

示例四:使用 search() 方法进行正则匹配

String.prototype.search() 方法与 indexOf() 类似,但它接受正则表达式作为参数,并返回匹配的索引位置或 -1。虽然它主要用于获取位置,但同样可以用于存在性检查。

// scripts/string-search.js

/**
 * 使用 search 方法检查正则匹配
 * @param {string} str
 * @param {RegExp} regex
 * @returns {boolean}
 */
function hasRegexMatch(str, regex) {
  if (!(regex instanceof RegExp)) {
    throw new TypeError('Second argument must be a RegExp object');
  }

  return str.search(regex) !== -1;
}

// 忽略大小写搜索
function caseInsensitiveContains(str, substring) {
  const regex = new RegExp(substring.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
  return hasRegexMatch(str, regex);
}

const text = 'Hello WORLD from JavaScript';
console.log(caseInsensitiveContains(text, 'world')); // true
console.log(caseInsensitiveContains(text, 'java')); // true

// 匹配数字序列
function containsNumberSequence(str, sequence) {
  const regex = new RegExp(sequence);
  return hasRegexMatch(str, regex);
}

console.log(containsNumberSequence('Order #12345', '123')); // true
console.log(containsNumberSequence('Version 2.0.1', '2.0')); // true (注意:. 在正则中是通配符)

// 修正:精确匹配数字序列(需要转义)
function containsExactNumberSequence(str, sequence) {
  const escapedSequence = sequence.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const regex = new RegExp(escapedSequence);
  return hasRegexMatch(str, regex);
}

console.log(containsExactNumberSequence('Version 2.0.1', '2.0')); // true (现在正确匹配)

// 性能考量:search vs test
// search() 会返回索引,内部需要执行完整的搜索过程
// test() 只需判断是否存在,可能在某些引擎中更高效
// 但从实际测试看,差异通常不明显

示例五:自定义算法与性能优化策略

在极端性能要求或特殊需求场景下,可能需要实现自定义的字符串搜索算法。虽然标准库方法已高度优化,但了解底层原理有助于应对特定挑战。

// scripts/string-custom.js

/**
 * Boyer-Moore-Horspool 算法的简化实现(用于演示)
 * 适用于较长的模式和文本
 */
class BoyerMooreHorspool {
  constructor(pattern) {
    this.pattern = pattern;
    this.patternLength = pattern.length;
    this.badCharShift = this.buildBadCharTable();
  }

  buildBadCharTable() {
    const table = {};
    // 对于模式中每个字符,记录其在模式中最后一次出现的位置
    for (let i = 0; i < this.patternLength - 1; i++) {
      table[this.pattern[i]] = this.patternLength - 1 - i;
    }
    // 默认移动距离为模式长度
    return table;
  }

  search(text) {
    const textLength = text.length;
    let textIndex = this.patternLength - 1;

    while (textIndex < textLength) {
      let patternIndex = this.patternLength - 1;
      let textOffset = textIndex;

      // 从模式末尾开始比较
      while (patternIndex >= 0 && text[textOffset] === this.pattern[patternIndex]) {
        patternIndex--;
        textOffset--;
      }

      if (patternIndex === -1) {
        return true; // 找到匹配
      } else {
        // 使用坏字符规则移动
        const badChar = text[textIndex];
        const shift = this.badCharShift[badChar] || this.patternLength;
        textIndex += shift;
      }
    }

    return false; // 未找到
  }
}

// 使用自定义算法
const bmh = new BoyerMooreHorspool('performance');
console.log(bmh.search('Optimizing JavaScript performance is critical')); // true

// 高频调用优化:缓存正则表达式
class PatternMatcher {
  constructor() {
    this.cache = new Map();
  }

  /**
   * 缓存正则表达式实例
   * @param {string} pattern
   * @param {string} flags
   * @returns {RegExp}
   */
  getRegex(pattern, flags = '') {
    const cacheKey = `${pattern}/${flags}`;
    if (!this.cache.has(cacheKey)) {
      this.cache.set(cacheKey, new RegExp(pattern, flags));
    }
    return this.cache.get(cacheKey);
  }

  /**
   * 使用缓存的正则表达式进行搜索
   * @param {string} str
   * @param {string} pattern
   * @param {string} [flags]
   * @returns {boolean}
   */
  test(str, pattern, flags) {
    const regex = this.getRegex(pattern, flags);
    return regex.test(str);
  }
}

const matcher = new PatternMatcher();
console.log(matcher.test('Hello World', 'world', 'i')); // true
console.log(matcher.test('JavaScript', '^Java', '')); // true

// 内存管理:定期清理缓存
setInterval(() => {
  if (matcher.cache.size > 1000) {
    // 简单的 LRU 策略:清空一半
    const entries = Array.from(matcher.cache.entries());
    const half = Math.floor(entries.length / 2);
    matcher.cache = new Map(entries.slice(half));
  }
}, 300000); // 每 5 分钟检查一次

实际开发中的高级技巧与最佳实践

在真实项目中,字符串包含性检查往往与应用架构、性能监控和用户体验紧密相关。应避免在循环或高频事件处理器中重复创建正则表达式,优先使用缓存机制。对于大型文本处理,考虑使用 Web Workers 避免阻塞主线程。

在表单验证中,includes() 通常足够使用,但需注意空字符串的处理:''.includes('') 返回 true,这在某些逻辑中可能需要特别处理。对于国际化应用,简单的忽略大小写可能不足以处理所有语言的变体,应考虑使用 Intl.Collator 进行语言敏感的比较。

性能方面,对于短字符串和简单模式,includes()indexOf() 性能相近且通常优于正则表达式。正则表达式在复杂模式匹配时优势明显,但其编译和执行开销较大,不应滥用。现代 JavaScript 引擎对 includes() 有特殊优化,使其成为首选。

可访问性(Accessibility)角度,当搜索结果影响 DOM 结构时,应通过 aria-live 区域通知屏幕阅读器用户结果数量的变化。使用 data-* 属性存储原始文本,便于后续处理。

最后,安全性考量:避免将用户输入直接拼接到正则表达式中而不进行转义,这可能导致正则表达式拒绝服务(ReDoS)攻击。始终验证和清理输入数据。在处理敏感信息(如密码、令牌)时,避免在日志中记录完整的字符串内容。


欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。


推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DTcode7

客官,赏个铜板吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值