JavaScript 中的数据类型分为两大类:原始类型(Primitive Types) 和 引用类型(Reference Types) 。下面是它们的详细分类及区别:
一、原始类型(Primitive Types)
原始类型是存储在栈内存中的简单数据段,它们直接存储值并且不可变。
1. undefined:
- 表示变量已经声明但未初始化。
- 例如:`let a; console.log(a); // 输出 undefined`
2. null:
- 表示“空”或“不存在”的对象。
- `null` 通常用于显式地表示一个变量没有值。
- 例如:`let b = null;`
3. boolean:
- 只有两个值:`true` 和 `false`。
- 主要用于条件判断。
- 例如:`let isActive = true;`
4. number:
- 包含整数和浮点数(包括 `Infinity` 和 `NaN`)。
- 例如:`let age = 25; let pi = 3.14;`
5. string:
- 表示文本数据,由零个或多个字符组成。
- 使用单引号或双引号包裹。
- 例如:`let name = "Alice"; let greeting = 'Hello!';`
6. bigint:
- 表示任意精度的整数。
- 使用 `n` 在数字末尾表示 BigInt 类型。
- 可以通过在整数末尾加上 `n` 或调用 `BigInt` (‘1234567789’)函数来创建 `BigInt`
- 例如:`let bigNumber = 12345678901234567890n;`
- `BigInt` 与 `Number` 类型不兼容,不能混合进行算术运算。
大数加法:
function addBigNumbers(num1, num2) {
// 确保 num1 和 num2 的长度一致
let maxLength = Math.max(num1.length, num2.length);
num1 = num1.padStart(maxLength, '0');
num2 = num2.padStart(maxLength, '0');
let carry = 0; // 进位
let result = '';
// 从最低位开始逐位相加
for (let i = maxLength - 1; i >= 0; i--) {
let sum = parseInt(num1[i]) + parseInt(num2[i]) + carry;
carry = Math.floor(sum / 10); // 计算进位
result = (sum % 10) + result; // 计算当前位
}
// 如果最高位有进位,则加上
if (carry > 0) {
result = carry + result;
}
return result;
}
// 示例:
console.log(addBigNumbers('12345678901234567890', '98765432109876543210'));
// 输出: 111111111011111111100
大数减法:
function subtractBigNumbers(num1, num2) {
// 确保 num1 和 num2 的长度一致
let maxLength = Math.max(num1.length, num2.length);
num1 = num1.padStart(maxLength, '0');
num2 = num2.padStart(maxLength, '0');
let borrow = 0; // 借位
let result = '';
// 从最低位开始逐位相减
for (let i = maxLength - 1; i >= 0; i--) {
let diff = parseInt(num1[i]) - parseInt(num2[i]) - borrow;
if (diff < 0) {
diff += 10;
borrow = 1; // 借位
} else {
borrow = 0;
}
result = diff + result;
}
// 去除前导零
result = result.replace(/^0+/, '');
// 如果结果为空,返回 '0'
return result.length > 0 ? result : '0';
}
// 示例:
console.log(subtractBigNumbers('98765432109876543210', '12345678901234567890'));
// 输出: 86419753208641975320
7. symbol:
- 表示独一无二的值,通常用于定义对象的唯一属性。
- 例如:`let sym = Symbol('unique');`
二、引用类型(Reference Types)
引用类型存储在堆内存中,变量存储的是对数据的引用,而非数据本身。
1. Object:
- 包含一组键值对,键是字符串或 Symbol,值可以是任意类型。
- 对象可以通过 `.` 或 `[]` 访问属性。
- 例如:`let person = { name: "Alice", age: 25 };`
2. Array:
- 一种特殊的对象,用于存储有序的列表。
- 通过索引(从 0 开始)访问元素。
- 例如:`let colors = ["red", "green", "blue"];`
3. Function:
- 也是一种对象,表示可调用的代码块。
- 可以传递参数和返回值。
- 例如:`function greet() { return "Hello!"; }`
4. Date:
- 用于表示和处理日期和时间。
- 例如:`let now = new Date();`
5. RegExp:
- 用于模式匹配的正则表达式。
- 例如:`let pattern = /abc/;`
6. Map 和 Set:
- `Map` 是一种键值对集合,键可以是任意类型。
- `Set` 是值的集合,值必须唯一。
- 例如:`let map = new Map(); let set = new Set();`
7. WeakMap 和 WeakSet:
- 类似 `Map` 和 `Set`,但键或值是弱引用,垃圾回收机制可以清理它们引用的对象。
三、原始类型与引用类型的区别
-
存储方式:
- 原始类型存储在栈内存中,直接保存值。
- 引用类型存储在堆内存中,变量保存的是对对象的引用地址。
-
可变性:
- 原始类型是不可变的,一旦创建,值不能更改。
- 引用类型是可变的,可以改变对象的属性或数组的元素。
-
比较方式:
- 原始类型比较的是值本身。
- 引用类型比较的是对象的引用地址。
-
复制方式:
- 原始类型复制的是值本身。
- 引用类型复制的是引用地址,因此两个变量会指向同一个对象。
示例
let x = 10; // 原始类型,存储的是值 10
let y = x; // 复制 x 的值,y 也为 10
y = 20; // 修改 y 的值,不会影响 x
let obj1 = { name: "Alice" }; // 引用类型,存储的是对象的引用地址
let obj2 = obj1; // 复制 obj1 的引用地址,obj2 指向同一个对象
obj2.name = "Bob"; // 修改 obj2 的属性,obj1 的属性也会被修改,因为它们指向同一个对象
四、数据类型判断
当你使用 Object.is() 或 === 来比较两个对象或数组时,实际上比较的是它们的引用地址,而不是它们的内容。
例如,[](数组)和{}(对象)创建不同对象,虽然内容相同,但是内存中的地址不同,所以false
但是字符串和数字是原始类型(也称为基本类型或基本数据类型)。原始类型的值是按值(而不是引用)存储和比较的。这意味着当你比较两个原始类型的值时,比较的是它们的实际值,而不是引用地址。
1. Object.is()
通常用于需要严格判断两个值是否相同且不受特殊值(如 NaN 和 -0)影响的场景中。
2.===
和===的区别在于这NaN和+0 -0,其他地方相同
3. typeof
typeof 运算符是由 JavaScript 引擎直接提供的一种检查数据类型的机制。其底层实现逻辑大致如下:
- 检查值的类型:
typeof运算符会首先检查操作数的类型。 - 返回预定义的字符串:根据检查到的类型,返回预定义的字符串。
- 特点:数组和null都识别为对象。
4. instanceof
instanceof 运算符用于检查对象是否是某个构造函数的实例,其底层实现涉及原型链的检查。具体步骤如下:
-
获取构造函数的
prototype属性:instanceof运算符会首先获取右操作数(构造函数)的prototype属性。 -
沿着原型链检查:然后,它会沿着左操作数(对象)的原型链向上检查,看是否能找到与构造函数的
prototype属性相同的对象。
instanceof和typeof的区别
typeof在对值类型number、string、boolean 、null 、 undefined、 以及引用类型的function的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回object
为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。
5.Object.prototype.toString.call()
var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call('mc')); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
五、深浅拷贝
示例:
深拷贝obj2 = JSON.parse(JSON.stringify(obj1));
浅拷贝obj2 = Object.assign({}, obj1);
浅拷贝是指创建一个新的对象或数组,但其属性或元素仍然引用原始对象或数组中的相同内存地址。换句话说,浅拷贝只复制对象或数组的第一层属性,而不复制嵌套的对象或数组。
方法:
- Object.assign():用于对象的浅拷贝。
- 扩展运算符 ...:用于对象和数组的浅拷贝。
深拷贝是指创建一个新的对象或数组,并且递归地复制所有嵌套的对象或数组,从而完全独立于原始对象或数组。深拷贝确保新对象或数组与原始对象或数组之间没有共享的内存地址。
方法:
-
JSON.parse(JSON.stringify()):简单对象或数组的深拷贝。
- JSON.parse() 数组转对象和 JSON.stringify()对象转数组是最简单且常用的方法,但有一些限制:无法拷贝函数、undefined、Symbol 、循环引用等特殊类型。
- 并非所有的对象都能转为JSON格式,如果对象之间存在循环引用,就会导致转换失败,father引用son,son引用father。
- 循环引用的解决办法:JSON.stringify(value, replacer, spaces缩进字符数)
- 手动递归函数:适用于更复杂的数据结构,能够处理函数、循环引用等情况。
function deepClone(obj) {
// 如果是 null 或者 是其他基本类型(number, string, boolean, etc.),直接返回
if (obj === null || (typeof obj !== 'object') return obj;
// 如果是 Date 类型,创建一个新的 Date 对象
if (obj instanceof Date) return new Date(obj);
// 如果是 Array,创建一个新数组并递归拷贝每一个元素
if (Array.isArray(obj)) {
const newArr = [];
for (let i = 0; i < obj.length; i++) {
newArr[i] = deepClone(obj[i]);
}
return newArr;
}
// 如果是对象,创建一个新对象并递归拷贝每一个属性
const newObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
- 第三方库:如 lodash 的 _.cloneDeep()。