ECMAScript变量可能包含两种不同数据类型的值:基本类型和引用类型。
基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。
基本类型和引用类型的概念
1.基本类型
基本的数据类型有:`undefined,boolean,number,string,null
基本类型的访问是按值访问的,就是说你可以操作保存在变量中的实际的值。
1.1赋值基本类型变量
基本类型的数据是存放在栈内存中的,如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上,两个变量之间相互独立,互不影响。
我们来看这段代码:
var num1 = 5;
var num2 = num1;
console.log(num2) //5
num2 = 3;
console.log(num2) //3
console.log(num1) //5
在此,num1 中保存的值是 5,当使用 num1 的值来初始化 num2 时,num2 中也保存了值 5。
但 num2 中的 5 与 num1 中的 5 是完全独立的,此后这两个变量可以参与任何操作而不会相互影响。
将num2的值更改为3时,num1的值仍是5(当复制的目标是引用类型时,则不同)。下图展示了基本数据类型在复制过程中js变量对象的变化。
2.引用类型
javascript中除了上面的基本类型(number,string,boolean,null,undefined)之外就是引用类型了。
也可以说是就是对象了,对象是属性和方法的集合也就是说引用类型可以拥有属性和方法,属性又可以包含基本类型和引用类型。
引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存中的位置, 也就是说不能直接操作对象的内存空间。
在操作对象时,实际上是在操作对象的引用而不是实际的对象。
2.1赋值引用类型变量
基本类型的数据是存放在堆内存中的,当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到 为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆内存中的一 个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另 一个变量,如下面的代码所示:
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); //"Nicholas"
首先,变量obj1保存了一个对象的新实例。然后,这个值被复制到了obj2中;换句话说,obj1 和 obj2 都指向同一个对象。这样,当为 obj1 添加 name 属性后,可以通过 obj2 来访问这个属性, 因为这两个变量引用的都是同一个对象。下面的数组赋值也是同样的原理。
var array1 = [1,2,3,4];
var array2 = array1;
array2[0] = 0;
console.log(array1) //[0, 2, 3, 4]
下图展示了保存在变量对象中的变量和保存在堆中的对象之间的这种关系。
2.2如何解决引用类型赋值的问题呢?
解决的办法:新建一个新的对象来保存需要赋值的对象数据。
2.2.1数组
针对简单的数组对象,可以采取遍历的方式,将要赋值数组元素重新写入到新定义的数组变量中即可解决问题:
var array1 = [1,2,3,4];
var array2 = [];
array1.forEach(function(item){
array2.push(item);
});
console.log(array2) //[1,2,3,4]
array2[0] = 0;
console.log(array1) //[1,2,3,4]
除此之外还有更简单的方法,使用Array对象的方法:slice函数 、 concat函数,JQuery的$.makeArray()函数
//slice方法
var array1 = [1,2,3,4];
var array2 = array1.slice(0);
console.log(array2) //[1,2,3,4]
array2[0] = 0;
console.log(array2) //[0,2,3,4]
console.log(array1) //[1,2,3,4]
//connact方法
var array1 = [1,2,3,4];
var array2 = array1.concat([]);
console.log(array2) //[1,2,3,4]
array2[0] = 0;
console.log(array2) //[0,2,3,4]
console.log(array1) //[1,2,3,4]
// $.makeArray方法
var array1 = [1,2,3,4];
var array2 = $.makeArray(array1);
console.log(array2) //[1,2,3,4]
array2[0] = 0;
console.log(array2) //[0,2,3,4]
console.log(array1) //[1,2,3,4]
三个函数的原理都是返回数组的一个副本(相当于另外开辟内存空间),所以并不会改变数组本身的的值
2.2.2对象
其实JavaScript 中的所有事物都是对象:,包括上面的数组,字符串、数值、函数...
针对对象的赋值采取的方法也是遍历拷贝的方式,但是要值得注意的是,这里的拷贝分为浅拷贝和深拷贝,在上述数组对象中针对数组元素是简单数据类型我采取的方式均称为浅拷贝。那么到底和为浅拷贝和深拷贝呢?
浅拷贝:针对对象属性的值是基本数据类型,比如数组的元素或者对象的的值是Number型,String型。
我们来看下面一段代码
//copy函数
function copy(obj){
var newObj = {};
for ( var key in obj) {
newObj[key] = obj[key];
}
return newObj;
}
var obj1 = {
name:'Bob',
country:'America'
};
var obj2 = copy(obj1);
console.log(obj2) //{name:'Bob',country:'America'}
obj2.country = 'Chinese';
console.log(obj1) //{name:'Bob',country:'America'}
在这里我写了一个复制对象的函数copy,通过for in遍历对象属性,重新绑定到新的对象newObj上,最终返回一新的对象newObj,至此两个对象互不影响。
深拷贝:针对对象属性的值是引用类型,比如数组、对象。
其实上述代码还有一个问题,当对象中的country对象时一个对象或者数组的时候,就会导致存在对象引用的问题。我们队上述代码做一些改动
//copy函数
function copy(obj){
var newObj = {};
for ( var key in obj) {
newObj[key] = obj[key];
}
return newObj;
}
var obj1 = {
name:'Bob',
country:['America','French']
};
var obj2 = copy(obj1);
console.log(obj2) //{name:'Bob',country:['America','French']}
obj2.country[0] = 'Chinese';
console.log(obj1) //{name:'Bob',country:['Chinese','French']} //obj2改变了country的值,导致了obj1中的country也发生改变
这是因为copy函数只是对第一层进行拷贝,无法拷贝深层的对象,这个copy为浅拷贝函数,我们需要通过递归,来拷贝深层的对象。将copy改造成递归函数。
function deepCopy(obj) {
if (typeof obj != 'object') {
return obj;
}
var newObj = {};
if (obj instanceof Array) {
newObj = [];
}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key]);
}
}
return newobj;
}
深拷贝的思路是:先判断参数是不是对象,如果不是对象直接return 返回,如果参数是对象,则新建一个对象,然后在区分是不是Array数组,最后for in遍历。