博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
形象化模拟作用域链,深入理解js作用域、闭包
阅读量:6082 次
发布时间:2019-06-20

本文共 5438 字,大约阅读时间需要 18 分钟。

前言

理解javascript中的作用域和作用域链对我们理解js这们语言。这次想深入的聊下关于js执行的内部机制,

主要讨论下,作用域,作用域链,闭包的概念。为了更好的理解这些东西,我模拟了当一个函数执行时,js引擎做了哪些事情--那些我们看不见的动作。

关键词:

  • 执行环境
  • 作用域
  • 作用域链
  • 变量对象
  • 活动对象
  • 闭包
  • 垃圾回收

执行环境与作用域链

我们都知道js的执行环境最外层是一个全局环境Global,在web浏览器的宿主环境下,window对象被认为是全局执行环境。在后台的nodejs环境global作为全局变量也是我们可以直接访问到的。

某个执行环境中所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境到应用退出--如关闭网页或浏览器)

每个函数也有自己的执行环境,当执行流进入函数时,函数的环境被推入一个环境栈中,函数执行完毕之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建创建变量对象的一个作用域链

如果环境是个函数,则将其活动对象作为变量对象。活动对象在最开始只包含一个变量,即arguments对象,作用域链的下一个变量对象来自下一个包含环境,一直延续到全局环境。

下面我们模拟下这个过程。

var name = "eric";function say(){    var name = "xu";    console.log(name);}say();//xu

输出“xu”,而不是“eric”,这个我们也许都很好理解,因为函数内部定义了局部同名变量name,而不会使用全局的name。上面的环境中包含全局变量namesay函数;当say执行时,js引擎做了些什么。下面我们模拟下引擎“偷偷”为我们做的事。

作用域链的产生过程

首先say()执行时会创建一个执行环境,为了形象一些,我这里以三个大括号可视化表示一个执行环境。如:say(){

{
{...}}}

这个执行环境中会自动拥有一个特殊的内部属性[[Scope]](为了更好的理解,可以把它想象成如果是全局环境的window,全局环境定义的变量和函数附着在这个变量上自动成为window的属性和方法,这样的一个局部功能“局部内全局对象”。但其实局部的变量和函数会被附着在其活动对象上,活动对象又是作用域链第一个变量对象。)

函数调用时与执行环境同时创建的就是相应的作用域链[[Scope Chain]],并赋值给特殊变量Scope;

//step 1:创建执行环境,为了形象一些,我这里以三个大括号可视化表示一个执行环境{
{
{...}}}
//step 2:创建作用域链,并赋值给特殊变量Scope,我们用数组来模拟这个作用域链,随后我会解释为什么用数组模拟var ScopeChain = [    FirstVariableObject,//函数内的变量对象    SecondVariableObject //包含这个函数的外面一层的变量对象,在上面的例子中已经是全局环境了。]Scope = ScopeChain;

在作用域链生成之前,其实还有步骤,那就是作用域链数组的两个变量对象的生成。那这两个变量对象是什么呢?

其实第一个变量对象就是函数的活动对象【activation object】,这个活动对象可以理解成这样一个对象

