java 静态块 作用域_“动静结合” 小白初探静态(词法)作用域,作用域链与执行环境(EC)...

从图书馆翻过各种JS的书之后,对作用域/执行环境/闭包这些概念有了一个比较清晰的认识。

栗子说明一切

第一个栗子

来看一个来自ECMA-262的栗子:

var x = 10;

(function foo() {

var y = 20;

(function bar() {

var z = 30;

// "x" and "y" are "free variables"

// and are found in the next (after

// bar's activation object) object

// of the bar's scope chain

console.log(x + y + z);

})();

})();

我们可以用下图展现上面的例子(父变量对象存储在函数的Scope属性内)

7191974c96f4ad96606fbdf86c4b2129.png

首先,可以很容易的理解到一个事实:在从控制台输出x+y+z的时候,x和y是在bar()函数中的作用域链中bar()的活动对象之下找到的。实际上,foo()函数和bar()函数在执行的时候,他们的scope属性就已经确定了,他们的scope属性确定为他们外层的变量对象(VO)的集合。从图中可知,内存结构可能是这样的:

// foo的scope属性是global的VO

foo.["[[Scope]]"] = { global.["Variable Object"] }

// bar的scope属性是foo的AO和global的VO的集合

bar.["[[Scope]]"] = {foo.["Activation Object"], global.["Variable Object"]}

第二个栗子

这个例子来自《高性能Javascript》

// 全局范围定义

function add(num1, num2) {

var sum = num1 + num2;

return sum;

}

当add()函数创建的时候,它的scope属性被确定为全局对象的VO,这个全局对象的VO可能包括window/navigator/document之类等等。关系如图:

20f50b42b9c71d859d37293e82008f2d.png

这个scope属性很特别,他是静态的,在函数创建的时候便能确定。图片中的作用域链,是全局执行环境中的作用域链。而在函数执行的时候,书中说道:

每个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建的时候,它的作用域链初始化为当前运行函数的scope属性中的对象。这些值按照他们出现在函数中的顺序,被复制到执行上下文的作用域链中。这个过程一旦完成,一个被称为“活动对象(AO)”的新对象就为执行上下文创建好了。活动对象作为函数运行时的变量对象,包含了所有的局部变量,命名参数,参数集合以及this。然后此活动对象被推入作用域链的最前端。

可以了解到,作用域链是个链表,是在函数执行的时候才存在的,也就是函数创建执行环境的时候才开始存在的,它先把这个函数的静态属性scope属性中的所有变量对象按照顺序复制到作用域链(所以这样就不会担心作用域链嵌套的问题),然后创建AO放在作用域链顶部“0号位”。例如再执行代码:

var total = add(5, 10);

图片如下图:

314bd331a1e4fc1df2a15699c0f7b09a.png

所以,我们也可以得到一个惊人的结论:

函数作用域链 = 活动对象(AO) + scope属性

关键的来了

这个结论中:活动对象(AO)是临时的,动态的,独一无二的。scope属性是静态的,确定的。

所以说,函数的作用域链,是函数执行的时候动态创建的,但是它又是基于静态词法的环境(scope属性)。所谓“动态创建”,是指在函数执行的时候,先创建之前没有的作用域链,再创建活动对象,然后活动对象推入作用域链最前端;所谓“基于静态的词法环境”是指函数定义的时候,这个函数本是没有作用域链的,有的只有scope属性,而这个属性指向了这个函数外部的执行环境,而这个外部的执行环境拥有作用域链(因为这是外部创建外部的执行环境才拥有作用域链的,这样有一点递归的味道)。P.S.其实有的版本也说,作用域链的确定应该是在活动变量创建完成之后的,这个有待钻研。

P.S 在ES5规范文档中,进入函数代码的流程:

f67b8f186bfbf4ffbbeb2bb6111d36db.png

扯到变量提升

变量提升的本质就是函数在创建执行环境中的变量对象的时候,记录下了函数声明,变量和参数等等。具体参见深入理解Javascript之执行上下文(Execution Context),下面是片段:

建立Variable Object对象顺序:

建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值

