JavaScript:遍历

前言

  将依据自身痛点学习,计划对原生JavaScript写一个系统,本文为第一篇,感兴趣的同窗能够关注我的公众号:ZeroToOneMe,或者github博客,将持续输出。前端

  JavaScript中能够实现遍历的数据类型主要是对象,其中包括普通对象与数组。实现遍历的方式有不少,本文梳理下JavaScript中能实现遍历的方式。java

对象&&数组

for...in

  语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非Symbol的属性。对于每一个不一样的属性,语句都会被执行。git

语法:github

for (variable in object) {
  xxx
}

参数:express

variable:在每次迭代时,将不一样的属性名分配给变量。<br/>
object:被迭代枚举其属性的对象。

  从定义上来看,for...in语句遍历对象和组数都是能够的,可是此方式使用在数组可能会带来一些咱们并不想看到的东西,看下面的实例一:编程

// 实例一
Array.prototype.name = 'fe'
let arr = [10, 20, 30, 40, 50]
arr.str = 'hello'
for(let index in arr) {
  console.log(index) // 0, 1, 2, 3, 4, str, name
}

  上面的实例一输出结果能够看出,直接获得不是数组元素值,而是数组的索引值(键名),同时还有其自定义属性以及其原型链上的属性和方法‘str’,‘name’两项,数组自己也是对象。数组

  即便使用getOwnPropertyNames()或执行hasOwnProperty() 来肯定某属性是不是对象自己的属性能够避免原型链上的属性和方法不输出,但仍是不能避免自定义的属性输出。实例二:缓存

// 实例二
Array.prototype.name = 'fe'
let arr = [10, 20, 30, 40, 50]
arr.str = 'hello'
for(let index in arr) {
  if (arr.hasOwnProperty(index)) {
    console.log(index) //  0, 1, 2, 3, 4, str
  }
}

数组使用for...in遍历,可能会存在如下几个问题:数据结构

  1. 获得的index索引值为字符串,不能直接作几何运算,须要先作数据类型转换处理;
  2. 可能以任意顺序作遍历的,即遍历顺序可能不是按照数组内部顺序;
  3. 会遍历数组全部可枚举的属性,包括原型。

  在必定程度上来看,使用for...in 遍历数组是一个很糟糕的选择,不推荐使用for...in 语句遍历数组,对开发经验欠缺的新人不友好,比较容易踩到坑,会遇到不少意想不到的问题。遍历数组更多的推荐是for...offoreach 这两种方式,下面也会详细的梳理这两种方式。闭包

注意点:

  • for...in比较适合遍历普通对象,遍历获得的结果是对象的键名,其顺序也是无序的,无关顺序。也能够经过break或者return false中断遍历。
  • 在迭代过程当中最好不要在对象上进行添加、修改或者删除属性的操做,除非是对当前正在被访问的属性。
// 实例三
let obj = {
  name: 'fe',
  age: 18
}
for(let key in obj) {
  console.log(key) // name age
}

for...of

  语句在可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象,NodeList 对象等)上建立一个迭代循环,调用自定义迭代钩子,并为每一个不一样属性的值执行语句。该语句是ES6新增遍历方式,功能仍是比较强大的。<br/>

<span>语法:</span>

for (variable of iterable) {
  xxx
}

参数:

variable:在每次迭代中,将不一样属性的值分配给变量。<br/>
iterable:一个具备可枚举属性而且能够迭代的对象。
// 实例四
let arr = [10, 20, 30, 40, 50]
arr.str = 'hello'
for(let value of arr) {
  console.log(value) // 10 20 30 40 50
}

  for...of语句经常使用在遍历数组,从实例四看出,for...of能够直接获得数组索引中的值,可是不会将其实例属性的值返回。缘由在于for...of循环调用遍历器接口,数组的遍历器接口只返回具备数字索引的属性,因此for...of循环不会返回数组arrstr属性。

  话说咱们平时的开发中经常使用for..of遍历数组,从上面的定义来看,for...of不只仅能遍历数组,也能够遍历String(字符串)、MapSet等数据结构,是由于这几个数据结构默认内置了遍历器接口,Iterator接口,即Symbol.iterator方法。

  也就是说有了遍历器接口,数据结构就能够用for...of循环遍历。遍历器(Iterator)是一种接口,为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就能够完成遍历操做。Iterator接口for...of都是ES6提出的特性,Iterator接口也就主要供for...of消费。

