Lodash中的cloneDeep

写在前面

ECMAScript中我们常说的拷贝分两种:深拷贝和浅拷贝,也有分为值拷贝和引用拷贝。深拷贝、浅拷贝的区分点就是对引用类型的对象进行不同的操作,前者拷贝引用类型执行的实际值(值拷贝),后者只拷贝引用(引用拷贝)。浅拷贝基本上也无需多复杂的实现,语言本身提供的 Object.assign也基本上可以满足日常所需,在 Lodash中,深浅拷贝都在一个 baseClone的方法中得以实现,函数内部根据 isDeep的值做区分。

本文主要探究并适当拓展一下较为复杂的深拷贝的实现方式,浅拷贝暂不讨论

拷贝,说到底就是拷贝数据。数据类型一般也就分为两种值类型和引用类型,我们先来看一下值类型的拷贝。

值类型

值类型,在ECMAScript中也叫做原始数据类型。ECMAScript中目前有以下几种基本数据类型。

const undefinedTag = '[object Undefined]'
const nullTag = '[object Null]'

const boolTag = '[object Boolean]'
const numberTag = '[object Number]'
const stringTag = '[object String]'

// es2015
const symbolTag = '[object Symbol]'

// es2019?
const bigIntTag = '[object BigInt]'

基本数据类型都是值传递,所以只要值基本数据类型的数据,直接返回自身即可

const pVal = [
  undefinedTag, nullTag,
  boolTag, numberTag, stringTag,
  symbolTag, bigIntTag
]
function clone (target) {
  let type = Object.prototype.toString.call(target)
  if (pVal.includes(type)) {
      return target
  } 
}

常见的引用类型

除了原始数据类型,剩下的都是引用数据类型,我们先看一下最常见的 ArrayObject

实现一个ForEach

clone实现对ArrayObjectclone之前,我们需要先实现一个 ForEach方法。

为什么要重新实现?

出于两个原因,需要在 clone的时候一个 foreach方法。

  1. 性能。 Array.prototype.forEach性能上表现一般。
  2. Object.prototype没有类似 forEach可以遍历对象值的方法,需要配合 Object.prototype.keysfor...in才能实现类似的效果,但是后者性能很差。
/**
 * 类似Array.prototype.forEach的forEach方法
 *
 * @param {Array} [array] 源数组
 * @param {Function} 遍历方法
 * @returns {Array} 返回原数组
 */
function forEach(array, iteratee) {
  let index = -1
  const length = array.length

  while (++index < length) {
    // 中断遍历
    if (iteratee(array[index], index, array) === false) {
      break
    }
  }
  return array
}

可以中断循环

Array.prototype.forEach是不支持中断循环的,但是我们实现的 forEach是可以的。

var arr = [1,2,3]

arr.forEach(i => {
 if (i === 2) {
  return false
 }
 console.log(i)
})
// 1
// 3
// 只能跳过当前遍历

forEach(arr, i => {
 if (i === 2) {
  return false
 }
 console.log(i)
})
// 1
// 只要在某次遍历中返回false,即可跳出整个循环

遍历对象/数组

因为对象跟数组的机构基本类似,数组可以看做一种特殊的 key-value形式,即 key为数组项下标, value为数组项的对象。 如果我们要统一遍历处理数组和对象,我们可以这么写:

const unknownObj = {} || []  
const props = Array.isArray(unknownObj) ? undefined : Object.keys(unknownObj)
forEach(props || unknownObj, (subValue, key) => {
  if (props) {
    key = subValue
    subValue = unknownObj[key]
  }
})

WeakMap的妙用

遇到循环引用怎么办?

clone的时候,遇到循环引用的对象,在递归的时候,如果不终止,会造成栈溢出。我们实现简单的 cloen对象的例子:

var cloneObj = function (obj) {
  var target = new obj.constructor()
  forEach(Object.keys(obj), (val, key) => {
   key = val
   val = obj[key]
   if (Object.prototype.toString.call(val) === "[object Object]") {
     target[key] = cloneObj(val)
   } else {
     target[key] = val
   }
  })
  return target
}

下面示例,证明此函数可用:

var a = {
    x: {
    y: 2
  }
}
var b = cloneObj(a)
b // { x: { y: 2 } }
b === a // false
b.x === a.x // false

下面示例,可以看到栈溢出:

var a = {
    x: 1
}
a.x = a
cloneObj(a) // Uncaught RangeError: Maximum call stack size exceeded

怎么解决这个问题呢?我们可以看到下面这点:

a.x === a // true

所以,只要把 a的值存起来,下次递归之前,如果要递归的值 a.x跟存储的值相等,那么就可以直接返回,不需要进行递归了。我们可以这么实现:

var cache = []
var cloneObj = function (obj) {
  var target = new obj.constructor()
  if (cache.includes(obj)) {
    return obj
  }
  cache.push(obj)
  forEach(Object.keys(obj), (val, key) => {
   key = val
   val = obj[key]
   if (Object.prototype.toString.call(val) === "[object Object]") {
     target[key] = cloneObj(val)
   } else {
     target[key] = val
   }
  })
  return target
}

var b = cloneObj(a)
a === b // false

虽然我们最后阻止了递归,但是这种写法也有缺陷。我们还需要声明额外的外部变量 cache,如果要封装成模块,①必须使用闭包, cache存储了 a的值,如果这个引用一直存在,②那么 a将一直存在内存里,不会被垃圾回收(garbage collection)。并且,③ includes方法每次都要遍历数组,非常消耗性能。

// 必须引入闭包
var markClone = function () {
    var cache = []
  return function (obj) {
    var target = new obj.constructor()
    if (cache.includes(obj)) {
      return obj
    }
    cache.push(obj)
    forEach(Object.keys(obj), (val, key) => {
     key = val
     val = obj[key]
     if (Object.prototype.toString.call(val) === "[object Object]") {
       target[key] = cloneObj(val)
     } else {
       target[key] = val
     }
    })
    return target
  }
}
var cloneObj = makeClone()
cloneObj({x: 1})

关于①,我们可以这么解决:

