初识JavaScript的执行环境、作用域链、函数和闭包

一、作用域链

1.1、执行环境

  • 执行环境(execution context)的本质就是一个对象,称为变量对象(variable object)。
  • 变量对象就是执行环境中包含了所有变量和函数的对象。
  • 执行环境如其名是在运行和执行代码的时候才存在的。
  • 我们可以说变量对象包含了活动对象(activation object),活动对象就是作用域链上正在被执行和引用的变量对象。

    1. 执行环境的类型总共只有两种——全局和局部(函数),但还可以通过 (1)with语句 (2)try-catch语句的catch块 来延长作用域链。
    2. 我们编写的代码中是无法访问到变量对象的(除了全局的window对象)。

1.2、作用域链

作用域链(scope chain)本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

  • 作用域链的作用:在作用域链前端的执行环境中的变量和函数可以访问在作用域链后端的执行环境中的变量和函数,反之则不能。
  • 在进入一个执行环境后,就会将该执行环境对应的一个变量对象压入执行环境栈(execution stack);在走出一个执行环境后,就会将对应的变量对象从执行环境栈中弹出并销毁。

1.3、预编译与声明提升

首先明确,作为脚本语言的js是没有预编译这种东西的,但js在运行时有一个过程同C/C++的预编译有些类似之处。为了便于理解,可以形象地将该过程称为”js的预编译”。

js在运行时遵循变量和函数的声明提升(declaration hoisting)机制。而所谓js的预编译,就是变量和函数声明提升的过程。

  • 浏览器每加载一个js文件、一个js代码块(写在一对<script>标签内的代码),以及每进入一个执行环境,都会先预编译,再执行。
  • 预编译就是定义变量对象及其属性(只定义不赋值);执行就是对变量对象的属性不断赋值和读取。
  • 函数的作用域链,是在进入该函数所在的最近的执行环境,并在该函数声明时,就确定的。——所以在函数声明后,函数的[[scope]]属性就引用了函数所在的执行环境的变量对象。(理解这点有助于后面理解闭包的原理)

看一个示例的执行过程:

var attr1='window.attr1';
function func1(){
    var attr1='func1.attr1';
    function func2(){
        alert(this.attr1);
    }
}
func1(); 
  1. 进入全局环境的预编译阶段:(1)定义全局变量对象(包含属性attr1、func1);(2)生成func1的作用域链(指向全局变量对象)。
  2. 进入全局环境的执行阶段:(1)为attr1赋值;(2)执行func1()。
  3. 进入func1环境的预编译阶段:(1)定义func1的变量对象(包含属性arguments、this、attr1、func2);(2)生成func2的作用域链(指向func1变量对象->全局变量对象)。
  4. 进入func1环境的执行阶段:(1)初始化arguments、this,为attr1赋值。
  5. 退出func1环境,销毁func1的变量对象,再次进入全局环境。

二、函数

js中的函数具有两重性:首先函数也是一个对象,每个函数都是Object类型的子类——Function类型的实例,具有对象可以具备的属性和方法。其次,一个函数可以被作为一个执行环境,函数内部定义的所有变量和函数都保存在函数的变量对象中。

2.1、函数作为对象

函数的属性:caller、length、prototype
函数的方法:call()、apply()、bind()

2.2、函数作为执行环境

在函数内部有两个特殊的对象:arguments和this。在运行一个函数时,这两个对象会首先在函数的执行环境中根据实际情形自动初始化。

(1)arguments对象
函数运行时,arguments对象、函数的参数、函数内部定义的变量和函数,都会保存在函数的活动对象中,同时arguments对象会根据入参实际值进行初始化。

(2)this对象
“this是包含它的函数作为方法被调用时所属的对象。”——这句话包含3个要点:1、包含它的函数。2、作为方法被调用时。3、所属的对象。

  • 若没有明确把函数赋给某个对象并调用,则this会初始化为window对象(非严格模式)或undefined(严格模式);若赋给某个对象并调用了,this就指向那个对象。
/*func1()是直接调用的,并没有作为某个对象的方法被调用,故this指向window对象
 */
var attr1 = 'window.attr1';
function func1(){
    var attr1 = 'func1.attr1';
    alert(this.attr);
}
func1();   //结果为"window.attr1"


