В последнее время при написании бизнес-кода под vue framework неизбежно возникает проблема глубокого и поверхностного копирования объектов, воспользуюсь случаем подытожить и зафиксировать.
Поскольку платформа статей WeChat может быть повторно отредактирована только один раз, если статья будет обновляться в будущем, она будет обновлена в моем личном блоге.Если вам интересно, вы можете посмотреть это: Адрес личного блога:blog.ironmaxi.com
куча и стек памяти
Прежде всего, поговорим о знакомом «стеке», и разграничим структуру данных и определение «стека» в памяти.
Куча и стек в структуре данных — это две разные структуры данных, в которых элементы данных расположены по порядку.
Мы хотим поговорить о области кучи и области стека в памяти.
В языке C область стека выделяет пространство для локальных переменных, а область кучи используется для выделения пространства памяти, применяемого программистами с адресом, увеличивающимся вверх.Кроме того, имеется статическая область для размещения статических переменных и пространства глобальных переменных; область только для чтения отводится под константы и место для кода программы. Вот простой пример:
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
main()
{
int b; // 栈
char s[] = "abc"; // 栈
char *p2; // 栈
char *p3 = "123456"; // 在常量区,p3在栈上。
static int c =0; // 全局(静态)初始化区
p1 = (char *)malloc(10); // 堆
p2 = (char *)malloc(20); // 堆
}
JavaScript — это язык высокого уровня, и нижний уровень по-прежнему использует C/C++ для компиляции и реализации, а его переменные делятся на базовые типы данных и ссылочные типы. К основным типам данных относятся:
- undefined
- null
- boolean
- number
- string
Эти типы занимают в памяти место фиксированного размера, а их значения сохраняются в пространстве стека путем доступа, копирования и сравнения по значению.
Справочные типы включают в себя:
- object
- array
- function
- error
- date
Значения этих типов не фиксированы по размеру.Адрес, хранящийся в памяти стека, указывает на объект в памяти кучи, доступ к которому осуществляется по ссылке.Грубо говоря, это то же самое, что и указатель в C язык.
Для переменных ссылочного типа знания, хранящиеся в памяти стека, являются адресом доступа объекта, а место для значения выделяется в памяти кучи.Поскольку размер таких значений не фиксирован, они не могут храниться в памяти стека. памяти стека, но размер адреса памяти фиксирован, поэтому можно сохранять адреса памяти кучи в памяти стека. Таким образом, при запросе переменной ссылочного типа сначала считывается адрес памяти кучи из стека, а затем по адресу извлекается соответствующее значение.
Очевидно, что когда все ссылочные типы в JavaScript создают экземпляры, они явно или неявно создают новые экземпляры соответствующих типов, фактически соответствующих языку C.malloc
Выделить функцию памяти.
Назначение переменных в JavaScript
Назначение переменных в js делится на «передача по значению» и «передача по ссылке».
Присвоение значения базового типа данных переменной является «передачей по значению», в то время как присвоение переменной значения ссылочного типа данных фактически является «передачей по ссылке».
Присвоение и сравнение переменных базовых типов данных — это всего лишь присвоение и сравнение значений, то есть копирование и сравнение данных в памяти стека См. следующий интуитивно понятный код:
var num1 = 123;
var num2 = 123;
var num3 = num1;
num1 === num2; // true
num1 === num3; // true
num1 = 456;
num1 === num2; // false
num1 === num3; // false
Назначение и сравнение переменных эталонного типа данных — это просто копирование и сравнение адреса кучи памяти, хранящегося в памяти стека.Участвуйте в следующем интуитивно понятном коде:
var arr1 = [1, 2, 3];
var arr2 = [1, 2, 3];
var arr3 = arr1;
arr1 === arr2; // false
arr1 === arr3; // true
arr1 = [1, 2, 3];
arr1 === arr2; // false
arr1 === arr3; // false
Еще один момент, который следует упомянуть, это прототипы верхнего уровня всех эталонных типов данных в js.Object
, которые являются всеми объектами.
Копии переменных в JavaScript
Копия в js делится на «мелкую копию» и «глубокую копию».
мелкая копия
Поверхностное копирование будет копировать только атрибуты объекта последовательно и не будет выполнять рекурсивное копирование, то есть будут назначены только атрибуты первого уровня целевого объекта.
Для данных, у которых первый слой целевого объекта является базовым типом данных, он присваивается напрямую, то есть «передается по значению»; Для данных эталонного типа данных на первом уровне целевого объекта это адрес памяти кучи, непосредственно хранящийся в памяти стека, то есть «передача адреса».
глубокая копия
Глубокая копия отличается от поверхностной тем, что она не только копирует атрибуты первого уровня целевого объекта, но и рекурсивно копирует все атрибуты целевого объекта.
Вообще говоря, при рассмотрении глубокого копирования составных типов в JavaScript часто бывает так, чтоDate
,Object
а такжеArray
Эти три составных типа обрабатываются. Самый распространенный метод, который мы можем придумать, — это сначала создать пустой новый объект, а затем рекурсивно пройти по старому объекту до тех пор, пока дочерний узел базового типа не будет назначен соответствующей позиции нового объекта.
Однако с этим методом есть проблема, то есть в JavaScript есть волшебный механизм прототипов, и этот прототип будет появляться при обходе, а потом нужно подумать, нужно ли прототип присваивать новому объекту. Тогда в процессе обхода мы можем рассмотреть возможность использованияhasOwnProperty
чтобы определить, следует ли отфильтровывать свойства, унаследованные от цепочки прототипов.
Практическая реализация мелкой копии плюс функция расширения
function _isPlainObject(target) {
return (typeof target === 'object' && !!target && !Array.isArray(target));
}
function shallowExtend() {
var args = Array.prototype.slice.call(arguments);
// 第一个参数作为target
var target = args[0];
var src;
target = _isPlainObject(target) ? target : {};
for (var i=1;i<args.length;i++) {
src = args[i];
if (!_isPlainObject(src)) {
continue;
}
for(var key in src) {
if (src.hasOwnProperty(key)) {
if (src[key] != undefined) {
target[key] = src[key];
}
}
}
}
return target;
}
Прецедент:
// 初始化引用数据类型变量
var target = {
key: 'value',
num: 1,
bool: false,
arr: [1, 2, 3],
obj: {
objKey: 'objValue'
},
};
// 拷贝+扩展
var result = shallowExtend({}, target, {
key: 'valueChanged',
num: 2,
bool: true,
});
// 对原引用类型数据做修改
target.arr.push(4);
target.obj['objKey2'] = 'objValue2';
// 比较基本数据类型的属性值
result === target; // false
result.key === target.key; // false
result.num === target.num; // false
result.bool === target.bool;// false
// 比较引用数据类型的属性值
result.arr === target.arr; // true
result.obj === target.obj; // true
jQuery.extend реализует глубокую и поверхностную копию плюс функцию расширения.
Опубликовано в jQuery@3.3.1jQuery.extend
Реализация:
jQuery.extend = jQuery.fn.extend = function() {
var options,
name,
src,
copy,
copyIsArray,
clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// 如果第一个参数是布尔值,则为判断是否深拷贝的标志变量
if (typeof target === "boolean") {
deep = target;
// 跳过 deep 标志变量,留意上面 i 的初始值为1
target = arguments[i] || {};
// i 自增1
i++;
}
// 判断 target 是否为 object / array / function 以外的类型变量
if (typeof target !== "object" && !isFunction(target)) {
// 如果是其它类型变量,则强制重新赋值为新的空对象
target = {};
}
// 如果只传入1个参数;或者是传入2个参数,第一个参数为 deep 变量,第二个为 target
// 所以 length 的值可能为 1 或 2,但无论是 1 或 2,下段 for 循环只会运行一次
if (i === length) {
// 将 jQuery 本身赋值给 target
target = this;
// i 自减1,可能的值为 0 或 1
i--;
}
for (; i < length; i++) {
// 以下拷贝操作,只针对非 null 或 undefined 的 arguments[i] 进行
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// 避免死循环的情况
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
// 如果是深拷贝,且copy值有效,且copy值为纯object或纯array
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
if (copyIsArray) {
// 数组情况
copyIsArray = false;
clone = src && Array.isArray(src)
? src
: [];
} else {
// 对象情况
clone = src && jQuery.isPlainObject(src)
? src
: {};
}
// 克隆copy对象到原对象并赋值回原属性,而不是重新赋值
// 递归调用
target[name] = jQuery.extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
};
Цель этого метода — расширить объект одним или несколькими другими объектами и вернуть расширенный объект.
Если цель не указана, расширяется само пространство имен jQuery. Это помогает авторам плагинов добавлять новые методы в jQuery.
Если для первого параметра установлено значение true, jQuery возвращает глубокую копию, рекурсивно копируя все найденные объекты; в противном случае копия имеет ту же структуру, что и оригинал. Неопределенные свойства не будут скопированы,Однако атрибуты из прототипа в объекте будут скопированы.
ES6 реализует глубокое и поверхностное копирование
Object.assign
Object.assign
метод можетлюбое количество собственных перечислимых свойств, принадлежащих исходному объектуКопирует в целевой объект и возвращает целевой объект.
Уведомление:
- Для свойств доступа этот метод выполняет
getter
функцию, а затем скопируйте полученное значение в целевой объект, если вы хотите скопировать само свойство аксессора, используйтеObject.getOwnPropertyDescriptor()
а такжеObject.defineProperties()
метод; - Свойства типа строка и символ будут скопированы;
- Исключение может возникнуть в процессе копирования свойства, например, свойство только для чтения целевого объекта имеет то же имя, что и свойство исходного объекта, тогда метод выдаст ошибку
TypeError
Исключение, процесс копирования прерывается, атрибуты, которые были успешно скопированы, не затрагиваются, а атрибуты, которые не были скопированы, не будут скопированы снова; - Этот метод пропускает те значения, которые
null
илиundefined
исходный объект;
Глубокое копирование с игнорированием цепочки прототипов с использованием JSON
var dest = JSON.parse(JSON.stringify(target));
Также у него есть недостатки:
Этот метод игнорирует значения, которыеundefined
свойств и функциональных выражений, но не игнорирует значения, которыеnull
характеристики.
Снова поговорим о свойствах цепочки прототипов
В проектной практике установлено, что есть как минимум два следующих способа избежать копирования свойств цепочки прототипов.
способ 1
Самый распространенный способ:
for (let key in targetObj) {
if (targetObj.hasOwnProperty(key)) {
// 相关操作
}
}
Недостатки: он проходит через все свойства в цепочке прототипов, что неэффективно;
способ 2
Вот способ ES6:
const keys = Object.keys(targetObj);
keys.map((key)=>{
// 相关操作
});
Примечание. Только массивы имен ключей всех имен ключей ENUMERABLE.
способ 3
Другой путь:
const obj = Object.create(null);
target.__proto__ = Object.create(null);
for (let key in target) {
// 相关操作
}