检查当前上下文中的函数声明: 每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用。如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

检查当前上下文中的变量声明: 每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。

扯到闭包

闭包,在离散数学中指的是满足性质A的一个最小关系集R,这可以理解这个关系集R,在性质A上封闭。闭包不是一种魔法,虽然可以通过闭包扯得很远很远,通过函数的作用域链的组成为AO+scope属性,为快速理解闭包中变量引用来自哪里提供了思路————没那么复杂,就直接再执行的函数定义处上看就行了。把函数定义的作用域看成是函数执行的作用域。这也是词法作用域迷人的地方。

Show Me the Code

说了那么多,有代码才是王道,毕竟“Talk is cheap”。

“面向对象”一般的编程:实现封装

var Counter = (function() {

var privateCounter = 0;

function changeBy(val) {

privateCounter += val;

}

return {

increment: function(dis) {

changeBy(dis);

},

decrement: function(dis) {

changeBy(-dis);

},

value: function() {

return privateCounter;

}

}

})();

console.log(Counter.value()); // 0

Counter.increment(1);

Counter.increment(2);

console.log(Counter.value()); // 3

Counter.decrement(5);

console.log(Counter.value()); // -2

返回的是一个对象,这个对象有三个属性,都是函数。而且这三个函数的scope属性都是指向一个集合,这个集合包括外层匿名函数的的AO,和全局变量的VO。分析一下Counter.value()这个调用:value这个属性对应的匿名函数定义的时候,它的scope属性确定,这个是词法作用域的特性,这个scope属性指向的是外部所有变量对象的集合(也就是上句说的那个集合)。在最后调用Counter.value()的时候,创建先构建作用域链,再创建执行环境,再创建执行环境的时候发现了一个变量标识符privateCounter。好,接下来在函数体内找找这个对应的值,找不到;到外层的函数,也就是那个Counter对应的匿名函数,诶找到了!好,将这个标识符和这个量“关联起来”。

结果,这样下来,返回的这个对象就类似于面向对象变成中的“外部接口”,而没有被返回的那部分(也就是代码中的var privateCounter和function changeBy)则成了“私有的”,无法从外部直接访问。这样的闭包模拟了数据的封装和隐藏,一股熟悉而浓郁的C++味道袭来。当然,这样用的确不错,但是关乎性能方面,MDN这样推荐道:

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。

执行环境到底是怎么建立的?

function foo(i) {

var a = 'hello';

var b = function privateB() {

};

function c() {

}

}

foo(22);

在调用foo(22)的时候,建立阶段如下:

fooExecutionContext = {

variableObject: { // 变量对象

arguments: {

0: 22,

length: 1

},

i: 22, // 形式参数声明在函数声明前

c: pointer to function c() // 注意,函数声明在变量声明前

a: undefined,

b: undefined

},

// 作用链和变量对象顺序问题,有待钻研,T.T

// 在官方文档中,貌似是作用域链先被创建(而且被称作词法环境组件)

scopeChain: { ... },

this: { ... }

}

由此可见,在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:

fooExecutionContext = {

variableObject: {

arguments: {

0: 22,

length: 1

},

i: 22,

c: pointer to function c()

a: 'hello',

b: pointer to function privateB()

},

scopeChain: { ... },

this: { ... }

}

我们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。

总结一下

分析代码的时候,务必回看函数的定义,毕竟人家函数是一等贵族。

记住函数作用域链 = (动)活动对象(AO) + (静)scope属性。

执行环境结构:f988ddcb87f5f701d30b75242b4a3241.png

执行环境创建后,才开始执行代码,变量对象才开始被赋值

变量提升 ==> 变量对象的创建

闭包 ===> 作用域链中静态的部分,即scope属性

官方文档的补充

d4c24cede3e2ba22474b7833bf20df08.png

我的理解:词法环境组件 ≈ 作用域;变量环境组件 ≈ 变量对象;

c3bcc8d0e315bcc61f7b56c25a4e9981.png

以初始化全局代码的时候,貌似是创建变量对象在先。(这样有什么特殊的意义吗?)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值