var cloneObj = function (obj, cache = []) {
  var target = new obj.constructor()
  if (cache.includes(obj)) {
    return obj
  }
  cache.push(obj)
  forEach(Object.keys(obj), (val, key) => {
   key = val
   val = obj[key]
   if (Object.prototype.toString.call(val) === "[object Object]") {
     target[key] = cloneObj(val, cache)
   } else {
     target[key] = val
   }
  })
  return target
}
cloneObj({x: 1})

剩下的两个问题,让我们交给 WeakMap

弱引用:WeakMap

普通的对象只支持字符串作为 key,即使你使用了其他的数据类型,也会调用其自身的 toString()方法:

var a = {}
a[{x:2}] = 3
a[234] = 'hello'
Object.keys(a) // ["234", "[object Object]"]

为了让其他对象也可以作为 keyECMAScript 6新增了 Map数据类型,支持任何数据类型作为 key

var m = new Map()
m.set(function(){}, 1)
m.set([1,3,5], 2)
m.set({x: 'abc'}, 3)
m.forEach((val, key) => console.log(val, key))
// 1 ƒ (){}
// 2 [1, 3, 5]
// 3 {x: "abc"}

WeakMap类型则略微有些不同,它只支持除原始数据类型之外的类型作为 key,且这些 key不可遍历,因为存储的是弱引用。

弱引用不计入引用计数,如果某个引用对象的引用计数变为0,那么它会在垃圾回收时,会被回收。同时,弱引用也失去关联。

我们使用 WeakMap替代 cache:

var cloneObj = function (obj, cache = new WeakMap()) {
  var target = new obj.constructor()
  if (cache.has(obj)) {
    return cache.get(obj)
  }
  cache.set(obj, target)
  forEach(Object.keys(obj), (val, key) => {
   key = val
   val = obj[key]
   if (Object.prototype.toString.call(val) === "[object Object]") {
     // 如果是循环引用,这行类似于 a.x = a,因为此时cloneObj方法返回的是target
     target[key] = cloneObj(val, cache)
   } else {
     target[key] = val
   }
  })
  return target
}
cloneObj({x: 1})

gethas方法执行效率(O(1))绝对比 include高多了(O(n)),我们解决了问题③。我们现在测试一下,我们是否解决了问题②。

测试垃圾回收

首先,打开命令行。

node --expose-gc

--expose-gc参数表示允许手动执行垃圾回收机制

然后执行:

// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined

// 定义getUsage方法,可以快速获取当前堆内存使用情况,单位M
> var getUsage = () => process.memoryUsage().heapUsed / 1024 / 1024 + 'M'

// 查看内存占用的初始状态,heapUsed 为 5M 左右
> getUsage();
'5.1407012939453125M'

> let wm = new WeakMap();
undefined

// 新建一个变量 key,指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined

// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组实际被引用了两次,
// 变量 key 引用一次,WeakMap 的键名引用了第二次
// 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
> wm.set(key, 1);
WeakMap {}

> global.gc();
undefined

// 这时内存占用 heapUsed 增加到 45M 了
> getUsage();
'45.260292053222656M'

// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null

// 再次执行垃圾回收
> global.gc();
undefined

// 内存占用 heapUsed 变回 5M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> getUsage();
'5.110954284667969M'

简洁版本

基于以上的内容,我们可以总结出一版简洁的版本,支持值类型、 ArrayObject类型的拷贝:

const sampleClone = function (target, cache = new WeakMap()) {
    // 值类型
    const undefinedTag = '[object Undefined]'
    const nullTag = '[object Null]'
    const boolTag = '[object Boolean]'
    const numberTag = '[object Number]'
    const stringTag = '[object String]'
    const symbolTag = '[object Symbol]'
    const bigIntTag = '[object BigInt]'
    // 引用类型
    const arrayTag = '[object Array]'
    const objectTag = '[object Object]'

    // 传入对象的类型
    const type = Object.prototype.toString.call(target)

    // 所有支持的类型
    const allTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag,symbolTag, bigIntTag, arrayTag, objectTag
    ]

    // 如果是不支持的类型
    if (!allTypes.includes(type)) {
        console.warn(`不支持${type}类型的拷贝,返回{}。`)
        return {}
    }

    // 值类型数组
    const valTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag,symbolTag, bigIntTag
    ]

    // 值类型直接返回
    if (valTypes.includes(type)) {
        return target
    }

    // forEach
    function forEach(array, iteratee) {
        let index = -1
        const length = array.length

        while (++index < length) {
            // 中断遍历
            if (iteratee(array[index], index, array) === false) {
                break
            }
        }
        return array
    }

    // 初始化clone值
    let cloneTarget = new target.constructor()
    // 阻止循环引用
    if (cache.has(target)) {
        return cache.get(target)
    }
    cache.set(target, cloneTarget)

    // 克隆Array 和Object
    const keys = type === arrayTag ? undefined : Object.keys(target)
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value
        }
        cloneTarget[key] = sampleClone(target[key], cache)
    })

    return cloneTarget
}

以上实现的对原始数据类型和ArrayObjectclone,基本上已经可以满足日常的使用,因为这是前端大多数情况下要处理的数据格式。

特殊的Array类型对象

LodashbaseClone.js中有这么几行代码:

function initCloneArray(array) {
  const { length } = array
  const result = new array.constructor(length)

  // Add properties assigned by `RegExp#exec`.
  if (length && typ array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index
    result.input = array.input
  }
  return result
}

initCloneArray方法用于初始化一个数组对象,但是如果满足一些特殊条件,会给它初始化两个属性 indexinput。注释说的也很明白,这是为了初始化 RegExp.prototype.exec方法执行后返回的特殊的Array数组。 我们可以看一下:

我们可以看到,这个数组对象与常见的数组的不同。对这种类型的数组对象进行克隆,参照 Lodash的处理方法即可。我们现在把它加入 sampleClone中。

var sampleClone = function (target, cache = new WeakMap()) {
    ...
  let cloneTarget
  if (Array.isArray(target)) {
      cloneTarget = initCloneArray(target)
  } else {
    cloneTarget = new target.constructor()
  }
  ...
}

疑问 groups属性是什么?

特殊的Object key

