说说JS中的浅拷贝与深拷贝
outline:
- 为什么要说JS中深拷贝与浅拷贝
- JS对类型的分类
- immutable与mutable
- 简单类型检测
- 浅拷贝VS深拷贝
为什么要说JS中深拷贝与浅拷贝
近来在研读underscore
的源码,发现其中一小段代码
_.mixin = function(obj) {_.each(_.functions(obj), function(name) {var func = _[name] = obj[name];_.prototype[name] = function() {var args = [this._wrapped];push.apply(args, arguments);return result(this, func.apply(_, args));};});
};
....
_.mixin(_);
这段代码就是要把我们在_
上绑的很多方法浅拷贝一份到_.prototype
.这里的浅拷贝引发一些思考.那么什么是浅拷贝、什么是深拷贝?在了解深、浅拷贝之前,我们需要了解下JS中对类型的分类,因为对于不同的类型,我们选择拷贝的方式也是不一样的.
JS对类型的分类
stackoverflow有人提了一个问题:
Stoyan Stefanov in his excellent book 'Object-Oriented JavaScript' says:
Any value that doesn't belong to one of the five primitive types listed above is an object.
Stoyan Stefanov说的这句话,在JS中要么就是primitive
类型,要们就是object
类型.
Primitive
A primitive (primitive value, primitive data type) is data that is not an object and has no methods. In >JavaScript, there are 6 primitive data types: string, number, boolean, null, undefined, symbol (new in ECMAScript 2015).
Most of the time, a primitive value is represented directly at the lowest level of the language implementation.
All primitives are immutable (cannot be changed).
MDN上指出了JS中的primitive类型一共就是string
number
boolean
null
undefined
symbol(ES2015)
6中类型,其余的都是object类型.MDN还说了primitive类型not an object以及has no methods.但是我们平时的使用都是这样的var str = "hello world";console.log(str.charAt(0))
.这段代码中明显str是primitive的变量,按照MDN的说法,str变量应该是not an object
并且has no methods
的,这里我们明显调用了str.charAt
方法.是我们错了还是MDN错了!!!!那我们再测试下str是不是一个object.Object.prototype.toString.call(str)
这段代码执行的结果居然是[object String]
.就是说str不仅是object
同时还has methods
.但是str确实是primitive类型的.
在MDN给出primitive type定义的同时,还给出了Primitive wrapper objects
的定义
Except for null and undefined, all primitive values have object equivalents that wrap around the >primitive values:
String for the string primitive.
Number for the number primitive.
Boolean for the Boolean primitive.
Symbol for the Symbol primitive.The wrapper's valueOf() method returns the primitive value.
也就是说对于这些primitive的类型,确实不是object,并且也没有methods.执行str.charAt
的时候是把string(primitive)类型转成了String(object)类型.ES5规范中这样解释:
这里虽然对于一些内部方法的调用我们并不清楚,但是基本也明确当我们在调用str.charAt
的时候,JS执行引擎把str变成了String对象,可以执行String上的方法.了解了JS中的类型分类,我们在说一说JS中mutable
和immutable
.
immutable与mutable
在上一段我们讲了JS中的类型分类,总体来说就两类就是object和primitive,判断依据就是只有string、number、boolean、null、undefined、symbol(ES2015)才是primitive的,其余均为object的.在我们引用MDN的一段话中,还提到了All primitives are immutable (cannot be changed).
那么这句话是什么意思.所有的primitive都是immutable(不可变的)
.这句话可能大家看完很不理解.var a = 1;a = 2; a= "hello world";
,这里a就是primitive的类型,不是可以修改么,那MDN的这句All primitives are immutable
是什么意思呢.
MDN的这句话其实是没错误的.碰到这种问题,查内存地址是最好的办法,可惜查内存地址难度太大,在chrome和nodejs上我都尝试了,都没有找个有一个比较直观的方式去看内存地址,如果有读者了解如何看内存可以和我联系.这里我们借用JS中的原型链来做一个小实验,也可以间接达到查看内存地址的目的.
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>immutalbe&mutable</title>
</head>
<body><script>
/*
* 这里的关键还是这个立即执行函数.立即执行函数与function的定义夹出了一个不可回收的区域,也就是var id = 0;
* (不明白的可以参考我的http://warjiang.github.io/devcat/2016/04/16/JSLecture/关于闭包的文章)
* 然后我们定义一个函数generateId,负责给id自增.
* 下面是关键
* 我们在Object.prototype上扩展了一个id的方法.
* 由于JS中的原型链,给Object.prototype扩展方法等于说给所有的对象都扩展了id的这个方法.
* 当某个对象调用id的方法会自动顺着原型链回溯到Object.prototype上的id方法.
* 调用这个方法的时候,方法中的this指向调用这个方法的对象.也就是给这个调用者扩展了id这个方法.
* 由于id在立即执行函数内,generateId和Object.prototype.id外,
* 所以id在执行过程中并不会被释放,而是从0开始不断加1
* 参考自http://stackoverflow.com/questions/2020670/javascript-object-id
*/(function() {var id = 0;function generateId() { return id++; console.log(id)};Object.prototype.id = function() {var newId = generateId();this.id = function() { return newId; };return newId;};})();var a = 1;console.log(a.id());//0a = 2;console.log(a.id());//1a = "hello world";console.log(a.id());//2</script>
</body>
</html>
这里id从0变为1、2就是说,我们的a赋值的过程并不是给a指向的内存赋值,而是说a重新指向了一个新的值.基于此,MDN所谓的primitive是immutable的,说的是primitive类型的value是immutable的,而variable是mutable的.所以说,对于primitive类型的变量,为其赋值,本质上就是让变量指向新的内存.
那么对于object类型的变量呢.我们也来做一个实验:
(function() {var id = 0;function generateId() { return id++; console.log(id)};Object.prototype.id = function() {var newId = generateId();this.id = function() { return newId; };return newId;};
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = o1;
console.log(o2.id());//0
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "o2"}
o2 = {name:'xx'}
console.log(o2.id())//1
console.log('o1',o1);//o1 Object {name: "o2"}
console.log('o2',o2);//o2 Object {name: "xx"}
从这个例子我们可以看出,对于Object类型的变量,直接赋值过程等于说让变量指向右值内存地址.如var o2 = o1
,o2就是指向o1指向的内存空间.但是当我们修改对象的属性的时候,就会修改原来内存中对象的属性值.如果o2.name = "o2"
会令o1.name =="o2"
.这里就会引发一个深拷贝、浅拷贝的问题.比如这里的o2 = o1就是一次浅拷贝.浅拷贝的时候,由于指向的内存地址是一样的,如果直接给对象赋值是不存在任何问题的比如var o2 = o1;o2 = {name:'xx'}
此时o1.id()返回0,o2.id()返回1.但是如果修改对象上的属性时,就会触发对象指向的内存中的对象的属性修改.
我们在来看另外一个例子:
(function() {var id = 0;function generateId() { return id++; console.log(id)};Object.prototype.id = function() {var newId = generateId();this.id = function() { return newId; };return newId;};
})();
var o1 = {name:'o1'};
console.log(o1.id());//0
var o2 = {}
o2.name = o1.name;
console.log(o2.id());//1
o2.name = "o2";
console.log('o1',o1);//o1 Object {name: "o1"}
console.log('o2',o2);//o2 Object {name: "o2"}
在这个例子中我们对于o2的赋值没有采用o2 = o1;而是采用了o2={},o2.name = o1.name.那么这样够不够.结合我么之前说的immutable和mutable,由于name对应的值是string类型的,是immutable的,所以这里我们拷贝到name是完全够的,是属于深拷贝.
看到这里,相信大家后面我们要做的深、浅拷贝可能有一定的想法了.浅拷贝就是直接赋值,或者说不完全的赋值(对于对象而言,后面我们会举例),浅拷贝对于primitive类型的或者说不会直接修改属性的对象而言比如Function是无害的,但是对于浅拷贝{k1:v1}或者说是[v1,v2]的对象,会出现严重的问题,即由于指向同一个内存对象,修改属性等于修改了所有指向该内存对象的属性.
那么下面我们就需要做类型检测,对于做深拷贝需要检测的情况很简单,如果检测出来是浅拷贝有害的,我们就做深拷贝,否则直接浅拷贝.
简单类型检测
这里我们只需要做Object和Array的类型检测,对于Function、Date等类型的我们都不是很需要.类型检测我们采用Object.prototype.toString方法
var isType = function(type){return function(obj){return Object.prototype.toString.call(obj) === '[object '+ type +']';}
}
var is = {isArray : isType('Array'),isObject : isType('Object'),
}
有了类型检测函数,下面我们就可以开心的做深拷贝了.
浅拷贝VS深拷贝
浅拷贝我们之前也说了,这里直接举个例子,说明其危害.
(function() {var id = 0;function generateId() { return id++; console.log(id)};Object.prototype.id = function() {var newId = generateId();this.id = function() { return newId; };return newId;};
})();
var o1 = {number: 1, string: "I am a string", object: {test1: "Old value" },arr: [ "a string", { test2: "Try changing me" }]
};var extend = function(result, source) {for (var key in source)result[key] = source[key];return result;
}var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//2
console.log('o2',o2.arr.id());//3
从id的值上看,o2和o1的内部属性值,number、string是采用的两个副本,但是object和arr确实采用的同一个副本.这种情况下如果我们修改o2.object = {name:'o2'}是没有问题的,由于直接复制本质上上内存指向修改的问题.但是如果我们修改o2.object.test1 = "New value",此时o1和o2会一起变!!!这种情况是我们不想看到的.对于object、array类型的最好做深拷贝(是否深拷贝看应用场景,读者需要斟酌),结合我们上面的类型检测,我们把extend函数修改一下
(function() {var id = 0;function generateId() { return id++; console.log(id)};Object.prototype.id = function() {var newId = generateId();this.id = function() { return newId; };return newId;};
})();
var isType = function(type){return function(obj){return Object.prototype.toString.call(obj) === '[object '+ type +']';}
}
var is = {isArray : isType('Array'),isObject : isType('Object'),
}var o1 = {number: 1, string: "I am a string", object: {test1: "Old value" },arr: [ "a string", { test2: "Try changing me" }]
};var extend = function(result, source) {for (var key in source){var copy = source[key];if(is.isArray(copy)){//Array deep copyresult[key] = extend(result[key] || [], copy);}else if(is.isObject(copy)){//Object deep copyresult[key] = extend(result[key] || {}, copy);}else{result[key] = copy;}}return result;
}var o2 = extend({},o1);
console.log('o1',o1.number.id());//0
console.log('o1',o1.string.id());//1
console.log('o1',o1.object.id());//2
console.log('o1',o1.arr.id());//3console.log('o2',o2.number.id());//4
console.log('o2',o2.string.id());//5
console.log('o2',o2.object.id());//6
console.log('o2',o2.arr.id());//7o2.object.test1 = "new Value";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Old value"
console.log(o2,JSON.stringify(o2))//o2.object.test1 == "new Value"o2.arr[1].test2 = "就不改你";
console.log(o1,JSON.stringify(o1))//o1.object.test1 == "Try changing me"
console.log(o2,JSON.stringify(o2))//o2.arr[1].test2 == "就不改你"
说说JS中的浅拷贝与深拷贝相关推荐
- js 浅拷贝直接赋值_JS中实现浅拷贝和深拷贝的代码详解
(一)JS中基本类型和引用类型 JavaScript的变量中包含两种类型的值:基本类型值 和 引用类型值,在内存中的表现形式在于:前者是存储在栈中的一些简单的数据段,后者则是保存在堆内存中的一个对象. ...
- 【转】JAVA中的浅拷贝和深拷贝
原文网址:http://blog.bd17kaka.net/blog/2013/06/25/java-deep-copy/ JAVA中的浅拷贝和深拷贝(shallow copy and deep co ...
- Python中的浅拷贝和深拷贝(一看就懂!!!)
浅拷贝和深拷贝一直傻傻的分不清,甚至有时候会理解反.今天就简单总结一下python中的浅拷贝和深拷贝. 总结 最直观的理解就是: 1.深拷贝,拷贝的程度深,自己新开辟了一块内存,将被拷贝内容全部拷贝过 ...
- js中的浅拷贝深拷贝深入理解
举个例子来说明一下什么是浅拷贝什么是深拷贝 浅拷贝 var x = {a: 1,b: {f: {g: 1}},c: [1, 2, 3] }; var y = shallow(x); 得出的结果可以看出 ...
- Java中的浅拷贝与深拷贝
一.引用拷贝与对象拷贝 class Person implements Cloneable{private String name;private int age;...省略get和set方法 pro ...
- 43 JavaScript中的浅拷贝与深拷贝
技术交流QQ群:1027579432,欢迎你的加入! 欢迎关注我的微信公众号:CurryCoder的程序人生 1.浅拷贝与深拷贝 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用. 深拷贝拷贝多层,每 ...
- C#中的浅拷贝和深拷贝
C#中有两种类型变量,一种是值类型变量,一种是引用类型变量.对于前者,copy是属于全盘复制:而对于后者,一般的copy只是浅copy,相当于只传递一个引用指针一样.因此对于后者进行真正copy的时候 ...
- python浅拷贝_Python中的浅拷贝和深拷贝
本文翻译自copy in Python (Deep Copy and Shallow Copy),讲述了在Python语言中浅拷贝与深拷贝的不同用法.全文系作者原创,仅供学习参考使用,转载授权请私信联 ...
- clone是深拷贝还是浅拷贝_Python中的浅拷贝和深拷贝
本文翻译自copy in Python (Deep Copy and Shallow Copy),讲述了在Python语言中浅拷贝与深拷贝的不同用法.全文系作者原创,仅供学习参考使用,转载授权请私信联 ...
最新文章
- 如何修改pdf文件的背景色
- 百度母婴技术团队—基于Reactjs实现webapp #1
- iOS开发之打电话,发短信,发送邮件
- 将x的二进制最后一位置为0
- Pytorch初学实战(一):基于的CNN的Fashion MNIST图像分类
- Android 优化布局层次结构
- matlab 2018 ccs,Matlab2018a 与ccs7生成tms320F2812代码调试记录
- ftp 服务器创建访问连接抱错_如何用固定IP连接FTP服务器?
- selinium如何多线程_求教个selenium+grid+testng多线程运行的问题
- 从技术上分析铁路售票系统
- 周立功USBCAN-2E-U的驱动安装及上位机安装
- Vue系列之-Idea进行Vue前端调试
- 【mysql】mysql表分区、索引的性能测试
- 51单片机数码管显示数字
- 计算机中求声音传输时间公式,计算机常用计算公式汇总
- 编译工具各种报错(步骤正确,代码正确,配置正确)均可认为无解,除非...
- 微信小程序 某个页面直接返回首页(微信小程序回退到首页)
- 基于ssm高校科研成果管理系统 java ideamysql
- windows下的ubuntu盘符问题
- 考研数据结构(2)笔记
热门文章
- 自学python还是报班-学习Python到底是培训还是自学合适呢?
- python免费试听-哈尔滨Python人工智能课程免费试听
- python培训好学吗-人工智能“速成班”Python好学吗 小心别被忽悠了
- python中文教程-中谷python中文视频教程(全38集)
- python经典案例-20个Python练手经典案例,能全做对的人确实很少!
- LeetCode209 长度最小的子数组(二分法)
- vim中搭建与sourceinsight类似功能
- phpstorm 控制台运行
- 使用dotnet-dump 查找 .net core 3.0 占用CPU 100%的原因
- Noip2016换教室