JavaScript系列—Js的内存模型

原文地址(https://zhuanlan.zhihu.com/p/62449359

摘要: 从内存角度理解 let 和 const 的意义。

// 声明一些变量并初始化它们
var a = 5;
let b = "xy";
const c = true;

// 分配新值
a = 6;
b = b + "z";
c = false; //  类型错误:不可对常量赋值

作为程序员,声明变量、初始化变量(或不初始化变量)以及稍后为它们分配新值是我们每天都要做的事情。

但是当这样做的时候会发生什么呢? JavaScript 如何在内部处理这些基本功能? 更重要的是,作为程序员,理解 JavaScript 的底层细节对我们有什么好处。

下面,我打算介绍以下内容:

  • JS 原始数据类型的变量声明和赋值
  • JavaScript 内存模型:调用堆栈和堆
  • JS 引用类型的变量声明和赋值
  • let vs const

JS 原始数据类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myNumber的变量,并用值23初始化它。

let myNumber = 23;

当执行此代码时,JS 将执行:

  1. 为变量(myNumber)创建唯一标识符(identifier)。
  2. 在内存中分配一个地址(在运行时分配)。
  3. 将值 23 存储在分配的地址。

虽然我们通俗地说,“myNumber 等于 23”,更专业地说,myNumber 等于保存值 23 的内存地址,这是一个值得理解的重要区别。

如果我们要创建一个名为 newVar 的新变量并把 myNumber 赋值给它。

let newVar = myNumber;

因为 myNumber 在技术上实际是等于 “0012CCGWH80”,所以 newVar 也等于 “0012CCGWH80”,这是保存值为23的内存地址。通俗地说就是 newVar 现在的值为 23

 

 

因为 myNumber 等于内存地址 0012CCGWH80,所以将它赋值给 newVar 就等于将0012CCGWH80赋值给 newVar

现在,如果我这样做会发生什么:

myNumber = myNumber + 1;

myNumber的值肯定是 24。但是newVar的值是否也为 24 呢?,因为它们指向相同的内存地址?

答案是否定的。由于 JS 中的原始数据类型是不可变的,当 myNumber + 1 解析为24时,JS 将在内存中分配一个新地址,将24作为其值存储,myNumber将指向新地址。

 

 

这是另一个例子:

let myString = "abc";
myString = myString + "d";

虽然一个初级 JS 程序员可能会说,字母d只是简单在原来存放adbc内存地址上的值,从技术上讲,这是错的。当 abc 与 d 拼接时,因为字符串也是 JS 中的基本数据类型,不可变的,所以需要分配一个新的内存地址,abcd 存储在这个新的内存地址中,myString 指向这个新的内存地址。

 

下一步是了解原始数据类型的内存分配位置。

JavaScript 内存模型:调用堆栈和堆

JS 内存模型可以理解为有两个不同的区域:调用堆栈(call stack)堆(heap)

调用堆栈是存放原始数据类型的地方(除了函数调用之外)。上一节中声明变量后调用堆栈的粗略表示如下。

在上图中,我抽象出了内存地址以显示每个变量的值。 但是,不要忘记实际上变量指向内存地址,然后保存一个值。 这将是理解 let vs. const 一节的关键。

是存储引用类型的地方。跟调用堆栈主要的区别在于,堆可以存储无序的数据,这些数据可以动态地增长,非常适合数组和对象。

JS 引用类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myArray的变量,并用一个空数组初始化它。

let myArray = [];

当你声明变量“myArray”并为其指定非原始数据类型(如“[]”)时,以下是在内存中发生的情况:

  1. 为变量创建唯一标识符(“myArray”)
  2. 在内存中分配一个地址(将在运行时分配)
  3. 存储在堆上分配的内存地址的值(将在运行时分配)
  4. 堆上的内存地址存储分配的值(空数组[])

 

从这里,我们可以 pushpop,或对数组做任何我们想做的。

myArray.push("first");
myArray.push("second");
myArray.push("third");
myArray.push("fourth");
myArray.pop();

代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug

let vs const

一般来说,我们应该尽可能多地使用const,只有当我们知道某个变量将发生改变时才使用let

让我们明确一下我们所说的“改变”是什么意思。

let sum = 0;
sum = 1 + 2 + 3 + 4 + 5;
let numbers = [];
numbers.push(1);
numbers.push(2);
numbers.push(3);
numbers.push(4);
numbers.push(5);

这个程序员使用let正确地声明了sum,因为他们知道值会改变。但是,这个程序员使用let错误地声明了数组 numbers ,因为他将把东西推入数组理解为改变数组的值

解释“改变”的正确方法是更改内存地址let 允许你更改内存地址。const 不允许你更改内存地址。

const importantID = 489;
importantID = 100; // 类型错误:赋值给常量变量

让我们想象一下这里发生了什么。

当声明importantID时,分配了一个内存地址,并存储489的值。记住,将变量importantID看作等于内存地址。

当将100分配给importantID时,因为100是一个原始数据类型,所以会分配一个新的内存地址,并将100的值存储这里。

然后 JS 尝试将新的内存地址分配给 importantID,这就是抛出错误的地方,这也是我们想要的行为,因为我们不想改变这个 importantID的值。

当你将100分配给importantID时,实际上是在尝试分配存储100的新内存地址,这是不允许的,因为importantID是用const声明的。

如上所述,假设的初级 JS 程序员使用let错误地声明了他们的数组。相反,他们应该用const声明它。这在一开始看起来可能令人困惑,我承认这一点也不直观。

初学者会认为数组只有在我们可以改变的情况下才有用,const 使数组不可变,那么为什么要使用它呢? 请记住:“改变”是指改变内存地址。让我们深入探讨一下为什么使用const声明数组是完全可以的。

const myArray = [];

在声明 myArray 时,将在调用堆栈上分配内存地址,该值是在堆上分配的内存地址。堆上存储的值是实际的空数组。想象一下,它是这样的:

 

如果我们这么做:

myArray.push(1);
myArray.push(2);
myArray.push(3);
myArray.push(4);
myArray.push(5);

执行 push 操作实际是将数字放入堆中存在的数组。而 myArray 的内存地址没有改变。这就是为什么虽然使用const声明了 myArray,但没有抛出任何错误。

myArray 仍然等于 0458AFCZX91,它的值是另一个内存地址22VVCX011,它在堆上有一个数组的值。

如果我们这样做,就会抛出一个错误:

myArray = 3;

由于 3 是一个原始数据类型,因此生成一个新的调用堆栈上的内存地址,其值为 3,然后我们将尝试将新的内存地址分配给 myArray,由于 myArray 是用 const 声明的,所以这是不允许的。

另一个会抛出错误的例子:

myArray = ["a"];

由于[a]是一个新的引用类型的数组,因此将分配调用堆栈上的一个新内存地址,并存储上的一个内存地址的值,其它值为 [a]。然后,我们尝试将调用堆栈内存地址分配给 myArray,这会抛出一个错误。

对于使用const声明的对象(如数组),由于对象是引用类型,因此可以添加键,更新值等等。

const myObj = {};
myObj["newKey"] = "someValue"; // 这不会抛出错误

为什么这些知识对我们有用呢

JavaScript 是世界上排名第一的编程语言(根据 GitHub 和 Stack Overflow 的年度开发人员调查)。 掌握并成为“JS 忍者”是我们所有人都渴望成为的人。

任何质量好的的 JS 课程或书籍都提倡使用let, const 来代替 var,但他们并不一定说出原因。 对于初学者来说,为什么某些 const 变量在“改变”其值时会抛出错误而其他 const变量却没有。 对我来说这是有道理的,为什么这些程序员默认使用let到处避免麻烦。

但是,不建议这样做。谷歌拥有世界上最好的一些程序员,在他们的 JavaScript 风格指南中说,使用 const 或 let 声明所有本地变量。默认情况下使用 const,除非需要重新分配变量,不使用 var 关键字(原文)。

虽然他们没有明确说明原因,但据我所知,有几个原因

  • 先发制人地限制未来的 bug。
  • 使用 const 声明的变量必须在声明时初始化,这迫使程序员经常在范围方面更仔细地放置它们。这最终会导致更好的内存管理和性能。
  • 要通过代码与任何可能遇到它的人交流,哪些变量是不可变的(就 JS 而言),哪些变量可以重新分配。

希望上面的解释能帮助你开始明白为什么或者什么时候应该在代码中使用 let 和 const 。

现在看看几道经典的思考题

问题一

从内存来看 null 和 undefined 本质的区别是什么?

解答

给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性 赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。

给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值

扩展下

声明了一个变量,但未对其初始化时,这个变量的值就是undefined,它是 JavaScript 基本类型 之一。

var data;
console.log(data === undefined); //true

对于尚未声明过的变量,只能执行一项操作,即使用typeof操作符检测其数据类型,使用其他的操作都会报错。

//data变量未定义
console.log(typeof data); // "undefined"
console.log(data === undefined); //报错

值 null 特指对象的值未设置,它是 JavaScript 基本类型 之一。

值 null 是一个字面量,它不像undefined 是全局对象的一个属性。null 是表示缺少的标识,指示变量未指向任何对象。

// foo不存在,它从来没有被定义过或者是初始化过:
foo;
"ReferenceError: foo is not defined"

// foo现在已经是知存在的,但是它没有类型或者是值:
var foo = null; 
console.log(foo);    // null

问题二

ES6语法中的 const 声明一个只读的常量,那为什么下面可以修改const的值?

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

解答

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值