2018-12-10 | JavaScript | UNLOCK

常用的数据处理方法

数组去重

使用 indexOfincludes

  indexOf(arr = []) {
    const res = []
    arr.forEach((item) => {
      if (res.indexOf(item) === -1) {  //!res.includes(item)
        res.push(item)
      }
    })
    return res
  }

filter

filter(arr = []) {
  return arr.filter((item, index, arr) => {
    return arr.indexOf(item) !== item
  })
}

filter(arr=[]) {
  // arr.concat() 复制出来一份原有的数组,且对复制出来的新数组的操作不会影响到原有数组
  return arr.concat()sort().filter((item,index,arr) => {
    return !item || arr[index - 1] !== item
  })
}

Object 键值对

const array = [{value: 1}, {value: 1}, {value: 2}];

unique(array = []) {
    const obj = {};
    return array.filter(function(item, index, array){
        // console.log(typeof item + JSON.stringify(item))
        return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true)
    })
}

console.log(unique(array)); // [{value: 1}, {value: 2}]

Set 、Map 和 …rest

使用 Set 可以区分出 NAN, 因为 NAN 不等于自身,所以 indexOf 不能查找到,而使用 Set 就可以解决这个问题
注意: 由于{} !== {} // true, 所以对象默认是不重复的,使用 JSON.stringify 可以解决这个问题

setArr(arr = []) {
  // Set 是值的集合,类似于数组,但是每个值都是唯一的
  return [...new Set(arr)]
}

mapArr(arr = []) {
  // Map 是一个键值对集合,先检查有没有这哥属性,没有就添加尽这个集合中
  const seen = new Map(arr)
  return arr.filter(item => !seen.has(a) && seen.set(a,1))
}

数组分组

使用 forEach 遍历分组

const list = [
  { label: 'a', value: 1 },
  { label: 'c', value: 3 },
  { label: 'b', value: 1 },
  { label: 'a', value: 3 },
  { label: 'c', value: 2 }
]
const collect = {}
list.forEach(item => {
  // console.log(item)
  if (!collect[item.label]) {
    collect[item.label] = []
  }
  collect[item.label].push(item)
})

// 处理后的数据
collect = {
  a: [1, 3],
  b: [1],
  c: [3, 2]
}

// 可以使用entries(),keys(),values()来获取所需要的值

使用 30 seconds of codereduce 的方法

const groupBy = (arr, fn) =>
  arr
    .map(typeof fn === 'function' ? fn : val => val[fn])
    .reduce((acc, val, i) => {
      acc[val] = (acc[val] || []).concat(arr[i])
      return acc
    }, {})
const collect = groupBy(list, item => item.label)

浅拷贝和深拷贝

浅拷贝

只是对基本类型进行了拷贝,类似于数组和对象这一种的引用类型的值,只会拷贝其在内存中的引用,依然是通过引用指向同一块堆内存,无论新旧数组发生改变,都会改变内存中值,两者的值同时发生改变

  • 数组自带的方法: sliceconcatArray.from()
    const arr = [1, 2, [3, 4]]
    arr.concat()
    arr.slice()
    Array.from(arr)
    
  • 对象和数组的结构也可以实现浅拷贝
    const [...arr] = [1, 2, [3, 4]]
    const obj = { ...{ a: 1, b: 2, f: { c: 3, d: 4 } } }
    
  • 简单的函数实现
    function clone(o) {
      const obj = {}
      for (let i in o) {
        obj[i] = o[i]
      }
      return obj
    }
    

