JavaScript 内功心法——数据类型

前端必备内功之数据类型

Posted by Shiyanping on November 2, 2018

一、数据类型概况

1.1 原始类型

原始类型:undefined,null,boolean,string,number,symbol

原始类型是没有函数可以调用的,比如 undefined.toString()

但是很多人就会有疑问为什么 '1'.toString() 可以转换,其实这种情况 '1' 已经被强制转换了,调用的是 String 类型的 toString 方法,其实 String 类型是对象类型。

其实原始类型在 JS 中还有很多坑,比如 0.1 + 0.2 !== 0.3,这个是因为 JS 中 number 类型是浮点类型的,所以会出现这样的 bug。

还有一个问题,就是 null 本身是原始类型,为什么使用 typeof null 的时候,判断出来的是 object 呢?因为 JS 最初的版本使用的 32 位系统,考虑性能问题,使用了低位存储变量,000 开头表示对象,null 表示为全零,所以错误的判断成了 object。

1.2 对象类型

JS 中除了原始类型,其他的都是对象类型。

原始类型存储的是值,但是对象类型存储的是地址(指针),当我们创建一个对象时,JS 就会在内容寸分配一个空间来存放值。

1
const test = [];

当我们定义个数组,赋值给 test 的时候,内存会给数组分配一个地址(可以理解为门牌号),暂时定义为 #001,test 变量就指向到这个 #001 的地址。

1
2
3
const a = [];
const b = a;
b.push(1);

这个时候 a 指向 #001 这个地址,b 也指向这个地址,当我们给 b 添加一个元素的时候,其实是给这个地址上添加了值,所以就 a 和 b 两个值其实都发生了变化。

我们再来看一个问题,当我们将一个对象类型当做函数参数传入之后会有什么效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test(o) {
  o.age = 18;
  o = {
    name: 'test2'
  };
  return o;
}

var obj = {
  name: 'test'
};

console.log('外部的对象', obj); // {name: "test"}
const obj2 = test(obj);
console.log('被修改的外部对象', obj); // {name: "test", age: 18}
console.log('新的对象', obj2); // {name: "test2"}

为什么在函数中修改传入的对象类型参数,会将外部引用的变量也改变呢,如果给变量重新赋值,输出的为什么又是另外一个变量,为什么不会对外部的变量进行改变呢?

我们抱着疑惑来看一下为什么,具体原因是:

  1. 当我们给函数传参时,其实传递的是对象的地址副本。
  2. 在函数中修改地址副本,所以引用这个地址的变量值都会改变。
  3. 当我们重新给变量进行赋值时,其实将这个变量引用到了一个新的地址上。

二、类型判断

我们在想判断变量类型时,可以使用 typeof 来判断数据类型,但是判断出来的类型有的时候不尽如人意。

如:

1
2
3
4
5
6
7
8
9
typeof undefined; //undefined
typeof Symbol(); // 'symbol'
typeof 123; //number
typeof '123'; //string
typeof true; //boolean
typeof [1, 2, 3]; //object
typeof { id: 11 }; //object
typeof null; //object
typeof console.log; //function

使用 typeof 判断对象类型时,除了 function 以外,其他的判断出来的结果都是 object,并不是我们想要的。

既然使用 typeof 判断对象类型时的结果不尽如人意,那么我们就有了另外的方法去判断对象类型。

使用操作符 instanceof,instanceof 是通过原型链来判断。

1
2
3
4
5
6
7
8
9
function Person() {}
const person = new Person();
person instanceof Person; // true

var str = 'hello world';
str instanceof String; // false

var str1 = new String('hello world');
str1 instanceof String; // true

使用 instanceof 虽然可以通过原型链去判断对象类型,但是在判断原始类型的时候会有问题,如果不是通过构造函数 new XX() 创建的,就会判断失败。

我们可以自己写一个用 instanceof 判断原始类型的方法:

1
2
3
4
5
6
class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === 'string';
  }
}
console.log('hello world' instanceof PrimitiveString); // true

