深挖JavaScript的变量和类型

117 阅读6分钟

深挖JavaScript的变量和类型

前端小白的学习笔记,大家多多指教🙌😎

JavaScript数据类型

原始类型(七种)

  1. Null:只包含一个值:null
  2. Undefined:只包含一个值:undefined
  3. Boolean:包含两个值:truefalse
  4. Number:整数或浮点数,还有一些特殊值(-Infinity+InfinityNaN),基于IEEE 754标准的双精度64位二进制格式的值。-(263 -1)到263 -1
  5. String:一串表示文本值的字符序列,JavaScript的字符串是不可更改的
  6. Symbol:一种实例是唯一且不可改变的数据类型,通常用来作为Object的key
  7. BigInt:es10中加入的一个新的数字类型,可以用任意精度表示整数

对象类型:

Object:它是由上述 7 种原始类型组成的一个包含了 key-value 对的数据类型,其中的value可以是任何类型。

除了常用的Object外,ArrayFunction等都属于特殊的对象。

为什么区分原始类型和对象类型

内存空间

之所以把他们区分为两种不同的类型,是因为它们在内存中存放的位置不一样,我们先弄清楚其内存空间的种类。

下图是JavaScript的内存模型:

image-20220612214200355.png

在JavaScript的执行过程中,主要有三种类型内存空间:代码空间、栈空间和堆空间

原始类型存放在栈中,引用类型存放在堆中。

那为什么一定要区分“堆”和“栈”两个存储空间呢?所有数据直接存放在“栈”中不就可以了吗?

答案是不可以的。这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。这类似于职责划分,栈主要管运行,堆主要管存储。

栈内存:

JavaScript中的原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间。由于栈中内存空间的大小是固定的,那么注定了存储在栈中的变量是不可变的。

以字符串为例,我们可以发现没有任何方法是可以直接改变字符串的。

 var str = 'volcanoLee';
 str.slice(1);
 str.substr(1);
 str.trim(1);
 str.toLowerCase(1);
 str[0] = 1;
 console.log(str);  // volcanoLee

这些方法都在原字符串的基础上产生了一个新字符串,而非直接去改变str。

堆内存:

引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。堆内存中存储的值可动态调整。

以数组为例,它的很多方法都可以改变它自身。

  • pop() 删除数组最后一个元素,如果数组为空,则不改变数组,返回undefined,改变原数组,返回被删除的元素
  • push()向数组末尾添加一个或多个元素,改变原数组,返回新数组的长度
  • shift()把数组的第一个元素删除,若空数组,不进行任何操作,返回undefined,改变原数组,返回第一个元素的值
  • unshift()向数组的开头添加一个或多个元素,改变原数组,返回新数组的长度
  • reverse()颠倒数组中元素的顺序,改变原数组,返回该数组
  • sort()对数组元素进行排序,改变原数组,返回该数组
  • splice()从数组中添加/删除项目,改变原数组,返回被删除的元素

原始类型和引用类型在操作上的区别

1.复制

原始类型

 var name = 'volcanoLee';
 var name2 = name;
 name2 = 'haha';
 console.log(name); // volcanoLee;
 ​

从变量name复制出一个变量name2,此时在内存中创建了一块新的空间用于存储volcanoLee,虽然两者是相同的,但两者指向的内存空间完全不同,因此这两个变量参与任何操作都互不影响。

引用类型

 var obj = {name:'volcanoLee'};
 var obj2 = obj;
 obj2.name = 'haha';
 console.log(obj.name); // haha
 ​

当我们复制引用类型的变量时,实际上复制的是栈中存储的地址,所以复制出来的obj2实际上和obj指向的堆中同一个对象。因此,我们改变其中任何一个变量的值,另一个变量都会受到影响,这就是为什么会有深拷贝和浅拷贝的原因。

2.比较

对于原始类型,比较时会直接比较它们的值,如果值相等,即返回true

对于引用类型,比较时会比较它们的引用地址,虽然两个变量在堆中存储的对象具有的属性值都是相等的,但是它们被存储在了不同的存储空间,因此比较值为false

3.值传递和引用传递

首先明确一点,ECMAScript中所有的函数的参数都是按值传递的。

当函数参数是引用类型时,我们将参数复制了一个副本到局部变量,只不过复制的这个副本是指向堆内存中的地址,我们在函数内部对对象的属性进行操作,实际上和外部变量指向堆内存中的值相同,但是这并不代表着引用传递,下面我们再按一个例子:

 let obj = {};
 function changeValue(obj){
   obj.name = 'volcanoLee';
   obj = {name:'haha'};
 }
 changeValue(obj);
 console.log(obj.name); // volcanoLee

可见,函数参数传递的并不是变量的引用,而是变量拷贝的副本,当变量是原始类型时,这个副本就是值本身,当变量是引用类型时,这个副本是指向堆内存的地址。所以,再次记住:

ECMAScript中所有的函数的参数都是按值传递的。

判断JavaScript数据类型的方式

typeof

可以准确判断一个变量是否为下面几个原始类型;

 typeof 'ConardLi'  // string
 typeof 123  // number
 typeof true  // boolean
 typeof Symbol()  // symbol
 typeof undefined  // undefined

用来判断引用类型时除函数之外都会被判定为object

另外:typeof null === 'object'

instanceof

instanceof操作符可以帮助我们判断引用类型具体是什么类型的对象:

 [] instanceof Array // true
 new Date() instanceof Date // true
 new RegExp() instanceof RegExp // true

手写一个getType函数,获取详细的数据类型

前两种类型判断方式分别有如下缺陷:

  1. typeof : 只能判断值类型,其他的就是function和object

  2. instanceof : 需要两个参数来判断是否相等,而不是获取类型

我们希望既能判断值类型又能判断具体的对象类型

方法:使用Object.prototype.toString.call()

image-20220701222807771.png

可以看到,Object.prototype.toString.call()能够准确判断出值的具体类型,也避免了typeof null === 'object'的一大bug。

那么接下来只需截取所需的字符串并改为小写,便可相对完美的返回判断类型。

 function getType(x) {
     const originType = Object.prototype.toString.call(x) // '[object String]'
     const spaceIndex = originType.indexOf(' ')
     const type = originType.slice(spaceIndex + 1, -1) // 'String'
     return type.toLowerCase() // 'string'
 }

参考

time.geekbang.org/column/arti…

juejin.cn/post/684490…