js数据类型及复制引用

类型与值

虽然js/es中声明变量的时候并不能限定变量的数据类型,然而实际上值的类型就是该变量的数据类型(虽说中途可以换成其他任意数据类型的值)。

内置类型

js目前有七种内置数据类型:undefinednullnumberstringbooleanobjectsymbol(ES6新增);其中除了object之外的都为基本类型(也叫原始类型,Primitive Type),与基本类型相对的就是复杂类型(即引用类型);因为object是由多个无序键值对组成的,这些值可以是基本类型也可以是object

JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。——《你不知道的JavaScript(中卷)》

typeof操作符

typeof操作符可以用来判断变量或表达式的类型,有以下几种可能:

变量值的类型或状态 typeof 返回值
值未定义 “undefined”
布尔值 “boolean”
字符串 “string”
数值 “number”
对象或null “object”
函数 “function”
Symbol类型(ES6) “symbol”

为何typeof null的结果是 “object”,这是js建立以来就存在的一个bug

有些时候, typeof 操作符会返回一些令人迷惑但技术上却正确的值。比如,调用 typeof null会返回”object”,因为特殊值 null 被认为是一个空的对象引用。——《JavaScript高级程序设计(第三版)》

1
typeof null === 'object'; // 从一开始出现JavaScript就是这样的

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了”object”。


ECMAScript提出了一个修复(通过opt-in),但被拒绝。这将导致typeof null ===’object’。
——MDN


typeof Object的结果并不是想象的'object',而是'function';可以这么理解,Object本身是一个构造器函数,而不是一个具体的对象实例,只有对象实例的typeof结果为'object'

1
2
3
4
5
6
7
8
9
var obj = new Object();
var fun = new Function();
var arr = new Array();

console.log(typeof Object); // 'function'
console.log(typeof Function); // 'function'
console.log(typeof obj); // 'object'
console.log(typeof fun); // 'function'
console.log(typeof arr); // 'object'
要注意的是,不是所有属于对象实例范畴的typeof结果必定就是'object',函数的实例从某种意义来说就是Function的实例,但是结果是'function'

值的复制与引用

复制:即复制一个值的复本,即从值的内存复制到一块新的内存,但是值的内容是一致的;

引用:指向该值,并非指向该值的内存,不是所谓的指针!

JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系。

JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

——《你不知道的JavaScript(中卷)》

在js中,值在进行赋值和传递的过程中,会根据值的类型不同来选择到底是复制复本还是引用;若是基本类型,在赋值/传递过程中是对值进行了复制,若是引用类型则直接对值进行引用;如:

1
2
3
4
5
6
7
8
9
10
var a = 2;
var b = a; // b是a的值的一个复本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push(4); // 通过引用的值本身的方法使值发生了改变,因而c,d指向的值也就改变了
c; // [1,2,3,4]
d; // [1,2,3,4]

简单值(即标量基本类型值, scalar primitive) 总是通过值复制的方式来赋值 / 传递,包括null、 undefined、字符串、数字、布尔和 ES6 中的 symbol值。

复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。

——《你不知道的JavaScript(中卷)》

要注意的是,引用仅仅只是对值的引用,而该变量并不等同于该值的内存地址,所以直接改变该变量的引用指向并不能改变其它变量对该值的引用,如:

1
2
3
4
5
6
7
8
var a = [1,2,3];
var b = a; // 由于a的值是引用类型,所以b直接引用了a的值
a; // [1,2,3]
b; // [1,2,3]
// 然后
b = [4,5,6]; // b的指向发生了改变,但并不能改变a对[1,2,3]的引用指向!
a; // [1,2,3]
b; // [4,5,6]

向函数中传递参数时,实际上就是把实参赋值给形参!因而实参值的类型决定了是复制还是引用,这就是js中的参数传递。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 然后
x = [4,5,6]; // x的指向发生改变,同理,之前的a对值的指向并没改变!
x.push( 7 );
x; // [4,5,6,7]
}

var a = [1,2,3];
foo(a);
// 传递参数过程相当于:var x = a;
// 由于a的值为引用类型,所以把a传递给形参x时,x就直接用了a的值
a; // 是[1,2,3,4],不是[4,5,6,7]

