数据类型--你真的了解了么?

237 阅读10分钟

一、对自我的一个认知

        说实话我一开始十分不太清楚,具体数据类型之间的区别的。我只是知道,基本数据类型和引用数据类型。但是说到他们之间特别细微的区别时,我还真是有点懵,感觉自己学的不够扎实、不够深入。

        正因为这样,所以我就到处找资料来解决我的这些疑问和困惑。我也一直在想为什么别人能分得那么清晰,别人为什么就能写出好的技术科普文章帮人解惑。也正是因为自己的不清楚,所以我从现在决心要每周更新一到两个大的知识点。以此来,帮助自己更加牢固的掌握JS这门语言的基础。俗话说:"基础不牢,地动山摇"。当然有错误或者有异议的地方还请有心人直指,自查和证实之后一定会及时更正。接下来,言归正传!

二、区分:数据类型

        想必看到这篇文章的大家都是懂编程或者从事编程这项工作或者爱好者。或多或少都是了解的。但是,这个看似简单的概念却不是所有人都能很清晰的说明白。

        数据类型这个词,应该在所有的编程语言都是存在,也是大家都绕不过去的一道坎。简单来讲,JavaScript这门语言的数据类型分为`基本(原始)数据类型`和`引用数据类型`。

  • 那些是基本(原始)数据类型
    • `NULL`: null,是个特别的存在,它是个历史遗留问题。只包含一个只那就是他本身,null。
    • `UNDEFINED`:undefined,表示未定义,通常是声明未定义,这个应该是个很常见的报错,只包含一个只那就是他本身,undefined。
    • `STRING`:字符串,这个估计是整个编程界最常用的数据类型之最。
    • `NUMBER`:数字,js中的数字和其他语言不一样他是不显示的区分32位、64位、长整型、整型、浮点型等等这些概念的。他就只认数字就好,至于具体的交给底层处理了。这也是js的灵活一之处。这个类型包含很多特殊值(NAN,-infinity,+infinity)
    • `BOOLEAN`:布尔值,这个类型虽然只有两个值true和false,但是在判断中却大有用处
    • `SYMBOL`:Symbol,这个是个独特的类型只有js有,json数据无法存储。有特殊用处。
    • `BIGINT`: bigint这个数据类型是在es10中提出的,在最新的Chrome中已经实现支持
  • 那些是引用数据类型
    • `OBJECT`: 没错首当其冲的就是对象了,实际他自己分一类完全不为过。那些Regexp、Date、Function、Array......等等他们也都算是对象的一种(别问我为什么知道typeof一下就知道😂

三、都是数据类型为什么要分的那么清楚

        可能一开始很多人会和我之前有相同的疑惑,不就是个数据类型么。为什么要分的那么清晰,不都还是一家人么。但是千万别这么想,一家人总会有分家的想法。等大家踩坑多了自然而然不会这么说了,像我🤣。

  • 存储上的差别
    • 原始类型:存储是放在”栈”空间上的,是的又是一个耳熟能详的词--堆栈。
      • 栈的特点
        • 空间比较小
        • 存储的值的大小是固定的
        • 可以直接操作在其上存储的值,简单快捷
        • 空间是有系统自动分配
    •  引用类型:存储是放在“堆”空间上的(不是说存放在堆空间上就和栈完全无关联了)
      • 堆的特点
        • 存储的值的大小不确定,可根据具体情况动态的变化(调整)
        • 空间比栈大,但是其运行效率低(具体和V8引擎有关)
        • 存储其上的值,是无法直接操作的,使用的是引用地址(这个地址是固定的长度,而且是存储在栈上的)来访问
        • 由代码来分配空间

从存储上的差别,我们不难看出。基本数据类型  是具有 不可变性  的。为什么这么说,因为当变量一旦声明就会由系统给他分配一个固定的存储空间。如果操作他的值,那么在操作完之后会由系统再次给分配一个空间且将值重新复制给这个变量。单存的文字可能大家是不会相信的,因为曾经的我也是如此。

var str = 'i believe';
// 不变性
str.slice(1);
console.log(str);  // i believestr.substr(1);
console.log(str);  // i believe
str.trim(1);
console.log(str);  // i believe
str.toLowerCase(1);
console.log(str);  // i believe

// 重新分配空间和赋值,但是并不说明str本身变了
// 只是在重新复制之后,新的str不在指向原来的值,
// 原来的值不存在引用计数会被GC回收 
str = str.toUpperCase(1);
console.log(str);  // I BELIEVEstr = str[0] = 1;
console.log(str);  // 1

关于垃圾回收和内存泄漏参考大神的文章: JavaScript中的垃圾回收和内存泄漏

但是,引用数据类型 就起存储的特性可知不在具它有不可变性,我们可以轻易的改变它,因为他是由代码来动态分配空间的

let MVplayer = {
    name: 'kobe',
    age: 40
}
// 未做操作之前
console.log(MVplayer);  // {name: 'kobe', age: 40}

// 用代码操作后
MVplayer.age = 18
// 操作之后
console.log(MVplayer); // {name: 'kobe', age: 18}
/** 
*结合之前说的引用类型是通过一个引用地址访问到其真实的值存储
*当你通过代码改变其值之后,其引用地址是没有改变的,改变的只是地址指向的值变化了 
*或者换个方式来说,就是代码操作后的引用数据类型变得只是引用地址指向的值的,引用地址是没有变化的
*这就是为什么经常有些新手会问,为什么我只是改了某一个值却使得另外一个值也跟着变化了
*/
  • 值的复制和比较
    • 当我们把一个基本类型的变量的值赋值给另一个基本类型的变量时,这个时候往往试讲这个值复制了一遍并且赋值给了新的变量,两个变量之间其实也没有什么太大的关联。个人认为只是值一样,存储的空间是完全不同的。那么实际上我们去判断这两个变量是否相等的时候得到的结果就是true ,因为事实上基本类型比较两个值是否相等实际上是比较两个变量存储的值是否相等
    • 但是当我们把一个引用类型的变量的值赋值给另一个变量时,这个时候我们往往会觉得是和基本类型的过程是一样的。正是因为这样的"认为"导致了很多时候会带来超预期的BUG。但是这完全是来自于个人本身对于底层知识的不了解才导致的。事实上,当一个引用类型的变量赋值给另一个变量时底层只是将该引用类型变量存储在栈中的地址传递给了拧一个变量,使的这个变量可以通过这个地址去访问到和引用类型变量指向的统一堆空间。而且,此时如果一旦改变该栈地址指向堆空间的值就会影响到引用类型变量的值。这个时候就产生了所谓的BUG。那么我们在比较未操作之前的引用类型变量和赋值之后的变量时 我们得到的结果实际上是相等的,因为他们比较的是他们对应存在栈中的地址是否相同,而不是两者指向的值是否一样。那么试想如果我们声明两个值相同的引用类型的变量a、b,那么两者比较的结果又是如何了。很显然,他们两个是不相等的返回的结果肯定是false,为什么?因为他们在栈中存储的地址并不相同,只是对应堆中存的结果刚好一模一样。但是这个一模一样并不能说明他们是一样的。
  • 对于传递的理解
    • 当我们定义一个函数和一个外部变量a,该函数需要传递一个参数(此时我们传递的是变量a)。然后在函数内改变变量a的值
      • 情形一:该变量a是一个基本类型的变量
        • 在函数执行完毕之后我们在函数外部打印出这个变量a,这个时候我们会猜测有 两种结果:一是:变量a不变还是我们之前声明并定义的值;二是:变量a已经变 成了改变之后的结果也就是经过函数加工过的值。但是实际上我们打印出来的确 是第一种结果。为什么呢?!因为按照我的理解实际上这个传递也只是把值复制 了一份传递给函数内部使用。
      • 情形二:该变量a是一个引用类型的变量
        • 在函数执行完毕之后我们在函数外部打印出这个变量a,这个时候我们会猜测有 两种结果:一是:变量a不变还是我们之前声明并定义的值;二是:变量a已经变 成了改变之后的结果也就是经过函数加工过的值。这种情形下我们打印出来的 是第二种结果。为什么呢?!因为按照我的理解实际上这个传递是把变量存储在 栈中的地址复制了一份传递给函数内部使用,那么这样一来我们在函数内部对于 变量的操作就会对栈地址指向值做出修改但是变量a存储的栈地址不变所以结果 自然是第二种。  
    • 但是总结来看实际上比对两种情形,我们不难发现实际上我们传递给函数的变量始终是对外部变量的值传递而不存在引用传递。有些人可能会说情形二明明就是引用传递啊。但是请你仔细想想之所以第二种情形下会影响到外部的变量是因为他们共同指向一块堆内存,那么正好说明他们在栈中存的地址没有发生变化。也就是说实际上第二种情形下传递的是引用类型变量的栈地址的复制而已
  • 实际上说那么多大家看看ECMAscript 规范就知道,实际上官方已经给我们规定好了,所有函数的参数都是按照值传递的

下一篇我会继续对各个数据类型进行分析。