Как много вы знаете о глубоком и поверхностном копировании js?

внешний интерфейс JavaScript Vue.js jQuery
Как много вы знаете о глубоком и поверхностном копировании js?

В последнее время при написании бизнес-кода под 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метод можетлюбое количество собственных перечислимых свойств, принадлежащих исходному объектуКопирует в целевой объект и возвращает целевой объект.

Уведомление:

  1. Для свойств доступа этот метод выполняетgetterфункцию, а затем скопируйте полученное значение в целевой объект, если вы хотите скопировать само свойство аксессора, используйтеObject.getOwnPropertyDescriptor()а такжеObject.defineProperties()метод;
  2. Свойства типа строка и символ будут скопированы;
  3. Исключение может возникнуть в процессе копирования свойства, например, свойство только для чтения целевого объекта имеет то же имя, что и свойство исходного объекта, тогда метод выдаст ошибкуTypeErrorИсключение, процесс копирования прерывается, атрибуты, которые были успешно скопированы, не затрагиваются, а атрибуты, которые не были скопированы, не будут скопированы снова;
  4. Этот метод пропускает те значения, которые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) {
  // 相关操作
}

微信公众号