Another alternative is KMP (Knuth–Morris–Pratt).
The KMP algorithm searches for a length-m substring in a length-n string in worst-case O(n+m) time, compared to a worst-case of O(n⋅m) for the naive algorithm, so using KMP may be reasonable if you care about worst-case time complexity.
Here's a JavaScript implementation by Project Nayuki, taken from https://www.nayuki.io/res/knuth-morris-pratt-string-matching/kmp-string-matcher.js:
// Searches for the given pattern string in the given text string using the Knuth-Morris-Pratt string matching algorithm. // If the pattern is found, this returns the index of the start of the earliest match in 'text'. Otherwise -1 is returned.
function kmpSearch(pattern, text) { if (pattern.length == 0) return 0; // Immediate match // Compute longest suffix-prefix table var lsp = [0]; // Base case for (var i = 1; i < pattern.length; i++) { var j = lsp[i - 1]; // Start by assuming we're extending the previous LSP while (j > 0 && pattern[i] !== pattern[j]) j = lsp[j - 1]; if (pattern[i] === pattern[j]) j++; lsp.push(j); } // Walk through text string var j = 0; // Number of chars matched in pattern for (var i = 0; i < text.length; i++) { while (j > 0 && text[i] != pattern[j]) j = lsp[j - 1]; // Fall back in the pattern if (text[i] == pattern[j]) { j++; // Next char matched, increment position if (j == pattern.length) return i - (j - 1); } } return -1; // Not found } console.log(kmpSearch('ays', 'haystack') != -1) // true console.log(kmpSearch('asdf', 'haystack') != -1) // false