MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript变量存储与数据类型分析

2022-08-062.2k 阅读

JavaScript变量存储机制

栈内存与堆内存基础概念

在JavaScript中,变量的存储主要依赖于栈内存(stack)和堆内存(heap)。栈内存是一种线性的数据结构,它按照后进先出(LIFO, Last In First Out)的原则存储数据。栈内存空间相对较小,但是访问速度极快。它主要用于存储基本数据类型(如numberstringbooleannullundefined)以及函数的调用栈信息。

例如,当我们定义一个变量并赋予它一个基本数据类型的值时:

let num = 10;

num这个变量以及它所对应的值10都会被存储在栈内存中。

堆内存则是一个相对松散的数据结构,它用于存储复杂的数据类型,如对象(Object)、数组(Array)和函数(Function)。堆内存空间较大,但访问速度相比栈内存较慢。因为对象和数组等数据结构的大小和结构在运行时可能会动态变化,所以它们被存储在堆内存中。

基本数据类型在栈内存中的存储

  1. 数值类型(number JavaScript中的数值类型采用IEEE 754标准的64位双精度浮点数表示。当我们声明一个数值变量时,它会在栈内存中占据一定的空间。
let age = 25;
let price = 19.99;

这里ageprice变量以及它们对应的值都存储在栈内存中。数值类型在栈内存中的存储是直接存储其值,所以对数值变量的操作直接作用于栈内存中的数据。

  1. 字符串类型(string 字符串类型在JavaScript中是不可变的。当我们声明一个字符串变量时,字符串的值也存储在栈内存中。
let name = 'John';
let message = 'Hello, world!';

虽然字符串内部可能是由多个字符组成,但从存储角度看,整个字符串作为一个整体存储在栈内存中。由于其不可变性,如果对字符串进行修改操作,如拼接字符串,实际上会在栈内存中创建一个新的字符串。

let str1 = 'Hello';
let str2 = str1 + ', world';

这里str2是一个新创建的字符串,存储在栈内存的新位置。

  1. 布尔类型(boolean 布尔类型只有两个值:truefalse。它们在栈内存中占据相对较小的空间。
let isDone = true;
let hasError = false;

布尔变量及其值直接存储在栈内存中,在逻辑判断等操作中被频繁使用。

  1. nullundefined null表示一个空值,undefined表示变量声明但未初始化。它们也都存储在栈内存中。
let empty = null;
let uninitialized;
console.log(uninitialized); // 输出: undefined

nullundefined虽然含义不同,但在存储上都是占据栈内存中的特定位置,用于表示缺失或空的值。

引用数据类型在堆内存中的存储

  1. 对象类型(Object 当我们创建一个对象时,对象本身存储在堆内存中,而在栈内存中存储的是指向堆内存中对象的引用(地址)。
let person = {
    name: 'Alice',
    age: 30,
    address: '123 Main St'
};

在上述代码中,person变量存储在栈内存中,它保存的是堆内存中对象的引用。当我们访问对象的属性时,实际上是通过栈内存中的引用找到堆内存中的对象,然后获取相应的属性值。

console.log(person.name); // 输出: Alice

如果我们将一个对象赋值给另一个变量,实际上是复制了栈内存中的引用,而不是对象本身。

let anotherPerson = person;
anotherPerson.age = 31;
console.log(person.age); // 输出: 31

这里anotherPersonperson指向堆内存中的同一个对象,所以对anotherPerson的修改会影响到person

  1. 数组类型(Array 数组本质上也是对象,所以数组的存储方式与对象类似。数组在堆内存中存储其元素,而在栈内存中存储指向堆内存中数组的引用。
let numbers = [1, 2, 3, 4, 5];

numbers变量在栈内存中,它引用堆内存中的数组。数组元素的访问也是通过栈内存中的引用找到堆内存中的数组,然后根据索引获取元素。

console.log(numbers[2]); // 输出: 3

同样,如果将一个数组赋值给另一个变量,也是复制引用。

let newNumbers = numbers;
newNumbers.push(6);
console.log(numbers.length); // 输出: 6
  1. 函数类型(Function 函数在JavaScript中也是对象,所以存储方式同样是在堆内存中存储函数的代码逻辑,而在栈内存中存储指向堆内存中函数的引用。
function add(a, b) {
    return a + b;
}

add变量在栈内存中,指向堆内存中的函数对象。当我们调用函数时,通过栈内存中的引用找到堆内存中的函数代码并执行。

let result = add(3, 5);
console.log(result); // 输出: 8

JavaScript数据类型深入分析

基本数据类型的特性与行为

  1. 数值类型的范围与精度 JavaScript的number类型能表示的数值范围非常大,其最小值约为5e-324,最大值约为1.7976931348623157e+308。然而,由于采用双精度浮点数表示,在处理小数时可能会出现精度问题。
let num1 = 0.1;
let num2 = 0.2;
console.log(num1 + num2); // 输出: 0.30000000000000004

这是因为在二进制表示中,0.10.2无法精确表示,导致相加后出现微小的误差。在进行涉及金额等对精度要求极高的计算时,需要特别注意,可以使用BigInt类型或者专门的小数运算库。

  1. 字符串的操作与特性 字符串除了前面提到的不可变性,还有丰富的操作方法。例如,length属性可以获取字符串的长度。
let str = 'JavaScript';
console.log(str.length); // 输出: 10

字符串的查找方法,如indexOf可以查找子字符串的位置。

let str = 'Hello, world!';
console.log(str.indexOf('world')); // 输出: 7

此外,字符串还可以通过split方法分割成数组。

let str = 'apple,banana,orange';
let fruits = str.split(',');
console.log(fruits); // 输出: ['apple', 'banana', 'orange']
  1. 布尔类型的逻辑运算 布尔类型主要用于逻辑判断,常见的逻辑运算符有&&(逻辑与)、||(逻辑或)和!(逻辑非)。
let isTrue = true;
let isFalse = false;
console.log(isTrue && isFalse); // 输出: false
console.log(isTrue || isFalse); // 输出: true
console.log(!isTrue); // 输出: false

逻辑与运算只有当两个操作数都为true时才返回true,逻辑或运算只要有一个操作数为true就返回true,逻辑非运算则是对操作数的布尔值取反。

  1. nullundefined的区别与应用场景 null通常表示有意的空值,例如在初始化变量时,如果预期该变量将来可能为空,可以先赋值为null
let element = null;
// 之后可能会给element赋值为一个实际的DOM元素

undefined则更多表示变量声明但未初始化,或者函数没有返回值时返回undefined

function noReturnValue() {}
let result = noReturnValue();
console.log(result); // 输出: undefined

在判断变量是否存在时,需要注意区分nullundefinedtypeof操作符对null返回'object',对undefined返回'undefined'

console.log(typeof null); // 输出: object
console.log(typeof undefined); // 输出: undefined

引用数据类型的特性与行为

  1. 对象的属性操作与遍历 对象的属性可以动态添加、删除和修改。
let person = {
    name: 'Bob'
};
// 添加属性
person.age = 28;
// 修改属性
person.name = 'Robert';
// 删除属性
delete person.age;

对象的遍历可以使用for...in循环。

let person = {
    name: 'Charlie',
    age: 32,
    profession: 'Engineer'
};
for (let key in person) {
    console.log(key + ': ' + person[key]);
}
// 输出:
// name: Charlie
// age: 32
// profession: Engineer

但是for...in循环会遍历对象自身及其原型链上的可枚举属性。如果只想遍历对象自身的属性,可以使用Object.keys方法。

let person = {
    name: 'David',
    age: 25
};
let keys = Object.keys(person);
keys.forEach(key => {
    console.log(key + ': ' + person[key]);
});
// 输出:
// name: David
// age: 25
  1. 数组的方法与特性 数组有许多实用的方法,如push用于在数组末尾添加元素,pop用于删除数组末尾的元素。
let numbers = [1, 2, 3];
numbers.push(4);
console.log(numbers); // 输出: [1, 2, 3, 4]
let removed = numbers.pop();
console.log(removed); // 输出: 4
console.log(numbers); // 输出: [1, 2, 3]

shift方法用于删除数组开头的元素,unshift方法用于在数组开头添加元素。

let numbers = [1, 2, 3];
let removedFirst = numbers.shift();
console.log(removedFirst); // 输出: 1
console.log(numbers); // 输出: [2, 3]
numbers.unshift(0);
console.log(numbers); // 输出: [0, 2, 3]

数组还支持迭代方法,如mapfilterreducemap方法会创建一个新数组,其元素是原数组元素经过指定函数处理后的结果。

let numbers = [1, 2, 3];
let squared = numbers.map(num => num * num);
console.log(squared); // 输出: [1, 4, 9]

filter方法会创建一个新数组,其元素是原数组中满足指定条件的元素。

let numbers = [1, 2, 3, 4, 5];
let evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 输出: [2, 4]

reduce方法可以对数组元素进行累加操作。

let numbers = [1, 2, 3, 4];
let sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 输出: 10
  1. 函数的特性与作用域 函数在JavaScript中具有头等公民的地位,这意味着函数可以像其他数据类型一样被赋值给变量、作为参数传递给其他函数以及作为函数的返回值。
function add(a, b) {
    return a + b;
}
let sumFunction = add;
let result = sumFunction(2, 3);
console.log(result); // 输出: 5

函数具有自己的作用域,函数内部定义的变量在函数外部无法访问。

function testScope() {
    let localVar = 10;
    console.log(localVar); // 输出: 10
}
// console.log(localVar); // 报错: localVar is not defined

JavaScript采用词法作用域(静态作用域),即函数的作用域在定义时就确定了,而不是在调用时确定。

let outerVar = 20;
function outerFunction() {
    let innerVar = 30;
    function innerFunction() {
        console.log(outerVar); // 输出: 20
        console.log(innerVar); // 输出: 30
    }
    innerFunction();
}
outerFunction();

在上述代码中,innerFunction可以访问outerFunction的变量,因为innerFunction定义在outerFunction内部,它的作用域链包含了outerFunction的作用域。

数据类型的转换

  1. 显式类型转换 显式类型转换是指我们通过特定的函数或操作符明确地将一种数据类型转换为另一种数据类型。
  • 数值转换:可以使用Number()函数将其他数据类型转换为数值。
let strNumber = '123';
let num = Number(strNumber);
console.log(num); // 输出: 123
let boolToNum = Number(true);
console.log(boolToNum); // 输出: 1
let nullToNum = Number(null);
console.log(nullToNum); // 输出: 0
let undefinedToNum = Number(undefined);
console.log(undefinedToNum); // 输出: NaN
  • 字符串转换:使用String()函数可以将其他数据类型转换为字符串。
let numToStr = String(123);
console.log(numToStr); // 输出: '123'
let boolToStr = String(false);
console.log(boolToStr); // 输出: 'false'
let objToStr = String({name: 'Eve'});
console.log(objToStr); // 输出: '[object Object]'
  • 布尔转换Boolean()函数用于将其他数据类型转换为布尔值。除了0nullundefined''(空字符串)、NaN会转换为false,其他值都会转换为true
let numToBool = Boolean(1);
console.log(numToBool); // 输出: true
let zeroToBool = Boolean(0);
console.log(zeroToBool); // 输出: false
let strToBool = Boolean('test');
console.log(strToBool); // 输出: true
let emptyStrToBool = Boolean('');
console.log(emptyStrToBool); // 输出: false
  1. 隐式类型转换 隐式类型转换是JavaScript在某些操作中自动进行的类型转换。例如,在比较操作中,如果操作数类型不同,会发生隐式类型转换。
let num = 5;
let str = '5';
console.log(num == str); // 输出: true,这里发生了隐式类型转换,将字符串'5'转换为数值5后进行比较
console.log(num === str); // 输出: false,因为===不会进行隐式类型转换,类型不同直接返回false

在算术运算中也会发生隐式类型转换。

let result = 5 + '3';
console.log(result); // 输出: '53',这里数值5被隐式转换为字符串后进行拼接

但是在减法、乘法等运算中,字符串会被尝试转换为数值。

let subResult = 5 - '3';
console.log(subResult); // 输出: 2,字符串'3'被转换为数值3后进行减法运算

了解JavaScript变量存储和数据类型的本质,对于编写高效、健壮的JavaScript代码至关重要。通过合理利用不同数据类型的特性以及理解其存储机制,可以更好地优化代码性能、避免潜在的错误。无论是开发前端页面交互,还是构建后端Node.js应用,对这些基础知识的深入掌握都是必不可少的。同时,随着JavaScript不断发展,新的数据类型和特性不断涌现,持续学习和关注这些变化也有助于我们保持技术的先进性。在实际项目中,我们需要根据具体需求选择合适的数据类型,并谨慎处理类型转换,以确保程序的正确性和稳定性。例如,在处理用户输入的数据时,要充分考虑可能的类型错误,进行必要的类型检查和转换,防止因数据类型不匹配导致的程序崩溃或逻辑错误。总之,对JavaScript变量存储与数据类型的深入理解是成为优秀JavaScript开发者的重要基石。