Javascript数据基本类型与引用类型

JavaScript 数据类型

说起Javascript的数据类型,通常我们会看到下面两种分类。

字符串、数字、布尔、数组、对象、Null、Undefined(w3c)

还有一种经常看到的就是把数组归类在对象中(这大概也是大家通常认为的吧):

数值(number):整数和小数(比如1和3.14)
字符串(string):字符组成的文本(比如”Hello World”)
布尔值(boolean):true(真)和false(假)两个特定值
undefined:表示“未定义”或不存在,即此处目前没有任何值
null:表示空缺,即此处应该有一个值,但目前为空
对象(object):各种值组成的集合

现在又要多加一种了,ES6新增 符号(symbol)

其中对象又可以分为:

狭义的对象(object)
数组(array)
函数(function)

js的数据类型根据在内存中的存储方式,又可以分为两类:

(1)值类型:数值、布尔值、null、undefined、String。
(2)引用类型:对象、数组、函数。

js基本数据类型有: number、string、boolean、undefined、null,对于基本类型,它们在内存中占有固定的大小空间,通过值的形式保存在桟内存中,我们通过按值来访问。

对于引用类型的赋值,需要在堆内存中为这个值分配空间。由于值的大小不固定(数组大小、对象属性和方法个数都不确定),因此不能把把它们保存在桟内存中。但是我们可以在桟内存中保存值的内存地址,因为内存地址大小是固定的。引用类型对象存储的实际是它们的引用地址,对象的实际内容单独存放,因为引用对象通常比较庞大,这是数据开销和内存开销优化的手段。

简而言之,堆内存存放引用值,栈内存存放固定类型值。“引用”是一个指向对象实际位置的指针。

举个栗子

1、基本数据类型

1
2
3
4
5
6
7
8
9
10
var a = 1;
var b = a;
a = 2;
console.log(a);// 2
console.log(b);// 1
var c = '123';
var d = c;
c = '1234';
console.log(c);//'1234'
console.log(d);//'123'

2、引用类型

在这里需注意的是,引用指向的是具体的对象,而不是另一个引用。

1
2
3
4
5
6
var a = { 'name': 'a'};
var b = a; //此时b.name为'a'
a.name = 'c'; //b.name = a.name = 'c'
b.name = 'd'; //b.name = a.name = 'd'
console.log(a.name);// d
console.log(b.name);// d

对象的克隆:

1
2
3
4
5
6
7
8
9
10
var a = { 'name': 'a','age':'1'};
var b = {};
for(var key in a){
b[key] = a[key];
}
b.name = 'b';
console.log(a);//{ 'name': 'a','age':'1'}
console.log(b);//{ 'name': 'b','age':'1'}
console.log(a.name);// a
console.log(b.name);// b

再来看另一个栗子:

1
2
3
4
5
var a = { 'name':'a'};
var b = a;
a = 1; //此时a已经不是引用类型
console.log(b.name); // a
console.log(a.name); //undefined

关于数组的一个栗子:

1
2
3
4
5
var a = ['1','2','3'];
var b = a;
b.push('4');
console.log(a); // ['1','2','3','4']
console.log(b); // ['1','2','3','4']

怎样实现对一个数组的克隆,同时操作克隆对象又不影响原数组呢?

1
2
3
4
5
6
7
8
9
var a = ['1','2','3'];
var b = [];
var n = a.length;
for(var m = 0; m < n; m++){
b.push(a[m]);
}
b.push('4');
console.log(a); // ['1','2','3']
console.log(b); // ['1','2','3','4']

关于函数的一个栗子

1
2
3
4
5
6
7
8
9
10
11
var a = function(){
var x = 0;
console.log(x);
}
var b = a;
b = function(){
var x = 1;
console.log(x);
}
a();// 0
b();// 1

基于上面的例子,有人总结了下面的内容:

函数的克隆,与其它引用对象有所不同,利用’=’赋值即可实现克隆。对克隆对象的操作,不会影响原对象。这是因为克隆之后的对象会单独复制一次并存储实际数据,属于真正的克隆。

但应该注意:

1
2
3
4
5
6
7
8
9
var a = function(){
var x = 0;
console.log(x);
}
a.test = 'a';
var b = a;
a.test = 'c';
console.log(a.test);//c
console.log(b.test);//c

