在JavaScript中,函数的作用域链是一个很难理解的东西。这是因为JavaScript中函数的作用域链和其他语言比如C、C++中函数的作用域链相差甚远。本文详细解释了JavaScript中与函数的作用域链相关的知识,理解这些知识可以帮助你在处理闭包的时候避免一些可能出现的问题。

  在JavaScript中,函数可以让你在一次调用中执行一系列的操作。有多种方式来定义一个函数,如下:

1、函数声明:

function maximum(x, y) {
if (x > y) return x;
else return y;
} maximum(, ) //返回6;

  这种语法通常用来定义全局作用域下的函数(全局函数)

2、函数表达式:

var obj = new Object();
obj.maximum = function (x, y) {
if (x > y) return x;
else return y;
}; obj.maximum(, ) //返回6;

  这种语法通常用来定义一个作为对象方法的函数

3、Function构造函数:

var maximum = new Function("x", "y", "if(x > y) return x; else return y;");
maximum(, ); //返回6;

  以这种形式定义函数通常没有很好的可读性(没有缩进),只在特定情况下使用。

函数定义:

  函数定义指的是在JavaScript引擎内部创建一个函数对象的过程。

  如果是全局函数的话,这个函数对象会作为属性添加到全局对象上;

  如果是内部函数(嵌套函数)的话,该函数对象会作为属性添加到上层函数的活动对象上,属性名就是函数名。需要指出的是,如果函数是以函数声明的方式定义的,则函数的定义操作会发生在脚本解析的时候。

  如下例中当JavaScript引擎完成脚本解析时,就已经创建了一个函数对象func,该函数对象作为属性添加到了全局对象中,属性名为"func"。

/*func函数可以被访问到,因为在脚本开始执行前func函数就已经存在了.*/
alert(func()); //返回8 //执行该语句会覆盖func的值为true.
var func = true; alert(func); //返回"true"; /*在脚本开始执行前,解析下面的语句就会定义一个函数对象func.*/
function func(x) {
return x * x * x;
}

  在下面的例子中,存在内部函数的情况。内部函数innerFn的定义操作发生在外部函数outerFn执行的时候(其实也是发生在执行前的解析阶段),同时,内部函数会作为属性添加到外部函数的活动对象上。

function outerFn() {
function innerFn() {}
}
outerFn(); //执行outerFn函数的时候会定义一个函数innerFn

  注意:对于使用Function构造函数定义的函数来说,函数定义操作就发生在执行Function构造函数的时候。

作用域链:

  函数的作用域链是由一系列对象(函数的活动对象+0个到多个的上层函数的活动对象+最后的全局对象)组成的。

  在函数执行的时候,会按照先后顺序从这些对象的属性中寻找函数体中用到的标识符的值(标识符解析),函数会在定义时将它们各自所处环境(全局上下文或者函数上下文)的作用域链存储到自身的[[scope]]内部属性中。

  首先看一个内部函数的例子:

function outerFn(i) {
return function innerFn() {
return i;
}
}
var innerFn = outerFn();
innerFn(); //返回4

  当innerFn函数执行时,成功返回了变量i的值4,但变量i既不存在于innerFn函数自身的局部变量中,也不存在于全局作用域中,那么变量i的值是从哪儿得到的?你也许认为内部函数innerFn的作用域链是由innerFn函数的活动对象+全局对象组成的,但这是不对的,只有全局函数的作用域链包含两个对象,这并不适用于内部函数。让我们先分析全局函数,然后再分析内部函数。

全局函数:

  全局函数的作用域链很好理解。

var x = ;
var y = ; function testFn(i) {
var x = true;
y = y + ;
alert(i);
}
testFn();

  全局对象:JavaScript引擎在脚本开始执行之前就会创建全局对象,并添加到一些预定义的属性"Infinity"、"Math"等。在脚本中定义的全局变量也会成为全局对象的属性。

  活动对象:当JavaScript引擎调用一些函数时,该函数会创建一个新的活动对象,所有在函数内部定义的局部变量以及传入函数的命名参数和arguments对象都会作为这个活动对象的属性。这个活动对象加上该函数的[[scope]]内部属性中存储的作用域链就组成了本次函数调用的作用域链。

内部函数:

  让我们分析一下下面的JavaScript代码。

function outerFn(i, j) {
var x = i + j;
return function innerFn(x) {
return i + x;
}
}
var func1 = outerFn(, );
var func2 = outerFn(, );
alert(func1()); //返回15
alert(func2()); //返回20

  在调用func1(10)和func2(10)时,你引用到了两个不同的i 。这是怎么回事?首先看下面的语句:

var func1 = outerFn(5,6);

  调用outerFn (5, 6)的时候定义了一个新的函数对象innerFn,然后该函数对象成为了outerFn函数的活动对象的一个属性。这时innerFn的作用域链是由outerFn的活动对象和全局对象组成的,这个作用域链存储在了innerFn函数的内部属性[[scope]]中,然后返回了该函数,变量func1就指向了这个innerFn函数。