在上面的 sampleClone中,我们使用了 Object.keys遍历出「所有」对象上的 key。但这个所有是存疑的,因为这个方法无法取到原型对象的 key,也无法取到 Symbol类型的 key,也无法遍历出不可枚举的值。

我之前的文章中列出了好几种获取属性的方法,使用这些方法配合着可以取到所有从原对象可以使用的值。 其实说到底,就是在 Object.keys的基础上,多使用几种方法,取到这些值。在 LodashbaseClone方法中,通过 isFlat标识是否拷贝原型对象上的属性,通过 isFull标识是否拷贝类型为 Symbolkey。需要注意的是,Lodash只拷贝可枚举的值。

我们通过传递参数实现一下:

// 来自lodash,使用for...in,返回对象上可枚举属性key+原型key的数组
function keysIn(object) {
  const result = []
  for (const key in object) {
    result.push(key)
  }
  return result
}

// 来自lodash,返回对象上可枚举Symbol key的数组
function getSymbols(object) {
  if (object == null) {
    return []
  }
  object = Object(object)
  return Object
    .getOwnPropertySymbols(object)
    .filter((symbol) => Object.prototype.propertyIsEnumerable.call(object, symbol))
}

// 来自lodash,返回对象上可枚举属性key + Symbol key的数组
function getAllKeys(object) {
  const result = keys(object)
  if (!Array.isArray(object)) {
    result.push(...getSymbols(object))
  }
  return result
}

// 来自lodash,返回对象原型链上可枚举(属性key + Symbol key)的数组 
function getSymbolsIn(object) {
  const result = []
  while (object) {
    result.push(...getSymbols(object))
    object = Object.getPrototyp(Object(object))
  }
  return result
}

// 来自lodash,返回对象上可枚举属性key + Symbol key + 原型链上可枚举(属性key + Symbol key)的数组 
function getAllKeysIn(object) {
  const result = []
  for (const key in object) {
    result.push(key)
  }
  if (!Array.isArray(object)) {
    result.push(...getSymbolsIn(object))
  }
  return result
}

var sampleClone = function (
    target, cache = new WeakMap(), includePrototypeKey, includeSymbolKey
  ) {
    ...
  // 最终获取对象keys数组使用的方法
    const keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys)

  ...

  const keys = type === arrayTag ? undefined : keysFunc(target)


  ...
}

类数组(Array Like)

关于类数组的概念,可在这篇文章中了解。类数组其实算是一种特殊的对象,最后也是通过我们自定义的 forEach进行拷贝, Lodash在取对象键数组的时候进行的区分。在我们刚才说到的 getAllKeys方法中,引用了一个 keys方法,这个方法里会根据是否是类数组使用不同的取值方法:

function keys(object) {
  return isArrayLike(object)
    ? arrayLikeKeys(object)
    : Object.keys(Object(object))
}

isArrayLike这个方法用来判断是否是类数组,可以在这篇文章中看到详细说明。 我们主要看一下,arrayLikeKeys这个方法如何取出类数组中的 key

// 将类数组value的所有key取出,放在一个新的数组中返回
// 如 ['a','b', 'c']
function arrayLikeKeys(value, inherited) {
  const isArr = Array.isArray(value)
  const isArg = !isArr && isArguments(value)
  const isBuff = !isArr && !isArg && isBuffer(value)
  const isType = !isArr && !isArg && !isBuff && isTypedArray(value)
  const skipIndexes = isArr || isArg || isBuff || isType
  const length = value.length
  const result = new Array(skipIndexes ? length : 0)
  let index = skipIndexes ? -1 : length
  while (++index < length) {
    result[index] = `${index}`
  }
  for (const key in value) {
    if ((inherited || Object.prototype.hasOwnProperty.call(value, key)) &&
        !(skipIndexes && (
        // Safari 9 has enumerable `arguments.length` in strict mode.
          (key === 'length' ||
           // Skip index properties.
           isIndex(key, length))
        ))) {
      result.push(key)
    }
  }
  return result
}

我们可以看到, arrayLikeKeys用了两步取出 key

第一步

判断是否拥有 IndexKey(即形如0,1,2,3,4...)的 key

// 数组、参数数组、Buff、Typed数组,都被视为有IndexKey的对象
const skipIndexes = isArr || isArg || isBuff || isType

// 将IndexKey都取出,放到数组里,其他类型的直接跳过
const length = value.length
const result = new Array(skipIndexes ? length : 0)
let index = skipIndexes ? -1 : length
while (++index < length) {
  result[index] = `${index}`
}

第二步

将除了 IndexKey之外的所有 key取出

// 参数inherited用来标识是否取继承自原型对象的key。

function arrayLikeKeys(value, inherited) {
    ...
  (inherited || Object.prototype.hasOwnProperty.call(value, key))
  ...
}

inherited= true表示继承原型对象的key,因为最外层是for...in,可以取到继承自原型对象的key

false或者Undefined,则使用Object.prototype.hasOwnProperty只取对象自身的key

function arrayLikeKeys(value, inherited) {
...
!(skipIndexes && (key === 'length' || isIndex(key, length)))
...
{
  result.push(key)
}
}

skipIndexes用来标识数组是否有 IndexKey,如果没有,说明当前的 key是「其他key」,直接进入下一步,将 key插入要返回的数组;如果有,继续往进行判断。

如果是有 IndexKey的数组,则判断当前的 key名是否是 length,因为在 safari 9中, arguments.length属性是可枚举的,但是 Lodash是不会拷贝 length这个key的,因为 Lodash只会拷贝可枚举的属性。 然后我们继续看 isIndex方法:

function isIndex(value, length) {
  const type = typ value
  length = length == null ? MAX_SAFE_INTEGER : length

  return !!length &&
    (type === 'number' ||
      (type !== 'symbol' && reIsUint.test(value))) &&
        (value > -1 && value % 1 == 0 && value < length)
}

这个方法用来判断当前 key是否是数组的 IndexKey。如果是,则跳过插入,因为之前已经在 while循环中插入过了,如果没有,插入。

assignValue?

处理完对象,在最后赋值的时候,我们看到 Lodash是这么写的:

assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))

没有直接用 =赋值,而是用了一个 assignValue方法,我们看一下这个方法:

