一文搞懂js数据类型和symbol类型
1.js数据类型大全
js常用的基本数据类型包括undefined、null、number、boolean、string、symbol;
js的引用数据类型也就是对象类型Object,比如:Object、Array、Function、Map、Set、Date、RegExp等;
注意:引用数据类型还有RegExp类型:正则表达式
正则表达式是一种对象类型,属于引用类型。在 JavaScript 中,基本类型包括数字、字符串、布尔值、空值和未定义值,而引用类型包括对象、数组、函数、正则表达式等。
正则表达式对象是 RegExp
构造函数的实例,它包含了用于匹配和操作字符串的正则表达式模式。因为正则表达式对象包含一些方法和属性,所以它是引用类型。当您使用 typeof
操作符检查一个正则表达式时,它会返回 "object"
,而不是基本类型中的 "string"
、"number"
等。
在 JavaScript 中,你可以使用两种方式创建正则表达式:
- 字面量方式:
const regex = /pattern/;
- 构造函数方式:
const regex = new RegExp("pattern");
其中,pattern
是你要匹配的字符串模式。
2.基本类型和引用类型的区别
(1)存储的位置不同:基本类型在栈(但是一旦被闭包引用则成为常驻内存,会存在内存中),引用类型在堆
注意:当你创建一个引用类型的变量时,栈内存中会存储一个指向堆内存中对象的引用。
(2)访问机制不同:基本数据类型直接访问值,而引用数据类型访问的是内存地址!
这种不同的访问机制导致了一些重要的行为差异。例如,基本类型的比较是按值比较的,而引用类型的比较是按引用比较的。这意味着两个包含相同内容的数组,如果是引用类型,它们可能不会被视为相等,因为它们引用的是不同的内存地址。
(3)拷贝方式不同:值拷贝和引用拷贝!
当你复制一个基本类型的值给另一个变量时,实际上是复制了该值的副本,而不是引用。
当你复制一个引用类型的值给另一个变量时,复制的是引用,这意味着两个变量指向同一个对象。
(4)函数参数传递的不同:虽然本质上都是值传递,但是引用类型体现出来的是按内存地址传递
3.堆和栈的区别
堆和栈有什么区别?
栈是一种线性数据结构,而堆是一种非线性数据结构!
堆的基本特点:堆是一种特殊的数据结构,通常是一个二叉树,它满足特定的堆属性。在堆中,父节点的值通常比其子节点的值具有某种关系(例如最小堆中,父节点的值小于或等于子节点的值;最大堆中,父节点的值大于或等于子节点的值)。
详细地进行对比:
数据结构:
- 堆:堆是一个动态分配的内存区域,用于存储引用类型的数据(例如对象、数组)和动态分配的数据(例如通过
new
关键字创建的对象)。 - 栈:栈是一个静态分配的内存区域,用于存储基本数据类型的值(例如数字、布尔值)和函数的调用堆栈信息。
- 堆:堆是一个动态分配的内存区域,用于存储引用类型的数据(例如对象、数组)和动态分配的数据(例如通过
内存分配:
堆:堆的内存分配是动态的,它通常由程序员手动或自动进行管理,需要显式地分配和释放内存。内存泄漏是堆的一个潜在问题,因为如果不释放不再需要的内存,它将一直占用系统资源。
栈:栈的内存分配是静态的,它由编译器或解释器自动管理。**当一个函数被调用时,栈会为其分配内存,当函数返回时,分配给该函数的内存会自动释放。**这确保了栈上的内存管理是一种后进先出(LIFO)的方式。
具体函数调用的时候是怎么样的栈内存变化呢?
当一个函数被调用时,计算机内存中的栈(Stack)会为该函数分配一块内存区域,这个内存区域通常称为函数的调用栈帧(Call Stack Frame)或栈帧(Stack Frame)。每个栈帧用于存储与函数调用相关的信息和数据,包括函数的参数、局部变量、返回地址以及其他执行上下文的信息。
以下是函数调用时栈的一般工作流程:
- 调用函数:当程序执行到一个函数调用语句时,会将函数的控制权转移到被调用的函数,并为该函数分配一个新的栈帧。
- 保存返回地址:在新的栈帧中,会保存调用函数的返回地址,以便在函数执行完成后返回到调用点。
- 分配局部变量:栈帧中还会分配内存用于存储函数的局部变量,这些变量只在函数内部可见。
- 执行函数体:函数的代码体开始执行,可以访问参数和局部变量,执行任何必要的计算。
- 递归或嵌套调用:如果在函数内部又调用了其他函数,会为每个新的函数调用分配一个新的栈帧,并按照相同的方式处理。
- 返回函数值:当函数执行完毕,它会将返回值存储在栈帧中,并将控制权返回到调用点,使用存储的返回地址。
- 释放栈帧:一旦函数返回并完成其任务,其栈帧将被销毁,释放分配给该函数的内存。
这个过程是一个后进先出(LIFO)的过程,因为栈的性质是最后进入的栈帧最先被处理,直到调用栈为空,程序才能完全结束。
这种栈的管理方式使得函数调用可以有效地嵌套和追踪,同时也确保了局部变量的隔离性,使得每个函数都可以拥有自己的变量空间,而不会与其他函数的变量发生冲突。当函数返回时,它的栈帧会被销毁,从而释放相关的内存,这有助于避免内存泄漏和有效地管理内存资源。
2.Symbol类型
1.基本介绍
Symbol
是 ES6(ECMAScript 2015)引入的一种新的原始数据类型。它是一种特殊的数据类型,用于创建唯一的、不可变的标识符(identifier)。每个由 Symbol
函数创建的值都是独一无二的,即使它们的描述相同,也不会相等。
Symbol的语法:Symbol([description])
其中,description
是一个可选的字符串参数,用于为创建的 Symbol
提供一个可读的描述。这个描述并不影响 Symbol
的唯一性,仅仅是用于调试和显示目的。
例如:
const symbol1 = Symbol('foo');
const symbol2 = Symbol('foo');
console.log(symbol1 === symbol2); // false,因为每个 Symbol 都是唯一的
2.Symbol的特点(重要)
1.symbol的值是唯一的,用来解决命名冲突问题
2.symbol不能与其他值进行运算
3.symbol定义的对象属性是不需要对外操作和访问的(不能通过遍历获取的),不能用for…in遍历循环,但可以使用Reflect.ownKeys来获取所有对象的键名
4.symbol定义的类的方法和对象的属性可以是模块化私有的(不是绝对的私有),外界只能通过symbol变量获取
//创建symbol
let s = Symbol();
console.log(s);
let s2 = Symbol("速度快");
console.log(s2);
let s3 = Symbol("速度快");
console.log(s2 === s3);
//symbol.for创建
let s4 = Symbol.for("速度快");
console.log(s4);
let s5 = Symbol.for("速度快");
console.log(s5);
console.log(s4 === s5);
3.Symbol主要的应用场景
- 创建唯一的、不需要对外操作和访问的属性名:
(1)唯一的属性名:作为对象的属性名,确保属性名的唯一性,避免意外的属性冲突。
解释:用通俗易懂的话来说就是,可能两个对象的属性都叫 name,本身没有啥影响,但是我们有强迫症就是想区分一下,那么可以分别用 Symbol 定义为 name1 和 name2,这样本质上都是 name 属性但是名字却可以不一样了(但是实际上定义普通变量也可以)
(2)不需要对外操作和访问的属性名**(不能通过遍历获取的):Symbol类型的key是不能通过Object.keys()或者for...in来枚举的**,它未被包含在对象自身的属性名集合(property names)之中,但是可以通Reflect.ownKeys()来获取所有Symbol类型的key。
所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
也正因为这样一个特性,当**使用JSON.stringify()**将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外!
//例:
const nameSymbol = Symbol('name');
const person = {
[nameSymbol]: 'Alice', //定义的时候需要用中括号包起来,就像访问的时候一样!!
age: 30,
};
console.log(person[nameSymbol]); // 'Alice'
//例:
let obj = {
[Symbol('name')]: '一斤代码',
age: 18,
title: 'Engineer'
}
Object.keys(obj) // ['age', 'title']
for (let p in obj) {
console.log(p) // 分别会输出:'age' 和 'title'
}
Object.getOwnPropertyNames(obj) // ['age', 'title']
JSON.stringify(obj) // {"age":18,"title":"Engineer"}
定义类/对象的私有属性和方法(不能直接访问的,只能通过symbol变量访问):在类中使用 Symbol 来创建私有属性或方法,防止被外部直接访问。——> Symbol创建的属性只能用 Symbol 变量访问**(可以把这个变量隐藏起来,外部如果没有获取这个变量就无法访问这个属性,模块化)**
弥补了 js 的 class 没有 private 关键字的缺陷!
const privateSymbol = Symbol('private');
class MyClass {
constructor() {
this[privateSymbol] = '私有属性';
}
[privateSymbol]() {
return '私有方法';
}
}
const myInstance = new MyClass();
console.log(myInstance[privateSymbol]); // '私有属性'
console.log(myInstance[privateSymbol]()); // '私有方法'
- 使用 Symbol 可以方便地定义全局唯一的常量:因为 Symbol 是唯一的,可以用于定义一组唯一的常量,避免与其他常量冲突。
这里不用写具体内容**(因为本质上是不需要具体内容的,只要保证不可变,这个常量就起到了作用)**,比直接定义要方便!
const RED = Symbol(); //不用const RED = 'red'了!
const GREEN = Symbol();
const BLUE = Symbol();
function getColor(color) {
switch (color) {
case RED:
return '红色';
case GREEN:
return '绿色';
case BLUE:
return '蓝色';
default:
return '未知颜色';
}
}
console.log(getColor(RED)); // '红色'
- 内置 Symbols:JavaScript 语言内置了一些 Symbol,用于特殊目的,例如迭代器、异步迭代等。例如,
Symbol.iterator
用于定义可迭代对象的迭代器。
const iterableObj = {
[Symbol.iterator]() {
let count = 0;
return {
next() {
count++;
if (count <= 5) {
return { value: count, done: false };
} else {
return { done: true };
}
}
};
}
};
for (const value of iterableObj) {
console.log(value); // 输出 1, 2, 3, 4, 5
}
重要的内置 Symbols 都有哪些?
JavaScript 中有一些内置的 Symbol,它们具有特殊的用途。以下是其中一些重要的内置Symbols:
- Symbol.iterator:用于定义一个对象的默认迭代器,使对象可以通过
for...of
循环进行迭代。主要用于自定义可迭代对象。(对象默认是不可迭代的) - Symbol.toStringTag:用于自定义对象的
Object.prototype.toString()
方法返回的字符串标签。通过设置该属性,你可以指定一个自定义的字符串来表示对象的类型。 - Symbol.species:主要用于自定义构造函数的派生类,以指定它们的返回值类型。在内置类如
Array
和Map
中,这个 Symbol 用于控制派生类的行为。 - Symbol.match、Symbol.replace、Symbol.search、Symbol.split:这些 Symbols 用于自定义字符串的正则匹配行为。它们允许你定义特定字符串方法的行为。
- Symbol.toPrimitive:用于自定义对象在进行类型转换时的行为,例如,将对象转换为字符串、数字或布尔值。可以重写对象的
valueOf
和toString
方法来实现。 - Symbol.hasInstance:用于自定义对象作为构造函数的实例时的
instanceof
运算符的行为。 - Symbol.isConcatSpreadable:用于自定义对象在使用
Array.prototype.concat()
方法时是否展开其内容。 - Symbol.unscopables:用于自定义对象的属性,在 with 语句中是否可见。它是一个对象,其中的属性名称对应了不应该出现在 with 语句作用域中的属性名称。
这些是一些重要的内置 Symbols,它们允许你自定义和扩展 JavaScript 的行为。不同的 Symbol 有不同的用途,你可以根据需要选择性地使用它们。