数据的类型

ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、String 和 Symbol。

Symbol(符号)是 ES6 新增的。


变量的类型

JS 的变量具有动态数据类型 → 变量的类型由变量值决定。



可以使用 typeof 操作符查看变量的数据类型,会返回下列字符串之一:

  1. "undefined" 表示值未定义
  2. "boolean" 表示值为布尔值
  3. "string" 表示值为字符串
  4. "number" 表示值为数值
  5. "object" 表示值为对象(不是函数)或 null 或正则表达式
  6. "function" 表示值为函数
  7. "symbol" 表示值为符号
const a = 123;
console.log(typeof a); // "number"

注意:typeof 在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用 typeof null 返回的是 "object"。这是因为特殊值 null 被认为是一个对空对象的引用。

严格来讲,函数在 ECMAScript 中也属于一种对象。因为函数有特殊属性 [[Call]],为此 就有必要通过 typeof 操作符来区分函数和其他对象。
函数不仅是对象,还可以拥有属性。函数对象的 length 属性是其声明的参数的个数。



可以使用 Object 的原型对象上挂载的 toString 方法查看变量的类型:(需要改变该方法的 this 指向)

const typeOf = function (obj) {
    console.log(Object.prototype.toString.call(obj).slice(8, -1).toLowerCase());
};

typeOf('superman'); // "string"
typeOf([]); // "array"
typeOf(new Date()); // "date"
typeOf(null); // "null"
typeOf(true); // "boolean"
typeOf(() => {}); // "function"


基本数据类型

基本类型的数据,保存在栈内存中。



Number 类型

console.log(typeof NaN); // "number"



String 类型

使用双引号 / 单引号 / 反引号包裹的值。

const str1 = '1';
const str2 = `1`;
console.log(typeof str1); // "string"

注意:以某种引号作为字符串的开头,就必须仍然以该种引号作为字符串的结尾。



Boolean 类型

Boolean 类型只有 2 个字面值:truefalse

console.log(typeof true); // "boolean"



Undefined 类型

Undefined 类型只有 1 个值,就是特殊值 undefined。当使用 varlet 声明了变量但没有初始化时,就相当于给该变量赋予了 undefined 值:

let message;
console.log(message == undefined); // true

一般来说,永远不用显式地给某个变量赋值为 undefined。字面量 undefined 用于明确区分空对象指针(null)和未初始化变量。



对未初始化的变量调用 typeof 会返回 "undefined",对未声明的变量调用 typeof 也会返回 "undefined",这就有点让人看不懂了。比如下面的例子:

let message; // 这个变量被声明了,只是值为 undefined
// let age // 确保没有声明过这个变量

console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"

即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的同时进行初始化。这样,当 typeof 返回 "undefined" 时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。



请注意上例中,虽然 age 是一个 undeclared 变量,但 typeof age 并没有报错。这是因为 typeof 有一个特殊的安全防范机制。

该安全防范机制对在浏览器中运行的 JS 代码来说很有帮助,可以在程序中检查全局变量 XXX 而不出现 ReferenceError 错误:

// 这样会抛出错误
if (atob) {
    atob = function () {
        /* ... */
    };
}

// 这样是安全的
if (typeof atob === 'undefined') {
    atob = function () {
        /* ... */
    };
}



Null 类型

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因:

let car = null;
console.log(typeof car); // "object"

在定义将来要保存对象值的变量时,建议使用 null 来初始化。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。



undefinednull 派生而来,表面上它们是相等的:

console.log(undefined == null); // true
console.log(undefined === null); // false


对数据的操作

基本类型数据

  • 基本类型数据存储在 栈(stack) 中。
  • 占用的内存空间一般较小,运行效率相对较高。
  • 栈内存由系统自动分配、自动回收,分配的内存大小是固定的。
const a = 10; // 申请 stack 内存, 存放 [基本类型数据 10]
let b = a; // JS 操作的是 stack 内存中存放的 [基本类型数据 10]
b += 10;
console.log(a); // 10
console.log(b); // 20



引用类型数据

  • 引用类型数据存储在 堆(heap) 中。
  • 占用的内存空间一般较大,运行效率相对较低。
  • 内存由代码指定分配,分配的大小不定,可动态调整。
const obj1 = {}; // 申请 heap 内存, 存放引用类型数据 {}; 申请 stack 内存, 存放 [引用类型数据 {} 对应的 heap 内存的地址]
const obj2 = obj1; // JS 操作的是 stack 内存中存放的 [引用类型数据 {} 对应的 heap 内存的地址]

obj2.name = 'superman';

console.log(obj1.name); // superman
console.log(obj2.name); // superman

可以看到,修改 obj1 的属性值,obj2 的属性值也会变。



注意:引用类型数据里面的基本类型数据,也是存储在栈中:

const obj = { name: 'superman' };

对象 objname 的属性值 "superman" 也是存储在 stack 内存中的。



深拷贝 & 浅拷贝

浅拷贝数组:

  1. arr.concat():用于合并数组,不会改变原数组
const arr = [1, 2, 3];
const copyArr = [].concat(arr);

console.log('arr', arr); // arr [1, 2, 3]
console.log('copyArr', copyArr); // copyArr [1, 2, 3]

copyArr.push(4, 5); // 对拷贝的数组添加数据

console.log('arr', arr); // arr [1, 2, 3]
console.log('copyArr', copyArr); // copyArr [1, 2, 3, 4, 5]
  1. arr.slice():用于切取指定数组元素,不会改变原数组
const arr = [1, 2, 3];
const copyArr = arr.slice();
  1. Array.from(arr):用于把类数组对象转成数组
const arr = [1, 2, 3];
const copyArr = Array.from(arr);
  1. 扩展运算符 & 解构赋值
const arr = [1, 2, 3];
const copyArr = [...arr];



浅拷贝对象:

  1. Object.assign() 用于合并对象
const obj = { name: 'superman' };
const copyObj = Object.assign({}, obj);

copyObj.name = 'monster';

console.log('obj', obj); // obj {name: 'superman'}
console.log('copyObj', copyObj); // copyObj {name: 'monster'}
  1. 扩展运算符 & 解构赋值
const obj = { name: 'superman' };
const copyObj = { ...obj };
  • 这两个方法都会 覆盖前面的同名属性。


深拷贝对象 & 数组:

  1. 使用 JSON 的 parsestringify 方法:
const arr = [{ name: 'superman' }];
const copyArr = JSON.parse(JSON.stringify(arr));

copyArr[0].name = 'super';

console.log('arr', arr); // arr [ {name: 'superman'} ]
console.log('copyArr', copyArr); // copyArr [ {name: 'super'} ]
  1. 使用 structuredClone 方法:
const obj = { name: '张三', age: { real: 21, fake: 18 } };
const cloneObj = structuredClone(obj);

cloneObj.age.real = 40;

console.log('cloneObj', cloneObj); // { name: '张三', age: { real: 40, fake: 18 } }
console.log('obj', obj); // { name: '张三', age: { real: 21, fake: 18 } }
  • 注意:structuredClone 无法拷贝函数