function assignValue(object, key, value) {
  const objValue = object[key]

  if (!(hasOwnProperty.call(object, key) && eq(objValue, value))) {
    if (value !== 0 || (1 / value) === (1 / objValue)) {
      baseAssignValue(object, key, value)
    }
  } else if (value === undefined && !(key in object)) {
    baseAssignValue(object, key, value)
  }
}

function baseAssignValue(object, key, value) {
  if (key == '__proto__') {
    Object.defineProperty(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    })
  } else {
    object[key] = value
  }
}

baseAssignValue其实就是赋值操作,但是要进入到这一步,还需要满足两个条件。我们看第一个:

!(hasOwnProperty.call(object, key) && eq(objValue, value))

只有在 key在原型链上才能满足,但是我们可能不需要处理原型对象属性,后面会有说到。

我们继续看第二个条件:

value === undefined && !(key in object)

需要属性不在对象上才能满足,这个条件应该是给 Lodash是中其他的函数调用的,我们也可以略过。

综上,我们在最后赋值的时候不需要,这个 assign方法,直接使用 =即可。

开始改造

我们现在根据以上内容涉及到的特殊对象,对我们的简单版本进行改良。

特殊的数组对象

正则生成的特殊数组对象是需要兼容的,如前文所示,直接在初始化数组的时候,将特殊的属性进行拷贝。

特殊的Key

LodashbaseClone方法中,支持这么2个参数:是否拷贝原型对象上的属性,是否拷贝 Symbol类型的值。我们挨个分析。

原型对象上的属性

我们先抛开如何实现「拷贝原型对象的属性」,直接去思考「我们是否需要拷贝原型对象的属性」呢? 我觉得不需要。原因有二。

  1. 每个对象都是类的实例,类的实例属性其实就是对象的原型对象属性。我们在实际的场景中,如果需要这么一个实例,直接使用类生成一个新的、一样的实例即可,并且,拷贝出一个一模一样的实例的场景也似乎没有。
  2. Lodash虽然提供了这么一个参数,但是从来没有使用过。我已经给开发者提了Issue

Symbol类型

Symbol算是一种基本的数据类型,自然是要支持的。可以对外暴露出一个参数,让用户决定是否拷贝。

所以,在我们接下来的增强版本中,将不会加入这个参数,也不会对对象的原型对象属性进行拷贝。

类数组

对类数组也是需要兼容的,如前文所示,在获取对象的 key的时候,使用对应的方法即可。另外,二进制数组我们暂时不处理,后面会单独加入。

增强版本

const hasOwnProperty = Object.prototype.hasOwnProperty
const getType = Object.prototype.toString


// 初始化一个数组对象,包括正则返回的特殊数组
function initCloneArray(array) {
    const { length } = array
    const result = new array.constructor(length)

    // Add properties assigned by `RegExp#exec`.
    if (length && typ array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
        result.index = array.index
        result.input = array.input
    }
    return result
}

// 获取key的方法
function getKeysFunc(isFull) {
    // 返回对象上可枚举Symbol key的数组
    function getSymbols(object) {
        if (object == null) {
            return []
        }
        object = Object(object)
        return Object
            .getOwnPropertySymbols(object)
            .filter((symbol) => Object.prototype.propertyIsEnumerable.call(object, symbol))
    }
    // 判断是否是合法的类数组的length属性
    function isLength(value) {
        return typ value === 'number' &&
            value > -1 && value % 1 === 0 && value <= Number.MAX_SAFE_INTEGER="" }="" 判断是否是类数组="" function="" isArrayLike(value)="" {="" return="" value="" !="null" &&="" typ="" 'function'="" isLength(value.length)="" 判断是否是合法的类数组的index="" isIndex(value,="" length)="" const="" reIsUint="/^(?:0|[1-9]\d*)$/" type="typ" length="length" =="null" ?="" :="" !!length="" (type="==" 'number'="" ||="" 'symbol'="" reIsUint.test(value)))="" (value=""> -1 && value % 1 === 0 && value < length)
    }
    // 是否是arguments
    function isArguments(value) {
        return typ value === 'object' && value !== null && getType.call(value) === '[object Arguments]'
    }
    // 返回类数组上key组成的数组
    function arrayLikeKeys(value, inherited) {
        const isArr = Array.isArray(value)
        const isArg = !isArr && isArguments(value)
        const skipIndexes = isArr || isArg
        const length = value.length
        const result = new Array(skipIndexes ? length : 0)
        let index = skipIndexes ? -1 : length
        while (++index < length) {
            result[index] = `${index}`
        }
        for (const key in value) {
            if ((inherited || hasOwnProperty.call(value, key)) &&
                !(skipIndexes && (
                    // Safari 9 has enumerable `arguments.length` in strict mode.
                    (key === 'length' ||
                        // Skip index properties.
                        isIndex(key, length))
                ))) {
                result.push(key)
            }
        }
        return result
    }


    // 返回对象上可枚举属性key
    function keys(object) {
        return isArrayLike(object)
            ? arrayLikeKeys(object)
            : Object.keys(Object(object))
    }

    // 返回对象上可枚举属性key + Symbol key的数组
    function getAllKeys(object) {
        const result = keys(object)
        if (!Array.isArray(object)) {
            result.push(...getSymbols(object))
        }
        return result
    }


    return isFull
        ? getAllKeys
        : keys
}

