面试时候,面试官问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),如number、string、boolean、null、undefined、symbol和bigint。栈内存的特点是执行速度快,但空间较小。每个函数调用都会在栈内存中创建一个新的栈帧,用于存储局部变量和函数参数。当函数执行完毕后,栈帧会被自动销毁,释放内存。
堆内存
堆内存用于存储复杂数据类型(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); // 磊磊
内存机制详解
简单数据类型
简单数据类型(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的内存机制,下面是一张图解示意图:
-
在词法环境和变量环境下,执行代码都会进入执行栈,但是对于复杂,会在栈内存存有一个引用地址,指向复杂类型所开辟的堆内存。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,'小娃娃'));
-
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⊙)