值类型

每个编程语言都需要变量,而变量都会有类型。在JS中,主要有两种类型:Primitive Value(aka: 基本类型)Object(aka: 引用类型)。基本类型主要有:string、number、boolean、undefined、null、symbol(ES6新增基本数据类型)。而引用类型主要有:Date、Array、Function、RegEpx等等。

基本类型和引用类型的区别

基本类型和引用类型的最大区别在于:基本类型存储的就是该变量在内存中的值,而引用类型存储的是指向内存中存储值的内存的地址。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// example_1.js
let prmValue_1 = 1;
let obj_1 = {
name: 'Barney',
age: 24
};

let prmValue_2 = prmValue_1;
let obj_2 = obj_1;

prmValue_1 = 2;
obj_1 = {
name: 'Billy',
age: 25
};

console.log(prmValue_2); // 1
console.log(obj_2.name, obj_2.age); // Billy 25

同样是在赋值之后,改变赋值对象的原有的值。结果是prmValue_2不受影响,obj_2的值却和重新赋值后的obj_1保持了一致。这就是基本类型和引用类型存储值的方式导致的结果。下面来拆解example_1.js中的代码,来具体解释这段代码的为何会得到这样的结果。

1
2
3
4
5
let prmValue_1 = 1;
let obj_1 = {
name: 'Barney',
age: 24
};

此时他们在内存中的关系如下图所示:

image-20191228155803470

接着,执行以下代码:

1
2
let prmValue_2 = prmValue_1;
let obj_2 = obj_1;

此时,内存中表示如下图所示:

image-20191228160610917

可以看到,prmValue_1prmValue_2分别存放了两个基本类型,值为1。而obj_1obj_2都存放的是同一个内存块的地址的引用。再接着执行下面的代码:

1
2
3
4
5
prmValue_1 = 2;
obj_1 = {
name: 'Billy',
age: 25
};

此时,各个变量在内存中的表示如下图所示:

image-20191228161513489

如图所示,此时的prmValue_1存放的值是2,而obj_1存放的地址引用则指向了另一个内存块。而obj_2的地址引用与obj_1指向的是同一个地址。所以,当obj_1的内存地址指向变化的时候,obj_2也会跟着一起变化。从而得到了上面的结果。

这就是基本类型与引用类型在赋值的时候,内存变化的全过程了。

类型判断

基本类型的判断可以用typeof去判断值类型,但是引用类型用typeof得到结果大部分的都是object,当然也有例外,比如Function

1
2
3
4
5
6
7
8
9
10
11
// example_2.js
typeof 1; // number
typeof '1'; // string
typeof true; // boolean
typeof undefined; // undefined
typeof Symbol(); // symbol

typeof null; // object 有坑,判断null的时候得出的结论是引用类型
typeof {}; // object
typeof []; // object
typeof function func() {}; // function 又一个坑,typeof判断function的时候,得到的结果是function,虽然function其实本身也是一种引用类型

所以,可以看到。用typeof去判断除null以外的基本类型和function可以得知值的具体类型,但是其他就不行了。那么其他怎么去判断呢?

答案是Object.prototype.toString()方法。类型判断结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example_3.js
Object.prototype.toString.call(''); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(newFunction()); // [object Function]
Object.prototype.toString.call(newDate()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(newRegExp()); // [object RegExp]
Object.prototype.toString.call(newError()); // [object Error]
Object.prototype.toString.call(document); // [object HTMLDocument]
Object.prototype.toString.call(window); //[object global] window 是全局对象 global 的引用

这样得到的结果值准确的,但是在可读性和代码复用上很不好。所以,可以全局建一个公共的judge/index.js的文件,然后分别判断每个类型:

1
2
3
4
5
6
7
8
9
10
// judge/index.js
export function isString (param) {
return Object.prototype.toString.call(param) === "[object String]"
}

export function isNull (param) {
return Object.prototype.toString.call(param) === "[object Null]";
}

...

强制类型转换

在ES5以前,强制类型转换是一个很令人苦恼的问题,因为==、>=、<=、<、>等操作符会把左右两边不是同一个类型的值强制转换成同一个类型,然后去做比较。即隐式强制类型转换。比如

1
2
3
13 == '13'; // true
10 + '1' <= '9'; // true 因为10 + 1 => '101', '101' < '9' 是这么字符串中的顺序比较ASCII值。而1的ASCII小于9,所以'101' < '9'的结果是true。
true == 1; // true

因为这一系列的隐式强制类型转换,可能会导致我们意想不到的结果,所以ES6引入了===操作符,===不会在比较左右两边的值的时候,将两边的值类型强制转换成同一个类型。因为===是比较value && type的。隐式强制类型转换有很多坑,可以单开,这里先埋坑。

除了隐式强制类型转换,还有显示强制类型转换,强制类型转换是我们主动去调用一些方法,将一个值类型转换成我们预期的值类型的方法,这种方法是可控的,所以,相较于隐式强制类型转换来说,显示强制类型转换安全的多。

1
2
3
4
5
6
7
8
9
10
11
// 显示强制类型转换
Boolean(''); // false
Boolean(null); //false
Boolean(undefined); // false
String(1); // '1'
String(null); // 'null'
String(undefined); // 'undefined'
Number('1'); // 1
Number(undefined); // 'NaN'
Number(null); // 0
Number('string'); // 'NaN'

可以看到,强制类型转换只存在于基本类型之间的转换,不要在非基本类型之间做强制类型转换,因为结果不可控。