深拷贝

  1. JSON 字符串化(你不知道的 JavaScript(中))

    工具函数 JSON.stringify() 在将 JSON 对象序列化为字符串时并非严格意义上的强制类型转换,而是使用了 ToString
    所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

    不安全的 JSON 值

    • undefined => null
    • function => null
    • symbol => null
    • 循环引用(对象之间循环引用,形成了无限循环) => 程序报错
    JSON.stringify(undefined) // undefined
    JSON.stringify(function() {}) // undefined
    JSON.stringify([1, undefined, function() {}, 4]) //"[1, null, null, 4]"
    JSON.stringify({ a: 2, b: function() {} }) // "{"a": 2}"
    

    如果队形中定义了 toJSON()方法,JSON 字符串化会首先调用该方法,然后用返回值进行序列化,包含循环引用的对象执行 JSON.stringify(...)会报错
    toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回 一个 JSON 字符串”

    const o = {}
    const a = {
      b: 42,
      c: o,
      d: function() {}
    }
    
    // 在a中创造一个循环引用
    o.e = a
    JSON.stringify(a) //循环引用会报错
    // 自定义JSON序列化
    a.toJSON = () => {
      // 序列化尽包涵 b
      return { b: this.b }
    }
    JSON.stringify(a) // "{"b": 42}"
    

    参数

    • replacer(数组或者函数)
      • 数组:必须是一个字符串数组,其中包含序列化要处理的对象 的属性名称,除此之外其他的属性则被忽略。
      • 函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数(key, value)。如果要忽略某个键就返回 undefined,否则返回指定的值。
    const a = { b: 42, c: '42', d: [1, 2, 3] }
    JSON.stringify(a, ['b', 'c']) // "{"b":42,"c":"42"}"
    JSON.stringify(a, function(k, v) {
      if (k !== 'c') return v
    })
    // "{"b":42,"d":[1,2,3]}"
    
    • space: 指定输出的缩进格式

    简单的深拷贝

    const newObj = JSON.parse(JSON.stringify(oldObj))
    

    缺陷:

    • 无法实现对函数 、RegExp 等特殊对象的克隆
    • 会抛弃对象的 constructor,所有的构造函数会指向 Object
    • 循环引用对象,会报错

2、真正意义上的深拷贝

在拷贝的时候判断一下属性值的类型,如果是对象,面对不同的对象(正则、数组、Date 等)要采用不同的处理方式,我们递归调用深拷贝函数

function deepCopy(obj) {
  if (typeof obj !== 'object') return
  const newObj = obj instanceof Array ? [] : {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
    }
  }
  return newObj
}

更加完善的深拷贝
来自面试官:请你实现一个深克隆

我们需要通过正则的扩展了解到 flags 属性等等,因此我们需要实现一个提取 flags 的函数.

const getRegExp = re => {
  var flags = ''
  if (re.global) flags += 'g'
  if (re.ignoreCase) flags += 'i'
  if (re.multiline) flags += 'm'
  return flags
}
/**
 * deep clone
 * @param  {[type]} parent object 需要进行克隆的对象
 * @return {[type]}        深克隆后的对象
 * classType 类型判断函数
 */
const clone = parent => {
  // 维护两个储存循环引用的数组
  const parents = []
  const children = []

  const _clone = parent => {
    if (parent === null) return null
    if (typeof parent !== 'object') return parent

    let child, proto

    if (classType(parent) === 'array')) {
      // 对数组做特殊处理
      child = []
    } else if (classType(parent) === 'regexp')) {
      // 对正则对象做特殊处理
      child = new RegExp(parent.source, getRegExp(parent))
      if (parent.lastIndex) child.lastIndex = parent.lastIndex
    } else if (classType(parent) === 'date')) {
      // 对Date对象做特殊处理
      child = new Date(parent.getTime())
    } else {
      // 处理对象原型
      proto = Object.getPrototypeOf(parent)
      // 利用Object.create切断原型链
      child = Object.create(proto)
    }

    // 处理循环引用
    const index = parents.indexOf(parent)

    if (index != -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
      return children[index]
    }
    parents.push(parent)
    children.push(child)
    // 递归
    for (let i in parent) {
      //过滤原型上的属性
      if(parent.hasOwnProperty(i)){
        child[i] = _clone(parent[i])
      }
    }

    return child
  }
  return _clone(parent)
}