龙空技术网

你是否听说过JavaScript的环境模型?

IT技术之家 396

前言:

如今兄弟们对“jsevalif”大概比较关怀,姐妹们都需要剖析一些“jsevalif”的相关资讯。那么小编在网摘上汇集了一些关于“jsevalif””的相关知识,希望姐妹们能喜欢,兄弟们一起来学习一下吧!

环境模型(Environment Model) 这一个概念,它用于解释Scheme的函数计算规则。由@佳木授权分享。

正文从这开始~~

《SICP》提到了 环境模型(Environment Model) 这一个概念,它用于解释Scheme的函数计算规则。同样,它也适用于JavaScript的函数计算规则。

环境是什么

节选《SICP》 3.2 The Environment Model of Evaluation

环境在计算过程必不可少,因为它决定了计算表达式的上下文。 可以这样认为,表达式本身在程序语言里毫无意义,表达式的意义取决于它计算时所在的环境。就算是(+ 1 1)这一条极其简单的表达式,也需要在符号+表示加法的上下文里才能进行计算。

JavaScript的解释器就充当着环境的角色。在该环境下,表达式1 + 1的计算结果为2,表达式Date()调用一个函数并返回当前的时间,表达式() => 1定义了一个返回1的函数……总之,对程序而言,环境就是在计算过程为符号提供实际意义的东西。

环境模型

变量环境

环境模型中的环境具体指的是变量环境。函数在计算时会根据 环境(environment) 决定变量的值,从而决定它的计算结果。

环境的创建和作用

函数在调用时会先创建一个环境,然后在该环境中计算函数的内容。

