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


我们来看一张图:

如何编写高质量的 JS 函数

文章插图
 
上图显示了一次函数调用的栈结构,从结构中可以看到,内部的组成部分,比如实参,局部变量,返回地址 。
看下面代码:
如何编写高质量的 JS 函数

文章插图
 
上面这行代码的底层含义就是,f() 函数在私有栈内存中执行完后,使用 return 后,将执行结果传递给 EAX (累加寄存器),常用于函数返回值 。
这里说一下 Return Addr ,Addr 主要目的是让子程序能够多次被调用 。
看下面代码:
如何编写高质量的 JS 函数

文章插图
 
如上,在 main 函数中进行多次调用子程序 say ,在底层实现上面,是通过在栈结构中保存一个 Addr 用来保存函数的起始运行地址,当第一个 say 函数运行完以后,Addr 就会指向起始运行地址,以备后面多次调用子程序 。
四、JS 引擎是如何执行函数上面从很多方面分析了函数执行的机制 。现在来简要分析一下,JS 引擎是如何执行函数的 。
推荐一篇博客《探索JS引擎工作原理》,我将在此篇博客的基础上分析一些很重要的细节 。
代码如下:
如何编写高质量的 JS 函数

文章插图
 
执行 A 函数时
JS 引擎构造的 ESCstack 结构如下:
简称 A 图:
如何编写高质量的 JS 函数

文章插图
 
执行 B 函数时
JS 引擎构造的 ESCstack 结构如下:
简称 B 图:
如何编写高质量的 JS 函数

文章插图
 
1、局部变量是如何被保存起来的核心代码:
如何编写高质量的 JS 函数

文章插图
 
这是在执行 B 函数 时,创建的 B 函数的执行环境(一个对象结构) 。里面有一个 AO(B) ,这是 B 函数的活动对象 。
那 AO(B) 的目的是什么?其实 AO(B) 就是每个链表的节点其指向的内容 。
同时,这里还定义了 [scope] 属性,我们可以理解为指针,[scope] 指向了 AO(A) ,而 AO(A) 就是函数 A 的活动对象 。
函数活动对象保存着 局部变量、参数数组、this 属性 。这也是为什么可以在函数内部使用 this 和 arguments 的原因 。
scopeChain 是作用域链,熟悉数据结构的同学肯定知道我函数作用域链本质就是链表,执行哪个函数,那链表就初始化为哪个函数的作用域,然后把当前指向的函数活动对象放到 scopeChain 链表的表头中 。
比如执行 B 函数,那 B 的链表看起来就是 AO(B) --> AO(A)
同时,A 函数也是有自己的链表的,为 AO(A) --> VO(G)。所以整个链表就串起来来,B 的链表(作用域)就是:AO(B) --> AO(A) --> VO(G)
链表是一个闭环,因为查了一圈,回到自己的时候,如果还没找到,那就返回 undefined。
思考题:[scope] 和 [[scope]] 为什么以这种形式命名?
2、通过 A 函数的 ECS 我们能看到什么我们能看到,JS 语言是静态作用域语言,在执行函数之前,整个程序的作用域链就确定了,从 A 图中的函数 B 的 B[[scope]] 就可以看到作用域链已经确定 。不像 lisp 那种在运行时才能确定作用域 。
3、执行环境,上下文环境是一种什么样的存在执行环境的数据结构是栈结构,其实本质上是给一个数组增加一些属性和方法 。
执行环境可以用 ECStack 表示,可以理解成 ECSack = [] 这种形式 。
栈(执行环境)专门用来存放各种数据,比如最经典的就是保存函数执行时的各种子数据结构 。比如 A 函数的执行环境是 EC(A) 。当执行函数 A 的时候,相当于 ECStack.push[A] ,当属于 A 的东西被放入到栈中,都会被包裹成一个私有栈内存 。
私有栈是怎么形成的?从汇编语言角度去看,一个栈的内存分配,栈结构的各种变换,都是有底层标准去控制的 。
4、开启上帝模式看穿 this
this 为什么在运行时才能确定
上面两张图中的红色箭头,箭头处的信息非常非常重要 。
看 A 图,执行 A 函数时,只有 A 函数有 this 属性,执行 B 函数时,只有 B 函数有 this 属性,这也就证实了 this 只有在运行时才会存在 。
this 的指向真相
看一下 this 的指向,A 函数调用的时候,属性 this 的属性是 window ,而 通过 var C = A(1) 调用 A 函数后,A 函数的执行环境已经 pop 出栈了 。此时执行 C() 就是在执行 B 函数,EC(B) 已经在栈顶了,this 属性值是 window 全局变量 。


推荐阅读