предисловие
Задача, решаемая в этой статье:
- Почему существуют глубокие клоны и поверхностные клоны?
- Поймите разницу между глубоким и поверхностным копированием в JavaScript.
- Примечания по копированию объектов в JavaScript
- Как копировать объекты и массивы в JavaScript
Часть кода можно найти здесь:Github. Если найден ошибки, приветствуете это.
1. Поймите причину проблемы
В JavaScript есть два типа типов данных: примитивные значения (Число, Логическое значение, Строка, NULL, Неопределенный) и ссылочные значения (Массив, Объект, Дата, RegExp, Функция). Примитивные значения относятся к простым сегментам данных, а ссылочные значения относятся к объектам, которые могут состоять из нескольких значений.
Доступ к примитивным типам данных осуществляется по значению, так как фактическим значением, хранящимся в переменной, можно управлять напрямую. Значение ссылочного типа — это объект, хранящийся в памяти.В отличие от других языков, JavaScript не допускает прямого доступа к местоположению в памяти, что означает, что он не может напрямую манипулировать пространством памяти объекта. Манипулируя объектами, вы фактическиссылка на объект операцииа не реальный объект. Для этого к значениям ссылочных типов обращаются по ссылке.
Помимо способа их сохранения, есть отличия при копировании значений примитивного и ссылочного типов из одной переменной в другую:
- Если вы копируете значение примитивного типа из одной переменной в другую, новое значение создается в объекте переменной, а затем копируется в место, назначенное новой переменной.
- При копировании значения ссылочного типа из одной переменной в другую он также копирует значение, хранящееся в объекте переменной, в пространство, выделенное для новой переменной. Разница в том, что копия значения на самом деле является указателем, и этот указатель указывает на объект, хранящийся в куче. После завершения операции копирования обе переменные фактически будут ссылаться на один и тот же объект. Следовательно, изменение одной из переменных влияет на другую.
См. код ниже:
// 基本类型值复制
var string1 = 'base type';
var string2 = string1;
// 引用类型值复制
var object1 = {a: 1};
var object2 = object1;
На следующем рисунке могут быть представлены результаты репликации для двух типов переменных:
К этому моменту мы должны понимать, что копирование объектов непосредственно в JavaScript фактически является копированием ссылок, что приведет к тому, что две переменные будут ссылаться на один и тот же объект, а изменения любой переменной будут отражены в другой переменной, что и является причиной всех проблем. куда.
Во-вторых, разница между глубоким и поверхностным копированием.
Разобравшись с проблемой копирования объектов в JavaScript, мы можем поговорить о разнице между глубоким и поверхностным копированием. Рассмотрим эту ситуацию, вам нужно скопировать объект, свойство этого объекта по-прежнему является объектом, например:
var object1 = {
a: 1,
obj: {
b: 'string'
}
}
мелкая копия
浅拷贝Есть две ситуации:
- Скопируйте объект напрямую, то есть скопируйте ссылку, две переменные
object1а такжеobject2все равно будут влиять друг на друга. - Это просто простая копия атрибутов первого уровня объекта.Значения базового типа больше не влияют друг на друга, но значения внутреннего ссылочного типа по-прежнему копируются, а значения внутреннего ссылочного типа по-прежнему будут влияют друг на друга.
// 最简单的浅拷贝
var object2 = object1;
// 拷贝第一层属性
function shallowClone(source) {
if (!source || typeof source !== 'object') {
return;
}
var targetObj = source.constructor === Array ? [] : {};
for (var keys in source) {
if (source.hasOwnProperty(keys)) {
// 简单的拷贝属性
targetObj[keys] = source[keys];
}
}
return targetObj;
}
var object3 = shallowClone(object1);
// 改变原对象的属性
object1.a = 2;
object1.obj.b = 'newString';
// 比较
console.log(object2.a); // 2
console.log(object2.obj.b); // 'newString'
console.log(object3.a); // 1
console.log(object3.obj.b); // 'newString'
Проблем с мелкими копиями много, нужно обратить внимание:
- Копировать можно только перечисляемые свойства.
- генерируется
拷贝对象прототип с原对象В отличие от прототипа, копируемый объект является просто экземпляром Object. - Свойства, унаследованные исходным объектом от его прототипа, также копируются в новый объект, точно так же, как и свойства исходного объекта, неотличимы.
- Дескрипторы свойств не могут быть скопированы, а свойство только для чтения может быть доступно для записи в скопированном объекте.
- Если свойство является объектом, свойство исходного объекта и свойство скопированного объекта будут указывать на объект и будут влиять друг на друга.
Не можете понять эти понятия? Взгляните на код ниже:
function Parent() {
this.name = 'parent';
this.a = 1;
}
function Child() {
this.name = 'child';
this.b = 2;
}
Child.prototype = new Parent();
var child1 = new Child();
// 更改 child1 的 name 属性的描述符
Object.defineProperty(child1, 'name', {
writable: false,
value: 'Mike'
});
// 拷贝对象
var child2 = shallowClone(child1);
// Object {value: "Nicholas", writable: false, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(child1, 'name'));
// 这里新对象的 name 属性的描述符已经发生了变化
// Object {value: "Nicholas", writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(child2, 'name'));
child1.name = 'newName'; // 严格模式下报错
child2.name = 'newName'; // 可以赋值
console.log(child1.name); // Mike
console.log(child2.name); // newName
Приведенный выше код проходит через конструкторChildпостроить объектchild1, прототип этого объектаParent. и модифицированныйchild1изnameдескриптор свойства, наборwritableдляfalse, то есть это свойство больше нельзя изменить. Если вы хотите дать прямоchild1.nameПрисваивание будет сообщено об ошибке в строгом режиме, а присваивание завершится ошибкой в нестрогом режиме (но об ошибке не будет сообщено).
Мы вызываем упомянутую ранее функцию поверхностного копированияshallowCloneкопироватьchild1объект, создается новый объектchild2, выходchild2изnameдескриптор атрибута, мы можем найтиchild2изnameдескриптор атрибута сchild1Больше не то же самое (стать доступным для записи). Включите режим отладки в VSCode, см.child1а такжеchild2прототипов, мы также обнаружим, что их прототипы также различны:
child1ПрототипParent,а такжеchild2ПрототипObject.
С помощью приведенных выше примеров и кратких описаний мы можем примерно понять некоторые проблемы поверхностного копирования, а также можем составить собственное мнение в процессе фактического использования.
глубокая копия
深拷贝Это рекурсивное копирование атрибутов объекта в новый объект.Два объекта имеют разные адреса, разные ссылки, а также атрибуты объекта в объекте (например, атрибут obj в объекте1), и две переменные полностью независимы. . . .
Никакой серебряной пули — основано на фактических потребностях
Поскольку с неглубоким копированием возникает так много проблем, почему мы говорим о поверхностном копировании? Во-первых, идеальная реализация глубокого копирования не так проста (или даже не существует), и могут быть проблемы с производительностью.Во-вторых, иногда глубокая копия действительно не нужна, поэтому нам не нужно беспокоиться о глубоком копировании и мелкой Копировать Не нужно возиться с собой, верно?
Одним словом: выбирайте разные методы в соответствии с вашими реальными потребностями.
В-третьих, реализуйте поверхностное копирование объектов и массивов.
поверхностная копия объекта
Два метода неглубокого копирования объектов были представлены ранее, поэтому здесь мы их объяснять не будем. Вот еще способы
1. Используйте метод Object.Assign
Object.assign()Используется для преобразования всех одного или нескольких исходных объектов可枚举的属性Значение копируется в целевой объект.Object.assign()Просто мелкая копия, похожая на упомянутую вышеshallowCloneметод.
var object1 = {
a: 1,
obj: {
b: 'string'
}
};
// 浅拷贝
var copy = Object.assign({}, object1);
// 改变原对象属性
object1.a = 2;
object1.obj.b = 'newString';
console.log(copy.a); // 1
console.log(copy.obj.b); // `newString`
2. Используйте Object.getOwnPropertyNames для копирования неперечислимых свойств.
Object.getOwnPropertyNames()Возвращает массив свойств объекта, включая неперечисляемые свойства (кроме тех, которые используют Symbol).
function shallowCopyOwnProperties( source )
{
var target = {} ;
var keys = Object.getOwnPropertyNames( original ) ;
for ( var i = 0 ; i < keys.length ; i ++ ) {
target[ keys[ i ] ] = source[ keys[ i ] ] ;
}
return target ;
}
3. Используйте Object.getPrototypeOf и Object.getownPropertyDescriptor для копирования прототипов и дескрипторов.
Если нам нужно скопировать прототип и дескриптор исходного объекта, мы можем использоватьObject.getPrototypeOfа такжеObject.getOwnPropertyDescriptorМетод получает прототип и дескриптор исходного объекта соответственно, а затем используетObject.createа такжеObject.definePropertyметоды для создания новых объектов и свойств объектов на основе прототипов и дескрипторов свойств.
function shallowCopy( source ) {
// 用 source 的原型创建一个对象
var target = Object.create( Object.getPrototypeOf( source )) ;
// 获取对象的所有属性
var keys = Object.getOwnPropertyNames( source ) ;
// 循环拷贝对象的所有属性
for ( var i = 0 ; i < keys.length ; i ++ ) {
// 用原属性的描述符创建新的属性
Object.defineProperty( target , keys[ i ] , Object.getOwnPropertyDescriptor( source , keys[ i ])) ;
}
return target ;
}
Мелкая копия массива
Как и выше, массив также может быть скопирован напрямую или элементы обходного массива могут быть скопированы напрямую для достижения цели поверхностного копирования:
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// 直接复制
var array1 = array;
// 遍历直接复制
var array2 = [];
for(var key in array) {
array2[key] = array[key];
}
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // newString
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
В этом нет ничего особенного, поговорим о других методах
Использование методов slice и concat
slice()Метод Мелко копирует выбранную часть массива (все элементы по умолчанию) в новый объект массива и возвращает объект массива, исходный массив не изменяется.concat()метод используется для объединения двух или более массивов. Этот метод не изменяет существующий массив, а возвращает новый массив.
Оба этих двух метода могут достичь цели копирования массива, и они являются неглубокими копиями, а объекты в массиве просто копируют ссылку:
var array = [1, 'string', {a: 1,b: 2, obj: {c: 3}}];
// slice()
var array1 = array.slice();
// concat()
var array2 = array.concat();
// 改变原数组元素
array[1] = 'newString';
array[2].c = 4;
console.log(array1[1]); // string
console.log(array1[2].c); // 4
console.log(array2[1]); // string
console.log(array2[2].c); // 4
В-четвертых, реализовать глубокое копирование объектов и массивов.
Есть примерно два способа реализовать глубокую копию:
- использовать
JSON.stringifyа такжеJSON.parseметод - Перемещаться по свойствам объекта (или элементам массива), копируя их по отдельности
Следующие два метода подробно описаны
1. Использование методов JSON.stringify и JSON.parse
JSON.stringifyа такжеJSON.parseЭто два метода встроенного объекта JavaScript JSON, которые в основном используются для сериализации объектов JavaScript в строки JSON и анализа строк JSON в собственные значения JavaScript. Это также своего рода черная магия, которая используется для реализации копирования объектов здесь:
var obj = { a: 1, b: { c: 2 }};
// 深拷贝
var newObj = JSON.parse(JSON.stringify(obj));
// 改变原对象的属性
obj.b.c = 20;
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }
Однако этот метод имеет определенные ограничения, то есть объект должен соответствовать формату JSON.Когда уровень глубокий и сериализуемый объект не полностью соответствует формату JSON, использование метода JSON для глубокого копирования вызовет проблемы.
При сериализации объектов JavaScript все函数及原型成员будет намеренно игнорироваться и не отражаться в результате, а значит, этот метод не может копировать функцию в объект. Кроме того, пропускаются любые свойства со значением undefined. В результате получаются свойства экземпляра, значения которых являются допустимыми типами данных JSON.
2. Используйте рекурсию
Рекурсия является распространенным способом решения этой проблемы: мы можем определить функцию, которая проходит через свойства объекта, и когда свойство объекта является значением примитивного типа, копировать его напрямую; когда свойство является значением ссылочный тип, снова вызовите функцию. Сделайте рекурсивную копию. Это основная идея, давайте посмотрим на конкретную реализацию ниже (без учета прототипов, дескрипторов, неперечислимых атрибутов и т. д., которые легко понять):
function deepClone(source) {
// 递归终止条件
if (!source || typeof source !== 'object') {
return source;
}
var targetObj = source.constructor === Array ? [] : {};
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key) {
if (source[key] && typeof source[key] === 'object') {
targetObj[key] = deepClone(source[key]);
} else {
targetObj[key] = source[key];
}
}
}
return targetObj;
}
var object1 = {arr: [1, 2, 3], obj: {key: 'value' }, func: function(){return 1;}};
// 深拷贝
var newObj= deepClone(object1);
// 改变原对象属性
object1.arr.push(4);
console.log(object1.arr); // [1, 2, 3, 4]
console.log(newObj.arr); // [1, 2, 3]
Для типа Function он копируется здесь напрямую, но по-прежнему имеет общий адрес памяти. Поскольку функция больше предназначена для завершения некоторых функций, изменение функции может быть прямым переназначением, в общем случае глубокая копия не рассматривается.
Вышеупомянутая глубокая копия является лишь относительно простой реализацией и не рассматривает очень сложные ситуации, такие как:
- Другие типы ссылок: копия функции, дата, регулярное выражение.
- Циклические ссылки в объектах могут привести к переполнению стека вызовов.
- Такие объекты, которые реализуют закрытые члены через область закрытия, на самом деле не могут быть скопированы.
Что такое объем закрытия
function myConstructor()
{
var myPrivateVar = 'secret' ;
return {
myPublicVar: 'public!' ,
getMyPrivateVar: function() {
return myPrivateVar ;
} ,
setMyPrivateVar( value ) {
myPrivateVar = value.toString() ;
}
};
}
var o = myContructor() ;
В приведенном выше коде объект o имеет три свойства, одно из которых является строкой, а два других — методами. В методе используется переменнаяmyPrivateVar,Существует вmyConstructor()в объеме функции , когдаmyConstructorЭта переменная создается при вызове конструктораmyPrivateVar, однако эта переменная не является объектом, созданным конструкторомoсвойство, но его можно использовать обоими методами.
Итак, если вы попытаетесь глубоко скопировать объектo, затем скопируйте объектcloneи скопированный объектoriginalВсе методы ссылаются на одно и то жеmyPrivateVarПеременная.
Однако, поскольку нет возможности изменить область замыкания, допустимо, чтобы объекты, созданные с помощью этого шаблона, не копировались надлежащим образом.
3. Использование очередей
Хотя рекурсивный метод прост и понятен, он имеет определенные проблемы с производительностью и не является хорошим выбором для копирования относительно больших объектов.
Теоретически рекурсию можно превратить в цикл, можно попробовать превратить рекурсию в глубоком копировании в цикл. Нам нужно пройтись по свойствам объекта. Если свойство является базовым типом, скопируйте его напрямую. Если свойство является ссылочным типом (объект или массив), нам нужно снова пройти по объекту и выполнить ту же операцию с его свойствами . Затем нам нужен контейнер для хранения объектов, которые необходимо пройти.Каждый раз, когда мы вынимаем объект из контейнера для обработки копирования, если в процессе обработки встречается новый объект, мы будем помещать его в этот контейнер для следующего раунда. Когда все объекты в контейнере обработаны, копия объекта завершена.
Идея примерно такая, давайте посмотрим на конкретную реализацию:
// 利用队列的思想优化递归
function deepClone(source) {
if (!source || typeof source !== 'object') {
return source;
}
var current;
var target = source.constructor === Array ? [] : {};
// 用数组作为容器
// 记录被拷贝的原对象和目标
var cloneQueue = [{
source,
target
}];
// 先进先出,更接近于递归
while (current = cloneQueue.shift()) {
for (var key in current.source) {
if (Object.prototype.hasOwnProperty.call(current.source, key)) {
if (current.source[key] && typeof current.source[key] === 'object') {
current.target[key] = current.source[key].constructor === Array ? [] : {};
cloneQueue.push({
source: current.source[key],
target: current.target[key]
});
} else {
current.target[key] = current.source[key];
}
}
}
}
return target;
}
var object1 = {a: 1, b: {c: 2, d: 3}};
var object2 = deepClone(object1);
console.log(object2); // {a: 1, b: {c: 2, d: 3}}
(над)