function add10(value) { //1 var increment = 10; //2 return value + increment; //3} //4add10(2); //5

表达式add10(2)(>5)的计算过程:

创建环境$add10。(>5)给环境$add10中的变量value赋值2。(>5)进入环境$add10。在环境$add10中,给变量increment赋值10。(>2)在环境$add10中,获得变量value的值2。(>3)在环境$add10中,获得变量increment的值10。(>3)计算表达式2 + 10得到12。(>3)返回12。(>3)离开环境$add10。

值得一提的是,形参也是变量,它在形参列表里定义,在函数调用时获得初始值。

变量绑定

环境使用变量绑定来存放变量的值, 绑定(binding) 与函数中的变量一一对应。

约束变量和自由变量

在函数中定义一个变量,变量的意义取决于函数的内容,它的作用范围也被约束在函数之中,此时的变量被称为 约束变量(bound variable) 。

在函数中使用一个没有定义的变量,它的作用范围不受函数的约束,此时的变量被称为 自由变量(free variable) 。

function main() { //1 var x = 10; //2 var addX = function (value) { //3 var increment = x; //4 return value + increment; //5 }; //6 var value = 2; //7 addX(value); //8} //9main(); //10

var关键字可以定义变量:

在函数main中,变量x(>2、4),addX(>3、8),value(>7、8)皆为约束变量。在函数addX中,变量value(>3、5),increment(>4、5)是约束变量,变量x(>4)是自由变量。

绑定与变量

在函数的计算过程中,变量定义会使当前的环境加入对应的绑定。

上文中表达式main()(>10)的计算过程产生了2个环境,$main和$addX:

环境$main拥有3个绑定,x,addX,*value。环境$addX拥有2个绑定,value,increment。

可见,绑定存放的是约束变量的值,约束变量的值可以直接从当前环境获取。

而自由变量的值需要从其他环境获取,该环境是自由变量定义时所在的环境,拥有自由变量的绑定。

上文中表达式addX(value)(>8)的计算过程:

获得环境$main中绑定*addX的值addX函数。(>8)获得环境$main中绑定*value的值2。(>8)修改环境$addX中绑定*value的值为2。(>8)获得环境$main中绑定*x的的值10。(>4)修改环境$addX中绑定*increment的值为10。(>4)获得环境$addX中绑定*value的值2。(>5)获得环境$addX中绑定*increment的值10。(>5)

计算function表达式或lambda表达式会得到一个函数,这种情况一般被称为函数定义。方便起见,本文将值是变量的函数称为函数。

就这样,函数在计算时只要找到对应的绑定,就能确定一个变量的值。

环境引用

环境不仅保存了变量绑定,还会保存一个 环境引用(environment pointer) ,环境引用指向其他的变量环境。通过环境引用,自由变量可以从其他环境寻找自己对应的绑定。

环境引用的来源

函数在定义时会把当前环境的引用记录下来。在调用函数后,新的环境会得到函数中的环境引用并将此保存。

也就是说,一个函数在计算时的环境,拥有函数在定义时的环境的引用。

var getCounter = function (start) { //1 return function () { //2 return start++; //3 }; //4}; //5var counter = getCounter(0); //6counter(); //7

表达式getCounter(0)(>6)和counter()(>7)分别创建了两个环境:

环境$getCounter拥有全局环境的引用。环境$counter拥有环境$getCounter的引用。

一些看似不在函数中定义的函数,其定义时也身处环境中,该环境被称为全局环境。函数getCounter就保存了全局环境的引用。

环境引用与绑定

函数在计算过程中定义函数,如同代码文本结构那样一层包裹一层,里层的函数定义是外层函数中的一条表达式,里层函数创建的环境通过引用连接外层函数创建的环境。

因此,一个变量在当前环境找不到对应的绑定时,可以通过引用一层层回溯到它定义时所在的环境,从而找到该绑定。自由变量便是通过这种方法找到自己对应的绑定。

上文中表达式counter()(>7)的计算过程:

使用变量counter。(>7)在当前环境(全局环境)找到变量绑定*counter,它的值是一个函数 。调用函数counter会创建环境$counter。(>7)环境$counter从函数counter得到环境$getCounter的引用。进入环境$counter。使用变量start。(>3)在环境$counter找不到绑定*start。环境$counter通过引用定位到环境$getCounter。在环境$getCounter中找到绑定*start。返回绑定*start的值0作为函数的计算结果。(>3)令绑定*start的值自增1,从0变为1。(>3)离开环境$counter。

每次计算表达式counter(),绑定*start的值都会自增1,并依次返回0,1,2,3……

总结

函数在定义时会保存当前 环境 的 引用 。

一旦函数被调用,就会创建一个新的环境,新的环境拥有函数定义时环境的引用。

函数中的变量定义表达式会给新环境加入 绑定 。

函数使用变量就是访问环境中对应的绑定。

如果变量在当前环境找不到对应的绑定,就会通过引用一层层回溯到它定义时所在环境,从而找到它的绑定。

而这种访问其他变量环境的机制,通常被人称为 闭包 。

模拟环境模型

下文将讲述如何用js模拟环境模型。在这个模拟环境模型中,不需要用到js的变量定义语法也能使用闭包。

代码实现

模拟环境模型不是编写函数的解释器,只是将环境变为可操作的实体,用来解释函数中的变量。

首先确定模拟环境的使用方式。为了能在函数中使用环境,环境将作为参数传入被调用的函数:

function $func($){ //$是$func调用时创建的环境。};

class Environment

函数通过环境使用变量,环境应有getVariable和setVariable方法。

变量在使用前要有定义,环境应有defineVariable方法。

此外,函数在定义时会保存当前环境的引用,环境应有defineFunction方法。

因此,代表环境的class是这样的:

class Environment { //变量定义 defineVariable(name) { } //变量取值 getVariable(name) { } //变量赋值 setVariable(name, value) { } //函数定义 defineFunction($func) { }}

bindingContainer member

环境可以看作是变量(绑定)的容器,应有一个bindingContainer成员用来存放变量。

考虑到前端js的全局变量可以在window对象上找到,bindingContainer使用Object类型的对象的话,可以与window[name]同样的形式bindingContainer[name]来访问变量。

因此,变量定义、取值、赋值可以表达为:

this.bindingContainer[name] = null; //定义value = this.bindingContainer[name]; //取值this.bindingContainer[name] = value; //赋值

defineVariable method

Environment的defineVariable方法实现很直接,为当前环境加入绑定:

defineVariable(name) { this.bindingContainer[name] = null;}

environmentPointer member

在当前环境使用的变量,绑定有可能在别的环境中,应有一个代表环境引用的成员environmentPointer。

且environmentPointer是Environment类型。

findBindingContainer method

取值和赋值都需要找到变量的绑定,应有一个共同的方法findBindingContainer用来查找绑定。

为了方便赋值进行,方法返回的是绑定的容器。

变量在当前环境找不到绑定时,会通过引用向上一层环境查找。这是递归的,因此findBindingContainer的表达为:

findBindingContainer(variable_name) { //判断当前环境是否存在绑定。 if (this.bindingContainer.hasOwnProperty(variable_name)) { //找到了绑定,返回绑定的容器。 return this.bindingContainer; } else { //在该环境中找不到绑定。 //判断引用是否达到了尽头。 if (this.environmentPointer === Environment.End) { //环境引用走到了尽头,抛出异常。 throw '不存在对应的绑定。'; } else { //通过环境引用在上一层环境中查找绑定。 return this.environmentPointer.findBindingContainer(variable_name); } }}

Object类型的对象自带hasOwnProperty方法判断自己是否拥有某个成员。

Environment.End member

显然,通过引用一直向上遍历环境是有尽头的,在这里规定环境的尽头Environment.End为null:

Environment.End = null;

getVariable method

有了findBindingContainer方法,便能轻易写出getVariable方法:

getVariable(name) { var binding_container = this.findBindingContainer(name); var value = binding_container[name]; return value;}

setVariable method

同上,setVariable方法的表达为:

setVariable(name, value) { var binding_container = this.findBindingContainer(name); binding_container[name] = value;}

defineFunction method

模拟环境模型不具备定义函数的功能,defineFunction只需令已定义的函数保存当前环境的引用。

js函数无法直接保存引用和创建模拟环境,因此需要一个用来代理函数的对象,假设defineFunction的表达为:

defineFunction(proxy) { proxy.saveEnvironmentPointer(this); var func = proxy.getCall(); return func;}

class $Function

代理函数的对象使用saveEnvironmentPointer方法保存环境引用,使用getCall方法返回实际被调用的函数。

被代理的函数,就是使用模拟环境的函数$func,显然$func不能被直接调用。它需要:

创建新的环境。在新环境中加入可能的实际参数。以新环境为参数。

因此,代理函数的对象应具有call方法,以此满足$func被调用的需求。

综上所述,代理函数的对象类型是这样的:

class $Function { saveEnvironmentPointer(environmentPointer) { } getCall() { } call(...args) { }}

$Function还应有这样三个成员:

使用模拟环境的函数$func。描述函数$func的参数列表parameterList。$func定义时所在环境的引用environmentPointer。

值得一提的是,函数$func只有一个表示环境的参数$,无法表达普通函数的参数列表。因此需要parameterList来描述它的参数列表,用一个字符串数组便能表达。

saveEnvironmentPointer method

saveEnvironmentPointer方法的实现很直接:

saveEnvironmentPointer(environmentPointer) { this.environmentPointer = environmentPointer;}

getCall method

getCall方法实际返回的是call方法:

getCall() { return this.call.bind(this);}

由于call方法的实现用到其他成员,call在返回时需要绑定this。

call method

如上文所述,call方法作为实际被调用的函数,它会:

创建新的环境。在新环境中加入可能的实际参数。以新环境为参数调用$func函数。

call(...args) { //创建新的环境,并传入上一层环境的引用。 var new_environment = new Environment(this.environmentPointer); //根据形参列表初始化新环境的绑定。 for (var [i, name] of this.parameterList.entries()) { new_environment.bindingContainer[name] = args[i]; } //将新环境作为参数传入使用模拟环境的函数并调用之。 var result = this.$func(new_environment); return result;}

Environment constructor

至此,补充一下Environment的构造方法,上一层环境引用在构造新环境时传入:

constructor(pointer) { this.environmentPointer = pointer; this.bindingContainer = {};}

$Function constructor

$Function在构造时只需要从外部传入$func和parameterList:

constructor($func, parameterList = []) { this.$func = $func; this.parameterList = parameterList;}

参数列表默认为空数组。

Environment.Global member

在使用模拟环境之前补充全局环境的定义:

//全局环境中的环境引用只能是Environment.End了。Environment.Global = new Environment(Environment.End);//前端js通过window可以访问全局变量,因此window作为全局环境的容器。Environment.Global.bindingContainer = window;

使用方式

获得模拟环境模型代码的整合。

例1

原代码:

var add = function (a, b) { return a + b;};add(1, 2); //3

使用模拟环境的代码:

Environment.Global.defineVariable('add');Environment.Global.setVariable( 'add', Environment.Global.defineFunction(new $Function( function ($) { //return a + b; return $.getVariable('a') + $.getVariable('b'); }, ['a', 'b'])));add(1, 2); //3

例2

原代码:

var getCounter = function (start) { return function () { var result = start; start += 1; return result; };};var counter = getCounter(0);counter(); //0counter(); //1counter(); //2

使用模拟环境的代码:

Environment.Global.defineVariable('getCounter');Environment.Global.setVariable( 'getCounter', Environment.Global.defineFunction(new $Function( function ($) { return $.defineFunction(new $Function(function ($) { $.defineVariable('result'); //result = start; $.setVariable('result', $.getVariable('start')); //start += 1; $.setVariable('start', $.getVariable('start') + 1); //return result; return $.getVariable('result'); })); }, ['start']) ));Environment.Global.defineVariable('counter');//counter = getCounter(0);Environment.Global.setVariable('counter', getCounter(0));counter(); //0counter(); //1counter(); //2

其他

Environment.js的主要意义是让人熟悉环境模型的概念,作为代码没有太多的使用价值。

这里有一个展示环境模型细节的版本。它通过console打印每一阶段的内容。这是它的demo。

Environment.detail.js在使用上与Environment.js有微小的差异,$Function的构造函数多了一个用作函数名的参数。

验证环境模型

以chrome为观察平台,通过console.dir方法可以展示一个对象的状态。

特别的,console.dir一个函数可以查看它的环境信息。

作用域

执行以下代码:

var foo = function () {};console.dir(foo);

在Console展开foo可见:

▼ƒ foo()... ▼[[Scopes]]: Scopes[1] ▶0: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

实际上, 作用域(scope) 就是环境的实现,从console.dir看到的[[Scopes]]属性便包含了环境的信息。

Global是作用域的类型之一,代表的是全局作用域。全局作用域全局环境,可见函数foo保存了全局环境的引用。

作用域链

执行以下代码:

var f1 = function () { var s1 = 0; var f2 = function () { return s1; }; console.dir(f2);};f1();

在Console展开f2可见:

▼ƒ f2()... ▼[[Scopes]]: Scopes[2] ▼0: Closure (f1) s1: 0 ▶1: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

[[Scopes]]是一个数组,它表示的是作用域链。环境也是一个链表,从环境模型的角度看待,这是把环境引用的关系转化为数组,数组前面的环境保存后面环境的引用。

函数f2保存了环境$f1的引用,环境$1保存了全局环境的引用。这种信息同样可以从[[Scopes]]获得。

Closure也是作用域的类型之一,还能从作用域“Closure (f1)”得知环境$f1包含了绑定*s1。

消失的变量

上文中,作用域“Closure (f1)”只包含了s1。f2也是变量,根据环境模型,它理应包含两个变量的状态,s1和f2。

实际上,这是环境模型的实践被js优化过所造成的结果。

解释器在执行代码之前会对代码进行分析。从分析中,可以知道一些变量除了定义它的函数,不在别的函数内出现,或者说不被别的函数使用。这意味着,这些变量可以在函数返回时从当前环境中移除,而不影响到后续代码的运行。

甚至,如果设置一种专门用来被别的环境引用的环境,那么只有那些被其他函数所用到的变量,才会加入到这种环境中。那些不被别的函数使用的变量,就能进一步地,在函数不需要它们时提前被释放。

js就是如此,作用域只会捕捉那些被其他函数使用的变量。

上文,函数f1中只有变量s1被其他函数使用,因此作用域“Closure (f1)”只捕获了变量s1。 下面是作用域捕获f2的例子。

var f1 = function () { var s1 = 0; var f2 = function () { return f2; }; console.dir(f2);};f1();▼ƒ f2()... ▼[[Scopes]]: Scopes[2] ▼0: Closure (f1) f2: ƒ () ▶1: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

这次函数f1中只有变量f2被其他函数使用,因此作用域“Closure (f1)”只捕获了变量f2。

消失的作用域

进一步地,如果一个函数没有定义变量,亦或是它的变量都不被其他函数所用,那么它创建的环境就没有被引用的必要,取而代之的是它本身保存的环境引用。

同样的,js会移除不必要的作用域。

执行以下代码:

var f1 = function () { var s1 = 0; var f2 = function () { }; console.dir(f2);};f1();▼ƒ f2()... ▼[[Scopes]]: Scopes[1] ▶0: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

可见,f1函数调用时创建的环境$1、作用域“Closure (f1)”被移除了。

另一个移除作用域的例子:

var f1=function(){ var f2=function(){ var f3=function(){ var f4=function(){ return f2; }; console.dir(f4); }; f3(); }; f2();};f1();▼ƒ f4()... ▼[[Scopes]]: Scopes[2] ▼0: Closure (f1) f2: ƒ () ▶1: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

再看看使用eval的例子:

var f1=function(){ var f2=function(){ var f3=function(){ var f4=function(){ return f2; }; console.dir(f4); }; f3(); }; f2();};f1();▼ƒ f4()... ▼[[Scopes]]: Scopes[4] ▶0: Closure (f3) {f4: ƒ, arguments: Arguments(0)} ▶1: Closure (f2) {f3: ƒ, arguments: Arguments(0)} ▶2: Closure (f1) {f2: ƒ, arguments: Arguments(0)} ▶3: Global {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

js的eval函数可以执行动态代码,解释器无法通过代码分析它未来的执行内容,只能让函数保留所有相关环境的引用。

关于本文

作者:@佳木

原文:

标签: #jsevalif