/*func2()依旧没有作为某个对象的方法被调用,故this指向window对象
 */
var attr1='window.attr1';
function func1(){
    var attr1='func1.attr1';
    function func2(){
        alert(this.attr1);
    }
    func2();
}
func1();   //结果为"window.attr1"


/*满足:1、包含this的函数是func1。2、func1是obj对象的方法。3、func1作为方法被obj对象调用。故this指向obj对象
 */
var attr1='window.attr1';
var obj={
    attr1 : 'obj.attr1',
    func1 : function(){
        alert(this.attr1);
    }
};
obj.func1();   //结果为"obj.attr1"

2.3 函数声明与函数表达式

众所周知,函数声明可以写在函数调用的后面,而函数表达式则必须写在函数调用的前面才可正常运行。究其原因,还是要提及”预编译”和”声明提升”。

看个易混淆的栗子:

var f = function foo(){
    alert(typeof foo);
};
f();
foo();

结果如何呢?——f()可正常执行,显示”function”;而试图执行foo()时会报”foo is not defined”的错误。
那为什么会这样呢?——因为在预编译时,只有f作为了变量对象的属性,而foo没有作为变量对象的属性,而在运行函数时,只会在变量对象的属性中去寻找该函数名,很显然foo是找不到的。(这段解释也只是我一厢情愿地猜测,小弟在此求教真正的解释)

三、闭包

3.1、什么是闭包

闭包,简单来说就是在一个函数内部定义的函数。如下的func2就是一个闭包。

function func1(){
    function func2(){
    }
}

然而这种闭包没有什么”大作为”,因为它只能在自己的小圈子(func1的执行环境)里混,而没有与外界产生联系。这时候我作一个小小的变动:

function func1(){
    return function func2(){
    }
}

此时这个闭包就蜕变了,它就冲出束缚它的牢笼走向了外面广阔的新天地。

3.2、闭包的用处和原理

闭包可以用在许多地方。它的最大用处有两个,一个是可以在函数外部读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

我们来分析下如下代码的执行过程:(弄清这个例子,也就能弄清闭包为什么会有这两个用处了)

function a() {
    var i = 0;
    function b() {
        alert(++i);
    }
    return b;
}

var result1 = a(); //第一次执行a()
var result2 = a(); //第二次执行a()

result1(); //第一次执行result1(),显示"1"
result1(); //第二次执行result1(),显示"2"
result2(); //第一次执行result2(),显示"1"

result1 = null;  //解除对变量对象的引用
result2 = null;  //解除对另一个变量对象的引用
  1. 在第一次执行a()时,生成a的变量对象,同时将该变量对象中的函数b返回给全局中的result1变量。重点来了:一般来讲,a()执行完毕后,a的变量对象也会随之销毁,但这种情况下就算a()执行完毕后,a的变量对象仍被全局的result1变量所间接引用(因为函数b被全局的result1变量所引用,而b的作用域链中又引用了a的变量对象),所以若不手动解除对a的变量对象的引用(也就是对函数b的引用),整个a的变量对象就一直会驻留在内存中。
  2. 接着又执行了一次a(),又会生成一个a的变量对象并驻留在内存中,然后执行与上一次相同的操作。但有额外一点需注意:虽然这个也是a的变量对象,但与第一次生成的a的变量对象,两者是平行的互不影响的。
  3. 在执行result1()时,会生成b的变量对象,同时b的作用域链中已经有a的变量对象,所以就可以在全局中访问原本不能访问的变量i。在执行完result1()后,b的变量对象是会销毁的,但a的变量对象不会销毁!
  4. 闭包不再使用后,要对闭包的引用赋null值,以释放内存!

参考书籍:
《JavaScript高级程序设计》 ——Nicholas C.Zakas
第4章 变量、作用域和内存问题
第5章 5.5 Function类型
第7章 7.1 递归 7.2 闭包

参考链接:
js 中的活动对象 与 变量对象 什么区别
javascript中的this到底指什么
JavaScript核心原理(一)执行环境、执行环境栈、变量对象、活动对象
实例讲解js中的预编译
javascript变量声明提升(hoisting)
学习Javascript闭包(Closure)
javascript深入理解js闭包

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值