你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。

instanceof 经过我们自己的改造可以判断原始类型之后,其实判断函数也会有问题。

1
2
3
const a = function() {};
a instanceof Function; //true
a instanceof Object; //true

在判断函数时,就会发生上面的结果。因为 function 的实例类型既是 Function,也是 Object。

所以我们就要想另外一种方法了,可以使用 Object.prototype.toString.call() 获取一个对象的类型,通过借用 Objcet 原型链上的 toString() 方法,将对象或者常规的变量进行字符串格式化,然后判断类型。

1
2
3
4
5
6
7
8
9
var toString = Object.prototype.toString;

toString.call(new Date()); // [object Date]
toString.call(new String()); // [object String]
toString.call(Math); // [object Math]
toString.call(/s/); // [object RegExp]
toString.call([]); // [object Array]
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]

三、类型转换

js 中的数据类型可以进行转换,转换的方式也有很多,但是大体上主要是分为显式转换和隐式转换。

3.1 显式转换

  • Number 方法

    1. 数字:转换后还是数字
    2. 字符串:如果可以被解析为数值,则为相应的数值,如果不能,则是 NaN,如果是空字符串那就是 0
    3. 布尔值:true 为 1,false 为 0
    4. undefined:NaN
    5. null:0 6.对象:先执行 object 原型上的 valueOf 方法,看是否能转换,如果不可以再执行原型链上的 toString,看是否可以转换,如果不可以报错
  • String 方法

    1. 数字:转换成对应的字符串
    2. 字符串:还是对应的字符串
    3. 布尔值:true 为’true’,false 为’false’
    4. undefined:undefined
    5. null:null
    6. 对象:先执行 toString 方法,看是否能转换,如果不可以再执行 valueOf,看是否可以转换,如果不可以报错

在对 object 类型进行转换时,我们会调用 toStringvalueOf 方法,这个时候我们也可以自己重写 toStringvalueOf 方法。

  • Boolean 方法

    NaN,null,undefined,0,-0,”“,false 都会转换成 false,其他的都会被转换成 true。

3.2 隐式转换

在使用四则运算比较运算符时都会进行隐式转换。

四则运算:

加法运算符不同于其他几个运算符,它有以下几个特点:

  • 如果两个值都是 number 类型,那就直接相加
  • 如果两个值有一方是字符串,那就将另外一方也转换成字符串
  • 如果一方不是字符串或者数字(对象,数组),那么会将它们转换为数字或者字符串,在转换的过程中会调用 valueOf 和 toString 方法,对于+号,是先调用 valueOf,然后 toString。

常规的操作:

1
2
3
1 + '1'; // '11'
true + true; // 2
4 + [1, 2, 3]; // "41,2,3"

我们再来一些比较奇葩的转换:

1
2
3
4
5
6
7
[] + []  // "",将空数组转换成了 ""
'a' + + 'b' // aNaN,会先看 + 'b' 这个表达式,转换为 NaN,所以 'a' + NaN 就是aNaN
{} + []  // 0,这个时候 {} 在 +号之前,会被看做是一个代码块,最终结果是直接转换 [] 为 0
[] + {}  // "[object Object]",[] 转换为 0,{} 调用对象的 toString 方法,转换成字符串,因为一方是字符串,所以 [] 进行了两次转换,[] -> 0 -> ''。
{} + {} // "[object Object][object Object]"
true + true // 2
1 + {a:1}  // "1[object Object]"

除了加法运算以外,其他的运算,只要有一放是数字,那么另一方就会被转换成数字。

1
2
3
4
4 * '3'; // 12
1 - '1'; // 0
4 * []; // 0
4 * [1, 2]; // NaN

比较运算符:

  • 如果是对象,就通过 valueOf 和 toString 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比较
1
2
3
4
5
6
7
8
9
let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  }
};
a > -1; // true

a 是对象,所以先通过 valueOf 转换成原始值再进行比较。