Поскольку непримитивные структуры данных передаются в 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;
}

На первый и второй взгляд это довольно ужасный синтаксис, но на третий…

Вот как это работает:

  1. Object.create() создает новый объект, а Object.getPrototypeOf() получает цепочку прототипов экземпляра original и добавляет их к вновь созданному объекту.
  2. 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 как к vars):

// 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.