alert(func1(10));//返回15

  在func1被调用时,它自身的活动对象被创建,然后添加到了[[scope]]中存储着的作用域链的最前方(新的作用域链,并不会改变[[scope]]中存储着的那个作用域链),这时的作用域链才是func1函数执行时用到的作用域链。从这个作用域链中,你可以看到变量‘i’的值实际上就是在执行outerFn(5,6)时产生的活动对象的属性i的值。

  现在让我们回到问题,"在调用func1(10)和func2(10)时,你引用到了两个不同的i 。这是怎么回事?"。

  答案就是在定义func1和func2时,函数outerFn中产生过两个不同的活动对象。

  现在又出现了一个问题, 一个活动对象在函数执行的时候创建,但在函数执行完毕返回的时候不会被销毁吗? 我用下面的三个例子来讲解这个问题。

(1)没有内部函数的函数

function outerFn(x) {
return x * x;
}
var y = outerFn();

  如果函数没有内部函数,则在该函数执行时,当前活动对象会被添加到该函数的作用域链的最前端。

  作用域链是唯一引用这个活动对象的地方。当函数退出时,活动对象会被从作用域链上删除,由于再没有任何地方引用这个活动对象,则它随后会被垃圾回收器销毁。

(2)包含内部函数的函数,但这个内部函数没有被外部函数之外的变量所引用

function outerFn(x) {
//在outerFn外部没有指向square的引用
function square(x) {
return x * x;
}
//在outerFn外部没有指向cube的引用
function cube(x) {
return x * x * x;
}
var temp = square(x);
return temp / ;
}
var y = outerFn();

  在这种情况下,函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数的作用域链中。

  当该函数退出时,活动对象会从当前函数的作用域链中删除,活动对象和内部函数互相引用着对方,outerFn函数的活动对象引用着嵌套的函数对象square和cube,内部函数对象square和cube的作用域链中引用了outerFn函数的活动对象。但由于它们都没有外部引用,所以都将会被垃圾回收器回收。

(3) 包含内部函数的函数,但外部函数之外存在指向这个内部函数的引用(闭包)

  2种情况:

function outerFn(x) {
//内部函数作为outerFn的返回值被引用到了外部
return function innerFn() {
return x * x;
}
} //引用着返回的内部函数
var square = outerFn();
square();
var square;

function outerFn(x) {
//通过全局变量引用到了内部函数
square = function innerFn() {
return x * x;
}
}
outerFn();
square();

  在这种情况下,outerFn函数执行时创建的活动对象不仅添加到了当前函数的作用域链的前端,而且还添加到了内部函数innerFn的作用域链中(innerFn的[[scope]]内部属性)。

  当外部函数outerFn退出时,虽然它的活动对象从当前作用域链中删除了,但内部函数innerFn的作用域链仍然引用着它。 由于内部函数innerFn存在一个外部引用square,且内部函数innerFn的作用域链仍然引用着外部函数outerFn的活动对象,所以在调用innerFn时,仍然可以访问到outerFn的活动对象上存储着的变量x的值。

多个内部函数:

  更有趣的场景是有不止一个的内部函数,多个内部函数的作用域链引用着同一个外部函数的活动对象。该活动对象的改变会反应到三个内部函数上。

function createCounter(i) {
function increment() {
++i;
}
function decrement() {
--i;
}
function getValue() {
return i;
}
function Counter(increment, decrement, getValue) {
this.increment = increment;
this.decrement = decrement;
this.getValue = getValue;
}
return new Counter(increment, decrement, getValue);
}
var counter = createCounter();
counter.increment();
alert(counter.getValue()); //返回6

  上例表示了createCounter函数的活动对象被三个内部函数的作用域链所共享。

闭包以及循环引用:

  上面讨论了JavaScript中函数的作用域链,下面谈一下在闭包中可能出现因循环引用而产生内存泄漏的问题。闭包通常指得是能够在外部函数外面被调用的内部函数。下面给出一个例子:

function outerFn(x) {
x.func = function innerFn() {}
}
var div = document.createElement("DIV");
outerFn(div);

  在上例中,一个DOM对象和一个JavaScript对象之间就存在着循环引用。DOM 对象div通过属性‘func’引用着内部函数innerFn。内部函数innerFn的作用域链(存储在内部属性[[scope]]上)上的活动对象的属性‘x’ 引用着DOM对象div。这样的循环引用就可能造成内存泄漏。