const enhanceClone = function (target, cache = new WeakMap(), isFull = true) {
    // 值类型
    const undefinedTag = '[object Undefined]'
    const nullTag = '[object Null]'
    const boolTag = '[object Boolean]'
    const numberTag = '[object Number]'
    const stringTag = '[object String]'
    const symbolTag = '[object Symbol]'
    const bigIntTag = '[object BigInt]'
    // 引用类型
    const arrayTag = '[object Array]'
    const objectTag = '[object Object]'

    // 传入对象的类型
    const type = getType.call(target)

    // 所有支持的类型
    const allTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag, symbolTag, bigIntTag, arrayTag, objectTag
    ]

    // 如果是不支持的类型
    if (!allTypes.includes(type)) {
        console.warn(`不支持${type}类型的拷贝,返回{}。`)
        return {}
    }

    // 值类型数组
    const valTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag,symbolTag, bigIntTag
    ]

    // 值类型直接返回
    if (valTypes.includes(type)) {
        return target
    }

    // forEach
    function forEach(array, iteratee) {
        let index = -1
        const length = array.length

        while (++index < length) {
            // 中断遍历
            if (iteratee(array[index], index, array) === false) {
                break
            }
        }
        return array
    }

    // 初始化clone值
    let cloneTarget
    if (Array.isArray(target)) {
        cloneTarget = initCloneArray(target)
    } else {
        cloneTarget = new target.constructor()
    }
    // 阻止循环引用
    if (cache.has(target)) {
        return cache.get(target)
    }
    cache.set(target, cloneTarget)

    // 确定获取key的方法
    const keysFunc = getKeysFunc(isFull)

    // 克隆Array 和Object
    const keys = type === arrayTag ? undefined : keysFunc(target)
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value
        }
        cloneTarget[key] = enhanceClone(target[key], cache, isFull)
    })

    return cloneTarget
}

</=>

至此,我们加入了几个特殊类型对象和数组的判断,我们暂且称之为增强版本。

Set 和 Map

SetMap都提供了 forEach方法可以遍历自身,处理循环也使用 WeakMap即可。 我们在 sampleCloen的基础上继续添加:

var cloneDeep = function (target, cache = new WeakMap()) {
    ...
  const setTag = '[object Set]'
  const mapTag = '[object Map]'
  ...

  // 引用类型数组
    const refTypes = [
        arrayTag, objectTag, setTag, mapTag, argTag
    ]
    // 如果不是指定的引用类型,直接返回空对象,提示无法拷贝
    if (!refTypes.includes(type)) {
        console.warn(`不支持${type}类型的拷贝,返回{}。`)
        return {}
    }

  // 克隆set
  if (type === setTag) {
      target.forEach(value => {
      cloneTarget.add(cloneDeep(value, cache))
    })
    return cloneTarget
  }
  // 克隆map
  if (type === mapTag) {
    target.forEach((value, key) => {
      cloneTarget.set(key, cloneDeep(value, cache))
    })
    return cloneTarget
  }
  ...
  return target
}

WeakMap 和 WeakSet

WeakMapWeakSet里面存储的都是一些「临时」的值,只要引用次数为0,会被垃圾回收机制自动回收,这个时机是不可预测的,所以一个WeakMapWeakSet里面目前有多少个成员也是不可预测的,ECMAScript也规定WeakMapWeakSet不可遍历。

所以,WeakMapWeakSet是无法拷贝的。

Lodash中遇到 WeakMap会返回原对象或者 {}

...
const weakMapTag = '[object WeakMap]'
...
const cloneableTags = {}
...
cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
...
// 如果传了原对象的父对象则返回原对象,否则返回{}
if (isFunc || !cloneableTags[tag]) {
  return object ? value : {}
}

也就是说,如果你直接拷贝一个WeakMap对象,会返回 {};但是,如果你只是拷贝对象内存的指针,还是可以的,而判断是指针还是对象的依据就是是否传入了父对象。所以,如果要正确的处理这些不可拷贝的对象,我们还要在函数的参数列表中加入父对象的参数。修改后如下:

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
    ...
    // 引用类型数组
    const refTypes = [
        arrayTag, objectTag, setTag, mapTag, argTag
    ]
    // 无法拷贝的数组
    const unableTypes = [
        weakMapTag, weakSetTag
    ]

    // 如果不是指定的引用类型,且不属于无法拷贝的对象,返回空对象
    if (!refTypes.includes(type)) {
        // 属于无法拷贝类型,如果传入了父对象,返回引用;反之,直接返回空对象
        if (unableTypes.includes(type)) {
            return parent ? target : {}
        } else {
            console.warn(`不支持${type}类型的拷贝,返回{}。`)
            return {}
        }
    }
  ...
  forEach(keys || target, (value, key) => {
        if (keys) {
            key = value
        }
        cloneTarget[key] = cloneDeep(target[key], cache, isFull, target)
    })
  ...
}  

疑问 为什么 Lodash只对 WeakMap做了处理,而没有考虑 WeakSet呢?

Arguments

Arguments也是特殊的类数组对象。它没有数组实例的原型方法,它的原型对象指向 Object的原型。我们看下它的属性:

let tryArgu = function (a, b) {
    console.log(Object.prototype.toString.call(arguments))
  console.log(Object.getPrototyp(arguments) === Object.prototype)
  console.log(arguments)
}
tryArgu(1,2)
// [object Arguments]
// true

我们看到,相比于普通的数组对象,多了 callee属性。这个属性是一个不可枚举的属性,值为当前函数。

arguments.callee === tryArgu // true
Object.getOwnPropertyDescriptor(arguments, 'callee')

知道了以上内容,克隆这种类型的对象的方法就十分简单了。 我们先看下 Lodash是如何处理的:

...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  result = (isFlat || isFunc) ? {} : initCloneObject(value)
  ...
}
...
function initCloneObject(object) {
  return (typ object.constructor === 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototyp(object))
    : {}
}
...

可以看到,它把 Arguments作为一个对象初始化了。我们试一下:

var tryArgu = function (a, b) {
    return _.cloneDeep(arguments)
}
tryArgu(1,2)
// {0: 1, 1: 2}

这么处理就是保证基本使用的时候不出错,保证 argumens[n]cloneTarget[n]相等。如果要真实还原我们应该怎么做呢?

因为我们没有 arguments的构造函数,所以我们初始化克隆对象的时候,只能通过一个函数返回一个真实的 arguments对象,然后把它的属性给修改掉。

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
  ...
  let cloneTarget
    if (Array.isArray(target)) {
        cloneTarget = initCloneArray(target)
    } else {
        if (type === argTag) {
            cloneTarget = (function(){return arguments})()
            cloneTarget.callee = target.callee
            cloneTarget.length = target.length
        } else {
            cloneTarget = new target.constructor()
        }
    }
  ...
}

需要注意的是, length属性指的是实参的个数,也可以被修改,我们保持跟原对象一致即可。

RegExp

我们平时可能习惯了使用字面量的形式去声明一个正则表达式,其实,每一个正则表达式都是一个 RegExp的实例,都拥有相应的属性和方法。我们也可以使用 new关键字实例化一个RegExp对象。

