一、ECMAScript 的数据类型
本文主要讲一下 js 的基本数据类型以及一些堆和栈的知识和什么是深拷贝、什么是浅拷贝、深拷贝与浅拷贝的区别,以及怎么进行深拷贝和怎么进行浅拷贝。
本文思维导图如下:
堆和栈的区别
其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。
堆和栈都是内存中划分出来用来存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
基本数据类型
基本数据类型主要是:undefined,boolean,number,string,null。
① 基本数据类型存放在栈中
存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。
② 基本数据类型值不可变
javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的,例如:
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc
这一点其实开始我是比较迷惑的,总是感觉 js 是一个灵活的语言,任何值应该都是可变的,真是图样图森破,我们通常情况下都是对一个变量重新赋值,而不是改变基本数据类型的值。就如上述引用所说的那样,在 js 中没有方法是可以改变布尔值和数字的。倒是有很多操作字符串的方法,但是这些方法都是返回一个新的字符串,并没有改变其原有的数据。
所以,记住这一点:基本数据类型值不可变。
③ 基本类型的比较是值的比较
基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的,例如:
var a = 1;
var b = 1;
console.log(a === b);//true
比较的时候最好使用严格等,因为 ==
是会进行类型转换的,比如:
var a = 1;
var b = true;
console.log(a == b);//true
引用类型
① 引用类型存放在堆中
引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配,例如。
var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};
② 引用类型值可变
引用类型是可以直接改变其值的,例如:
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
③ 引用类型的比较是引用的比较
所以每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。例如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。
二、浅析一波深浅拷贝
传值与传址
了解了基本数据类型与引用类型的区别之后,我们就应该能明白传值与传址的区别了。
在我们进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。例如:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10
所以说,基本类型的赋值的两个变量是两个独立相互不影响的变量。
但是引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。例如:
var a = {}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象
a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true
浅拷贝
在深入了解之前,我认为上面的赋值就是浅拷贝,哇哈哈,真的是图样图森破。上面那个应该只能算是“引用”,并不算是真正的浅拷贝。
一下部分参照知乎中的提问: javascript中的深拷贝和浅拷贝
浅拷贝实现的几种方法:
- 简单的引用复制
- Object.assign()
- Array.slice()和Array.concat()
- ES6中的拓展运算符
1.简单的引入复制
function shallowClone(copyObj) {
var obj = {};
for ( var i in copyObj) {
obj[i] = copyObj[i];
}
return obj;
}
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f); // true
2.Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true
3.Array的slice和concat方法
Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:
- 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
- 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
如果向两个数组任一中添加了新元素,则另一个不会受到影响。例子如下:
var array = [1,2,3];
var array_shallow = array;
var array_concat = array.concat();
var array_slice = array.slice(0);
console.log(array === array_shallow); //true
console.log(array === array_slice); //false,“看起来”像深拷贝
console.log(array === array_concat); //false,“看起来”像深拷贝
可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出Array的concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。如下:
var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
array_concat[1][0] = 5; //改变array_concat中数组元素的值
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]
array_slice[2].name = "array_slice"; //改变array_slice中对象元素的值
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
4.ES6中的拓展运算符
展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。
let oldObj = { type: { id: 1 } }
let newObj = { ...oldObj } // 此时拷贝了{id : 1}的引用地址
oldObj.type.id = 2 // 改变引用对象里面的值
console.log(oldObj) // {type: {id: 2}}
console.log(newObj) // {type: {id: 2}} 随着oldObj的改变而改变
oldObj.type = { id: 3 } // 改变引用的对象,实际改变了引用对象的地址
console.log(oldObj) // {type: {id: 3}}
console.log(newObj) // {type: {id: 2}} 不随着oldObj的改变而改变
赋值(=)和浅拷贝的区别(番外)
那么赋值和浅拷贝有什么区别呢,我们看下面这个例子:
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};
先定义个一个原始的对象 obj1,然后使用赋值得到第二个对象 obj2,然后通过浅拷贝,将 obj1 里面的属性都赋值到 obj3 中。也就是说:
- obj1:原始数据
- obj2:赋值操作得到
- obj3:浅拷贝得到
然后我们改变 obj2 的 name 属性和 obj3 的 name 属性,可以看到,改变赋值得到的对象 obj2 同时也会改变原始值 obj1,而改变浅拷贝得到的的 obj3 则不会改变原始对象 obj1。这就可以说明赋值得到的对象 obj2 只是将指针改变,其引用的仍然是同一个对象,而浅拷贝得到的的 obj3 则是重新创建了新对象。
然而,我们接下来来看一下改变引用类型会是什么情况呢,我又改变了赋值得到的对象 obj2 和浅拷贝得到的 obj3 中的 language 属性的第二个值和第三个值(language 是一个数组,也就是引用类型)。结果见输出,可以看出来,无论是修改赋值得到的对象 obj2 和浅拷贝得到的 obj3 都会改变原始数据。
这是因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。所以就会出现改变浅拷贝得到的 obj3 中的引用类型时,会使原始数据得到改变。
深拷贝:将 B 对象拷贝到 A 对象中,包括 B 里面的子对象
浅拷贝:将 B 对象拷贝到 A 对象中,但不包括 B 里面的子对象
类型 | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
深拷贝
顾名思义,深拷贝就是完完整整的将一个对象从内存中拷贝一份出来。所以无论用什么办法,必然绕不开开辟一块新的内存空间
结合上面浅拷贝,我们来画个图对比一下来引入我们的深拷贝:
浅拷贝:
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
那实现深拷贝有以下几种方法:
- 序列化反序列化法
JSON.parse(JSON.stringify())
- JQ的extend方法
$.extend( [deep ], target, object1 [, objectN ] )
- lodash 中的深拷贝
_.cloneDeep( )
- 递归实现
1.JSON.parse(JSON.stringify())
JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝。
let test = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function() {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
err: new Error('我是一个错误')
}
console.log(JSON.parse(JSON.stringify(test)))
结果图示:
总结:
这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于 正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。 还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。
2.JQ中的extend $.extend( [deep ], target, object1 [, objectN ] )
extend方法中第一个参数为boolean类型,且为true的话为进行深拷贝,第二个参数就是拷贝对象,第三个之后的参数为被拷贝的对象
let newObj = $.extend(true,{},test)
console.log(newObj)
结果图示:
总结:
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
3.lodash 中的深拷贝 _.cloneDeep( )
我们把测试用例用到的深拷贝函数换成 lodash 的:
console.log(_.cloneDeep(test))
结果图示:
总结:
著名的 lodash 中的 cloneDeep 方法同样是使用这种方法实现的,只不过它支持的对象种类更多,具体的实现过程读者可以参考 lodash 的 baseClone 方法
4. 递归实现
自己动手实现一个拷贝方法(简易版及复杂版)
简易版
let deepCopy = function (obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
let newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 如果obj的子属性是对象,则进行递归操作,否则直接赋值
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
复杂版:
var $ = (function(){
var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');
function type() {
return Object.prototype.toString.call(this).slice(8, -1);
}
for (var i = types.length; i--;) {
$['is' + types[i]] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(types[i]);
}
return $;
})();//类型判断
function copy(obj,deep){
if(obj === null || typeof obj !== "object"){
return obj;
}
var name, target = $.isArray(obj) ? [] : {}, value;
for(name in obj){
value = obj[name];
if(value === obj) {
continue;
}
if(deep && ($.isArray(value) || $.isObject(value))){
target[name] = copy(value,deep);
}else{
target[name] = value;
}
}
return target;
}