[Расширенная фаза 4-4] Как Lodash реализует глубокое копирование

внешний интерфейс JavaScript

Обновление: Спасибо за вашу поддержку. Недавно я бросил сводку данных для всех, чтобы прочитать систему. В будущем будет больше контента и больше оптимизации.Нажмите здесь, чтобы просмотреть

------ Далее идет текст ------

введение

В предыдущей статье я представил, как реализовать глубокую копию, объясняя объекты, массивы, циклические ссылки, потерянные ссылки,SymbolИ практика глубокого копирования в случае рекурсивного взрыва стека, давайте посмотрим сегодняLodashКак реализовать практику глубокого копирования в функциях, отличных от вышеперечисленных, обычных, Date, Buffer, Map, Set, цепочки прототипов и т. д. Исходный код этой статьи основан наLodashВерсия 4.17.11.

Узнать большеGitHub

Общий процесс

Вход

Входной файлcloneDeep.js, вызвать основной файл напрямуюbaseClone.jsМетоды.

// 木易杨
const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4

function cloneDeep(value) {
    return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}

Первый параметр — это копируемый объект, а второй — побитовая маска (Bitwise).Для подробного ознакомления с битовой маской см. раздел расширения ниже.

метод baseClone

затем мы входим./.internal/baseClone.jsПуть для просмотра конкретного метода, основная логика реализации находится в этом методе.

Сначала введем параметры этого методаbaseClone(value, bitmask, customizer, key, object, stack)

  • значение: объект для копирования

  • битовая маска: битовая маска, где 1 — глубокая копия, 2 — копия свойств в цепочке прототипов, а 4 — копия свойства Symbols

  • настройщик: индивидуальныйcloneфункция

  • key: ключ для передачи значения

  • объект: родительский объект переданного значения

  • стек: стек стека, используемый для обработки циклических ссылок.

Я объясню это в следующих частях, вы можете выбрать интересующую вас часть для чтения.

  • битовая маска
  • сделанный на заказcloneфункция
  • не объект
  • Массив и обычный
  • Объекты и функции
  • циклическая ссылка
  • Map & Set
  • Цепочка символов и прототипов

полный код baseClone

Эта часть является основным кодом. Функции разделены следующим образом. Подробная часть реализации функций подробно объяснит каждую функцию.

