共计 4131 个字符,预计需要花费 11 分钟才能阅读完成。
- 原文地址:JavaScript’s Memory Model
- 原文作者:Ethan Nam
- 译者:Chor
// 申明一些变量并进行初始化
var a = 5
let b = 'xy'
const c = true
// 从新赋值
a = 6
b = b + 'z'
c = false // TypeError: Assignment to constant variable
对咱们程序员来说,申明变量、进行初始化和赋值简直是每天都在做的一件事件。不过,这些操作实质上做了什么事件呢?JavaScript 是如何在外部对这些进行解决的?更重要的是,理解 JavaScript 的底层细节对咱们程序员有什么益处?
本文的纲要如下:
- JS 根本类型的变量申明和赋值
- JS 的内存模型:调用栈和堆
- JS 援用类型的变量申明和赋值
- Let vs const
JS 根本类型的变量申明和赋值
咱们先从一个简略的例子讲起:申明一个名为 muNumber
的变量,并初始化赋值为 23。
let myNumber = 23
当执行这一行代码的时候,JS 将会 ……
- 为变量创立一个惟一的标识符(
myNumber
) - 在栈内存中调配一块空间(将在运行时实现调配)
- 将值 23 保留在这个调配进来的空间中
咱们习惯的说法是“myNumber
等于 23”,但更谨严的说法应该是,myNumber
等于保留着值 23 的那个内存空间的地址。这两者的区别很要害,须要搞清楚。
如果咱们创立一个新变量 newVar
并将 myNumber
赋值给它 ……
let newVar = myNumber
…… 因为 myNumber
实际上等于内存地址“0012CCGWH80”,因而这一操作会使得 newVar
也等于“0012CCGWH80”,也就是等于保留着值 23 的那个内存地址。最终,咱们可能会习惯说“newVar
当初等于 23 了”。
那么,如果我这样做会产生什么呢?
myNumber = myNumber + 1
myNumber
天然会“等于”24,不过 newVar
和 myNumber
指向的可是同一块内存空间啊,newVar
是否也会“等于”24 呢?
并不会。在 JS 中,根本数据类型是 不可扭转的,在“myNumber + 1”被解析为“24”的时候,JS 实际上将会在内存中重新分配一块新的空间用于寄存 24 这个值,而 myNumber
将会转而指向这个新的内存空间的地址。
再看一个类型的例子:
let myString = 'abc'
myString = myString + 'd'
JS 初学者可能会认为,无论字符串 abc
寄存在内存的哪个中央,这个操作都会将字符 d
拼接在字符串前面。这种想法是谬误的。别忘了,在 JS 中字符串也是根本类型。当 abc
与 d
拼接的时候,在内存中会重新分配一块新的空间用于寄存 abcd
这个字符串,而 myString
将会转而指向这个新的内存空间的地址(同时,abc
仍然位于原先的内存空间中)。
接下来咱们看一下根本类型的内存调配产生在哪里。
JS 的内存模型:调用栈和堆
简略了解,能够认为 JS 的内存模型蕴含两个不同的区域,一个是调用栈,一个是堆。
除了函数调用之外,调用栈同时也用于寄存根本类型的数据。以上一大节的代码为例,在申明变量后,调用栈能够粗略示意如下图:
在下面这张图中,我对内存地址进行了形象,以显示每个变量的值,但请记住,(正如之前所说的)变量始终指向某一块保留着某个值的内存空间。这是了解 let vs const 这一大节的要害。
再来看一下堆。
堆是援用类型变量寄存的中央。堆绝对于栈的一个要害区别就在于,堆能够寄存动静增长的无序数据 —— 尤其是数组和对象。
JS 援用类型的变量申明和赋值
在变量申明与赋值这方面,援用类型变量与根本类型变量的行为表现有很大的差别。
咱们同样从一个简略的例子讲起。上面申明一个名为 myArray
的变量并初始化为一个空数组:
let myArray = []
当你申明一个变量 myArray
并通过援用类型数据(比方 []
)为它赋值的时候,在内存中的操作是这样的:
- 为变量创立一个惟一的标识符(
myArray
) - 在堆内存中调配一块空间(将在运行时实现调配)
- 这个空间寄存着此前所赋的值(空数组
[]
) - 在栈内存中调配一块空间
- 这个空间寄存着指向被调配的堆空间的地址
咱们能够对 myArray
进行各种数组操作:
myArray.push("first")
myArray.push("second")
myArray.push("third")
myArray.push("fourth")
myArray.pop()
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
变量是正确的,毕竟 sum
变量的值的确会扭转;不过,用 let
申明 numbers
是谬误的。而谬误的本源在于,这些人认为往数组中增加元素是在扭转它的值。
所谓的“扭转”,实际上指的是内存地址的扭转。let
申明的变量容许咱们批改内存地址,而 const
则不容许。
const importantID = 489
importantID = 100 // TypeError: Assignment to constant variable
咱们钻研一下这里为什么会报错。
当申明 importantID
变量之后,某一块内存空间被调配进来,用于寄存 489 这个值。牢记咱们之前所说的,变量 importantID
素来只等于某一个内存地址。
当把 100 赋值给 importantID
的时候,因为 100 是根本类型的值,内存中会调配一块新的空间用于寄存 100。之后,JS 试图将这块新空间的地址赋值给 importantID
,此时就会报错。这其实正是咱们冀望的后果,因为咱们基本就不想对这个十分重要的 ID 进行改变 …….
这样就说得通了,用 let
申明数组是谬误的(不适合的),应该用 const
才行。这对初学者来说的确比拟困惑,毕竟这齐全不合乎直觉啊!初学者会认为,既然是数组必定须要有所改变,而 const
申明的常量明明是不可改变的啊,那为何还要用 const
?不过,你必须得记住:所谓的“扭转”指的是内存地址的扭转。咱们再来深刻了解一下,为什么在这里应用 const
齐全没问题,并且相对是更好的抉择。
const myArray = []
在申明 myArray
之后,调用栈会调配一块内存空间,它所寄存的值是指向堆中某个被分配内存空间的地址。而堆中的这个空间才是实际上寄存空数组的中央。看上面的图了解一下:
如果咱们进行这些操作:
myArray.push(1)
myArray.push(2)
myArray.push(3)
myArray.push(4)
myArray.push(5)
这将会往堆中的数组增加元素。不过,myArray
的内存地址可是至始至终都没扭转的。这也就解释了为什么 myArray
是用 const
申明的,然而对它(数组)的批改却不会报错。因为,myArray
始终等于内存地址“0458AFCZX91”,该地址指向的空间寄存着另一个内存地址“22VVCX011”,而这第二个地址指向的空间则真正寄存着堆中的数组。
如果咱们这么做,则会报错:
myArray = 3
因为 3 是根本类型的值,这么做会在内存中调配一块新的空间用于寄存 3,同时会批改 myArray
的值,使其等于这块新空间的地址。而因为 myArray
是用 const
申明的,这样批改就必然会报错。
上面这样做同样会报错:
myArray = ['a']
因为 [‘a’]
是一个新的援用类型的数组,因而在栈中会调配一块新的空间来寄存堆中的某个空间地址,堆中这块空间则用于寄存[‘a’]
。之后咱们试图把新的内存地址赋值给 myArray
,这样显然也是会报错的。
对于用 const
申明的对象,它和数组的体现也是一样的。因为对象也是援用类型的数据,能够增加键,更新值,诸如此类。
const myObj = {}
myObj['newKey'] = 'someValue' // this will not throw an error
晓得这些有什么用?
GitHub 和 Stack Overflow 年度开发者调查报告) 的相干数据显示,JavaScript 是排名第一的语言。精通这门语言并成为一名“JS 巨匠”可能是咱们梦寐以求的。在任何一门像样的 JS 课程或者一本书中,都会提倡咱们多应用 const
和 let
,少应用 var
,但他们基本上都没有解释这其中的原因。很多初学者会纳闷为什么有些用 const
申明的变量在“批改”的时候的确会报错,而有些变量却不会。我可能了解,正是这种反直觉的体验让他们更喜爱随处都应用 let
,毕竟谁也不想踩坑嘛。
不过,这并不是咱们举荐的形式。Google 作为一家领有顶尖程序员的公司,它的 JavaScript 格调指南中就有这么一段话:用 const
或者 let
申明所有的局部变量。除非一个变量有从新赋值的须要,否则默认应用 const
进行申明。绝不允许应用 var
关键字 (起源)。
尽管他们没有指出个中原因,不过我认为有上面这些理由:
- 事后防止未来可能产生的 bug
- 用
const
申明的变量在申明的时候就必须进行初始化,这会疏导开发者关注这些变量在作用域中的体现,最终有助于促成更好的内存治理与性能体现。 - 带来更好的可读性,任何接管代码的人都能晓得,哪些变量是不可批改的(就 JS 而言),哪些变量是能够从新赋值的。
心愿本文可能帮忙你了解应用 const
或者 let
申明变量的个中原因以及利用场景。
参考:
- Google JS Style Guide
- Learning JavaScript: Call By Sharing, Parameter Passing
- How JavaScript works: memory management + how to handle 4 common memory leaks
交换
目前专一于前端畛域和交互设计畛域的学习,酷爱分享和交换。感兴趣的敌人能够关注公众号,一起学习和提高。