Lodash中的cloneDeep

7,952 阅读13分钟

写在前面

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
  } 
}

常见的引用类型

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

实现一个ForEach

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

为什么要重新实现?

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

  1. 性能。 Array.prototype.forEach 性能上表现一般。
  2. Object.prototype 没有类似 forEach 可以遍历对象值的方法,需要配合 Object.prototype.keys 和 for...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]"]

为了让其他对象也可以作为 key , ECMAScript 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})

get 和 has 方法执行效率(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'

简洁版本

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

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
}

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

特殊的Array类型对象

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

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

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

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

image.png

我们可以看到,这个数组对象与常见的数组的不同。对这种类型的数组对象进行克隆,参照 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 的基础上,多使用几种方法,取到这些值。在 Lodash 的 baseClone 方法中,通过 isFlat 标识是否拷贝原型对象上的属性,通过 isFull 标识是否拷贝类型为 Symbol 的 key 。需要注意的是,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.getPrototypeOf(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 = typeof 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

Lodash 在 baseClone 方法中,支持这么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 && typeof 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 typeof value === 'number' &&
			value > -1 && value % 1 === 0 && value <= Number.MAX_SAFE_INTEGER
	}
	// 判断是否是类数组
	function isArrayLike(value) {
		return value != null && typeof value !== 'function' && isLength(value.length)
	}
	// 判断是否是合法的类数组的index
	function isIndex(value, length) {
		const reIsUint = /^(?:0|[1-9]\d*)$/
		const type = typeof value
		length = length == null ? Number.MAX_SAFE_INTEGER : length

		return !!length &&
			(type === 'number' ||
				(type !== 'symbol' && reIsUint.test(value))) &&
			(value > -1 && value % 1 === 0 && value < length)
	}
	// 是否是arguments
	function isArguments(value) {
		return typeof 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

WeakMap 和 WeakSet 里面存储的都是一些「临时」的值,只要引用次数为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.getPrototypeOf(arguments) === Object.prototype)
  console.log(arguments)
}
tryArgu(1,2)
// [object Arguments]
// true

image.png

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

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

image.png

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

...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  result = (isFlat || isFunc) ? {} : initCloneObject(value)
  ...
}
...
function initCloneObject(object) {
  return (typeof object.constructor === 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototypeOf(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 = typeof value === 'function'
...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  result = (isFlat || isFunc) ? {} : initCloneObject(value)
  ...
} else {
  if (isFunc || !cloneableTags[tag]) {
    return object ? value : {}
  }
  ...
}

我们可以看到,如果传了父对象,则返回原来的函数,反之,返回空对象。它并没有拷贝函数,网上也有一些文章真的对函数进行了拷贝,用到了 eval 和 new 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 && typeof 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 typeof value === 'number' &&
			value > -1 && value % 1 === 0 && value <= Number.MAX_SAFE_INTEGER
	}
	// 判断是否是类数组
	function isArrayLike(value) {
		return value != null && typeof value !== 'function' && isLength(value.length)
	}
	// 判断是否是合法的类数组的index
	function isIndex(value, length) {
		const reIsUint = /^(?:0|[1-9]\d*)$/
		const type = typeof value
		length = length == null ? Number.MAX_SAFE_INTEGER : length

		return !!length &&
			(type === 'number' ||
				(type !== 'symbol' && reIsUint.test(value))) &&
			(value > -1 && value % 1 === 0 && value < length)
	}
	// 是否是arguments
	function isArguments(value) {
		return typeof 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
  ...  
}  

参考