执行环境
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。——《JavaScript高级程序设计》
实际上,执行环境在JS机制内部就是用一个对象来表示的,称执行环境对象,简称环境对象。
这个执行环境对象到底又是何时、怎么产生的呢?有以下两种情况:
- 在页面中的脚本开始执行时,就会产生一个‘全局执行环境’。它的范围最大,层级最高。在web浏览器中,这个对象就是window对象。
- 当一个函数被调用的时候,也会创建一个属于该函数的执行环境,称作‘局部执行环境’,它对应自己的环境对象。
执行环境就分为全局执行环境和局部执行环境两种,每个执行环境都有一个属于自己的环境对象
环境对象的三个有意思的属性。变量对象、[[scope]]、this。
环境对象中的变量对象
执行环境定义了变量或函数有权访问的其他数据。但是这些数据到底被存储在哪里呢?
其实,每个执行环境都有一个与之关联的变量对象,在环境中定义的所有变量和函数都保存在这个对象中,我们在代码无法访问这个对象,但是解析器在处理数据时会在内部使用它。
实际上:一个执行环境中的所有变量和函数都保存在它对应的环境对象的变量对象(属性)中。
认识[[scope]]前先理解作用域
作用域就是变量或者函数可以被访问的代码范围,或者说作用域就是变量和函数所起作用的范围。
在JS中,作用域分为全局作用域、局部作用域两种。
我们来看看这两种作用域的具体描述:
①在页面中的脚本开始执行时,就会产生一个“全局作用域”。它是最外围(范围最大,或者说层级最高)的一个作用域。全局作用域的变量、函数
可以在代码的任何地方访问到。
②当一个函数被创建的时候,会创建一个“局部作用域”。局部作用域中的函数、变量只能在某些局部代码中可以访问到。
var g = 'Global';
function outer() {
var out = 'outer';
function inner() {
var inn = 'inner';
}
}
上面这个例子,产生的作用域就如下图所示:
请注意上面①、②这两段话!!!是不是觉得很熟悉,似曾相识?!没错,这两段话和介绍全局/局部执行环境(全局/局部环境对象)时候的描述几乎一摸一样!作用域是不是和环境对象有着千丝万缕的联系呢?与此同时,我们再仔细回忆一下:1、作用域就是变量或者函数可以被访问的代码范围。2、一个执行环境中定义的所有变量和函数都保存在它对应的环境对象中。
结合上面所述,其实不难得出:尽管作用域的描述更像是一个概念,但如果一定要将它具象化,问它到底是什么东西,与执行环境有什么关系?其实,作用域所对应的(不是相等、等于)是环境对象中的变量对象。
环境对象中的[[scope]]
环境对象中的[[scope]]属性值是一个指针,它指向该执行环境的作用域链。
到底什么是作用域链呢?
作用域链本质上就是一个有序的列表,而列表中的每一项都是一个指向不同环境对象中的变量对象的指针。
那么,这个作用域链到底是怎么形成的呢?它里面指向变量对象的指针的顺序又是如何规定的呢?我们用下面这个简单的例子说明。
var g = 'Hello';
function inner() {
var inn = 'Inner';
var res = g + inn;
return res;
}
inner();
当执行了inner();这一行代码后,
- 代码执行流进入inner函数内部,此时,JS内部会先创建inner函数的局部执行环境,然后创建该环境的作用域链。
- 这个作用域链的最前端,就是inner执行环境自己的环境对象中的变量对象,
- 作用域链第二项,就是全局环境的环境对象中的变量对象。
- 这条作用域链如下图所示:
形成了这样的作用域链之后,就可以有秩序地访问一个变量了。那如何访问一个变量呢?
以这个例子为例:
- 当执行inner();进入函数体内后,执行g + inn;
- 一行,需要访问变量g、inn,此时JS内部机制就会沿着这条作用域链查找所需变量。
- 在当前inner函数的作用域中找到了变量inn,值为'Inner',查找终止。
- 但是却没有找到变量g,于是沿着作用域链向上查找,进入全局作用域,
- 在全局变量对象中找到了变量g,值为'Hello',查找终止。计算得出res为'HelloInner',并在最后返回结果。
与上面所讲机制完全相同,如果是多层执行环境嵌套,则作用域链是这么形成的:
当代码执行进入一个执行环境时,JS内部会开始创建该环境的作用域链。作用域链的**最前端**,始终都是**当前执行环境的
执行环境对象中的变量对象**。如果这个环境是**局部执行环境(函数执行环境)**,则将其**活动对象**作为**变量对象
**。作用域链中的下一个是来自**外层环境对象的变量对象**,而再下一个则是来自**再外层环境对象的变量对象**......
这样**一直延续到全局环境对象的变量对象**。所以,全局执行环境的变量对象始终都是作用域链中的最后一个对象。
闭包 什么是闭包
闭包就是有权访问另一个函数作用域中的变量的函数。——《JavaScript高级程序设计》
有两成理解:
1. 闭包是一个函数。
2. 闭包有权去访问另一个函数作用域中的变量。
对于闭包,最简单的大白话可以这么理解:
①外部函数声明内部函数,内部函数引用外部函数的局部变量,这些变量不会被释放。
或者这么理解:
②当一个函数【函数A】中返回另一个函数的函数体并且会引用函数A的变量的时候(是返回一个函数,不是返回函数的调用或者函数的执行结果;既返回的是函数体),就会形成闭包,被返回的这个函数就叫做闭包函数。
上面两句话看似不同,其实本质是一样的。来看一个最简单的闭包的例子:
function sum() {
var num1 = 100;
// 这里返回的是函数(体),不是函数的调用
return function(num2) {
return num1 + num2;
}
}
// 此时result指向sum返回的那个匿名函数
// 注意!此时该匿名函数并没有被执行
let result = sum();
result(200);
上面几行代码,为什么就会形成闭包呢?我们来分析一下,代码执行中JS内部到底做了什么?
有一点必须明确,就是一般情况下,一个函数执行完内部的代码,函数调用时所创建的执行环境,环境对象【包括变量对象,[[scope]]等】都会被销毁,他们的生命周期就只有函数调用到函数执行结束这一段时间。
但是上面的例子,就会出现例外。
- 当执行sum()时,调用该函数,创建它的环境对象,
- 其中作用域链中第一项是自己环境的变量对象,
- 第二项是全局环境的变量对象。
- 当创建匿名函数的时候,也会创建匿名函数的环境对象,
- 其中作用域链第一项是自己环境的变量对象,
- 第二项是sum环境的变量对象,
- 第三项是全局变量对象。
这时,问题就来了。
- 按说,当函数sum执行完return之后,他的执行环境、变量对象、作用域链都会被销毁。
- 可是这时候却不能销毁他的变量对象,因为返回的匿名函数(此时由result指向该函数)并没有执行,
- 这个匿名函数的作用域链中还引用着sum函数的变量对象。
- 换句话说,即使,sum执行完了,其执行环境的作用域链会被销毁,
- 但是它的变量对象还会保存在内存中,
- 我们在sum函数外部,还能访问到它内部的变量num1、num2,这就是形成闭包的真正原因。
- 但是,当result()执行完后,这些变量对象、作用域链就会被销毁。
闭包存在的问题
因为闭包形成后,会在函数执行完仍将他的变量对象保存在内存中,当引用时间过长或者引用对象很多的时候,会占用大量内存,严重影响性能。
来看下面的例子:(这个例子曾经是Tencent微众银行的笔试原题,出现在《JS高程》的7.2.3章节——P184)
function assignHandler() {
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
assignHandler函数中定义的匿名函数是作为element元素的事件处理函数的,且内部使用了element元素(访问元素的id`),因此assignHandler函数执行完,对于element的引用也会一直存在,element元素会一直保存在内存中。
将上面的例子改成下面这样,就能解决这个问题。
function assignHandler(){
var element = document.getElementById("someElement");
// 这里获取element的id,为其创建一个副本
// 这样是为了在下面事件处理函数中解除对element元素的引用
var id = element.id;
element.onclick = function(){
alert(id);
};
// 将element置为null,断开对element元素的引用
// 这样方便垃圾回收机制回收element所占的内存
element = null;
}
引用和感谢:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。