专栏首页OECOMjs的深拷贝和浅拷贝

js的深拷贝和浅拷贝

说到深拷贝与浅拷贝,为什么会有这两种概念呢,根本原因就在于js的两种数据类型:基本数据类型和引用数据类型,两种数据类型存储方式不同。

内存的堆区与栈区

首先要讲一下大家耳熟能详的「堆栈」,要区分一下数据结构和内存中的「堆栈」定义。 数据结构中的堆和栈是两种不同的、数据项按序排列的数据结构。 而我们重点要讲的是内存中的堆区与栈区。 在 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); // 堆
}

说到这里,咱们再说一下基本数据类型都包括:

  • undefined
  • null
  • boolean
  • number
  • string

这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,通过按值访问、拷贝和比较。

引用类型包括:

  • object
  • array
  • function
  • error
  • date

这些类型的值大小不固定,栈内存中存放地址指向堆内存中的对象,是按引用访问的,说白了就跟 C 语言的指针一样的道理。

对于引用类型变量,栈内存中存放的知识该对象的访问地址,在堆内存中为该值分配空间,由于这种值的大小不固定,因此不能把他们保存到栈内存中;但内存地址大小是固定的,因此可以将堆内存地址保存到栈内存中。这样,当查询引用类型的变量时,就先从栈中读取堆内存地址,然后再根据该地址取出对应的值。

很显而易见的一点就是,JavaScript 中所有引用类型创建实例时,都是显式或隐式地 new 出对应类型的实例,实际上就是对应 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中考虑复合类型的深层复制的时候,往往就是指对于 DateObjectArray 这三个复合类型的处理。我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。

不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 hasOenProperty 方法来判断是否过滤掉那些继承自原型链上的属性。

动手实现一份浅拷贝加扩展的函数

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.1 中 jQuery.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;
};

该方法的作用是用一个或多个其他对象来扩展一个对象,返回被扩展的对象。

如果不指定target,则给jQuery命名空间本身进行扩展。这有助于插件作者为jQuery增加新方法。

如果第一个参数设置为true,则jQuery返回一个深层次的副本,递归地复制找到的任何对象;否则的话,副本会与原对象共享结构。 未定义的属性将不会被复制,然而从对象的原型继承的属性将会被复制

通过循环来实现深拷贝

function copy(arr){
    var obj=arr.constructor==Array?[]:{};
  //第二种方法 var obj=arr instanceof Array?[]:{}
    for(var item in arr){
        if(typeof arr[item]==="object"){
            obj[item]=copy(arr[item]);
        }else{
            obj[item]=arr[item];
        }
    }
    return obj;
}

ES6 实现深浅拷贝

Object.assign

Object.assign 方法可以把 任意多个的源对象所拥有的自身可枚举属性 拷贝给目标对象,然后返回目标对象。

注意:

  1. 对于访问器属性,该方法会执行那个访问器属性的 getter 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 Object.getOwnPropertyDescriptor()Object.defineProperties() 方法;
  2. 字符串类型和 symbol 类型的属性都会被拷贝;
  3. 在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝;
  4. 该方法会跳过那些值为 nullundefined 的源对象;

利用 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) {
  // 相关操作
}

文章参考链接:js深浅拷贝知多少

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Object.assign应用详解

    Object.assign的主要作用就是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时将目标对象返回。如果目标对象是一个已经存在的对象,此对象将被改...

    无邪Z
  • JavaScript立即执行函数(IIFE)的使用

    js的立即执行函数(IIFE)有两种写法,分别为:(function ( ){})( ) 与 (function ( ){}( )) ,这两种写法基本上是没有区...

    无邪Z
  • HTML5中download属性的应用

    写这篇文章主要是来向大家介绍一下HTML5当中download属性的用法和之前下载的区别。需要的朋友可以看一下。

    无邪Z
  • Object.assign应用详解

    Object.assign的主要作用就是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时将目标对象返回。如果目标对象是一个已经存在的对象,此对象将被改...

    无邪Z
  • 为什么Python不用设计模式?

    花下猫语:今天分享的文章来自公众号“码农翻身”,其作者是前 IBM 架构师刘欣。刘老师的文章极具特点,通过讲故事的方式写技术,既有趣又有料。我写 Python ...

    Python猫
  • 2. Android_Jetpack组件---Lifecycles源码解析

    版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyh...

    Hankkin
  • java8实战读书笔记:Lambda表达式语法与函数式编程接口

    测试:如下语句是否是正确的lambda表达式。 (1) () -> {} (2) () -> "Raoul" (3) () -> {return "Mar...

    丁威
  • java8实战读书笔记:Lambda表达式语法与函数式编程接口

    测试:如下语句是否是正确的lambda表达式。 (1) () -> {} (2) () -> "Raoul" (3) () -> {return "Mar...

    JAVA葵花宝典
  • ABP入门系列(10)——扩展AbpSession

    源码路径:Github-LearningMpaAbp 一、AbpSession是Session吗? 1、首先来看看它们分别对应的类型是什么? 查看源码发现Ses...

    圣杰
  • netty案例,netty4.1中级拓展篇四《Netty传输文件、分片发送、断点续传》

    小傅哥

扫码关注云+社区

领取腾讯云代金券