JS中的数据类型
面试题:JS 中的数据类型有哪些?基本类型和引用类型的区别是什么?
- 简单值和复杂值
- 两者之间本质区别
- 两者之间行为区别
简单值和复杂值
JS 中的数据类型就分为两大类:
- 简单值(基本类型、原始类型)
- 复杂值(引用值、引用类型)
1. 简单值
一共有 7 种:
- number:数字
- string:字符串
- boolean:布尔值
- undefined:未定义
- null:空
- symbol:符号
- bigint:大数
console.log(null + 1); // 1
console.log(undefined + 1); // NaN
目前来讲,关于 null 和 undefined 主要区别总结如下:
- null:从语义上来讲就是表示对象的 “无”
- 转为数值时会被转换为 0
- 作为原型链的终点
- undefined:从语义上来讲就是表示简单值的“无”
- 转为数值为 NaN
- 变量声明了没有赋值,那么默认值为 undefined.
- 调用函数没有提供要求的参数,那么该参数就是 undefined
- 函数没有返回值的时候,默认返回 undefined.
2. 复杂值
复杂值就一种:object
之所以被称之为复杂值,就是因为这种类型的值可以继续往下拆分,分为多个简单值或者复杂值。
const obj = {name: "张三",age: 18,scores: {"htmlScore": 99,"cssScore": 95}
};
像数组、函数、正则这些统统都是对象类型,属于复杂值
console.log(typeof []); // object
console.log(typeof function () {}); // function
console.log(typeof /abc/); // object
函数的本质也是对象。
function func() {}
// 该函数我是可以正常添加属性和方法的
func.a = 1; // 添加了一个属性
func.test = function () {console.log("this is a test function");
}; // 添加了一个方法
console.log(func.a); // 1
func.test(); // this is a test function
在函数内部有一个特别的内部属性 [[Call]]
,这个是属于内部代码,开发者层面是没有办法调用的。但是有了这个属性之后,表示这个对象是可以被调用。
因为函数是可调用的对象,为了区分 普通对象 和 函数对象,因此当我们使用 typeof 操作符检测一个函数时,它返回的是 function。
也正因为这种设计,所以 JS 中能够实现高阶函数。高阶函数的定义:
- 接受一个或多个函数作为输入
- 输出一个函数
因为在 JS 中,函数的本质就是对象,因此可以像其他普通对象一样,作为参数或者返回值进行传递。这也是 JS 中所说的函数是一等公民这个说法的由来。
两者之间本质区别
介绍完了简单值和复杂值之后,接下来我们从内存存储的角度,来看一下这两种本质上的区别。
我们知道,内存的存储区域可以分为 栈 和 堆 这两大块。
- 栈内存:栈内存因为其数据大小和生命周期的可预测性而易于管理和快速访问。栈支持快速的数据分配和销毁过程,但它不适合复杂的或大规模的数据结构。
- 堆内存:堆内存更加灵活,可以动态地分配和释放空间,适合存储生命周期长或大小不确定的数据。使用堆内存可以有效地管理大量的数据,但相对于栈来说,其管理成本更高,访问速度也较慢。
对于简单值而言,它们通常存储在栈内存里面。上面说了,栈内存的特点是管理简单且访问速度快,适用于存储 大小固定、生命周期短 的数据。简单值的存储通常包括直接在栈内存中分配的数据空间,并且直接存储了数据的实际值。
而对于复杂值而言,具体的值是存储在 堆内存 里面的。因为复杂值往往大小是不固定的,无法在栈区分配一个固定大小的内存,因此具体的数据放在堆里面。那么这就没有栈区什么事儿了么?倒也不是,栈区会存储一个内存地址,通过该内存地址可以访问到堆区里面具体的数据。
另外讲到这里,还有一个非常重要的点要提一下,那就是 JS 中在调用函数的时候,通通都是值传递,而非引用传递。
function test(obj) {obj.a = 1000;
}
const obj = {};
console.log(obj); // {}
test(obj);
console.log(obj); // { a: 1000 }
上面的代码,有一定的迷惑性。你看到上面的代码,觉得调用函数之后,obj 发生了真实的修改,所以这是一个引用传递。
但是这里仍然是一个值传递。只不过这个值的背后对应的是一个地址值,这个地址值和简单值一模一样,会被复制一份传递给函数,然后函数内部拿到的是地址值,就可以通过这个地址值找到同一份堆区数据。
function test(obj) {obj = {b:1}; // 这里就赋值了一个新对象,不再使用原来的对象obj.a = 1000;
}
const obj = {};
console.log(obj); // {}
test(obj);
console.log(obj); // {}
如果是真正的引用传递,那么函数内部的 obj 和外部的 obj 是绑在一起的,函数内部对 obj 做任何修改,都会影响外部。但是上面的代码中,很明显在函数内部对 obj 重新赋值后,断开了内外的联系,因此在 JS 中只有值传递。
两者之间行为区别
聊完了本质区别后,接下来我们再来聊一下两者之间行为的区别,主要就下面这么几个点:
- 访问方式
- 比较方式
- 动态属性
- 变量赋值
1. 访问方式
简单值是 按值访问,也就是说,一个变量如果存储的是一个简单值,当访问这个变量的时候,得到就是对应的值。
const str = "Hello";
console.log(str);
复杂值是虽然也是 按值访问 ,但是由于值对应的是一个 内存地址值,一般不能够直接使用,还需要进一步获取地址值背后对应的值。
const obj = {name: "张三"};
console.log(obj.name);
2. 比较方式
这个比较重要,无论是简单值也好,复杂值也好,都是进行的值比较。不过由于复杂值对应的值是一个 内存地址值,因此只有在这个内存地址值相同时,才会被认为是相等。
const a = {}; // 内存地址不一样,假设 0x0012ff7c
const b = {}; // 内存地址不一样,假设 0x0012ff7d
console.log(a === b); // false
3. 动态属性
对于复杂值来讲,可以动态的为其添加属性和方法,这一点简单值是做不到的。
如果为简单值动态添加属性,不会报错,会静默失败,访问时返回的值为 undefined
但如果为简单值动态添加方法,则会报错 xxx is not a function.
const a = 1;
a.b = 2;
console.log(a.b); // undefined
a.c = function(){}
a.c(); // error
4. 变量赋值
最后说一下关于赋值,记住,它们都是 将值复制一份 然后赋值给另外一个变量。
不过由于复杂值复制的是 内存地址,因此修改新的变量会对旧的变量有影响。
let a = 5;
let b = a;
b = 10; // 不影响 a
console.log(a);
console.log(b);
let obj = {};
let obj2 = obj;
obj2.name = "张三"; // 会影响 obj
console.log(obj); // { name: '张三' }
console.log(obj2); // { name: '张三' }
obj2 = { name: '张三' };
obj2.age = 18; // 不会影响 obj
console.log(obj)
console.log(obj2)
-EOF-