浅复制和深复制

首先深复制和浅复制只针对像Object,Array这样的复杂对象的。

  • 浅复制:复制引用,所有引用对象指向同一数据,只复制一层对象的属性
  • 深复制:递归复制了所有层级

浅复制

ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。

简单的赋值就是浅复制。对象和数组在赋值的时候都是引用传递。复制后的引用都是指向同一地址,彼此之间的操作会互相影响。所以浅复制会导致obj1obj2指向同一内存地址。

注意:浅复制后的原始类型数值是互不影响的。

sample2

重新赋值的情况:
obj2 = {x:100} 创建了一个新对象,然后把这个对象的引用交给obj2

sample1

深复制

讨巧的 JSON 方法

JSON 方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构

function jsonClone(obj) {
    return JSON.parse(JSON.stringify(obj));
}
var clone = jsonClone({ a:1 });

Underscore 的 clone 方法

Underscore 的clone方法创建一个浅复制的克隆 object。任何嵌套的对象或数组都通过引用拷贝,不会复制。

Underscore 源码

Zepto 的 extend 方法

Zepto 的extend方法通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。

Zepto 源码

zepto 使用了递归的方法,可以模拟成以下代码:

function deepCopy(result, source) {
        for (var key in source) {
            var copy = source[key];
            if (source === copy) continue; // 如window.window === window,会陷入死循环,需要处理一下
            if (Object.prototype.toString.call(copy) === "[object Object]") {
                result[key] = deepCopy(result[key] || {}, copy);
            } else if (Object.prototype.toString.call(copy) === "[object Array]") {
                result[key] = deepCopy(result[key] || [], copy);
            } else {
                result[key] = copy;
            }
        }
        return result;
    }

函数参数传递

传值还是传引用?

function change(num, obj1, obj2)
{
    num = num * 10;
    obj1.item = "changed";
    obj2 = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
change(num, obj1, obj2);
console.log(num);          // 10
console.log(obj1.item);    // changed
console.log(obj2.item);    // unchanged

传值

函数内的num,obj1,obj2都将是一份新的内存,与调用函数之前定义的三个变量毫无关系。函数内无论怎么修改这三个参数,外部定义的三个变量的值始终不变

传值:传内存拷贝

传引用

函数内的num,obj1,obj2都分别指向同一块内存,该内存就是调用函数之前定义的三个变量时创建的内存。函数内对这三个参数所做的任何改动,都将反映到外部定义的三个变量上。

传引用:传内存指针

从上面的代码可以看出,JavaScript 中函数参数的传递方式既不是传值,也不是传引用,而是叫 call-by-sharing。

Call-by-sharing

它的意思是:传引用的拷贝。

  • change()中的num,obj1,obj2都是一个引用
  • 他们的内容是某块内存的地址
  • 这个地址的值来自于外部定义的三块内存,是那三块内存地址的一份拷贝
  • 同时,在函数内部这三个参数的值是可以直接被修改的,可以指向其他对象(由于 JavaScript 中没有指针或引用运算符,只能直接修改)

因此,我们从内存和引用的角度再来看看change()的定义:

function change(num, obj1, obj2)
{
    num = num * 10; // 对num赋值,修改num的指向,新内存的内容为old_num * 10
    obj1.item = "changed";  // 修改原始obj1内存中的内容
    obj2 = {item: "changed"};   // 对obj2赋值,修改obj2指向,新内存的内容为{item: "changed"}
}

这里我们忽略num = num * 10具体是如何完成的。这依赖于具体语言的解释器,实际上包含了数个对指针的操作。

由于 call-by-sharing 本质上也是传值,因此,也可以说 JavaScript 的传参方式都是传值。

除了 JavaScript 之外,Python, Java, Ruby, Scheme 等语言也是采用 call-by-sharing 的求值策略。

参考资料:

JavaScript中函数参数的传递方式
javascript中的深拷贝和浅拷贝?
深入剖析 JavaScript 的深复制