点击上方“IT平头哥联盟”,选择“置顶或者星标”
一起进步~

环境模型(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} //4
add10(2); //5表达式add10(2)(>5)的计算过程:
值得一提的是,形参也是变量,它在形参列表里定义,在函数调用时获得初始值。
环境使用变量绑定来存放变量的值, 绑定(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} //9
main(); //10var关键字可以定义变量:
在函数的计算过程中,变量定义会使当前的环境加入对应的绑定。
上文中表达式main()(>10)的计算过程产生了2个环境,
可见,绑定存放的是约束变量的值,约束变量的值可以直接从当前环境获取。
而自由变量的值需要从其他环境获取,该环境是自由变量定义时所在的环境,拥有自由变量的绑定。
上文中表达式addX(value)(>8)的计算过程:
计算function表达式或lambda表达式会得到一个函数,这种情况一般被称为函数定义。方便起见,本文将值是变量的函数称为函数。
就这样,函数在计算时只要找到对应的绑定,就能确定一个变量的值。
环境不仅保存了变量绑定,还会保存一个 环境引用(environment pointer) ,环境引用指向其他的变量环境。通过环境引用,自由变量可以从其他环境寻找自己对应的绑定。
函数在定义时会把当前环境的引用记录下来。在调用函数后,新的环境会得到函数中的环境引用并将此保存。 也就是说,一个函数在计算时的环境,拥有函数在定义时的环境的引用。
var getCounter = function (start) { //1
return function () { //2
return start++; //3
}; //4}; //5var counter = getCounter(0); //6
counter(); //7表达式getCounter(0)(>6)和counter()(>7)分别创建了两个环境:
$getCounter拥有全局环境的引用。一些看似不在函数中定义的函数,其定义时也身处环境中,该环境被称为全局环境。函数getCounter就保存了全局环境的引用。
函数在计算过程中定义函数,如同代码文本结构那样一层包裹一层,里层的函数定义是外层函数中的一条表达式,里层函数创建的环境通过引用连接外层函数创建的环境。
因此,一个变量在当前环境找不到对应的绑定时,可以通过引用一层层回溯到它定义时所在的环境,从而找到该绑定。自由变量便是通过这种方法找到自己对应的绑定。
上文中表达式counter()(>7)的计算过程:
每次计算表达式counter(),绑定*start的值都会自增1,并依次返回0,1,2,3……
函数在定义时会保存当前 环境 的 引用 。
一旦函数被调用,就会创建一个新的环境,新的环境拥有函数定义时环境的引用。
函数中的变量定义表达式会给新环境加入 绑定 。
函数使用变量就是访问环境中对应的绑定。
如果变量在当前环境找不到对应的绑定,就会通过引用一层层回溯到它定义时所在环境,从而找到它的绑定。
而这种访问其他变量环境的机制,通常被人称为 闭包 。
下文将讲述如何用js模拟环境模型。在这个模拟环境模型中,不需要用到js的变量定义语法也能使用闭包。
模拟环境模型不是编写函数的解释器,只是将环境变为可操作的实体,用来解释函数中的变量。
首先确定模拟环境的使用方式。为了能在函数中使用环境,环境将作为参数传入被调用的函数:
function 函数通过环境使用变量,环境应有getVariable和setVariable方法。
变量在使用前要有定义,环境应有defineVariable方法。
此外,函数在定义时会保存当前环境的引用,环境应有defineFunction方法。
因此,代表环境的class是这样的:
class Environment {
//变量定义
defineVariable(name) {
}
//变量取值
getVariable(name) {
}
//变量赋值
setVariable(name, value) {
}
//函数定义
defineFunction($func) {
}}环境可以看作是变量(绑定)的容器,应有一个bindingContainer成员用来存放变量。
考虑到前端js的全局变量可以在window对象上找到,bindingContainer使用Object类型的对象的话,可以与window[name]同样的形式bindingContainer[name]来访问变量。
因此,变量定义、取值、赋值可以表达为:
this.bindingContainer[name] = null; //定义
value = this.bindingContainer[name]; //取值this.bindingContainer[name] = value; //赋值Environment的defineVariable方法实现很直接,为当前环境加入绑定:
defineVariable(name) {
this.bindingContainer[name] = null;}在当前环境使用的变量,绑定有可能在别的环境中,应有一个代表环境引用的成员environmentPointer。 且environmentPointer是Environment类型。
取值和赋值都需要找到变量的绑定,应有一个共同的方法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为null:
Environment.End = null;有了findBindingContainer方法,便能轻易写出getVariable方法:
getVariable(name) {
var binding_container = this.findBindingContainer(name);
var value = binding_container[name];
return value;}同上,setVariable方法的表达为:
setVariable(name, value) {
var binding_container = this.findBindingContainer(name);
binding_container[name] = value;}模拟环境模型不具备定义函数的功能,defineFunction只需令已定义的函数保存当前环境的引用。
js函数无法直接保存引用和创建模拟环境,因此需要一个用来代理函数的对象,假设defineFunction的表达为:
defineFunction(proxy) {
proxy.saveEnvironmentPointer(this);
var func = proxy.getCall();
return func;}代理函数的对象使用saveEnvironmentPointer方法保存环境引用,使用getCall方法返回实际被调用的函数。
被代理的函数,就是使用模拟环境的函数func,显然func不能被直接调用。它需要:
因此,代理函数的对象应具有call方法,以此满足$func被调用的需求。
综上所述,代理函数的对象类型是这样的:
class $Function {
saveEnvironmentPointer(environmentPointer) {
}
getCall() {
}
call(...args) {
}}$Function还应有这样三个成员:
值得一提的是,函数func只有一个表示环境的参数,无法表达普通函数的参数列表。因此需要parameterList来描述它的参数列表,用一个字符串数组便能表达。
saveEnvironmentPointer方法的实现很直接:
saveEnvironmentPointer(environmentPointer) {
this.environmentPointer = environmentPointer;}getCall方法实际返回的是call方法:
getCall() {
return this.call.bind(this);}由于call方法的实现用到其他成员,call在返回时需要绑定this。
如上文所述,call方法作为实际被调用的函数,它会:
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(pointer) {
this.environmentPointer = pointer;
this.bindingContainer = {};}Function在构造时只需要从外部传入func和parameterList:
func, parameterList = []) {
this.参数列表默认为空数组。
在使用模拟环境之前补充全局环境的定义:
//全局环境中的环境引用只能是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使用模拟环境的代码:
) {
//return a + b;
return 例2
原代码:
var getCounter = function (start) {
return function () {
var result = start;
start += 1;
return result;
};};var counter = getCounter(0);
counter(); //0
counter(); //1
counter(); //2使用模拟环境的代码:
) {
return 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)”得知环境
上文中,作用域“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函数可以执行动态代码,解释器无法通过代码分析它未来的执行内容,只能让函数保留所有相关环境的引用。
关于本文 作者:@佳木 原文:https://zhuanlan.zhihu.com/p/58864841