// 木易杨
function baseClone(value, bitmask, customizer, key, object, stack) {
    let result

    // 标志位
    const isDeep = bitmask & CLONE_DEEP_FLAG		// 深拷贝,true
    const isFlat = bitmask & CLONE_FLAT_FLAG		// 拷贝原型链,false
    const isFull = bitmask & CLONE_SYMBOLS_FLAG	// 拷贝 Symbol,true

    // 自定义 clone 函数
    if (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)

    // Map
    if (tag == mapTag) {
        value.forEach((subValue, key) => {
            result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
        })
        return result
    }

    // Set
    if (tag == setTag) {
        value.forEach((subValue) => {
            result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
        })
        return result
    }

    // TypedArray
    if (isTypedArray(value)) {
        return result
    }

    // Symbol & 原型链
    const keysFunc = isFull
    	? (isFlat ? getAllKeysIn : getAllKeys)
    	: (isFlat ? keysIn : keys)

    const props = isArr ? undefined : keysFunc(value)
    
    // 遍历赋值
    arrayEach(props || value, (subValue, key) => {
        if (props) {
            key = subValue
            subValue = value[key]
        }
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    
    // 返回结果
    return result
}

Подробная реализация функций

битовая маска

Битовая маска кратко представлена ​​выше, а параметры определены следующим образом.

// 木易杨
// 主线代码
const CLONE_DEEP_FLAG = 1		// 1 即 0001,深拷贝标志位
const CLONE_FLAT_FLAG = 2		// 2 即 0010,拷贝原型链标志位,
const CLONE_SYMBOLS_FLAG = 4	// 4 即 0100,拷贝 Symbols 标志位

Битовые маски используются для обработки случаев, когда одновременно присутствует несколько логических опций, гдеЗначение каждого параметра в маске равно степени числа 2.. Преимущество по сравнению с прямым использованием переменных состоит в том, что вы можете сэкономить память (1/32) (отMDN)

// 木易杨
// 主线代码
// cloneDeep.js 添加标志位,1 | 4 即 0001 | 0100 即 0101 即 5
CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG

// baseClone.js 取出标志位
let result // 初始化返回结果,后续代码需要,和位掩码无关
const isDeep = bitmask & CLONE_DEEP_FLAG 	// 5 & 1 即 1 即 true
const isFlat = bitmask & CLONE_FLAT_FLAG	// 5 & 2 即 0 即 false
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 即 4 即 true

Обычно используются следующие основные операции:

  • a | b: добавить флаги a и b
  • mask & a: Выньте флаг немного
  • mask & ~a: Очистить флаг a
  • mask ^ a: выньте часть, отличную от
// 木易杨
var FLAG_A = 1; // 0001
var FLAG_B = 4; // 0100

// 添加标志位 a 和 b => a | b
var mask = FLAG_A | FLAG_B => 0101 => 5

// 取出标志位 a => mask & a
mask & FLAG_A => 0001 => 1
mask & FLAG_B => 0100 => 4

// 清除标记位 a => mask & ~a
mask & ~FLAG_A => 0100 => 4

// 取出与 a 的不同部分 => mask ^ a
mask ^ FLAG_A => 0100 => 4
mask ^ FLAG_B => 0001 => 1
FLAG_A ^ FLAG_B => 0101 => 5

сделанный на заказcloneфункция

// 木易杨
// 主线代码
if (customizer) {
	result = object ? customizer(value, key, object, stack) : customizer(value)
}
if (result !== undefined) {
    return result
}

Приведенный выше код относительно понятен, есть настройкаcloneПри использовании функции, если есть родительский объект значения value, он передается вvalue、key、object、stackЭти значения, нет родительского объекта, переданного напрямую вvalueВыполнить пользовательскую функцию. возвращаемое значение функцииresultВозвращает результат выполнения, если он не пуст.

Эта часть предназначена для настройкиcloneМетод, предоставляемый функцией.

не объект

// 木易杨
// 主线代码
//判断要拷贝的值是否是对象,非对象直接返回本来的值
if (!isObject(value)) {
    return value;
}

// ../isObject.js
function isObject(value) {
    const type = typeof value;
    return value != null && (type == 'object' || type ='function');
}

Обработка здесь такая же, как моя обработка в [Дополнительно 3-3], разница в том, что добавлено суждение об объекте.function, для копирования функций см. раздел «Функции» ниже.

Массив и обычный

// 木易杨
// 主线代码
const isArr = Array.isArray(value)
const hasOwnProperty = Object.prototype.hasOwnProperty

if (isArr) {
    // 数组
    result = initCloneArray(value)
    if (!isDeep) {
        return copyArray(value, result)
    }
} else {
    ... // 非数组,后面解析
}

// 初始化一个数组
function initCloneArray(array) {
  	const { length } = array
    // 构造相同长度的新数组
  	const result = new array.constructor(length)

  	// 正则 `RegExp#exec` 返回的数组
  	if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
   	 	result.index = array.index
    	result.input = array.input
  	}
  	return result
}
    
// ... 未完待续,最后部分有数组遍历赋值    

Когда входящий объект является массивом, создайте массив той же длиныnew array.constructor(length), что эквивалентноnew Array(length),так какarray.constructor === Array.

// 木易杨
var a = [];
a.constructor === Array; // true

var a = new Array;
a.constructor === Array // true

если есть обычныйRegExp#execВозвращаемый массив, копирование свойствindexиinput. Логика оценки: 1. Длина массива больше 0. 2. Первый элемент массива имеет строковый тип 3. Массив существует.indexАтрибуты.

// 木易杨
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index
    result.input = array.input
}

где регулярное выражениеregexObj.exec(str)При успешном совпадении возвращается массив и обновляются свойства объекта регулярного выражения. Возвращаемый массив будет использовать текст, который полностью соответствует первому элементу, и заполнит заднюю часть массива успешным совпадением в обычных скобках. Возвращается, когда совпадение не удаетсяnull.

// 木易杨
var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
console.log(result);
// [
//	0: "Quick Brown Fox Jumps" 	// 匹配的全部字符串
//	1: "Brown"					// 括号中的分组捕获
//	2: "Jumps"
//	groups: undefined
//	index: 4					// 匹配到的字符位于原始字符串的基于0的索引值
//	input: "The Quick Brown Fox Jumps Over The Lazy Dog" // 原始字符串
//	length: 3
// ]

Если не глубокая копия, передайтеvalueиresult, который напрямую возвращает массив после поверхностной копии. Метод поверхностного копирования здесь заключается в зацикливании, а затем копировании.

// 木易杨
if (!isDeep) {
	return copyArray(value, result)
}

// 浅拷贝数组
function copyArray(source, array) {
  let index = -1
  const length = source.length
  array || (array = new Array(length))
  while (++index < length) {
    array[index] = source[index]
  }
  return array
}

Объекты и функции

