(建议收藏)手写实现搞一搞:indexOf()知多少?

1,348 阅读2分钟

前言

很多同学在面试中经常会遇到到一些手写实现类的题目。不要慌,跟笔者一起,开始这一系列的学习吧。最好的时间就是现在~ 今天我们先来学习下日常开发中经常用到的indexOf()的实现。过程中,一定要注意多练习、思考、总结!

基本用法

indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。——【w3school定义】

indexOf()比较了解的同学应该知道,其表层特征如下:

  1. String 和 Array 两种数据类型都可以调用该API;
  2. 该接口有两个参数: str.indexOf(searchVal, fromIndex);
  3. 其中,searchVal支持多字符,且对大小写敏感;
  4. 匹配成功,返回首次匹配子内容的索引;
  5. 匹配失败,返回-1;

另外,经过笔者反复测试,更多特征如下:

  1. fromIndex < 0,String则置为0,Array则置为 fromIndex+=searchVal.length,需要区分处理;
  2. fromIndex > searchVal.length,则都返回 -1;
  3. indexOf操作 String时, searchVal不是字符串会被尝试转换为字符串;

好了,目前我们对indexOf()的用法有了相对清晰的了解。接下来实现一番。

String类型

如果是 String类型 数据使用indexOf(),我们通常可以使用两种方式来实现:

  1. 正则表达式
  2. 循环遍历

正则表达式

/**
 * 正则表达式实现
 * @param {*} str
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function sIndexOf(str, searchVal, fromIndex = 0) {
  const len = arr.length;
  if (fromIndex < 0) fromIndex = 0
  if (fromIndex >= len) return -1
  // 定义匹配规则
  let reg = new RegExp(`${searchVal}`, 'g') // 为了支持lastIndex,自定义开始匹配位置,需要开启'g',全局匹配
  // 初始化开始搜索位置
  reg.lastIndex = fromIndex
  // 执行匹配
  let ret = reg.exec(str)
  // console.log(ret)
  // 返回匹配结果
  return ret ? ret.index : -1
}

在实现中,首先对fromIndex做了边界处理。使用正则表达式定义了查找匹配规则,并对开始搜索位置做了初始化,最终通过正则表达式完成查找操作。这种方式相对简洁和高效【推荐】。

循环遍历

/**
 * String实现-2:循环遍历。 需要支持searchVal为多字符串时的匹配。
 *
 * @param {*} str
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function sIndexOf2(str, searchVal, fromIndex = 0) {
  let strLen = str.length
  let searchValLen = (searchVal + '').length
  if (fromIndex < 0) fromIndex = 0
  if (fromIndex >= strLen) return -1
  for (let i = fromIndex; i <= strLen - searchValLen; i++) {
    if (searchVal == str.slice(i, searchValLen + i)) return i
  }
  return -1
}

在实现中,同样需要对fromIndex做边界处理。另外,在循环查找操作时,需要考虑 searchVal 为多字符的情况,上述实现都有体现,大家可以动动手尝试下。

测试用例

根据上面针对String类型的实现,可以通过以下测试用例来验证一下。笔者已完成测试,通过。

/* 
  测试用例
*/
var str = '12345aAxyz'
var arr = [1, 2, 3, 4, 5, 'a', 'A', 'xyz']


console.log('\n###### String indexOf 测试 ######')

// 原生
console.log('正常匹配=>', str.indexOf('2'))  // => 1
console.log('非字符串转字符串=>', str.indexOf(2))  // => 1
console.log('多字符串匹配=>', str.indexOf('xyz'))  // => 7
console.log('大小写敏感=>', str.indexOf('A'))  // => 6
console.log('自定义开始位置=>', str.indexOf(2, 3))  // => -1
console.log('开始位置小于0,则置为0=>', str.indexOf(2, -3))  // => 1
console.log('开始位置>搜索字符串长度,则返回-1=>', str.indexOf(2, 10))  // => -1

// 手写
console.log('\n####### String sIndexOf 测试 #######')

