JavaScript 深入了解基本类型和引用类型

321 阅读6分钟

一个变量可以存放两种类型的值,基本类型的值(primitive values)引用类型的值(reference values)

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:UndefinedNull布尔值(Boolean)字符串(String)、数值(Number)对象(Object)

基本类型

JavaScript 中共有 6 种基本数据类型:UndefinedNullBooleanNumberStringSymbol (new in ES 6)

约定:基本数据类型原始数据类型等意。

基本数据类型的值是按值访问的。

  • 基本类型的值是不可变的

    var str = "123hello321";
    str.toUpperCase();     // 123HELLO321
    console.log(str);      // 123hello321
    
  • 基本类型的比较是它们的值的比较

    var a = 1;
    var b = true;
    console.log(a == b);    // true
    console.log(a === b);   // false
    

    上面 a 和 b 的数据类型不同,但是也可以进行值的比较,这是因为在比较之前,自动进行了数据类型的 隐式转换

    • == : 只进行值的比较
    • === : 不仅进行值得比较,还要进行数据类型的比较
  • 基本类型的变量是存放在栈内存(Stack)里的

    var a,b;
    a = "zyj";
    b = a;
    console.log(a);   // zyj
    console.log(b);   // zyj
    a = "呵呵";       // 改变 a 的值,并不影响 b 的值
    console.log(a);   // 呵呵
    console.log(b);   // zyj
    

图解如下:栈内存中包括了变量的标识符变量的值

image

引用类型

除过上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型Array 类型Date 类型RegExp 类型Function 类型 等。

引用类型的值是按引用访问的。

  • 引用类型的值是可变的

    var obj = {name:"zyj"};   // 创建一个对象
    obj.name = "percy";       // 改变 name 属性的值
    obj.age = 21;             // 添加 age 属性
    obj.giveMeAll = function(){
      return this.name + " : " + this.age;
    };                        // 添加 giveMeAll 方法
    obj.giveMeAll();
    
  • 引用类型的比较是引用的比较

    var obj1 = {};    // 新建一个空对象 obj1
    var obj2 = {};    // 新建一个空对象 obj2
    console.log(obj1 == obj2);    // false
    console.log(obj1 === obj2);   // false
    

    因为 obj1 和 obj2 分别引用的是存放在堆内存中的2个不同的对象,故变量 obj1 和 obj2 的值(引用地址)也是不一样的!

  • 引用类型的值是保存在堆内存(Heap)中的对象(Object)

    与其他编程语言不同,JavaScript 不能直接操作对象的内存空间(堆内存)。

    var a = {name:"percy"};
    var b;
    b = a;
    a.name = "zyj";
    console.log(b.name);    // zyj
    b.age = 22;
    console.log(a.age);     // 22
    var c = {
      name: "zyj",
      age: 22
    };
    

    图解如下:

    • 栈内存中保存了变量标识符和指向堆内存中该对象的指针

    • 堆内存中保存了对象的内容

    image

检测类型

  • typeof:通常检测 基本类型

    1.对于基本类型,除 null 以外,均可以返回正确的结果。 2.对于引用类型,除 function 以外,一律返回 object 类型。 3.对于 null ,返回 object 类型。 4.对于 function 返回 function 类型。

var a; typeof a; // undefined

a = null; typeof a; // object

a = true; typeof a; // boolean

a = 666; typeof a; // number

a = "hello"; typeof a; // string

a = Symbol(); typeof a; // symbol

a = function(){} typeof a; // function

a = []; typeof a; // object a = {}; typeof a; // object a = /aaa/g; typeof a; // object


- [instanceof](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof):通常检测 **引用类型**,**判断 `A` 是否为 `B` 的实例**

```javascript
({}) instanceof Object              // true
([]) instanceof Array               // true
(/aa/g) instanceof RegExp           // true
(function(){}) instanceof Function  // true
  • Object.prototype.toString:通常检测 基本类型 & 引用类型可以准确识别对象值属于哪种内置类型,它比 typeof & instanceof 更加准确。

    [[Class]]是任意内置对象的内部属性,值为一个类型字符串,可以用来判断值的类型。

    在 JavaScript 代码里,唯一可以访问该属性的方法就是通过 Object.prototype.toString ,通常方法如下:

    Object.prototype.toString.call(value)
    
    Object.prototype.toString.call(123) // '[object Number]'
    Object.prototype.toString.call('123') // '[object String]'
    Object.prototype.toString.call(null) // '[object Null]'
    Object.prototype.toString.call(undefined) // '[object Undefined]'
    Object.prototype.toString.call(Math) // '[object Math]'
    Object.prototype.toString.call({}) // '[object Object]'
    Object.prototype.toString.call([]) // '[object Array]'
    ({}).toString() // [object Object]
    

类型转换

因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的 运算 都会先进行类型转换。

幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:

image

在这个里面,较为复杂的部分是 Number 和 String 之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。

1. StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

  • 30;// 十进制 => 30
  • 0b111;// 二进制 => 7
  • 0o13;// 八进制 => 11
  • 0xFF;// 十六进制 => 255
  • 1e3; // 科学计数法 => 1000
  • -1e-2;// 科学计数法 => -0.01

其余类型 => NaN

2. NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。

console.log(1e3 + 'ok'); // 1000ok

3. 装箱转换 (基本类型 => object)

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

注意!装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

比如下面的number并未发生改变,只会生成numberObj临时对象。

方案一:我们可以利用一个函数的 call 方法来强迫产生装箱。

var number = 123;
var numberObj = (function(){ return this; }).call(number);
numberObj instanceof Number; // true
number instanceof Number; // false 

方案二:使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。

var number = 123;
var numberObj = Object(number);
numberObj instanceof Number; // true
number instanceof Number; // false 

4. 拆箱转换 (object => 基本类型)

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o * 2 / o + ''
// valueOf
// toString
// TypeError

可以看到,一般的运算都会先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。

而到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 String(o),那么你会看到调用顺序就变了。

String(o) 
// toString 
// valueOf 
// TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

var o = {
  valueOf : () => {console.log("valueOf"); return {}},
  toString : () => {console.log("toString"); return {}}
}

o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


console.log(o + "")
// toPrimitive
// hello

问答时间

  1. 为什么有的编程规范要求用 void 0 代替 undefined?

    因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取 undefined 值。

  2. 0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?

    根据浮点数的定义,非整数的 Number 类型无法用 ==(=== 也不行) 来比较:

    console.log( 0.1 + 0.2 == 0.3); // false
    

    实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:

    console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON); // true
    
  3. 为什么给对象添加的方法能用在基本类型上?

    . 运算符 提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

    Number.prototype.hello = () => console.log('hello');
    (123).hello(); // hello
    
  4. 判断一个变量是不是数组?

    var arr = [1, 2, 3]
    Array.isArray(arr)
    arr instanceof Array
    arr.constructor === Array
    Object.prototype.toString.call(arr) === '[object Array]'
    

参考