如何实现完整的对象克隆?

完整的对象克隆,包括克隆普通对象、引用对象。因此实现完整克隆时,需要对克隆对象进行判断。对于基本类型和引用类型的方法不同。对于引用对象的完整克隆(深度克隆/对象的深度克隆/对象的深度复制,不同的称谓),包括对象的值也是一个对象也要进行完整克隆。

其实简单理解,可以认为我们需要创建一个对象b,使它与a有一样的属性和方法,但b的操作又不能影响a。

引用网上一个深度克隆的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function clone(obj) {
var o, i, j, k;
if (typeof(obj) != "object" || obj === null) return obj;
if (obj instanceof Array) {
o = [];
i = 0;
j = obj.length;
for (; i < j; i++) {
if (typeof(obj[i]) == "object" && obj[i] != null) {
o[i] = arguments.callee(obj[i]);
} else {
o[i] = obj[i];
}
}
} else {
o = {};
for (i in obj) {
if (typeof(obj[i]) == "object" && obj[i] != null) {
o[i] = arguments.callee(obj[i]);
} else {
o[i] = obj[i];
}
}
}
return o;
}

简化下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone(obj) {
var targetObj = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === 'object') {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === 'object') {
targetObj[key] = deepClone(obj[key]);
}
else {
targetObj[key] = obj[key];
}
}
}
}
return targetObj;
}

同样,我们借用 JSON.parse 和 JSON.stringify 也可以实现深拷贝

1
2
3
4
5
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}

补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Person = function() {};
Person.prototype = {
basic: {
Name: "xiaoming"
},
sayHello: function() {
console.log("Hello,I'm " + this.basic.Name)
}
}
var p1 = new Person();
p1.sayHello();// Hello.I 'm xiaoming
var p2 = new Person();
p2.sayHello();// Hello, I 'm xiaoming
p1.basic.Name = "xiaowang";
p1.sayHello();// Hello, I, m xiaowang
p2.sayHello();// Hello, I, m xiaowang

可能会疑问,改变了p1的属性,为什么p2的也改变了。这就得从函数的基于原型链实现继承中对属性和方法的访问说起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var Person = function() {};

//重写函数原型为basic/sayHello
Person.prototype = {
basic: {
Name: "xiaoming"
},
sayHello: function() {
console.log("Hello,I'm " + this.basic.Name)
}
}

//实例化对象
var p1 = new Person();
var p2 = new Person();

//实例本身没有sayHello方法,因此调用原型链上的sayHello方法。
p1.sayHello();// Hello.I 'm xiaoming
p2.sayHello();// Hello, I 'm xiaoming

//修改实例对象的属性和方法时,如果此对象的该属性或方法不存在,只会在该对象上创建该属性,而不是修改原型链上的属性
p1.address = 'beijing';
console.log(p1.address);//beijing
console.log(p2.address);//undefined

p1.basic = 'test';
console.log(p1.basic);// test,p1自己的属性
console.log(p2.basic);//Object {Name: "xiaoming"},p2依旧访问原型链上的属性

//注意了
delete p1.basic;
console.log(p1.basic);//删除了p1的属性,访问原型链上的属性

//更改实例对象在原型链上的属性,如果按照前面的说法,p1可能会创建自己的basic.name属性并赋值为xiaowang。实际情况并非如此。如果实例对象要修改自身的一级属性(p1.basic = "xxx";),并且该属性不存在,那么,会在该实例对象上创建该属性,并赋值该属性。此时,不会影响对原型链上的该属性造成影响。如果实例对象要修改自身的N(N>1)级属性(p1.basic.name = "xxx";)时,若该实例不存在N级前的N-1级中的某个属性,就会到原型链上查找该属性,若未找到,查找下一级原型链,如果到最后都没有找到,给出TypeError错误,如果找到了,那么就在该原型链上修改此属性
p1.basic.Name = "xiaowang";//自身不存在该属性,访问原型链,在原型链上找到该属性,并修改
p1.sayHello();// Hello, I, m xiaowang, 原型链已被修改
p2.sayHello();// Hello, I, m xiaowang, 原型链已被修改
console.log(p1.father.name);//Uncaught TypeError: Cannot read property 'name' of undefined(…)