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(_);
AI 代码解读
这段代码就是要把我们在_
上绑的很多方法浅拷贝一份到_.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());//0
a = 2;
console.log(a.id());//1
a = "hello world";
console.log(a.id());//2
</script>
</body>
</html>
AI 代码解读
这里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"}
AI 代码解读
从这个例子我们可以看出,对于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"}
AI 代码解读
在这个例子中我们对于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'),
}
AI 代码解读
有了类型检测函数,下面我们就可以开心的做深拷贝了.
浅拷贝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());//3
console.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
AI 代码解读
从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 copy
result[key] = extend(result[key] || [], copy);
}else if(is.isObject(copy)){
//Object deep copy
result[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());//3
console.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());//7
o2.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 == "就不改你"
AI 代码解读