Поскольку непримитивные структуры данных передаются в JavaScript по ссылке, мы случайно изменим исходный объект или массив, переданный в функцию в качестве аргумента. Вот краткая иллюстрация такого поведения:
var userJohn = { firstName: 'john', lastName: 'campbell', dob: '01/01/1987', accountNumber: '12345678' }; function maskSensitiveInfo (user) { var sensitiveFields = ['dob', 'accountNumber']; sensitiveFields.forEach(function (field) { user[field] = 'hidden'; }); return user; } var userJohnMasked = maskSensitiveInfo(userJohn); console.log(userJohnMasked === userJohn); // we expect this to be false, but it is true!
В приведенном выше примере userJohn
был изменен внутри функции maskSensitiveInfo()
. В частности, строка user[field] = 'hidden';
изменяет userJohn
напрямую вместо изменения ее копии.
Такое поведение ожидается для непримитивных типов данных; то есть любые переменные, которые содержат тип данных, отличный от 6 примитивов (логические, числовые, строковые, символьные, нулевые и неопределенные).
Я думаю, что это также причина того, почему многие руководства по стилю кода JavaScript, которые я встречал до сих пор, рекомендуют не изменять аргументы внутри функций. Руководство по стилю Airbnb является примером.
Как неглубоко скопировать объект
Вместо этого они рекомендуют создавать копию любого объекта / массива внутри функций - в основном в качестве превентивной меры, чтобы исключить возможность нежелательных (и трудно отслеживаемых) побочных эффектов, вызванных изменением чего-либо вне функции изнутри.
function maskSensitiveInfo (user) { var userClone = Object.assign({}, user); var sensitiveFields = ['dob', 'accountNumber']; sensitiveFields.forEach(function (field) { userClone[field] = 'hidden'; }); return userClone; } var userJohnMasked = maskSensitiveInfo(userJohn); console.log(userJohnMasked === userJohn); // correctly false!
В приведенном выше фрагменте функция maskSensitiveInfo()
теперь создает копию переданного объекта user
и использует ее для промежуточных шагов, которые изменяют его, перед тем, как вернуть его. Вот почему userJohnMasked
больше не является тем же объектом, что и userJohn
(т.е. userJohnMasked === userJohn
оценивается как false).
Предостережение для неглубокого копирования: только на один уровень в глубину
Мы только что увидели неглубокое копирование. Это так называемый «неглубокий», потому что Object.assign()
будет копировать только значения первого уровня и назначать их новому объекту. Если, например, userJohn
имеет ключ с именем «account», а его значением является другой объект, этот объект не копируется, но опять же только ссылка.
Этот фрагмент иллюстрирует суть дела:
var userJohn = { firstName: 'john', lastName: 'campbell', dob: '01/01/1987', accountNumber: '12345678', account: { number: '12345678', type: 'savings' } }; function maskSensitiveInfo (user) { var userClone = Object.assign({}, user); var sensitiveFields = ['dob', 'accountNumber']; sensitiveFields.forEach(function (field) { userClone[field] = 'hidden'; }); userClone.account.number = 'hidden'; userClone.account.type = 'hidden'; return userClone; } var userJohnMasked = maskSensitiveInfo(userJohn); console.log(userJohnMasked.account.number); // 'hidden' console.log(userJohn.account.number); // we expect to be '12345678' but it is 'hidden'
Исходный объект userJohn
случайно видоизменяется строкой userClone.account.number = 'hidden'
. Это одна из тех ужасных ошибок, которые очень сложно определить.
Чтобы сделать глубокую копию (вместо неглубокой копии), лучше всего использовать внешнюю библиотеку, такую как jQuery или Lodash:
// jQuery method var newObject = jQuery.extend(true, {}, oldObject); // lodash method var deep = _.cloneDeep(objects);
Более подробную информацию о глубоком клонировании можно найти в этой ветке SO.
Вот как вы мелко / глубоко клонируете объекты, созданные с использованием синтаксиса литерала объекта var x = {};
или var x = Object.create();
, то есть они буквально создаются непосредственно из класса Object в JavaScript.
А как насчет объектов, которые являются экземплярами настраиваемого реализованного класса? Как мы увидим, поскольку у нас, скорее всего, будут методы, реализованные в пользовательских классах, описанных выше методов будет недостаточно - методы экземпляра не будут скопированы!
Клонирование экземпляра класса, включая его методы
Недавно я обнаружил кое-что, что для меня было неочевидным - очевидно, методы, определенные в определении класса, автоматически добавляются в цепочку прототипов объекта экземпляра.
Итак, чтобы скопировать методы из одного экземпляра класса в другой, нам нужно будет скопировать цепочку прототипов поверх копирования переменных экземпляра.
Вот волшебная серия встроенных методов, которые можно использовать для создания копии экземпляра настраиваемого класса:
function copyInstance (original) { var copied = Object.assign( Object.create( Object.getPrototypeOf(original) ), original ); return copied; }
На первый и второй взгляд это довольно ужасный синтаксис, но на третий…
Вот как это работает:
Object.create()
создает новый объект, аObject.getPrototypeOf()
получает цепочку прототипов экземпляраoriginal
и добавляет их к вновь созданному объекту.Object.assign()
делает то, что мы видели ранее, а именно (поверхностно) копирует переменные экземпляра во вновь созданный объект.
Это очень удобно для настраиваемых классов, таких как Stack
, Queue
или LinkedList
.
Однако иногда вам нужно добавить несколько дополнительных строк к методу copyInstance()
, в зависимости от того, есть ли в вашем классе какие-либо переменные экземпляра, которые также необходимо скопировать. В моем случае мне пришлось клонировать массив, который хранится как переменная экземпляра с именем this.stack
в реализации Stack:
this.stack = [3, 2, 5, 4, 1]; // clone stack using .slice() this.clonedStack = this.stack.slice(0);
Вот пример использования стека, который у меня недавно был (он использует ES6, поэтому, если вы не знакомы, просто относитесь ко всем const
и let
как к var
s):
// modified copyInstance method that works specifically for my Stack class function copyInstanceStack (original) { var copied = Object.assign( Object.create( Object.getPrototypeOf(original) ), original ); // CREATE SHALLOW COPY OF INSTANCE VARIABLE copied.stack = copied.stack.slice(0); return copied; } // custom implemented Stack data structure class Stack { constructor () { // THIS NEEDS TO COPIED this.stack = []; } // ALL THESE METHODS NEED TO BE COPIED AS WELL push (data) { const newNode = new Node(data); const index = this.stack.length; if (this.stack.length === 0) newNode.minIndex = 0; else { const prevMinIndex = this.stack[index - 1].minIndex; const val = this.stack[prevMinIndex].data; newNode.minIndex = data < val ? index : prevMinIndex; } this.stack.push(newNode); } pop () { return this.stack.pop(); } min () { if (this.stack.length === 0) return null; const minIndex = this.peek().minIndex; return this.stack[minIndex].data; } peek () { if (this.stack.length === 0) return null; return this.stack[this.stack.length - 1]; } isEmpty () { if (this.stack.length === 0) return true; return false; } } // standalone function that sorts a stack instance function sortStack (stack) { // MAKE COPY TO PREVENT DIRECT MUTATION OF ORIGINAL STACK INSTANCE let unsorted = copyInstanceStack(stack); let sorted = new Stack(); // ignore the details, including for completeness... while (!unsorted.isEmpty()) { let current = unsorted.pop().data; let placed = false; while (!placed) { if (sorted.isEmpty() || sorted.peek().data >= current) { sorted.push(current); placed = true; } else { unsorted.push(sorted.pop().data); } } } return sorted; } let s1 = new Stack(); s1.push(4); s1.push(2); s1.push(3); s1.push(6); s1.push(5); s1.push(1); let s2 = sortStack(s1); console.log(s2); // sorted stack order console.log(s1); // original stack order (ie. not mutated, yay!)
Резюме
Я не планировал, что этот пост будет таким длинным, но я хотел быть полным с моими примерами, потому что я знаю, что это важная контекстная информация для понимания идеи клонирования.
Вот краткое изложение, чтобы упростить задачу:
- Непримитивные типы данных, такие как объекты и массивы, передаются в функции по ссылке, в отличие от примитивов, которые передаются по значению.
- Передано по ссылке означает, что если функция изменяет или переназначает аргумент, который является объектом или массивом, исходная переменная вне функции также изменяется / переназначается - это может стать неприятной, трудно обнаруживаемой ошибкой.
- По указанным выше причинам Airbnb, среди прочих компаний, избегает прямого изменения или переназначения аргументов внутри функций.
- Решение состоит в том, чтобы создать клон перед работой с клонированной переменной внутри функции, чтобы предотвратить побочные эффекты.
- Есть два типа клонирования: мелкое и глубокое.
- Shallow клонирует только один уровень на глубину, что означает, что если какая-либо пара "ключ-значение" в объекте содержит другой объект или если объект хранится как элемент в массиве, они по-прежнему будут ссылками вместо клонированных ценности
- Глубокое клонирование учитывает вложенные объекты, эффективно рекурсивно создавая копии до самого глубокого слоя, обеспечивая отсутствие связанных ссылок на исходный объект.
- Чтобы клонировать экземпляр настраиваемого класса, включая его настраиваемые методы внутри класса, необходимо скопировать цепочку прототипов (поскольку методы, определенные внутри класса, добавляются в цепочку прототипов), а также переменные экземпляра
В основном я пишу это, чтобы убедиться, что у меня есть заметка об этой эзотерической, но важной части JavaScript, но я определенно надеюсь, что она была полезна и для вас!
Вам понравилось это читать? Я писал подобные посты, вращающиеся о технологиях, обществе и жизни, то и дело, уже больше года. В этом году я планирую писать 5 сообщений в неделю, и я бы хотел, чтобы вы присоединились ко мне в этом личном путешествии. Вы можете прочитать больше в моем блоге или подписаться, чтобы получать самые интересные сообщения на свой почтовый ящик - это бесплатно.
Изначально опубликовано на Nick Ang.