console.log('正常匹配=>', sIndexOf(str, '2')) // => 1
console.log('非字符串转字符串=>', sIndexOf(str, 2)) // => 1
console.log('多字符串匹配=>', sIndexOf(str, 'xyz'))  // => 7
console.log('大小写敏感=>', sIndexOf(str, 'A')) // => 6
console.log('自定义开始位置=>', sIndexOf(str, 2, 3)) // => -1
console.log('开始位置小于0,则置为0=>', sIndexOf(str, 2, -3)) // => 1
console.log('开始位置>搜索字符串长度,则返回-1=>', sIndexOf(str, 2, 10)) // => -1

// 手写
console.log('\n####### String sIndexOf2 测试 #######')

console.log('正常匹配=>', sIndexOf2(str, '2')) // => 1
console.log('非字符串转字符串=>', sIndexOf2(str, 2)) // => 1
console.log('多字符串匹配=>', sIndexOf2(str, 'xyz'))  // => 7
console.log('大小写敏感=>', sIndexOf2(str, 'A')) // => 6
console.log('自定义开始位置=>', sIndexOf2(str, 2, 3)) // => -1
console.log('开始位置小于0,则置为0=>', sIndexOf2(str, 2, -3)) // => 1
console.log('开始位置>搜索字符串长度,则返回-1=>', sIndexOf2(str, 2, 10)) // => -1

Array类型

循环遍历

/**
 * Array实现: 循环遍历
 *
 * @param {*} arr
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function aIndexOf(arr, searchVal, fromIndex = 0) {
  const len = arr.length;
  if (fromIndex < 0) fromIndex += len
  if (fromIndex >= len) return -1
  for (let i = fromIndex; i < len; i++) {
    if (arr[i] === searchVal) return i
  }
  return -1
}

在实现中,值得注意的是对 fromIndex 的边界处理略有不同,根据本文开头对原生 indexOf() 的特征描述。当Array类型数据调用时,fromIndex < 0的情况下,fromIndex 最终会从末尾计数,即:fromIndex += len。最后,通过循环遍历完成查找操作。

测试用例

根据上面针对Array类型的实现,可以通过以下测试用例来验证一下。笔者已完成测试,通过。

// 原生
console.log('\n####### Array indexOf 测试 #######')

console.log(arr.indexOf(2)) // 1
console.log(arr.indexOf('2'))// -1
console.log(arr.indexOf('xyz')) // 7
console.log(arr.indexOf('A')) // 6
console.log(arr.indexOf(2, 3)) // -1
console.log(arr.indexOf(2, -3)) // -1

// 手写
console.log('\n####### Array aIndexOf 测试 #######')

console.log(aIndexOf(arr, 2))// 1
console.log(aIndexOf(arr, '2'))// -1
console.log(aIndexOf(arr, 'xyz'))// 7
console.log(aIndexOf(arr, 'A'))// 6
console.log(aIndexOf(arr, 2, 3))// -1
console.log(aIndexOf(arr, 2, -3))//-1

合并

实现

至此,上面分别针对 String 和 Array 两种不同类型的数据使用 indexOf()的情况做了详细实现。接下来,需要将两者合并为统一的接口来对外提供调用服务。代码如下:

String.prototype._indexOf = Array.prototype._indexOf = function (searchVal, fromIndex) {
  let data = this
  let isArray = Array.isArray(data)
  let isString = Object.prototype.toString.call(data) == '[object String]'
  if (!isArray && !isString) throw new TypeError('String or Array')
  if (isArray) return aIndexOf(data, searchVal, fromIndex)
  if (isString) return sIndexOf(data, searchVal, fromIndex)
}

在实现中,将以上逻辑封装在了 _indexOf() 方法类,并分别挂载到了 StringArray 的原型上。

测试用例

根据上面的合并实现,可以通过以下测试用例来验证一下。笔者已完成测试,通过。

// 原生
console.log('\n####### 原生 indexOf() 测试 #######')

console.log(arr.indexOf(2)) // 1
console.log(arr.indexOf('2'))// -1
console.log(arr.indexOf('xyz')) // 7
console.log(arr.indexOf('A')) // 6
console.log(arr.indexOf(2, 3)) // -1
console.log(arr.indexOf(2, -3)) // -1


// 手写
console.log('\n####### 手写 _indexOf() 测试 #######')

console.log(arr._indexOf(2)) // 1
console.log(arr._indexOf('2'))// -1
console.log(arr._indexOf('xyz')) // 7
console.log(arr._indexOf('A')) // 6
console.log(arr._indexOf(2, 3)) // -1
console.log(arr._indexOf(2, -3)) // -1

完整代码

/**
 * String实现-1:正则表达式实现
 * 
 * @param {*} str
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function sIndexOf(str, searchVal, fromIndex = 0) {
  const len = arr.length;
  if (fromIndex < 0) fromIndex = 0
  if (fromIndex >= len) return -1
  // 定义匹配规则
  let reg = new RegExp(`${searchVal}`, 'g') // 为了支持lastIndex,自定义开始匹配位置,需要开启'g',全局匹配
  // 初始化开始搜索位置
  reg.lastIndex = fromIndex
  // 执行匹配
  let ret = reg.exec(str)
  // console.log(ret)
  // 返回匹配结果
  return ret ? ret.index : -1
}

/**
 * String实现-2:循环遍历。 需要支持searchVal为多字符串时的匹配。
 *
 * @param {*} str
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function sIndexOf2(str, searchVal, fromIndex = 0) {
  let strLen = str.length
  let searchValLen = (searchVal + '').length
  if (fromIndex < 0) fromIndex = 0
  if (fromIndex >= strLen) return -1
  for (let i = fromIndex; i <= strLen - searchValLen; i++) {
    if (searchVal == str.slice(i, searchValLen + i)) return i
  }
  return -1
}

/**
 * Array实现: 循环遍历
 *
 * @param {*} arr
 * @param {*} searchVal
 * @param {number} [fromIndex=0]
 * @returns
 */
