前言

上一篇文章 「前端面试题系列9」浅拷贝与深拷贝的含义、区别及实现 中提到了深拷贝的实现方法,从递归调用,到 JSON,再到终极方案 cloneForce。

不经让我想到,lodash 中的 _.cloneDeep 方法。它是如何实现深拷贝的呢?今天,就让我们来具体地解读一下 _.cloneDeep 的源码实现。

源码中的内容比较多,为了能将知识点讲明白,也为了更好的阅读体验,将会分为上下 2 篇进行解读。今天主要会涉及位掩码、对象判断、数组和正则的深拷贝写法。

ok,现在就让我们深入源码,共同探索吧~

_.cloneDeep 的源码实现

它的源码内容很少,因为主要还是靠 baseClone 去实现。

/** Used to compose bitmasks for cloning. */
const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4function cloneDeep(value) {return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}
复制代码

刚看到前两行的常量就懵了,它们的用意是什么?然后,传入 baseClone 的第二个参数,似乎还将那两个常量做了运算,其结果是什么?这么做的目的是什么?

一番查找之后,终于明白这里其实涉及到了 位掩码位运算 的概念。下面就来详细讲解一下。

位掩码技术

回到第一行注释:Used to compose bitmasks for cloning。意思是,用于构成克隆方法的位掩码。

从注释看,这里的 CLONE_DEEP_FLAGCLONE_SYMBOLS_FLAG 就是位掩码了,而 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 其实是 位运算 中的 按位或 方法。

这里有个不常见的概念:位运算。MDN 上对位运算的解释是:它经常被用来创建、处理以及读取标志位序列——一种类似二进制的变量。虽然可以使用变量代替标志位序列,但是这样可以节省内存(1/32)。

不过实际开发中,位运算用得很少,主要是因为位运算操作的是二进制位,对开发者来说不太好理解。用得少,就容易生疏。但实际上,位运算是一种很棒的思想,它计算得更快,代码量还更少。位运算,常用于处理同时存在多个布尔选项的情形。掩码中的每个选项的值都是 2 的幂,位运算是 32 位的。

在计算机程序的世界里,所有的数据都是以二进制的形式储存的。位运算,说白了就是直接对某个数据在内存中的二进制位,进行运算操作。比如 &|~^>>,这些都是 按位运算符,它们有一些神奇的用法。以系统权限为例:

const PERMISSION_A = 1; // 0001
const PERMISSION_B = 2; // 0010
const PERMISSION_C = 4; // 0100
const PERMISSION_D = 8; // 1000// 当一个用户同时拥有 权限A 和 权限C 时,就产生了一个新的权限
const mask = PERMISSION_A | PERMISSION_C; // 0101,十进制为 5// 判断该用户是否有 权限C,可以取出 权限C 的位掩码
if (mask & PERMISSION_C) {...
}// 该用户没有 权限A,也没有 权限C
const mask2 = ~(PERMISSION_A | PERMISSION_C); // ~0101 => 1010// 取出 与权限A 不同的部分
const mask3 = mask ^ PERMISSION_A; // 0101 ^ 0001 => 0100
复制代码

回到源码的 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 就得到一个新的结果传入 baseClone 中,十进制为 5,至于它是用来干什么的,就需要继续深入到 baseClone 的源码中去看了。

baseClone 的源码实现

先贴一下源码,其中一些关键的判断已经做了注释

function baseClone(value, bitmask, customizer, key, object, stack) {let result// 根据位掩码,切分判断入口const isDeep = bitmask & CLONE_DEEP_FLAGconst isFlat = bitmask & CLONE_FLAT_FLAGconst isFull = bitmask & CLONE_SYMBOLS_FLAG// 自定义 clone 方法,用于 _.cloneWithif (customizer) {result = object ? customizer(value, key, object, stack) : customizer(value)}if (result !== undefined) {return result}// 过滤出原始类型,直接返回if (!isObject(value)) {return value}const isArr = Array.isArray(value)const tag = getTag(value)if (isArr) {// 处理数组result = initCloneArray(value)if (!isDeep) {// 浅拷贝数组return copyArray(value, result)}} else {// 处理对象const isFunc = typeof value == 'function'if (isBuffer(value)) {return cloneBuffer(value, isDeep)}if (tag == objectTag || tag == argsTag || (isFunc && !object)) {result = (isFlat || isFunc) ? {} : initCloneObject(value)if (!isDeep) {return isFlat? copySymbolsIn(value, copyObject(value, keysIn(value), result)): copySymbols(value, Object.assign(result, value))}} else {if (isFunc || !cloneableTags[tag]) {return object ? value : {}}result = initCloneByTag(value, tag, isDeep)}}// 用 “栈” 处理循环引用stack || (stack = new Stack)const stacked = stack.get(value)if (stacked) {return stacked}stack.set(value, result)// 处理 Mapif (tag == mapTag) {value.forEach((subValue, key) => {result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))})return result}// 处理 Setif (tag == setTag) {value.forEach((subValue) => {result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))})return result}// 处理 typedArrayif (isTypedArray(value)) {return result}const keysFunc = isFull? (isFlat ? getAllKeysIn : getAllKeys): (isFlat ? keysIn : keys)const props = isArr ? undefined : keysFunc(value)// 遍历赋值arrayEach(props || value, (subValue, key) => {if (props) {key = subValuesubValue = value[key]}// Recursively populate clone (susceptible to call stack limits).assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))})return result
}
复制代码