关于JS里的函数作用域链的总结的更多相关文章

  1. 对JS闭包和函数作用域的问题的深入讨论,如何理解JS闭包和函数作用域链?

    首先先引用<JavaScript权威指南>里面的一句话来开始我的博客:函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的. 因此,就出现了如下的几串代码: ...

  2. 深入理解JS函数作用域链与闭包问题

    function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } ); a.fun(); a.f ...

  3. [ JS 进阶 ] 闭包,作用域链,垃圾回收,内存泄露

    原网址:https://segmentfault.com/a/1190000002778015 1. 什么是闭包? 来看一些关于闭包的定义: 闭包是指有权访问另一个函数作用域中变量的函数 --< ...

  4. javascript篇-----函数作用域,函数作用域链和声明提前

    在一些类似C语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的(也就是我们不能在代码段外直接访问代码段内声明的变量),我们称之为块级作用域,然而,不同于 ...

  5. JS 执行环境与作用域链

    1.执行环境 JavaScript 代码都是在执行环境中被执行的.执行环境是一个概念,一种机制,用来完成JavaScript运行时在作用域.生命周期等方面的处理,它定义了变量或函数是否有权访问其他数据 ...

  6. JS -- The Scope Chain 作用域链

    The Scope Chain JavaScript is a lexically scoped language: the scope of a variable can be thought of ...

  7. JS执行环境,作用域链及非块状作用域

    JS中的执行环境,顾名思义就是变量或函数所执行时的环境.在我的理解中,执行环境和作用域相差不大. 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中.而在函数执行之后 ...

  8. js学习笔记之作用域链和闭包

    在学习闭包之前我们很有必要先了解什么是作用域链 一.作用域链 作用域链是保证对执行环境有权访问的所有变量和函数的有序访问. 这句话其实还是蛮抽象的,但是通过下面一个例子,我们就能清楚的了解到作用域链了 ...

  9. JS进阶系列之作用域链

    在之前写的进阶系列里面,提到了执行上下文在创建阶段,要创建变量对象.确定作用域链还有确定this的指向,本次将重点讲解一下作用域链. JavaScript代码的执行过程 在讲解作用域链之前,首先了解一 ...

随机推荐

  1. Ring0层创建事件,Ring3层接收

    在学习驱动过程中,一个很重要的内容就是Ring3层与Ring0层的通信,方法有很多种,互斥体,信号量,文件等等,用的比较普遍的,还是事件.所以在学习的过程中,做了一个简单的Demo,主要是体会一下方法 ...

  2. 《Java编程思想》笔记 第十九章 枚举类型

    1.基本enum特征 所有创建的枚举类都继承自抽象类 java.lang.Enum; 一个枚举类,所有实例都要在第一句写出以 ,隔开. 如果只有实例最后可以不加 : 枚举类因为继承了Enum,所以再不 ...

  3. Selenium2+python自动化39-关于面试的题【转载】

    前言 最近看到群里有小伙伴贴出一组面试题,最近又是跳槽黄金季节,小编忍不住抽出一点时间总结了下, 回答不妥的地方欢迎各位高手拍砖指点.   一.selenium中如何判断元素是否存在? 首先selen ...

  4. 用 python 来操作 docx, xlsx 格式文件(一)(使用 xlsxwriter 库操作xlsx格式文件)

    需要从数据库读取日志生成相应的 docx,xlsx 文件做相应的记录 所以自然要用到docx, xlsxwriter 库 但是这些库的应用场景非常广泛,任何需要对 word,excel 文件执行重复性 ...

  5. 非负权值有向图上的单源最短路径算法之Dijkstra算法

    问题的提法是:给定一个没有负权值的有向图和其中一个点src作为源点(source),求从点src到其余个点的最短路径及路径长度.求解该问题的算法一般为Dijkstra算法. 假设图顶点个数为n,则针对 ...

  6. AC日记——斐波那契数列(升级版) 洛谷 P2626

    斐波那契数列(升级版) 思路: 水题: 代码: #include <cmath> #include <cstdio> #include <cstring> #inc ...

  7. [JSOI2008]Star War

    星球之间互相直接或间接地连接帝国开始使用死星有计划地摧毁反抗军占领的星球给出星球间隧道的连通情况,已经帝国打击的顺序要求以尽量快的速度求出每一次打击之后反抗军占据的星球的联通快的个数(若两个星球,直接 ...

  8. USACO1.3.2修理牛棚

    在学习一段时间贪心并写了一些贪心题之后,又一次看到了农夫和牛幸福美满的生活故事(雾).嘛,闲话少说,上题目 在一个暴风雨的夜晚,农民约翰的牛棚的屋顶.门被吹飞了. 好在许多牛正在度假,所以牛棚没有住满 ...

  9. 2、Flask实战第2天:URL传参

    当我们访问网站/的时候,会执行hell_world函数,并把这个函数的返回值返回给浏览器,这样浏览器就显示hello world了 @app.route('/') def hello_world(): ...

  10. 【二维单调队列】BZOJ1047-[HAOI2007]理想的正方形

    [题目大意] 有一个a*b的整数组成的矩阵,现请你从中找出一个n*n的正方形区域,使得该区域所有数中的最大值和最小值的差最小. [思路] 裸的二维单调队列.二维单调队列的思路其实很简单: (1)对于每 ...