前端 浅拷贝和深拷贝

一、什么是浅拷贝、什么是深拷贝

我们都知道js的数据类型分为基本类型和引用类型,一般讨论到浅拷贝和深拷贝的都是针对引用类型的,像Object和Array这样的复杂类型,

1、浅拷贝:以Object为例
var  a  =  {
    name:  'Wendy'
};

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

可以看出,对于Object类型,当我们将a赋值给b,然后更改b中的属性,a也会随着变化。
也就是说a和b指向了同一块内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝。

2、深拷贝

如果给b放到新的内存中,将a的各个属性都复制到新内存里,就是深拷贝。
也就是说,当b中的属性有变化的时候,a内的属性不会发生变化。

1.png

二、浅拷贝的实现

这里说两个实现方式:

1、Object.assign()

用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

var  target  =  {a:  1,  b:  1};
var  obj1  =  {a:  2,  b:  2,  c: {ca:1}};
var  obj2  =  {c:  {ca:  3,  cb:  2,  cd:  1}};
var  result  =  Object.assign(target,  obj1,  obj2);

console.log(target);    // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 33}}
console.log(target  ===  result);    // true

可以看到,Object.assign()拷贝的只是属性值,假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。所以Object.assign()只能用于浅拷贝或是合并对象。这是Object.assign()值得注意的地方。

2、函数实现

function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }

    return target;
}
var obj = {a:1, b:2, c:[1,2,3], d:{da:1}}
var clone = shadowClone(obj) //{a:1, b:2, c:[1,2,3], d:{da:1}}

三、深拷贝的实现

1、JSON.parse和JSON.stringify

对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:

var clone = JSON.parse(JSON.stringify(target))      

当然,这种方法需要保证对象是 JSON 安全的,所以只适用于部分情况。

2、浅拷贝+递归

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 注意这里
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

但是这样写还不够严谨,比如:

  • 没有对参数做校验
  • 判断是否是对象的逻辑不够严谨
  • 如果用了严谨的对象判断,那么就没有考虑到数组的情况

先看第一个,函数需要校验参数,如果不是对象直接返回

function clone(source) {    
  if (!isObject(source)) 
  return source;    // xxx
}

第二个typeof校验实际上只能区分基本类型和引用类型,其对于Date、RegExp、Array类型返回的是"object"。

2.png

目前判断一个对象类型的最好的办法是Object.prototype.toString.call()

function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]';
}

再抽象一些

var types = ["Array", "Boolean", "Date", "Number", "Object", "RegExp", "String", "Window", "HTMLDocument"];
for(var i = 0, c = types[i ];i<types.length;I++ ){
    is[c] = (function(type){
        return function(obj){
          return Object.prototype.toString.call(obj) == "[object " + type + "]";
        }
    )(c);
}

完善下第三个问题就是

function isObject(x) {return Object.prototype.toString.call(x) === '[object Object]';}


function clone(source) {
    var target = {};
    if(!isObject(source)) target = source;
    else{
      for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (isObject(source[i]) || Array.isArray(source[i])) {
                target[i] = clone(source[i]); // 注意这里
            } else {
                target[i] = source[i];
            }
        }
      }
    }

    return target;
}

其实递归方法最大的问题在于爆栈,当数据的层次很深,需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)

我们用斐波拉契数列为例,普通递归写法:

function f(n) {
  if (n === 0 || n === 1) return n 
  else return f(n - 1) + f(n - 2)
}

这种写法,简单粗暴,但是有个很严重的问题。调用栈随着n的增加而线性增加,当n为一个大数时,就会爆栈了(栈溢出,stack overflow)。这是因为这种递归操作中,同时保存了大量的栈帧,调用栈非常长,消耗了巨大的内存。

三、破解递归爆栈

其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环,

1、尾递归

要说尾递归,就要先了解尾函数,尾函数就是指函数调用最后一步是调用另一个函数

举个🌰:

function f(x){  return g(x);}//属于尾调用
function f(x){  let y = g(x);  return y;}// 不属于,因为调用函数g之后有其它操作
function f(x){  return g(x) + 1;}//不属于,因为调用后还要其它操作,即使在同一行函数内

递归函数是调用自身的函数,尾递归就是尾调用自身的函数,对尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

简单解释下栈溢出问题,由于函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。

3.png

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

接下来,把斐波拉契数列升级为尾递归看看

function fTail(n, a=0, b=1){
  if(n===0) return a 
  else{
    return fTail(n-1, b, a+b)
  } 
}
fTail(5) => fTail(4, 1, 1) => fTail(3, 1, 2) => fTail(2, 2, 3) => fTail(1, 3, 5) => fTail(0, 5, 8) => return 5

被尾递归改写之后的调用栈永远都是更新当前的栈帧而已,这样就完全避免了爆栈的危险

但是,想法是好的,从尾调用优化到尾递归优化的出发点也没错,但是浏览器目前还没有支持


4.png

那么我们可以手动优化下:
直接改函数内部,循环执行

function fLoop(n, a = 0, b = 1) {
  while (n--) { 
    [a, b] = [b, a + b] 
  } 
  return a 
}

这个函数相对比较简单,我们把深拷贝代码用循环实现下:​​

function cloneLoop(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

以上
参考文章:
You don't know JS(上)第109页
深拷贝的终极探索
JavaScript 调用栈、尾递归和手动优化
尾调用优化

在简书上发布相关文章是对自己不断学习的激励;如有什么写得不对的地方,欢迎批评指正;给我点赞的都是小可爱 ~_~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,738评论 0 38
  • 我是从下生那天就背负着罚款五千元的小男孩,直到长大了听母亲说当时家境贫寒为了罚款这事是东躲西藏,月子都没坐完身...
    xiangpeng阅读 234评论 0 0
  • 今天早上晨读的时候,感觉自己的状态还是一般。对文字的感受也不是很深,不是非常的投入。过后我听到录音的回放时候。我感...
    喜悦之兰阅读 200评论 0 1
  • 第十一点,不专业 很多人都知道要让自己变得专业一点,只是很多人不知道,有时候,我们专业,不是为了去说服客户,而是避...
    卖葡萄酒的奶爸阅读 258评论 0 0