位掩码的作用

/** Used to compose bitmasks for cloning. */
const CLONE_DEEP_FLAG = 1 // 深拷贝标志位
const CLONE_FLAT_FLAG = 2 // 原型链标志位
const CLONE_SYMBOLS_FLAG = 4 // Symbol 标志位function baseClone(value, bitmask, customizer, key, object, stack) {// 根据位掩码,取出位掩码,切分判断入口,bitmask 的十进制为 5const isDeep = bitmask & CLONE_DEEP_FLAG // 5 & 1 => 1 => trueconst isFlat = bitmask & CLONE_FLAT_FLAG // 5 & 2 => 0 => falseconst isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 => 4 => true...
}
复制代码

每个常量基本都加了注释,之前传入 baseClone 的 bitmask 为十进制的 5,其目的就是为了在 baseClone 中进行判断入口的切分。

是否为对象的判断

// 如果不是对象,则直接返回该值
if (!isObject(value)) {return value
}// ./isObject.js
function isObject(value) {const type = typeof valuereturn value != null && (type == 'object' || type == 'function')
}
复制代码

这里需要说的就是,是否为对象的判断。用的基本方法是 typeof,但是因为 typeof null 的值也是 'object',所以最后的 return 需要对 null 做额外处理。

处理数组和正则

const isArr = Array.isArray(value)if (isArr) {result = initCloneArray(value)if (!isDeep) {return copyArray(value, result)}
} else {... // 非数组的处理
}// 用于检测对象自身的属性
const hasOwnProperty = Object.prototype.hasOwnProperty// 初始化需要克隆的数组
function initCloneArray(array) {const { length } = arrayconst result = new array.constructor(length)// Add properties assigned by `RegExp#exec`.if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {result.index = array.indexresult.input = array.input}return result
}
复制代码

为了不干扰源数组的数据,这里首先会用 initCloneArray 初始化一个全新的数组。

其中,new array.constructor(length) 相当于 new Array(length),只是换了种不常见的写法,作用是一样的。

接下来的这个判断,让我一头雾水。

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

判断条件首先确定 length > 0,然后 array[0] 的类型是 string,最后 array 拥有 index 这个属性。

看到判断条件里的两条执行语句更懵了,需要赋值 indexinput,这又是为什么?/(ㄒoㄒ)/~~

回头看到第一行注释,有个关键点 RegExp#exec。MDN 中给的解释:exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。文档下方有个例子:

var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
console.log(result);// 输出的 result 是一个数组,有 3 个元素和 4 个属性
// 0: "Quick Brown Fox Jumps"
// 1: "Brown"
// 2: "Jumps"
// groups: undefined
// index: 4
// input: "The Quick Brown Fox Jumps Over The Lazy Dog"
// length: 3
复制代码

哇哦~ 原来 indexinput 在这里。所以,源码中的为何要那样赋值,就迎刃而解了。

再回到 baseClone 中来,如果不是深拷贝,那就只要做数组的第一层数据的赋值即可。

if (!isDeep) {return copyArray(value, result)
}// ./copyArray.js
function copyArray(source, array) {let index = -1const length = source.lengtharray || (array = new Array(length))while (++index < length) {array[index] = source[index]}return array
}
复制代码

总结

位掩码技术,是一种很棒的思想,可以写出更为简洁的代码,运行得也更快。对象的判断,需要特别注意 null,它的 typeof 值 也是 object。正则的 exec() 方法会返回一个结果数组或 null,其中就会有 index 和 input 属性。

阅读源码的过程比较痛苦,深感自身的不足。从不懂到查阅资料,再到写出来,耗费了我大量的时间,不过写作的过程也给了我不小的收获。修行之路任重而道远,给自己打打气,继续砥砺前行吧。

未完待续。。。

岗位内推

莉莉丝游戏招 高级前端 啦!!!

你玩过《小冰冰传奇([刀塔传奇])》么?你玩过《剑与家园》么?还有本篇的封面,为我司的新游戏《AFK arena》,现已占领各大海外应用市场(友情提示:要小心,这游戏有毒嗷~)。

有兴趣的同学,可以 关注下面的公众 号加我微信 详聊哈~

转载于:https://juejin.im/post/5cbc57a2f265da0380436cca