// 实例五
let arr = [10, 20, 30, 40, 50]
arr.str = 'hello'
let item = arr[Symbol.iterator]() // 遍历器对象
console.log(item.next()) // { value: 10, done: false }
console.log(item.next()) // { value: 20, done: false }
console.log(item.next()) // { value: 30, done: false }
console.log(item.next()) // { value: 40, done: false }
console.log(item.next()) // { value: 50, done: false }
console.log(item.next()) // { value: undefined, done: true }

  实例五是Iterator的遍历过程,经过手动调用其对象的next()方法实现信息获取。默认的Iterator接口部署在数据结构的Symbol.iterator属性。对于原生部署Iterator接口的数据结构,不用本身写遍历器生成函数,for...of循环会自动遍历。能够参考实例四。

// 实例六
let str = 'abc'
let item = str[Symbol.iterator]()

console.log(item.next()) // { value: 'a', done: false }
console.log(item.next()) // { value: 'b', done: false }
console.log(item.next()) // { value: 'c', done: false }
console.log(item.next()) // { value: undefined, done: true }

for (let item of str) {
  console.log(item) // a b c
}

  实例六为字符串的遍历演示,还有其余几种数据结构(MapSetTypedArrayarguments对象,NodeList对象)原生部署了Iterator接口,能够直接使用for...of完成遍历,在此不在提供代码实例,能够自行测试。

  for..of不能遍历对象,由于对象默认没有部署Iterator接口,没 有默认部署Iterator接口缘由在于对象属性是无序的,哪一个属性先遍历,哪一个属性后遍历没法肯定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。也能够手动为对象部署Iterator接口,看下面的实例七。

// 实例七
let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this
    let index = 0
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true }
        }
      }
    };
  }
}
for (let item of obj) {
  console.log(item) // hello world
}

总结下for..of的几个特征:

  1. 不只能遍历数组,同时也能遍历部署了遍历器接口Iterator接口的数据结构;
  2. 相对比较简洁直接遍历数组的方式;
  3. 避开了for-in循环的全部缺陷。

对象

Object.keys()

  返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用for...in循环遍历该对象时返回的顺序一致。

<span>语法:</span>

Object.keys(obj)

参数:

obj:要返回其枚举自身属性的对象

  返回一个全部元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。

// 实例八
let obj = {
  name: 'fe',
  age: 18
}
let result = Object.keys(obj)
console.log(result) // [ 'name', 'age' ]

注意点:

  • 在ES5里,若是此方法的参数不是对象(而是一个原始值),那么它会抛出 TypeError。在ES6中,非对象的参数将被强制转换为一个对象。
consoloe.log(Object.keys('foo')) // TypeError: "foo" is not an object (ES5)
consoloe.log(Object.keys('foo')) // ["0", "1", "2"] (ES6)

  对象也能够经过和Object.keys(obj)搭配,使用for...of来遍历普通对象的属性。

// 实例九
let obj = {
  name: 'fe',
  age: 18
}
for(let key of  Object.keys(obj)) {
  console.log(obj[key]) // fe 18
}

数组

for循环

  用于建立一个循环,它包含了三个可选的表达式,三个可选的表达式包围在圆括号中并由分号分隔, 后跟一个在循环中执行的语句(一般是一个块语句)。

<span>语法:</span>

for ([initialization]; [condition]; [final-expression]) {
  statement
}