ActivationObject = {    arguments: []  //活动对象最开始仅包含arguments(就是函数内隐藏的arguments)}

然后内部this根据环境,加入活动对象

ActivationObject = {    arguments: [],  //活动对象最开始仅包含arguments(就是函数内隐藏的arguments)    this: window    //这里的this根据执行环境和调用对象的不同,会动态变化,上面的例子因为是全局环境执行的所以this指向window}

然后开始寻找var的变量定义,或者函数声明(我们都知道的函数声明会被提升)。

此时的活动对象变成:

//活动对象,即函数内部所有变量的综合,会自动成为第一个变量对象ActivationObject = {    arguments: [],    this: window,    name: undefined //注意引擎此时并不会初始化赋值,只有读到赋值那一行时才会赋值}

这样我们就能很好的理解我们熟悉的经典例子,为什么下面的console.log不会报错,也不是输出'xu',而是undefined

因为我们的活动对象会自动变为第一个活动对象,所以第一个变量对象就等于活动对象

FirstVariableObject = ActivationObject;

同理作用域中的第二个变量对象SecondVariableObject,或者我们也可以命名为GlobalVariableObject,因为在上面的例子中已经是全局环境了

//作用域链的第二个,也是最后一个(全局变量对象)SecondVariableObject = {    this: window,    say: function (){...},    name: "eric"}

第二个变量对象不包含arguments,因为它是全局环境,而不是函数。say函数声明被提升作为window的全局方法,还有全局的name属性。都被挂在第二层的作用域链的变量对象上。

至此作用域链创建完毕。作用域链会成为这样的好理解的样子:

//形象的作用域链Scope = ScopeChain = [    {        arguments: [],        this: window,        name: undefined    },    {        this: window,        say: function (){...},        name: "eric"    }]

作用域链查找在js执行过程中的模拟

然后js开始一句一句解析say函数的代码,

第一句,var name = "xu"

此时,活动对象的name值才会将undefined变为'xu';

然后执行第二句console.log(name);

这句中有一个变量name,这个时候作用域链就该出场了。

js引擎会开始执行查找,首先从ActivationObject活动对象中开始找,因为经过var name = "eric";

此时作用域链的第一个,即活动对象已经变成

{    arguments: [],    this: window,    name: 'xu'}

所以输出‘xu’,而不是‘eric’

如果我们将say函数,做下改动如下:

var name = "eric";function say(){    var age = 99;    console.log(name);}say();//eric

因为内部的没有定义name变量,这个结果不出意料的我们都知道,但这个过程我把它模拟成以下查找过程:

//从当前函数的活动对象开始,一层一层向上查找,直到顶层全局作用域//break这句相当重要,当前这一层找到了,不再向上一层找了。即在这一层环境中找到了变量namefor (var i=0;i

我觉得这段代码,可以非常形象的表达了作用域链的查找过程

即首先查找第一个变量对象,其实就是函数内部的活动对象,如果找到则不进行下一个变量对象的查找,如果内部函数没有,才会沿着作用域链找下一个值,直到顶层的全局环境。

这就是为什么我用数组去模拟作用域链的原因,因为作用域链可以理解是个有序列表(其实作用域链的本质就是指向变量对象的指针列表),查找过程是按顺序查找的。

通过上面的形象化解释,是不是非常好理解作用域和作用域链了呢!!!

垃圾回收

我们都知道在函数执行完毕之后,内部的变量和内部定义的函数会随之销毁,也就是被垃圾回收机制所回收,如下:

function talk(){    var name = 'eric';    function say(){        console.log(name);    }    say();}talk();

当talk函数执行后,内部的变量name和声明的函数say会从内存中销毁,但闭包的情况就不会。如:

function createTalk(){    var name = 'eric';    var age = 99;    return function (){        var innerName = name;        console.log(innerName);    }}var talk = createTalk();talk();

闭包中没有释放局部变量的原因

闭包的本质其实是有权访问另一个函数作用域中变量的函数

根据我们上面模拟的作用域链模型,上面的例子中当talk执行时,整个作用域链可以形象化为:

ScopeChain = [    {        arguments:[],        this: window,        innerName: undefined    },    {        arguments:[],        this: window,        name: eric,        age: 99    },    {        this: window,        createTalk: function (){...},        talk: function (){...} //内部return的匿名函数    },]

这样当createTalk执行后,talk变量仍然保持了对函数内部变量和内部匿名函数的引用,因此即使createTalk执行完毕,虽然其执行环境被销毁,但返回的匿名函数的作用域链被初始化为createTalk()函数的活动对象和全局变量对象,内部变量仍然没有被垃圾回收机制所回收。虽然返回的匿名函数,仅使用了外一层的name变量,而没有使用age变量。但其内部保存的仍然是整个外层变量对象,即

{    arguments:[],    this: window,    name: eric,    age: 99}

而不仅仅是外层的name变量一个值,因为查找过程中,使用的是整个的变量对象来查找的。因为是查找,所以存在遍历整个对象的过程,而不是简单的赋值

这就是为什么闭包会占用更多的内存的原因,因为其保存了整个变量对象。虽然我们的例子可能就几个,但在实际应用中可能存在非常多。

这也是我们要谨慎使用闭包的原因。

闭包的经典实例

接下来我们看一个经典的闭包示例。

var result = [];for (var i=0;i<10;i++){    result[i] = function (){        return i;    }}

结果或许大家都知道了,result数组的任何一个执行,都会返回10。下面我们用上面模拟的作用链,形象话的看下,

比如result[9]()函数执行的初始化作用域链如下:

ScopeChain = [    //第一层是内部匿名函数的变量对象    {        arguments:[],        this: window    },    //第二层是外部的,也就是全局变量对象    {        this: window,        result: [Array],        i: 10 //此时全局环境的i已经经过for循环变成了10    },]

自然任何一个result的值调用函数,都会是返回10。

通过变形符合预期的闭包如下:

var result = [];for (var i=0;i<10;i++){    result[i] = function (num){        return function (){            return num;        }    }(i);}

上面这个经典的闭包返回的就是我们想要的各自的i,为了更好理解,我还是使用形象的作用域链。

当匿名函数执行时,看下它的初始作用域链:

ScopeChain = [    //第一层为传入参数i的自执行函数    {        arguments:[],        this: window,    },    {        arguments:[num],        num: 9,         this: window,    }    {        this: window,        result: [Array],        i: 10    }]

我们可以理解为多了一层作用域链的变量对象,使其能保留对num副本的引用,而不是对i的引用。

好了,通过深入理解作用域链,我们能跟好的理解js的运行机制和闭包的原理。

转载地址:http://lqkwa.baihongyu.com/

你可能感兴趣的文章
alert 多语言的处理
查看>>
Ubuntu 最好用的CHM阅读器KchmViewer
查看>>
c# 高效率导出多维表头excel
查看>>
知识积累:CGI,FastCGI,PHP-CGI与PHP-FPM
查看>>
关于PHP定时执行任务的实现(转)
查看>>
PHP定时执行任务的实现(转)
查看>>
magento的一些小技巧(转)
查看>>
C++ 运行时类型识别 知道实例父类类型,显示出子类类型
查看>>
Android获取状态栏高度、标题栏高度、编辑区域高度
查看>>
bzoj1452 二维树状数组
查看>>
bzoj2561
查看>>
bzoj1093
查看>>
(转)使用vs调试的时候,如何知道程序阻塞在哪里?
查看>>
Linux其他:环境变量配置
查看>>
设置防止攻击session(疑惑)
查看>>
PHP 服务器及TP5框架遇到的几个错误
查看>>
用VMware克隆CentOS 6.5如何进行网络设置
查看>>
redis conf文件详解(转)
查看>>
7月心情
查看>>
jsp jsp九个内置对象
查看>>