// 字面量
var reg = /abc/gi
// new
var reg2 = new RegExp('abc', 'gi')

一个正则表达式由两部分组成:**source **和 flags

reg.source // 'abc'
reg.flags // 'gi'

**source  **中的字符又可以大致分为6类。详见

类名类别英文名举例
字符类别Character Classes\d
匹配任意阿拉伯数字。等价于[0-9]
字符集合Character Sets[xyz]
匹配集合中的任意一个字符。如匹配'sex' 中的'x'
边界Boundaries$
匹配结尾。如 /t$/不匹配'tea'中的't',但是匹配'eat'中的't'
分组和反向引用Grouping & Back References(x)
匹配'x'并且捕获匹配项。如匹配'xyzxyz'中的两个'x'。
数量词Quantifiersx*
匹配前面的模式'x'0次或者多次。
断言Assertionsx(?=y)
仅匹配被y跟随的x,如'xy abcx ayx'只匹配第一个x

flags包含6个字符,可以组合使用。详见

字符对应属性用途
gglobal全局匹配;找到所有匹配,而不是在第一个匹配后停止
iignoreCase忽略大小写
mmultiline多行; 将开始和结束字符(^和$)视为在多行上工作(也就是,分别匹配每一行的开始和结束(由 \n 或 \r 分割),而不只是只匹配整个输入字符串的最开始和最末尾处。
uunicodeUnicode; 将模式视为Unicode序列点的序列
ysticky粘性匹配; 仅匹配目标字符串中此正则表达式的lastIndex属性指示的索引(并且不尝试从任何后续的索引匹配)。
sdotAlldotAll模式,.可以匹配任何字符(包括终止符 '\n')。

想要知道当前正则表达式是否含有某个flags,可以直接通过属性获取。值得注意的是,这些属性只能获取,不能设置,因为正则表达式实例一旦被构建,就不能再改动了,改动后,就是另一个正则表达式了。

let reg = /abc/uys
reg.global // false
reg.ignoreCase // false
reg.multiline // false
reg.unicode // true
reg.sticky // true
reg.dotAll // true

除了这些,正则表达式还有一个值得注意的属性: lastIndex。这个属性的值是正则表达式开始匹配的位置,使用正则对象的 testexec方法,而且当修饰符为 gy时, 对 lastIndex是有可能变化的,当然,你也可以设置它的值。

var reg = /ab/g
var str = 'abababababab'
reg.lastIndex // 0
reg.test(str) // true
reg.lastIndex // 2
reg.test(str) // true
reg.lastIndex // 4

好了,正则表达式我们基本上都已经搞清楚了,我们看下 Lodash是如何对正则对象进行拷贝的。

const reFlags = /\w*$/
function cloneRegExp(regexp) {
  const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
  result.lastIndex = regexp.lastIndex
  return result
}

/\w*$/中的 \w等同于 [A-Za-z0-9_]*表示匹配0次或者多次, $表示从结尾开始匹配。所以,这个正则的意思就是从结尾开始匹配,找到不是[A-Za-z0-9_]的字符为止,返回中间这些匹配到的。我们试一下:

/\w*$/.exec('abc/def') 
// [0: 'def', groups: undefined, index: 4, input: 'abc/def', length: 1]
/\w*$/.exec('abcdef')
// [0: 'abcdef', groups: undefined, index: 0, input: 'abcdef', length: 1]

RegExp的第二个参数应该传递的类型是 String,如果不是,则会调用对象的 toString方法。所以,上面的返回结果在构建实例的时候会被隐式转换成 String

/\w*$/.exec('abc/def').toString() // 'def'

我不知道为啥 Lodash兜了这么大一圈子,直接用 regexp.flags不就行了吗?如果说它是为了向后兼容,可能会有新的 flags那个 [A-Za-z]应该也够了,没必要用 \w吧? 我暂时找不到答案,网上的其他一些实现方法也并没有用到这个正则匹配,所以,我们也对这个进行改良。

function cloneRegExp(regexp) {
  const result = new regexp.constructor(regexp.source, regexp.flags)
  result.lastIndex = regexp.lastIndex
  return result
}

加入到我们的深拷贝中

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
...
    const regexpTag = '[object RegExp]'
...
    if (Array.isArray(target)) {
        cloneTarget = initCloneArray(target)
    } else {
        if (type === argTag) {
            cloneTarget = (function(){return arguments})()
            cloneTarget.callee = target.callee
            cloneTarget.length = target.length
        } else if(type === regexpTag) {
            cloneTarget = cloneRegExp(target)
        } else {
            cloneTarget = new target.constructor()
        }
    }    
...
}

Date

时间的实例对象,其实是由某个时间值+一些时间处理函数构成的。所以拷贝起来也简单,直接用这个时间值作为参数,生成一个新的实例即可。

// 构建实例时,不传入参数,值即为实例创建的时间
var now = new Date()

Object.prototype.toString.call(now) // "[object Date]"

now + '' // "Thu Dec 26 2019 15:08:50 GMT+0800 (中国标准时间)"

+now // 1577344130208

我们看一下, Lodash是如何取到时间值的:

function initCloneByTag(object, tag, isDeep) {
  const Ctor = object.constructor
    ...
  case dateTag:
      return new Ctor(+object)
    ...
}

跟我们设想的一样。

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
    ... else if(type === dateTag) {
            cloneTarget = new target.constructor(+target)
        } else {
            cloneTarget = new target.constructor()
        }
    }    
...
}

Function 和 Error

这两个对象, Function没有拷贝的必要, Error的拷贝则是毫无意义的。 函数,存储的是抽象的逻辑,本身不跟外部的状态有关。比如你可以拷贝 1+2中的 1或者 2,它们是占据内存的具体值,但是函数就是 function add(x, y) { return x + y },拷贝这种逻辑的意义不大。 我们看下 Lodash怎么做的:

const isFunc = typ value === 'function'
...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  result = (isFlat || isFunc) ? {} : initCloneObject(value)
  ...
} else {
  if (isFunc || !cloneableTags[tag]) {
    return object ? value : {}
  }
  ...
}