「读懂源码系列3」lodash 是如何实现深拷贝的(上)相关推荐

  1. 「读懂源码系列2」我从 lodash 源码中学到的几个知识点

    前言 上一篇文章 「前端面试题系列8」数组去重(10 种浓缩版) 的最后,简单介绍了 lodash 中的数组去重方法 _.uniq,它可以实现我们日常工作中的去重需求,能够去重 NaN,并保留 {.. ...

  2. 手把手教你读懂源码,View事件的注册和接收详细剖析

    关于Android的Touch事件传递机制,只是知道事件传入Activity后的流程,但是这些事件是如何传递给Activity的一直模糊不清.现在再来好好回顾一遍,顺道整理一点儿东西出来,同时分享给大 ...

  3. 读logback源码系列文章(五)——Appender --转载

    原文地址:http://kyfxbl.iteye.com/blog/1173788 明天要带老婆出国旅游几天,所以这段时间暂时都更新不了博客了,临走前再最后发一贴 上一篇我们说到Logger类的inf ...

  4. 源码系列第1弹 | 带你快速攻略Kafka源码之旅入门篇

    大家过年好,我是 华仔, 又跟大家见面了. 从今天开始我将为大家奉上 Kafka 源码剖析系列文章,正式开启 「Kafka的源码之旅」,跟我一起来掌握 Kafka 源码核心架构设计思想吧. 今天这篇我 ...

  5. 读Zepto源码之操作DOM

    2019独角兽企业重金招聘Python工程师标准>>> 这篇依然是跟 dom 相关的方法,侧重点是操作 dom 的方法. 读Zepto源码系列文章已经放到了github上,欢迎sta ...

  6. 读Zepto源码之Deferred模块

    Deferred 模块也不是必备的模块,但是 ajax 模块中,要用到 promise 风格,必需引入 Deferred 模块.Deferred 也用到了上一篇文章<读Zepto源码之Callb ...

  7. 读Zepto源码之Ajax模块 1

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  8. 带着问题读 TiDB 源码:Power BI Desktop 以 MySQL 驱动连接 TiDB 报错

    原文来源: https://tidb.net/blog/d343818b 作者:张翔 常有人说,阅读源码是每个优秀开发工程师的必经之路,但是在面对像类似 TiDB 这样复杂的系统时,源码阅读是一个非常 ...

  9. zepto ajax php实例,读Zepto源码之Ajax模块

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  10. java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

    java 源码系列 - 带你读懂 Reference 和 ReferenceQueue https://blog.csdn.net/gdutxiaoxu/article/details/8073858 ...

最新文章

  1. 数据结构——算法之(010)( 字符串的左旋转操作)
  2. 解决ifconfig命令未找到
  3. 数据结构-简单实现二叉树的先序、中序、后序遍历(java)
  4. BUUCTF(pwn)not_the_same_3dsctf_2016
  5. 前端 --- 关于DOM的介绍
  6. 非nodejs方式的vue.js的使用
  7. IOS之Swift5.x和OC网络请求JSON
  8. SQLite数据库常用语句及MAC上的SQLite可视化工具MeasSQLlite使用
  9. 如何使用 C# 判断一个文件是否为程序集
  10. pdo mysql fedora_在Fedora 23 Server和Workstation上安装LAMP(Linux, Apache, MariaDB和PHP)
  11. fast-rcnn win10 tensorflow部署
  12. 云服务器怎么多人进去编辑文档,一台云服务器多人使用
  13. 【转载】Chrome 0day漏洞:不要用Chrome查看pdf文件
  14. revit 转换ifc_Revit官方教程:Revit模型如何导成IFC格式?
  15. Java加密方式(AES,DES,RSA,DSA,MD5)
  16. html的根标签是什么,html标签
  17. 小牛M+怎么样 看过你才知道
  18. ios应用升级到ios15后闪退
  19. Qt5设置应用程序图标
  20. 勃林格殷格翰与Lifebit合作识别全球传染病暴发;百济神州和Shoreline Biosciences达成合作 | 医药健闻...

热门文章

  1. Atitit httpclient 概述  rest接口
  2. Atitit 人员成本优化 实习生制度 attilax总结 1.1. 适合领域 于测试 与 轻度运维领域 轻度研发开发领域 1 1.2. 适合领域 行政领域 1 1.3. 要不要适当发放点生活补贴
  3. Atitit attilax在自然语言处理领域的成果
  4. PAIP。AHK IDE及相关DOC
  5. 再谈几种语言的运行速度比较:看第三方比较结论!
  6. 浅谈SSD应用和发展趋势
  7. 由争议拼多多之货找人想到的 BlockChain Storage 之5、区块链存储 - 存储供需的智能匹配...
  8. 【路径规划】基于matlab自动化拣货最优路径【含Matlab源码 1713期】
  9. 【优化算法】搜索引擎优化算法(BES)【含Matlab源码 1426期】
  10. 【情感识别】基于matlab GUI SVM语音情感识别【含Matlab源码 869期】