// 木易杨
// 主线代码
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
    ... // 数组情况,详见上面解析
} else {
    // 函数
    const isFunc = typeof value == 'function'

    // 如果是 Buffer 对象,拷贝并返回
    if (isBuffer(value)) {
        return cloneBuffer(value, isDeep)
    }
    
    // Object 对象、类数组、或者是函数但没有父对象
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
        // 拷贝原型链或者 value 是函数时,返回 {},不然初始化对象
        result = (isFlat || isFunc) ? {} : initCloneObject(value)
        if (!isDeep) {
            return isFlat
                ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
            	: copySymbols(value, Object.assign(result, value))
        }
    } else {
        // 在 cloneableTags 中,只有 error 和 weakmap 返回 false
        // 函数或者 error 或者 weakmap 时,
        if (isFunc || !cloneableTags[tag]) {
            // 存在父对象返回value,不然返回空对象 {}
            return object ? value : {}
        }
        // 初始化非常规类型
        result = initCloneByTag(value, tag, isDeep)
    }
}

Из приведенного выше кода видно, что функцияerrorиweakmapвозвращает пустой объект {} и фактически не копирует функцию.

valueтипObjectОбъект и массив при вызовеinitCloneObjectИнициализируйте объект и, наконец, вызовите Object.createСоздавайте новые объекты.

// 木易杨
function initCloneObject(object) {
    // 构造函数并且自己不在自己的原型链上
    return (typeof object.constructor == 'function' && !isPrototype(object))
        ? Object.create(Object.getPrototypeOf(object))
    	: {}
}

// 本质上实现了一个instanceof,用来测试自己是否在自己的原型链上
function isPrototype(value) {
    const Ctor = value && value.constructor
    // 寻找对应原型
    const proto = (typeof Ctor == 'function' && Ctor.prototype) || Object.prototype
    return value === proto
}

вObjectКонструктор является функциональным объектом.

// 木易杨
var obj = new Object();
typeof obj.constructor; 
// 'function'

var obj2 = {};
typeof obj2.constructor;
// 'function'

Для объектов нетрадиционного типа они инициализируются соответствующими типами.

// 木易杨
function initCloneByTag(object, tag, isDeep) {
    const Ctor = object.constructor
    switch (tag) {
        case arrayBufferTag:
            return cloneArrayBuffer(object)

        case boolTag: // 布尔与时间类型
        case dateTag:
            return new Ctor(+object) // + 转换为数字

        case dataViewTag:
            return cloneDataView(object, isDeep)

        case float32Tag: case float64Tag:
        case int8Tag: case int16Tag: case int32Tag:
        case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
            return cloneTypedArray(object, isDeep)

        case mapTag: // Map 类型
            return new Ctor

        case numberTag: // 数字和字符串类型
        case stringTag:
            return new Ctor(object)

        case regexpTag: // 正则
            return cloneRegExp(object)

        case setTag: // Set 类型
            return new Ctor

        case symbolTag: // Symbol 类型
            return cloneSymbol(object)
    }
}

скопировать обычный тип

// 木易杨
// \w 用于匹配字母,数字或下划线字符,相当于[A-Za-z0-9_]
const reFlags = /\w*$/
function cloneRegExp(regexp) {
    // 返回当前匹配的文本
    const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
    // 下一次匹配的起始索引
    result.lastIndex = regexp.lastIndex
    return result
}

инициализацияSymbol тип

// 木易杨
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
    return Object(symbolValueOf.call(symbol))
}

циклическая ссылка

Стек создан для решения проблемы циклических ссылок.

// 木易杨
// 主线代码
stack || (stack = new Stack)
const stacked = stack.get(value)
// 已存在
if (stacked) {
    return stacked
}
stack.set(value, result)

Если значение, которое нужно скопировать, уже есть в стеке, значит есть цикл, и вы можете просто вернуть его напрямую. Когда значение не находится в стеке, оно сохраняется в стеке и передается вvalueиresult. здесьresultявляется ссылкой на объект, следующей заresultИзменения также отражаются в стеке.

Map & Set

valueзначениеMapнабирать, повторятьvalueи рекурсивно егоsubValue, обход завершается и возвращаетсяresultрезультат.