参数:

  • initialization:一个表达式 (包含赋值语句) 或者变量声明;<br/>
  • condition:一个条件表达式被用于肯定每一次循环是否能被执行;<br/>
  • final-expression:每次循环的最后都要执行的表达式;<br/>
  • statement:只要condition的结果为true就会被执行的语句。<br/>`
// 实例十 
let arr = [1, 2, 3]
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i]) // 1 2 3
}

  for循环为编程语言中最原始的一种遍历方式,涵盖了几乎全部的编程语言,在JavaScript中其只能遍历数组,不能遍历对象。实例十是for循环最常规的写法。咱们不注意的话,也比较容易带来本能够避免的几个“问题”,咱们须要注意:

  1. 在for循环中使用var或者未声明的变量,变量做用于全局,会来没必要要的麻烦,使用let声明变量生成块级做用域,或者造成闭包;
  2. 在循环开始以变量的形式缓存下数组长度,能够得到更好的效率,若在循环内部有可能改变数组长度, 请务必慎重处理, 避免数组越界。
// 实例十一
let arr = [1, 2, 3]
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i]) // 1 2 3
}

forEach

  按升序为数组中含有效值的每一项执行一次callback 函数,那些已删除或者未初始化的项将被跳过。

<span>语法:</span>

array.forEach(function(currentValue, index, arr), thisValue)

参数:

  • currentValue(必选):数组当前项的值<br/>
  • index(可选):数组当前项的索引<br/>
  • arr(可选):数组对象自己<br/>
  • thisValue(可选):当执行回调函数时用做 this 的值(参考对象),默认值为undefined<br/>

注意点:

  • 没有办法停止或者跳出forEach循环,除了抛出一个异常。
  • 只能用return退出本次回调,进行下一次回调,并老是返回undefined 值,即便你return了一个值。
  • forEach被调用时,不直接改变调用它的对象,可是对象可能会被callback 改变。

常见规则(一样适用于filtermap方法):

  1. 对于空数组是不会执行回调函数的。
  2. 遍历的范围在第一次调用callback前就会肯定。
  3. 为每一个数组元素执行callback函数。
  4. 函数在执行时,原数组中新增长的元素将不会被callback访问到。
  5. 若是已经存在的值被改变,则传递给callback的值是遍历到他们那一刻的值。
  6. 被删除的元素将不会被访问到。
// 实例十二
let arr = [1, 2, 3, , 5] // 倒数第二个元素为空,不会遍历
let result = arr.forEach((item, index, array) => {
  arr[0] = '修改元素值'
  console.log(item) // 1 2 3 5
})
console.log(arr) // ["修改元素值", 2, 3, , 5]
// 实例十三
let arr = [1, 2, 3, , 5] // 倒数第二个元素为空,不会遍历
let result = arr.forEach((item, index, array) => {
  arr[1] = '修改元素值'
  console.log(item) // 1 修改元素值 3 5
})
console.log(arr) // [1, '修改元素值', 3, , 5]

  实例十二和实例十三都对数组元素值作了修改,然而实例十二中item值没有变化,实例十三中item值变化了,为啥???

  实例十二在执行forEach时,修改了arr[0]的值,数组arr的值发生了改变,而后控制台输出item的值没有变,缘由是仅仅是修改了arr[0]的值,item的值没有变化,控制台输出的item的值依旧仍是以前forEach保存的item的值。

  实例十三在执行forEach时,修改了arr[1]的值,控制台输出item与数组arr的值均发生变化,在第一次遍历的时候,数组arr的第二个元素值已经变化了,在第二次遍历的时候,forEach回调函数的参数值发生了变化,即控制台输出item值发生变化。

  若是已经存在的值被改变,则传递给callback的值是forEach遍历到他们那一刻的值。

// 实例十四
let arr = [1, 2, 3, , 5] // 倒数第二个元素为空,不会遍历
let result = arr.forEach((item, index, array) => {
  arr.push('添加到尾端,不会被遍历') // 调用 forEach 后添加到数组中的项不会被 callback 访问到
  console.log(item) // 1 2 3 5
  return item // return只能结束本次回调 会执行下次回调
  console.log('不会执行,由于return 会执行下一次循环回调')
})
console.log(result) // 即便return了一个值,也仍是返回undefined
// 实例十五
let arr = [1, 2, 3, , 5] // 倒数第二个元素为空,不会遍历
let result = arr.forEach((item, index, array) => {
  if (item === 2) {
    arr.shift()
  }
  console.log(item) // 1 2 5
})

  已删除的项不会被遍历到。若是已访问的元素在迭代时被删除了(例如使用 shift()),以后的元素将被跳过。

filter

  “过滤”,为数组中的每一个元素调用一次callback函数,并利用全部使得callback返回true或等价于true的值的元素建立一个新数组。

<span>语法:</span>

let newArray = arr.filter(function(currentValue, index, arr), thisValue)

参数:

  • currentValue(必选):数组当前项的值;<br/>
  • index(可选):数组当前项的索引;<br/>
  • arr(可选):数组对象自己;<br/>
  • thisValue(可选):当执行回调函数时用做 this 的值(参考对象),默认值为undefined

注意点:

  • 不修改调用它的原数组自己,固然在callback执行时改变原数组另说;
  • callback函数返回值不必定非要是Boolean值,只要是弱等于== true/false也没问题。
// 实例十六
let arr = [1, 2, 3, , 5] // 倒数第二个元素为空,不会遍历
let result = arr.filter(function(item, index, array) {
  arr.push(6) // 在调用 filter 以后被添加到数组中的元素不会被 filter 遍历到
  arr[1] = 4 // 若是已经存在的元素被改变了,则他们传入 callback 的值是 filter 遍历到它们那一刻的值
  console.log(item) // 1 4 3 5
  return item < 5 // 返回数组arr中小于5的元素
})
console.log(result) // [1, 4, 3]
// 实例十七
let arr = [0, 1, 2, 3, 4] 
let result = arr.filter(function(item, index, array) {
  return item // 作类型转换
})
console.log(result) // [1, 2, 3, 4]

map

  建立一个新数组,其结果是该数组中的每一个元素都调用一个提供的函数后返回的结果。其基本用法与forEach相似,不一样在于map的callback须要有return值。

<span>语法:</span>

let newArray = arr.map(function(currentValue, index, arr), thisValue)

参数:

  • currentValue(必选):数组当前项的值
  • index(可选):数组当前项的索引
  • arr(可选):数组对象自己
  • thisValue(可选):当执行回调函数时用做this的值(参考对象),默认值为undefined

注意点:

  • 不修改调用它的原数组自己,固然在callback执行时改变原数组另说。
  • callback函数只会在有值的索引上被调用;那些历来没被赋过值或者使用delete删除的索引则不会被调用。

  map方法的主要做用实际上是对原数组映射产生新数组,看实例十八:

// 实例十八
let arr = [1, 2, 3]
let result = arr.map(function(item, index, array) {
  return item * 2
})
console.log(result) // [2, 4, 6]
console.log(arr) // [1, 2, 3] // 原数组arr没有变

reduce

  对累加器和数组中的每一个元素(从左到右)应用一个函数,最终合并为一个值。

<span>语法:</span>

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

参数:

  • total(必选):累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue
  • currentValue(必选):数组中正在处理的元素
  • currentIndex(可选):数组中正在处理的当前元素的索引 若是提供了initialValue,则起始索引号为0,不然为1
  • arr(可选):调用reduce()的数组
  • initialValue(可选):做为第一次调用callback函数时的第一个参数的值。 若是没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用reduce将报错

注意点:

  • 若是没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。若是提供initialValue,从索引0开始。

<p>
回调函数第一次执行时:
</p>

  • 若是调用reduce()时提供了initialValuetotal取值为initialValuecurrentValue取数组中的第一个值;
  • 若是没有提供initialValue,那么total取数组中的第一个值,currentValue取数组中的第二个值;
  • 若是数组为空且没有提供initialValue,会抛出TypeError
  • 若是数组仅有一个元素(不管位置如何)而且没有提供initialValue, 或者有提供initialValue可是数组为空,那么此惟一值将被返回而且callback不会被执行。
// 实例十九
let sum = [1, 2, 3].reduce(function (total, currentValue, currentIndex) {
  return total + currentValue
})
console.log(sum) // 6

结语

  本文梳理了对象和数组几种比较经常使用的遍历方式。数组的方法有不少,有部份内容本文没有涉及到,例如someeveryreduceRightfindfindIndex等方法,感兴趣的同窗能够自行了解。<br/>
  笔者如今仍是一个前端新人,对遍历的实现方式不太清楚,借此梳理的机会,熟悉相关的实现,文章若有不正确的地方欢迎各位大佬指正,也但愿有幸看到文章的同窗也有收获,一块儿成长!

——本文首发于我的公众号———
图片描述最后,欢迎你们关注个人公众号,一块儿学习交流。

相关文章
相关标签/搜索