我们可以看到,如果传了父对象,则返回原来的函数,反之,返回空对象。它并没有拷贝函数,网上也有一些文章真的对函数进行了拷贝,用到了 evalnew Function这种标准不提倡使用的命令和语法,我觉得意义不大,感兴趣的可以看看。

我们沿用 Lodash中的做法即可。

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
    ... else if(type === functionTag) {
            cloneTarget = parent ? target : {}
        } else {
            cloneTarget = new target.constructor()
        }
    }    
...
}

继续说 Error,为什么会说它的拷贝毫无意义呢?因为压根儿不可能有这样的使用场景.... 但是,错误对象能不能被拷贝呢?我们可以试一下。先看下面的例子:

var err1 = new Error('错误1')
Object.keys(err1) // []
for(let x in err1) { console.log(x) } // undefined
Object.getOwnPropertyNames(err1) // ['stack','message']
err1.message // '错误1'
err1.stack 
// 'Error: 错误1
    at <anonymous>:1:12'

我们看到 err1对象有2个不可枚举的属性, message是创建时传入的参数, stack是记录的错误创建的堆的位置。 at后面的内容是抛出错误的<文件名>:行号:列号

那么,如何拷贝呢? Error构造函数其实是可以传递三个参数的,第一个是 message,第二个是文件名,第三个是行号。但是后两个参数是非标准的,现在好像没什么浏览器支持,但是即使支持,没有列号,信息也是不完全的。

我们也可以新建一个错误对象,然后将 message的值作为参数传入,将原对象的 stack覆盖掉新建对象的 stack属性,这个其实是可行的。也就是说,我们抛出错误,两个对象都可以跳转到第一个对象报错的地方。但是,它们本身其实是不同的:它们有着不同的调用链。

在它们调用链的最顶端,保存的都是对象被创建的那个位置,这个是无法改变的。所以,这种方法看起来拷贝了常用的属性和方法,但是因为它们创建的位置不同(也不可能相同)。

此处把例子列出来比较麻烦,感兴趣的可以自己实验。

我们看下 Lodash如何处理错误对象的:

...
cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
...
if (isFunc || !cloneableTags[tag]) {
  return object ? value : {}
}
...

跟处理函数和 WeakMap的方式一样。我们只需把它加入我们之前定义的不可拷贝数组类型即可。

const unableTypes = [
        weakMapTag, weakSetTag, errorTag
]

Promise

Promise实例的拷贝比较简单,因为它存储的事当前的状态,如果在 then方法中不对当前状态做任何处理,那么它会返回一个保存当前状态的新的实例对象。所以拷贝 Promise,调用它的 then方法,然后什么也不做就行了。

const cloneDeep = function (target, cache = new WeakMap(), isFull = true, parent) {
    ... else if(type === promiseTag) {
            cloneTarget = target.then()
        } else {
            cloneTarget = new target.constructor()
        }
    }    
...
}

ArrayBuffer

TODO

完整版本

基于上述的各种类型,我们可以整合出一个比较全面的版本,来处理 ECMAScript中所有数据类型的克隆。

一些我们常见的对象如 window('[object Window]') 、 document('[object HTMLDocument]'),它们是浏览器的内置对象,属于BOM和DOM,并不属于ECMAScript语言中内置对象,不在本文研究的范围之内。

const hasOwnProperty = Object.prototype.hasOwnProperty
const getType = Object.prototype.toString


// 初始化一个数组对象,包括正则返回的特殊数组
function initCloneArray(array) {
    const { length } = array
    const result = new array.constructor(length)

    // Add properties assigned by `RegExp#exec`.
    if (length && typ array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
        result.index = array.index
        result.input = array.input
    }
    return result
}

// 获取key的方法
function getKeysFunc(isFull) {
    // 返回对象上可枚举Symbol key的数组
    function getSymbols(object) {
        if (object == null) {
            return []
        }
        object = Object(object)
        return Object
            .getOwnPropertySymbols(object)
            .filter((symbol) => Object.prototype.propertyIsEnumerable.call(object, symbol))
    }
    // 判断是否是合法的类数组的length属性
    function isLength(value) {
        return typ value === 'number' &&
            value > -1 && value % 1 === 0 && value <= Number.MAX_SAFE_INTEGER="" }="" 判断是否是类数组="" function="" isArrayLike(value)="" {="" return="" value="" !="null" &&="" typ="" 'function'="" isLength(value.length)="" 判断是否是合法的类数组的index="" isIndex(value,="" length)="" const="" reIsUint="/^(?:0|[1-9]\d*)$/" type="typ" length="length" =="null" ?="" :="" !!length="" (type="==" 'number'="" ||="" 'symbol'="" reIsUint.test(value)))="" (value=""> -1 && value % 1 === 0 && value < length)
    }
    // 是否是arguments
    function isArguments(value) {
        return typ value === 'object' && value !== null && getType.call(value) === '[object Arguments]'
    }
    // 返回类数组上key组成的数组
    function arrayLikeKeys(value, inherited) {
        const isArr = Array.isArray(value)
        const isArg = !isArr && isArguments(value)
        const skipIndexes = isArr || isArg
        const length = value.length
        const result = new Array(skipIndexes ? length : 0)
        let index = skipIndexes ? -1 : length
        while (++index < length) {
            result[index] = `${index}`
        }
        for (const key in value) {
            if ((inherited || hasOwnProperty.call(value, key)) &&
                !(skipIndexes && (
                    // Safari 9 has enumerable `arguments.length` in strict mode.
                    (key === 'length' ||
                        // Skip index properties.
                        isIndex(key, length))
                ))) {
                result.push(key)
            }
        }
        return result
    }

    // 返回对象上可枚举属性key
    function keys(object) {
        return isArrayLike(object)
            ? arrayLikeKeys(object)
            : Object.keys(Object(object))
    }

    // 返回对象上可枚举属性key + Symbol key的数组
    function getAllKeys(object) {
        const result = keys(object)
        if (!Array.isArray(object)) {
            result.push(...getSymbols(object))
        }
        return result
    }

    return isFull
        ? getAllKeys
        : keys
}