引用类型的深复制与浅复制

由于引用类型的『特殊性质』,导致我们对于引用类型的赋值和传递可能会产生意想不到的副作用,所以我们平时在对引用类型的值进行赋值或传递的时候,一定要思考『我们需要的是深复制还是浅复制?』。

深复制:即重新开辟一块内存,将值的内容复制到新的内存中,即新的值与原先的值不在同一内存了!需要注意的事,深复制必须是每个层级的属性值都要深复制,比如某对象的属性也为对象,对这个属性如果直接浅复制的话,则并没有完成深复制!

浅复制浅复制就是对值的引用,即不会重新开辟新的内存,复制后指向的是同一个值;在js中直接对引用类型进行赋值或传递就是浅复制!

首先深复制和浅复制只针对像 Object, Array 这样的复杂对象的。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。

——https://www.zhihu.com/question/23031215/answer/46220227

因此对引用类型进行深复制不会造成改变当前对象的属性值会影响到原对象的属性值的情况,然而浅复制有这种风险!

PS:有一次我做了一道前端面试题中要求对引用类型进行复制,然而我以为可以直接用var obj2 = new Object(obj)的方法就可以实现深复制,结果实践证明大错特错!该方法也是直接对原对象进行引用:

1
2
3
4
5
var obj = {a: "1"};
var bar = new Object(obj);
bar.a = "xxf";

console.log(bar, obj); // { a: 'xxf' } { a: 'xxf' }

如何实现引用类型的深复制?

由于引用类型存在着嵌套引用类型的情况,也就是需要根据每一层级的情况来逐一判断,单纯的遍历可能无法解决所有的层级,一般需要用到递归;

P.s:不知道有没有一种不需要循环递归且能适用于所有引用类型的深复制方法?

1. Object.assign()方法

assign是ES6新增的Object方法,用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target);如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

1
Object.assign(target, source1, ..., sourceN);

会返回合并后的对象?可以用assign方法进行一层复制,即若该对象的某一属性为引用类型,那么实现的还是浅复制(引用)!如:

1
2
3
4
5
6
7
8
9
10
let target = {a: 1, b: 'xxf'};
let source = {a: 4, c: {name: 'n'}};

Object.assign(target, source);

console.log(target); // { a: 4, b: 'xxf', c: { name: 'n' } }

target.c.name = 'my';

console.log(source); // { a: 4, c: { name: 'my' } }

2. JSON方法

使用JSON.parse()JSON.stringify()方法也能实现一定程度的深复制,只不过无法复制对象的函数属性,且无法继承原对象的原型链!如:

1
2
3
4
5
6
7
8
9
10
11
let source = {a: 4, c: {name: 'n'}, b: function () {

}};

let target = JSON.parse(JSON.stringify(source));

console.log(target); // { a: 4, c: { name: 'n' } }

target.c.name = 'my';

console.log(source); // { a: 4, c: { name: 'n' }, b: [Function: b] }

3. 递归处理

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
39
function deepClone(data) {
var obj;

switch(data.constructor){ // 使用构造器函数对不同类型的对象进处理
case Array: // array和object类型的数据需要对嵌套进行处理
obj = [];
for(var i in data){
obj.push(deepClone(data[i]));
}
break;
case Object:
obj = {};
for(var k in data){
obj[k] = deepClone(data[k]);
}
break;
default: // 默认的基本数据类型和function直接赋值
obj = data;
break;
}

return obj;
}

var o1 = {a: 1, b:[2, 3, 4], c: {name: "xxf"}, d: function () {

}};
var o2 = deepClone(o1);

console.log(o2); // { a: 1, b: [ 2, 3, 4 ], c: { name: 'xxf' }, d: [Function: d] }

o2.c.name = "xyz";
o2.b[1] = 1.5;
o2.d = function () {
return 123;
};

console.log(o1, o2.d === o1.d);
// { a: 1, b: [ 2, 3, 4 ], c: { name: 'xxf' }, d: [Function: d] } false

参考资料

  1. 你不知道的JavaScript(中卷)—— 第二章