Skip to content

深拷贝和浅拷贝解析 #9

@wxqweb

Description

@wxqweb

区别

深拷贝和浅拷贝的主要区别就是在内存中的存储类型不同。

何谓堆?何为栈?

栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。

那么接下来回顾一下js中的数据类型,主要分为基本数据类型引用类型

  • 基本数据类型:undefined, boolean, number, string, null.
  • 引用类型:object.

基本数据类型

  1. 基本数据类型存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。

  2. 基本数据类型值不可变,字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值,并不会改变原始值。

js中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。

var str = "abc";
console.log(str[1]="2"); // 2
console.log(str); // abc
  1. 基本类型的比较是值的比较,只要它们的值相等那么就认为它们是相等的,比较的时候最好使用严格等,因为 == 是会进行类型转换的。
var a = 1;
var b = 1;
console.log(a === b); //true

var c = 1;
var d = true;
console.log(c == d); //true

引用类型

  1. 引用类型存放在堆中,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配。

  2. 引用类型值可变,可以直接改变其值的,如:

var a = [1,2,3];
a[1] = 10;
console.log(a[1]); // 10
  1. 引用类型的比较是引用的比较,对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false

传值与传址

在赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。如:

var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10

注:基本类型的赋值的两个变量是两个独立相互不影响的变量。

引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。如:

var a = {}; // a保存了一个空对象的实例
var b = a;  // a和b都指向了这个空对象

a.name = 'arissy';
console.log(a.name); // 'arissy'
console.log(b.name); // 'arissy'

b.age = 18;
console.log(b.age);// 18
console.log(a.age);// 18

console.log(a == b);// true

引用类型的赋值

浅拷贝

赋值(=)和浅拷贝是有区别的,千万不要认为赋值就是浅拷贝,看个例子就明白了:

var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};

var obj2 = obj1;


var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}

obj2.name = "lisi";
obj3.age = "20";

obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];

console.log(obj1);  
//obj1 = {
//    'name' : 'lisi',
//    'age' :  '18',
//    'language' : [1,["二","三"],["四","五"]],
//};

console.log(obj2);
//obj2 = {
//    'name' : 'lisi',
//    'age' :  '18',
//    'language' : [1,["二","三"],["四","五"]],
//};

console.log(obj3);
//obj3 = {
//    'name' : 'zhangsan',
//    'age' :  '20',
//    'language' : [1,["二","三"],["四","五"]],
//};

先定义个一个原始的对象 obj1,然后使用赋值得到第二个对象 obj2,然后通过浅拷贝,将 obj1 里面的属性都赋值到 obj3 中。也就是说:

  • obj1:原始数据
  • obj2:赋值操作得到
  • obj3:浅拷贝得到
标题 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

深拷贝

深拷贝是对对象以及对象的所有子对象进行拷贝,思路就是递归调用刚刚的浅拷贝,把所有属于对象的属性类型都遍历赋给另一个对象即可。

来看一段 Zepto 中深拷贝的代码:

// 内部方法:用户合并一个或多个对象到第一个对象
// 参数:
// target 目标对象  对象都合并到target里
// source 合并对象
// deep 是否执行深度合并
function extend(target, source, deep) {
    for (key in source)
        if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
            // source[key] 是对象,而 target[key] 不是对象, 则 target[key] = {} 初始化一下,否则递归会出错的
            if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                target[key] = {}

            // source[key] 是数组,而 target[key] 不是数组,则 target[key] = [] 初始化一下,否则递归会出错的
            if (isArray(source[key]) && !isArray(target[key]))
                target[key] = []
            // 执行递归
            extend(target[key], source[key], deep)
        }
        // 不满足以上条件,说明 source[key] 是一般的值类型,直接赋值给 target 就是了
        else if (source[key] !== undefined) target[key] = source[key]
}

// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target){
    var deep, args = slice.call(arguments, 1);

    //第一个参数为boolean值时,表示是否深度合并
    if (typeof target == 'boolean') {
        deep = target;
        //target取第二个参数
        target = args.shift()
    }
    // 遍历后面的参数,都合并到target上
    args.forEach(function(arg){ extend(target, arg, deep) })
    return target
}

在 Zepto 中的 $.extend 方法判断的第一个参数传入的是一个布尔值,判断是否进行深拷贝。

附:只有一层的对象可使用简单的方法, 如JSON.parse(JSON.stringify()), Object.assign(), Object.create()

参考文章:https://juejin.im/post/59ac1c4ef265da248e75892b#heading-3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions