❝累计的量变形成好几次质变后,彼此就不在同一个维度了 ❞
outer
的字段指向外部作用域的环境[[Scope]]
的内部属性,该属性指向它的「诞生环境」outer
指针构建的作用域链反应了作用域之间的嵌套关系
2. 动态:执行上下文的堆栈反应了函数调用关系currying
)在我们之前的文章中,不管是讲作用域、闭包还是全局变量。Environment
都作为关键部分,出现好多次。所以,今天我们从多方面来讨论一下Environment。(有些知识点可能在前面的文章中涉及到)
在ECMAScript规范定义中,Environment是用于管理变量的「数据结构」:它是字典类型,键值(key
)是变量名,值(value
)是对应存储变量的值(7+1:7种基本类型,1种引用类型[Object])。
每一个作用域都有与之关联的Environment,即:
❝作用域和Environment是「一一对应」的 ❞
同时,Environment能够支持如下与变量相关的语法特性:
接下来,我们依次来解释Environment与各个语法特性直接的关系。
存在如下的函数调用关系:
function f(x) {
return x * 2;
}
function g(y) {
const tmp = y + 1;
return f(tmp);
}
// 函数调用
g(4) == 10 // true
函数每次被调用,都需要为函数的变量(参数和局部变量)提供「新的存储空间」(Environment对象)。V8通过被称为执行上下文
(execution contexts)的栈结构(stack)来管理这些存储空间,而执行上下文是对Environment对象的「引用」。
❝而Environment对象是存放在堆内存(heap)中的 ❞
之所以Environment对象被存放于堆内存,是因为虽然函数执行完(从执行上下文中出栈[pop]),但是在其他变量引用了该变量环境中的变量(形成了闭包)。
从V8垃圾回收的角度解释,刚才的环境变量处于可达到(reachable),即:GC不会把该部分的数据清除。而如果直接存放在执行上下文里的话,在函数执行完,也就是函数的栈帧被pop后,该部分的数据是无法被访问的。(说的有点远,后期会有专门针对GC的文章)
反正,绕来绕去,记住一点就是「Environment对象是存放在堆中的」。
我们模拟V8处理函数的过程,来窥探下执行上下文、作用域之间的关系。
function f(x) {
// 断点 3
return x * 2;
}
function g(y) {
const tmp = y + 1;
// 断点 2
return f(tmp);
}
// 断点1
g(3)
g()
之前g()
f()
还有一点需要指出:每次执行到return
时,就会从栈中删除(pop)一个执行上下文。
g()
之前此时,执行上下文堆栈只有一条记录,并且该记录指向全局作用域。在全局作用域中存在两个记录
f()
g()
执行上下文的顶层记录(序号为1)指向由调用g()
而生成的环境变量。在该环境变量中包含g()
调用时需要的变量信息。
y = 3
tmp = 4
同断点2类似,执行上下文的顶层记录指向由调用f()
而生成的环境变量。
接着,我们继续探索作用域链是如何通过Environment实现的。
function f(x) {
function square() {
const result = x * x;
return result;
}
return square();
}
//函数调用
f(6)
上面代码中,存在3个作用域:全局作用域,函数作用域f()
和内部函数作用域square()
。
通过平时开发和查询一些资料,我们可以得出两个结论:
❝每个作用域的环境变量通过一个称为
outerEnv
(简称outer
)的字段指向外部作用域的环境。 ❞
当我们查找一个变量的值时,我们首先在当前环境中搜索它的名称,如果当前环境没有;然后在外部环境中搜索,外部环境也没有;然后在外部环境的外部环境中搜索,一直搜到全局作用域,如果全局作用域也没有该变量,那该变量就是undefined
。
每一次的函数调用,都会创建一个新的环境变量。该环境变量的外部环境就是「定义」该函数的所在的环境。每个函数对象都有一个名为[[Scope]]的内部属性,该属性指向它的「诞生环境」,并且这个属性的值是在编译阶段被赋值的。
function f(x) {
function square() {
const result = x * x;
// 断点 3
return result;
}
// 断点 2
return square();
}
// 断点 1
f(6)
f()
之前f()
square()
f()
之前此时,全局作用域有一个针对f()
的记录。也就是说f()
的诞生环境是全局作用域,因此,函数对象(f)的内部属性[[Scope]]指向全局作用域。
在JS全局变量中讲过,在全局作用域下,针对函数声明的变量是存放在变量环境对象中,同时JS中一切皆对象,函数变量也是一种变量类型。并且,该函数变量的初始化是在V8的编译阶段(变量提升)。最后的结果就是,在f()
还没执行之前,在全局环境下已经存在了对应的变量,并且该变量指向了与之对应的函数对象。
f()
由于调用了f(6)
,此时会生成对应的环境变量。该环境变量的外部环境就是f()
的诞生环境(全局环境,在作用域链的最顶层)。该环境变量的outer
属性的值被赋为f
函数[[Scope]]指向的值。同时,针对函数square
的变量对象也被创建,于此同时,该对象的内部属性[[Scope]]被赋值为当前环境变量(通过调用f(6)创建)
square()
继续上述的处理模式。最近环境变量(调用square()生成的)的outer
是通过对应函数变量的[[Scope]]来设置的。通过outer
创建的作用域链,我们有权访问result
/square
/f
。
❝环境变量在两个方面影响变量 1. 静态:通过每个环境变量的
outer
指针构建的作用域链反应了作用域之间的嵌套关系 2. 动态:执行上下文的堆栈反应了函数调用关系 ❞
还是老样子,通过一段代码来分析它们之间的关系。
function add(x) {
return (y) => { // (A)
return x + y;
};
}
add(3)(1); // (B)
add()
是一个返回函数的函数。当我们在B行调用嵌套函数add(3)(1)
时,第一个参数用于add()
,第二个参数用于它返回的函数。
为什么会这样呢?因为在A行创建的函数(箭头函数)在离开初始作用域时(调用add()
生成的)不会失去与该作用域的连接。关联的环境通过该连接保持「鲜活」,并且函数仍然可以访问该环境中的变量x (x在内部函数中是有权访问的)。
这种调用嵌套函数add()的有一个优点:如果只进行第一次函数调用,就会得到一个版本的add(),它的形参x已经被填充了。
const plus2 = add(2);
plus2(5) == 7 //true
将具有N个参数的函数转换为N个各具有一个参数的嵌套函数,称为「柯里化」(currying
)(这是函数式编程的概念)。add()
函数就是被柯里化的函数。如果对React
开发比较熟悉的同学,是不是会想到redux-middleWare
,它也是利用柯里化处理参数的。
只填写函数的某些参数称为偏函数(partial application
)。JS中Function.prototype.bind()
就是偏函数的典型。
我们来简单讲一下bind()
:bind()
方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
function addArguments(arg1, arg2) {
return arg1 + arg2
}
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);
var result2 = addThirtySeven(5);
// 37 + 5 = 42
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略
跑偏了,我们回归到主线来。继续分析Environment。
老样子,之间上代码。
function add(x) {
return (y) => {
// 断点 3: plus2(5)
return x + y;
}; // 断点 1: add(2)
}
const plus2 = add(2);
// 断点 2
plus2(5)
add(2)
add(2)
执行之后plus2(5)
add(2)
从图中我可以看到几个信息:
add(2)
而生成的环境变量。add(2)
返回的函数对象已经被实例化了(右下角),并且它的内部属性[[Scope]]指向了它的诞生环境:即当前执行环境(调用add(2)
生成)。虽然,函数对象被实例化,但是与之对应的变量plus2
处于暂时性死区(temporal dead zone)并且值为undefined
。
add(2)
执行之后plus2
指向了通过调用add(2)
返回的函数。虽然add(2)
已经从执行上下文堆栈中移除,但是由于plus2
所指向的函数对象引用了add(2)
的环境变量,使其还是处于可达到(reachable)。
plus2(5)
调用plus2(5)
生成了对应的变量对象,并且将该变量对象的outer
指针与plus2
函数对象的[[Scope]]相等。通过outer
将多个作用域进行关联,此时在plus2(5)
中有权访问变量x
。