// 拷贝正则对象
function cloneRegExp(regexp) {
    const result = new regexp.constructor(regexp.source, regexp.flags)
    result.lastIndex = regexp.lastIndex
    return result
}

// 拷贝arguments对象

function cloneArguments(args) {
    const result = (function(){return arguments})()
    result.callee = args.callee
    result.length = args.length
    return result
}

const cloneDeep = function (target, isFull = true, cache = new WeakMap(), parent) {
    // 值类型
    const undefinedTag = '[object Undefined]'
    const nullTag = '[object Null]'
    const boolTag = '[object Boolean]'
    const numberTag = '[object Number]'
    const stringTag = '[object String]'
    const symbolTag = '[object Symbol]'
    const bigIntTag = '[object BigInt]'
    // 引用类型
    const arrayTag = '[object Array]'
    const objectTag = '[object Object]'
    const setTag = '[object Set]'
    const mapTag = '[object Map]'
    const argTag = '[object Arguments]'
    const regexpTag = '[object RegExp]'
    const dateTag = '[object Date]'
    const funcTag = '[object Function]'
    const promiseTag = '[object Promise]'
    // 无法拷贝的引用类型
    const weakMapTag = '[object WeakMap]'
    const weakSetTag = '[object WeakSet]'
    const errorTag = '[object Error]'

    // 传入对象的类型
    const type = getType.call(target)

    // 所有支持的类型
    const allTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag, symbolTag, bigIntTag, arrayTag, objectTag,
        setTag, mapTag, argTag, regexpTag, dateTag, funcTag, promiseTag,
        weakMapTag, weakSetTag, errorTag
    ]

    // 如果是不支持的类型
    if (!allTypes.includes(type)) {
        console.warn(`不支持${type}类型的拷贝,返回{}。`)
        return {}
    }

    // 值类型数组
    const valTypes = [
        undefinedTag, nullTag,boolTag, numberTag, stringTag,symbolTag, bigIntTag
    ]
    // 值类型直接返回
    if (valTypes.includes(type)) {
        return target
    }

    // forEach
    function forEach(array, iteratee) {
        let index = -1
        const length = array.length

        while (++index < length) {
            // 中断遍历
            if (iteratee(array[index], index, array) === false) {
                break
            }
        }
        return array
    }

    // 初始化clone值
    let cloneTarget
    if (Array.isArray(target)) {
        cloneTarget = initCloneArray(target)
    } else {
        switch (type) {
            case argTag:
                cloneTarget = cloneArguments(target)
                break
            case regexpTag:
                cloneTarget = cloneRegExp(target)
                break
            case dateTag:
                cloneTarget = new target.constructor(+target)
                break
            case funcTag:
                cloneTarget = parent ? target : {}
                break
            case promiseTag:
                cloneTarget = target.then()
                break
            case weakMapTag:
            case weakSetTag:
            case errorTag:
                !parent && console.warn(`${type}类型无法拷贝,返回{}。`)
                cloneTarget = parent ? target : {}
                break
            default:
                cloneTarget = new target.constructor()
        }
    }
    // 阻止循环引用
    if (cache.has(target)) {
        return cache.get(target)
    }
    cache.set(target, cloneTarget)

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(cloneDeep(value, cache))
        })
        return cloneTarget
    }
    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, cloneDeep(value, cache))
        })
        return cloneTarget
    }

    // 确定获取key的方法
    const keysFunc = getKeysFunc(isFull)

    // 克隆Array 和Object
    const keys = type === arrayTag ? undefined : keysFunc(target)
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value
        }
        cloneTarget[key] = cloneDeep(target[key], isFull, cache, target)
    })

    return cloneTarget
}

</=>

以上代码上可能还有些性能或者写法的问题需要优化,到作为演示 clone的实现过程已经够用。后续如果 ECMAScript中又新加了数据类型,继续拓展这个方法就行。

位掩码(bitmasks)的妙用

位运算在日常的开发工作中很少会有涉及,也**非常不推荐使用,**因为它的易读性很差。但是在很底层的框架中却常有用到,因为相比于普通计算,它的效率高多了。除了计算,也有一些别的用法,比如在 lodash中,就有多处使用了位掩码。 设想你要设计一个权限系统,某个用户的权限分布,可以用以个简单的 json表示:

{
    "id": 1,
  "RightA": true,
  "RightB": false,
  "RightC": false,
  "RightD": true
}

随着系统的扩大,权限越来越多,这个对象也会越来越大,无论是在网络传输、还是内存占用上,都会导致效率下降。我们可以试着用位掩码去优化这个问题。

我们看下面这个表格,一些十进制数字的二进制表示:

十进制二进制
000000000
100000001
200000010
300000011
400000100
5000000101
600000110
700000111
800001000
900001001

如果我们用 1表示 true, 0表示 false,二进制的位置表示权限,那么之前的 JSON对象可以简化为:

{
    "id": 1,
  "right": 9
}

这相当于把信息都存储到 一个数字中了,我们现在试着从这个数字中取出信息:

const Right = 9
const RightA = 1
const RightB = 2
const RightC = 4
const RightD = 8

hasRightA = !!(Right & RightA) // true
hasRightB = !!(Right & RightB) // false
hasRightC = !!(Right & RightC) // false
hasRightD = !!(Right & RightD) // true

如果我们又重新设置了权限,需要把信息拼凑好返回:

// 修改后的信息
var change = {
    "id": 1,
  "RightA": true,
  "RightB": false,
  "RightC": true,
  "RightD": false
}

right = parseInt(1010, 2) // 10

lodash中,位掩码被用于多个参数的传递:

/** Used to compose bitmasks for cloning. */
const CLONE_DEEP_FLAG = 1
const CLONE_FLAT_FLAG = 2
const CLONE_SYMBOLS_FLAG = 4

function baseClone(value, bitmask, customizer, key, object, stack) {
  let result
  const isDeep = bitmask & CLONE_DEEP_FLAG
  const isFlat = bitmask & CLONE_FLAT_FLAG
  const isFull = bitmask & CLONE_SYMBOLS_FLAG
  ...  
}  

参考

原文链接:juejin.im

上一篇:@ndhoule/every
下一篇:如何优雅处理图片异常

相关推荐

官方社区

扫码加入 JavaScript 社区