剖析JS底层数据类型和堆栈内存

277 阅读7分钟

面试时候,面试官问JavaScript数据类型有哪些?我们要怎么回答?下面剖析JS底层数据类型和堆栈内存 ,欢迎加入讨论学习~~

情景

面试官问你:JavaScript数据类型有哪些?

你答:8种,有number、string、boolean...bigint,还有..还有object。对,就是这八种

我答:7种,分为简单数据类型和复杂数据类型。简单数据类型有:numeric(number + bigint),string...Symbol,其中bigint和symbol是es6新增的,es5是没有的;复杂数据类型有object对象,分为数组,函数

一张图展示

JavaScript 内存机制

在JavaScript中,内存管理是理解数据类型和变量赋值行为的关键。根据内存分配机制,复杂数据类型(如对象和数组)存储在堆内存中,而栈内存中存储的是指向这些值的引用。这种机制确保了内存的有效利用和程序的高效运行。下面详细说下栈内存,堆内存:

栈内存

栈内存用于存储简单数据类型(Primitive Data Types),如numberstringbooleannullundefinedsymbolbigint。栈内存的特点是执行速度快,但空间较小。每个函数调用都会在栈内存中创建一个新的栈帧,用于存储局部变量和函数参数。当函数执行完毕后,栈帧会被自动销毁,释放内存。

堆内存

堆内存用于存储复杂数据类型(Reference Data Types),如对象、数组和函数。堆内存的特点是空间大,但访问速度相对较慢。堆内存中的数据可以通过栈内存中的引用地址来访问。当一个对象不再被任何引用指向时,垃圾回收机制会自动回收这部分内存。可以手动设置null,进行垃圾回收(这个文章后面讲)

示例代码

// 复杂数据类型存储在堆内存中,栈内存中存储的是指向堆内存的引用
let obj = {
    name: '磊磊',
    job: 'AI大咖',
    company: '阿里',
};

// 添加属性;堆内存,动态变化
obj.hometown = '北京';

// 简单数据类型存储在栈内存中,值直接复制
let a = 1;
let b = a;
b = 3;

// 引用,地址指向同一个堆内存
let obj2 = obj;

// 赋值,不会改变obj里面的name
let obj3 = { ...obj };

// 修改obj2的name属性,会影响obj
obj2.name = '小明';

// 输出结果
console.log(obj.name, obj2.name); // 小明 小明
console.log(a, b); // 1 3

// 输出obj3的name属性
console.log(obj3.name); // 磊磊

image.png

内存机制详解

简单数据类型

简单数据类型(Primitive Data Types)在赋值时会进行值的复制。这意味着每个变量都有自己独立的副本,修改其中一个变量不会影响其他变量。

let a = 1;
let b = a;
b = 3;
console.log(a, b); // 1 3

复杂数据类型

复杂数据类型(Reference Data Types)在赋值时会复制引用地址,而不是值本身。这意味着多个变量可以指向同一个对象,修改其中一个变量会影响所有指向该对象的变量。

当然这还存在安全隐患。由于复杂数据类型在赋值时复制的是引用地址,因此多个变量可以指向同一个对象。这种机制虽然提高了内存利用率,但也带来了潜在的安全隐患。例如,一个对象的属性被意外修改,可能会影响到所有引用该对象的变量。

let obj1 = {
    name: '磊磊',
    job: 'AI大咖',
    company: '阿里',
};

let obj2 = obj1;
obj2.name = '小明';
console.log(obj1.name, obj2.name); // 小明 小明

垃圾回收

JavaScript的垃圾回收机制会自动回收不再被任何引用指向的堆内存。常见的垃圾回收算法包括标记-清除(Mark and Sweep)和引用计数(Reference Counting)。这里先简单就内存分配进行说明

let a = null; 
console.log(a);
// 占大量空间,在堆内存,只在栈内存放引用地址
let largeObject = {
    data : new Array(10000000).fill('a'),
}
console.log(largeObject);
largeObject = null; 
console.log(largeObject);// underfined

largeObject 赋值null,将栈内存的地址指向null,堆内存对象不再被引用,会被回收。

图解