function aIndexOf(arr, searchVal, fromIndex = 0) {
  const len = arr.length;
  if (fromIndex < 0) fromIndex += len
  if (fromIndex >= len) return -1
  for (let i = fromIndex; i < len; i++) {
    if (arr[i] === searchVal) return i
  }
  return -1
}

// 最终实现
String.prototype._indexOf = Array.prototype._indexOf = function (searchVal, fromIndex) {
  let data = this
  let isArray = Array.isArray(data)
  let isString = Object.prototype.toString.call(data) == '[object String]'
  if (!isArray && !isString) throw new TypeError('String or Array')
  if (isArray) return aIndexOf(data, searchVal, fromIndex)
  if (isString) return sIndexOf(data, searchVal, fromIndex)
}

小结

本文从 indexOf() 的使用出发,了解了它的一些表层和隐藏的特性。并针对不同数据类型的特殊处理方式,分别来根据特性来时手写实现indexOf。其中涉及到到 正则表达式、原型、边界值处理、严谨性判断等细节,想要想全写全还是要费点精力哒~ 建议大家动手写写,相信一定会有更加深刻的体会。respect~~~

交流

如果觉得本文对你有帮助,点赞关注不失联,你的支持是对笔者最大的鼓励!

微信关注 「 乘风破浪大前端 」公众号,发现更多有趣好玩的前端知识和实战。

干货系列文章汇总如下,欢迎 startfollow 交流学习👏🏻。

github.com/sggmico/fe-…

关于本文如有任何意见或建议,欢迎评论区讨论、指正。

也许你还想看:

  1. 【Vue2.0源码系列】:响应式原理
  2. 【Vue2.0源码系列】:computed vs methods
  3. 【讲真】:Vue图片懒加载怎么做?
  4. 【专题实战】:带你彻底搞懂BFC及其应用

👇可以直接点击 「阅读原文」,获取最新更新、发现更多有趣好用的前端知识和实战。

欢迎加入乘风破浪技术交流群,内推、摸鱼、求助皆可。