如何编写高质量的 JS 函数( 二 )


赋值操作是从计算机组成原理角度看,内存分为好几个区域,比如代码区域,栈区域,堆区域等 。
这几个区域每一个存储空间的内存地址都是不一样 。也就是说,赋值(引用类型)的操作就是将堆区域的某一个地址,通过总线管道流入(复制)到对应栈区域的某一个地址中,从而使栈区域的某一个地址内的存储空间中有了引用堆区域数据的地址 。业界叫句柄,也就是指针 。只不过在高级语言中,把指针隐藏了,直接用变量代替指针 。
所以一个简单的赋值,其在计算机底层实现上,都是很复杂的 。这里,也许通过汇编语言,可以更好的去理解赋值的真正含义,比如 1 + 1 用汇编语言编写,就是下面代码:

如何编写高质量的 JS 函数

文章插图
 
从上面代码中,我们可以看到,把 1 赋值给 ax ,使用到了 mov 指令 。而 mov 是 move 移动的缩写,这也证明了,在赋值这个操作上,本质上是数据或者数据的句柄在一张地址表中的流动 。
PS: 所以如果是值类型,那就是直接把数据,流(移动)到指定内存地址的存储空间中 。
以上是我从计算机底层去解释一些创建函数方面最基础的现象,先阐述到这里 。
(3)执行函数执行函数过程也非常重要,我用个人的总结去解释执行这个过程 。
思考一个点 。
我们知道,函数体的代码是以字符串形式的保存在堆内存中的 。如果我们要执行堆内存中的代码,首先要将字符串变成真正的 JS 代码,就像数据传输中的序列化和反序列化 。
思考题一:为什么会存在序列化和反序列化?大家可以自行思考一下,有些越简单的道理,背后越是有着非凡的思想 。
(4)将字符串变成真正的 JS 代码每一个函数调用,都会在函数上下文堆栈中创建帧 。栈是一个基本的数据结构 。
为什么函数执行要在栈中执行呢?
栈是先进后出的数据结构,也就意味着可以很好的保存和恢复调用现场 。
来看一段代码:
如何编写高质量的 JS 函数

文章插图
 
函数上下文堆栈是什么?
函数上下文堆栈是一个数据结构,如果学过 C++ 或者 C 的,可以理解成是一个 struct (结构体) 。这个结构体负责管理函数执行已经关闭变量作用域 。函数上下文堆栈在程序运行时产生,并且一开始加入到栈里面的是全局上下文帧,位于栈底 。
(5)开始执行函数首先要明白一点:执行函数(函数调用)是在栈上完成的。
这也就是为什么 JS 函数可以递归 。因为栈先进后出的数据结构,赋予了其递归能力 。
继续往下看,函数执行大致有以下四个步骤:
第一步:形成一个供代码执行的环境,也是一个栈内存 。
这里,我们先思考几个问题:
这个供代码执行的环境是什么?
这个栈内存是怎么分配出来的?
这个栈内存的内部是一种什么样的样子?
第二步:将存储的字符串复制一份到新开辟的栈内存中,使其变为真正的 JS 代码 。
第三步:先对形参进行赋值,再进行变量提升,比如将 var function 变量提升 。
第四步:在这个新开辟的作用域中自上而下执行 。
思考题:为什么是自上而下执行呢?
将执行结果返回给当前调用的函数
思考题:将执行结果返回给当前调用的函数,其背后是如何实现的呢?
三、谈谈底层实现1、计算机中最本质的闭包解释函数在执行的时候,都会形成一个全新的私有作用域,也叫私有栈内存 。
目的有如下两点:
  • 第一点:把原有堆内存中存储的字符串变成真正的 JS 代码 。
  • 第二点:保护该栈内存的私有变量不受外界的干扰 。
函数执行的这种保护机制,在计算机中称之为 闭包。
可能有人不明白,咋就私有了呢?
没问题,我们可以反推 。假设不是私有栈内存的,那么在执行一个递归时,基本就结束了,因为一个函数上下文堆栈中,有很多相同的 JS 代码,比如局部变量等,如果不私有化,那岂不乱套了?所以假设矛盾,私有栈内存成立 。
2、栈内存是怎么分配出来?JS 的栈内存是系统自动分配的,大小固定 。如果自动适应的话,那就基本不存在除死循环这种情况之外的栈溢出了 。
3、这个栈内存的内部是一种什么样的样子?举个例子,每天写 return 语句,那你知道 return 的底层是如何实现的吗?每天写子程序,那你知道子程序底层的一些真相吗?


推荐阅读