为了更好地理解JavaScript的内存机制,下面是一张图解示意图: lQLPJwolXX0xi1XNAxbNA6GwcTMvgUKnqsAHJT4pL__-AA_929_790.png

  • 在词法环境和变量环境下,执行代码都会进入执行栈,但是对于复杂,会在栈内存存有一个引用地址,指向复杂类型所开辟的堆内存。obj3, 一个引用值赋给另一个变量,实际是复制了一个指针,指向同一个对象,修改一个,另一个也会改变。

  • 两类数据类型赋值方式不一样:简单数据类型:拷贝,复杂:引用

拷贝即便修改,不会改变原来的数据;而引用处理的是同一块地址,修改原来的数据也会改变,上面obj2就是这样。obj2在栈内存在obj1旁边,复制一份obj1引用地址,每次进行修改堆内存的object。值得再提一下,其实这也是有安全隐患的。同样,obj2添加name属性,obj1打印也是存在name属性。

Symbol

symbol是唯一标识符,es6 新增类型

let a = Symbol('a');
console.log(a);// 输出 Symbol(a)
console.log(typeof a);// symbol,这里typeof 后面讲解
// '===' 或 '==' 比较内存地址,
console.log(Symbol('a') === Symbol('a'));// false
console.log(Symbol('a') == Symbol('a'))// false
// Symbol.for() 方法,并且有相同标识符
console.log(Symbol.for('a') === Symbol.for('a'));// true

numeric(number+bigint)

在这里,我把number和bigint分在同一个类型中——数值类型numeric。其实,JavaScript作为面向对象的编程语言,在计算大数据方面并没有优势,有时候还会出错。例如:

let a = 0.1;
let b = 0.2;
console.log(a+b); //结果: 0.30000000000000004

不擅长计算,浮点数计算不准确,数值类型 number ,以二进制存储。 在c语言里面long long 可以解决这种问题, es6为了解决这个问题,增加了bigint。

// 数值范围有限,使用科学计数法
let num1 = 9999999999999999999999999;
let num2 = 1;
//console.log(num1+num2); // 1e+20
let num3 = 123232122222222212222222222;
//console.log(num1+num3);//1.123423498e+21
// 后面加个n,使用 bigint
let num4 = 9999999999999999999999999n;
let num5 = 112231232n;
console.log(num4+num5);//10000000000000000112231231n ,正确

有点子绕的代码,放上台面

function greet(name){
    console.log(`hello123`,name);//使用反引号和${}来插入变量
}
//greet('倾城');
// 添加属性
greet.age = 18;
// 添加方法
greet.proGreeting = function(name){
    return `hello11,${name}`;
}

console.log(greet.proGreeting);// 打印function定义
//console.log(greet.age);
function invokeGreeting(pro,name) {
    // return proGreeting(this)
    return pro(name);
}
// 函数作为参数,
console.log(invokeGreeting(greet,'微微'));
console.log(greet.proGreeting('测试'));
console.log(invokeGreeting(greet.proGreeting,'小娃娃'));

image.png

  • greet 函数被用作 invokeGreeting 函数的参数。当 greet 函数被调用时,它会打印出 "hello123" 以及传入的 name 参数。然而,greet 函数本身并没有返回任何值,因此它的返回值是 undefined

  • greet.proGreeting 函数使用了模板字符串(反引号和 ${})来将 name 参数插入到返回的字符串中。这样,当你调用 greet.proGreeting('微微') 时,它会返回 hello11, 微微

  • 此外,invokeGreeting 函数现在正确地调用了传入的 pro 函数,并将 name 参数传递给它。因此,当你调用 invokeGreeting(greet, '微微') 时,它会调用 greet.proGreeting('微微') 并打印出 hello11, 微微

  • 最后,console.log(invokeGreeting(greet.proGreeting, '小娃娃')); 这一行调用了 invokeGreeting 函数,并将 greet.proGreeting 作为 pro 参数传递。这意味着它会直接调用 greet.proGreeting 函数,并将 '小娃娃' 作为 name 参数传递给它。因此,这一行会打印出 hello11, 小娃娃

谢幕!想要了解更多有关AI或是JS相关内容可以移步到我的主页(⊙o⊙)