JavaScript的堆栈原理,浅拷贝与深拷贝

两者都是存放临时数据的地方

堆:先进先出;一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收

栈:先进后出;就像一个桶,由编译器自动分配释放 ,存放函数的参数值,局部变量的值等

缓存问题:

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

JS的数据类型:6种基本数据类型 string,number,boolean,null,undefined ,symbol 和1中引用类型object(包含object,array,function)

基本数据类型值指保存在栈内存中的简单数据段。访问方式是按值访问。

引用数据类型:引用数据类型值指保存在堆内存中的对象。也就是,变量中保存的实际上的只是一个指针地址,这个指针指向内存中的另一个位置,该位置保存着对象。访问方式是按引用访问。

当我们声明一个变量 var a = 1;时,变量a是存放在栈内存中的。

当声明新的变量var b = a;时,是向a的值赋给了b,此时b也是存放在栈内存中的

如果此时修改b的值,即b = 2;b 在栈内存中的数据将修改为2

对于引用类型,如果声明var obj = { name: "tom"}; 则在栈内存中存放obj的地址,地址指向堆内存中obj的数据

当声明新的对象变量 var obj2 = obj;时,obj2 和obj的地址一样,指向同样的内容

如果改变obj2的值  obj2.name = "ada"; 则obj和obj2的值都将发生改变

浅拷贝和深拷贝的区分

首先深复制和浅复制只针对像 Object, Array 这样的复杂对象的。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。

浅拷贝:

var arr1 = [1,2,3];
var arr2 = arr1;

修改arr2的值一定会影响arr1的值。

数组的concat、slice,对象的assign拷贝

1)运用for循环

var arr1 = [1,2,3];
var arr3 = [];
for(let i=0;i<arr1.length;i++){
	arr3[i] = arr1[i];
}
console.log(arr3);
arr3[0] = 3;
console.log(arr3);
console.log(arr1);

2)concat() 

var arr1 = [1,2,3];
var arr4 = arr1.concat();
console.log(arr4)
arr4[0] = 5;
console.log(arr4)
console.log(arr1);

3)slice()

var arr1 = [1,2,3];
var arr5 = arr1.slice(0);
console.log(arr5);
arr5[0] = 4;
console.log(arr5)
console.log(arr1);

当数组中包含对象时,发现就不行了

var arr6 = [1,2,[3,4],{name:'ccy'}];
var arr7 = arr6.concat();
console.log(arr7);
arr7[3].name  = "tom";
console.log(arr7);
console.log(arr6);

当数组arr6中包含对象时, Array.prototype.slice 和 Array.prototype.cancat 拷贝出来数组中的对象还是共享同一内存地址,所以本质上归属浅拷贝

 Object.assign 原理也是一样的(对于对象的属性都为基本类型可以当成深拷贝)

var a = {
	age:18,
	name:'ccy',
	info:{
		address:'wuhan',
		interest:'playCards'
	}
};
var b = Object.assign(a);
a.info.address = 'shenzhen';
console.log(a.info,b.info);

深拷贝

那怎样才能对对象进行深拷贝呢,请扶好眼镜。

自定义深拷贝函数

var clone = function(obj){
        var construct = Object.prototype.toString.call(obj).slice(8,-1);
        var res;
        if(construct === 'Array'){
            res = [];
        }else if(construct === 'Object'){
            res = {}
        }
        for(var item in obj){
            if(typeof obj[item] === 'object'){
                res[item] = clone(obj[item]);
            }else{
                res[item] = obj[item];
            }
        }
        return res;
    };

乍一看好像能处理对象的属性为对象的问题,可以循环遍历直至属性为基本类型;

但是仔细想,如果遇到对象的属性存在相互引用的话会出现死循环的情况。可以再加一次判断,对象的属性如果引用对象指针则跳出当前循环。

深拷贝是可以完美的解决浅拷贝的弊端,重新开辟一块地址,深拷贝出来的属性的基本类型值都是相同的。

JSON内置对象深拷贝

 JSON 对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝

var a = {
	age:1,
	name:'ccy',
	info:{
		address:'wuhan',
		interest:'playCards'
	}
};
var b = JSON.parse(JSON.stringify(a));
a.info.address = 'shenzhen';
console.log(a.info,b.info);

JSON 可处理一般的对象进行深拷贝,但是不能处理函数、正则等对象

 我们可以对自定义的拷贝函数再进行优化 

var clone = function(obj){
        function getType(obj){
            return Object.prototype.toString.call(obj).slice(8,-1);
        }
        function getReg(a){
            var c = a.lastIndexOf('/');
            var reg = a.substring(1,c);
            var escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\w': '\\w', '\s': '\\s', '\d': '\\d'};
            for(var i in escMap){
                if(reg.indexOf(i)){
                    reg.replace(i,escMap[i]);
                }
            }
            var attr = a.substring(c+1);
            return new RegExp(reg, attr);
        }
        var construct = getType(obj);
        var res;
        if(construct === 'Array'){
            res = [];
        }else if(construct === 'Object'){
            res = {}
        }
        for(var item in obj){
            if(obj[item] === obj) continue;//存在引用则跳出当前循环
            if(getType(obj[item]) === 'Function'){
                res[item] = new Function("return "+obj[item].toString())();
            }else if(getType(obj[item]) === 'RegExp'){
                res[item] = getReg(obj[item].toString());
            }else if(getType(obj[item]) === 'Object'){
                res[item] = clone(obj[item]);
            }else{
                res[item] = obj[item];
            }
        }
        return res;
    };

 

基本可以实现函数、正则对象的深拷贝,在本地只做了简单的测试,如果存在问题,请及时评论指出。

 当然像函数库 lodash 的 _.cloneDeep、 JQuery 的 $.extend都实现了深拷贝,有兴趣的同学可自行看下源码。

 

jQuery.extend()方法源码实现

jQuery的源码 - src/core.js #L121源码及分析如下:

jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1,
  length = arguments.length,
  deep = false;
  //以上其中的变量:options是一个缓存变量,用来缓存arguments[i],name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value。
  //copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组,clone深拷贝中用来临时存对象或数组的src。

  // 处理深拷贝的情况
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳过布尔值和目标 
    i++;
  }

  // 控制当target不是object或者function的情况
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 当参数列表长度等于i的时候,扩展jQuery对象自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 扩展基础对象
      for (name in options) {
        src = target[name];	
        copy = options[name];

        // 防止永无止境的循环,这里举个例子,如var i = {};i.a = i;$.extend(true,{},i);如果没有这个判断变成死循环了
        if (target === copy) {
          continue;
        }
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是数组的话就让clone副本等于src否则等于空数组。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是对象的话就让clone副本等于src否则等于空数组。
          }
          // 递归拷贝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) {
          target[name] = copy; // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性。
        }
      }
    }
  }
  // 返回修改的对象
  return target;
};

jQuery的extend方法使用基本的递归思路实现了浅拷贝和深拷贝,但是这个方法也无法处理源对象内部循环引用,例如:

var a = {"name":"aaa"};
var b = {"name":"bbb"};
a.child = b;
b.parent = a;
$.extend(true,{},a);//直接报了栈溢出。Uncaught RangeError: Maximum call stack size exceeded

参考资料:https://www.cnblogs.com/ccylovehs/p/9963513.html

https://www.cnblogs.com/zshno1/p/10812075.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值