// 木易杨
// 主线代码
if (tag == mapTag) {
    value.forEach((subValue, key) => {
        result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}

valueзначениеSetнабирать, повторятьvalueи рекурсивно егоsubValue, обход завершается и возвращаетсяresultрезультат.

// 木易杨
// 主线代码
if (tag == setTag) {
    value.forEach((subValue) => {
        result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

Разница выше в том, что API для добавления элементов отличается, т.е.Map.setиSet.add.

Цепочка символов и прототипов

Здесь мы представляемSymbolи копия свойств цепочки прототипов через флагиisFullиisFlatконтролировать, копировать или нет.

// 木易杨
// 主线代码
// 类型化数组对象
if (isTypedArray(value)) {
    return result
}

const keysFunc = isFull // 拷贝 Symbol 标志位
	? (isFlat 			// 拷贝原型链属性标志位
       ? getAllKeysIn 	// 包含自身和原型链上可枚举属性名以及 Symbol
       : getAllKeys)	// 仅包含自身可枚举属性名以及 Symbol
	: (isFlat 
       ? keysIn 		// 包含自身和原型链上可枚举属性名的数组
       : keys)			// 仅包含自身可枚举属性名的数组

const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
    if (props) {
        key = subValue
        subValue = value[key]
    }
    // 递归拷贝(易受调用堆栈限制)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result

Давайте сначала посмотрим, как получить массив атрибутов, таких как self, цепочка прототипов и Symbol.keys.

// 木易杨
// 创建一个包含自身和原型链上可枚举属性名以及 Symbol 的数组
// 使用 for...in 遍历
function getAllKeysIn(object) {
    const result = keysIn(object)
    if (!Array.isArray(object)) {
        result.push(...getSymbolsIn(object))
    }
    return result
}

// 创建一个仅包含自身可枚举属性名以及 Symbol 的数组
// 非 ArrayLike 数组使用 Object.keys
function getAllKeys(object) {
    const result = keys(object)
    if (!Array.isArray(object)) {
        result.push(...getSymbols(object))
    }
    return result
}

пройти вышеkeysInиkeysПолучите обычные перечисляемые свойства черезgetSymbolsInиgetSymbolsПолучать SymbolПеречислимые свойства.

// 木易杨
// 创建一个包含自身和原型链上可枚举属性名的数组
// 使用 for...in 遍历
function keysIn(object) {
    const result = []
    for (const key in object) {
        result.push(key)
    }
    return result
}

// 创建一个仅包含自身可枚举属性名的数组
// 非 ArrayLike 数组使用 Object.keys
function keys(object) {
    return isArrayLike(object)
        ? arrayLikeKeys(object)
    	: Object.keys(Object(object))
}

// 测试代码
function Foo() {
  this.a = 1
  this.b = 2
}
Foo.prototype.c = 3

keysIn(new Foo)
// ['a', 'b', 'c'] (迭代顺序无法保证)
     
keys(new Foo)
// ['a', 'b'] (迭代顺序无法保证)

Обычные свойства пересекают цепочку прототипов, используяfor.. in,ТакSymbolКак пройти цепь прототипа, вот велосипедные и использованныеObject.getPrototypeOfВойдите в цепочку прототиповSymbol.

// 木易杨
// 创建一个包含自身和原型链上可枚举 Symbol 的数组
// 通过循环和使用 Object.getPrototypeOf 获取原型链上的 Symbol
function getSymbolsIn (object) {
    const result = []
    while (object) { // 循环
        result.push(...getSymbols(object))
        object = Object.getPrototypeOf(Object(object))
    }
    return result
}

// 创建一个仅包含自身可枚举 Symbol 的数组
// 通过 Object.getOwnPropertySymbols 获取 Symbol 属性
const nativeGetSymbols = Object.getOwnPropertySymbols
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable

function getSymbols (object) {
    if (object == null) { // 判空
        return []
    }
    object = Object(object)
    return nativeGetSymbols(object)
        .filter((symbol) => propertyIsEnumerable.call(object, symbol))
}

Возвращаемся к основному коду и получаемkeysсостоит изpropsПосле массива пройдите и рекурсивно.

// 木易杨
// 主线代码
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
    // props 时替换 key 和 subValue,因为 props 里面的 subValue 只是 value 的 key
    if (props) { 
        key = subValue
        subValue = value[key]
    }
    // 递归拷贝(易受调用堆栈限制)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})

// 返回结果,主线结束
return result

ПосмотримarrayEachРеализация , в основном реализует обход, а вiterateeВыход, когда он возвращает false.

// 木易杨
// 迭代数组
// iteratee 是每次迭代调用的函数
function arrayEach(array, iteratee) {
    let index = -1
    const length = array.length

    while (++index < length) {
        if (iteratee(array[index], index, array) === false) {
            break
        }
    }
    return array
}

ПосмотримassignValueреализация, которая присваивает значениеobject[key].

// 木易杨
const hasOwnProperty = Object.prototype.hasOwnProperty

// 如果现有值不相等,则将 value 分配给 object[key]。
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)
        }
    // 值未定义而且键 key 不在对象中    
    } 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
    }
}

// 比较两个值是否相等
// (value !== value && other !== other) 是为了判断 NaN
function eq(value, other) {
  return value === other || (value !== value && other !== other)
}

Ссылаться на

lodash

Исследование исходного кода глубокой копии lodash

побитовые операторы

RegExp.prototype.exec()