理解javascript中的做用域和做用域链对咱们理解js这们语言。此次想深刻的聊下关于js执行的内部机制,
主要讨论下,做用域,做用域链,闭包的概念。为了更好的理解这些东西,我模拟了当一个函数执行时,js引擎作了哪些事情--那些咱们看不见的动做。javascript
关键词:java
咱们都知道js的执行环境最外层是一个全局环境Global,在web浏览器的宿主环境下,window对象被认为是全局执行环境。在后台的nodejs环境global做为全局变量也是咱们能够直接访问到的。
某个执行环境中全部代码执行完毕后,该环境被销毁,保存在其中的全部变量和函数定义也随之销毁(全局环境到应用退出--如关闭网页或浏览器)node
每一个函数也有本身的执行环境,当执行流进入函数时,函数的环境被推入一个环境栈中,函数执行完毕以后,栈将其环境弹出,把控制权返回给以前的执行环境。web
当代码在一个环境中执行时,会建立建立变量对象的一个做用域链
。
若是环境是个函数,则将其活动对象做为变量对象。活动对象在最开始只包含一个变量,即arguments对象,做用域链的下一个变量对象来自下一个包含环境,一直延续到全局环境。数组
下面咱们模拟下这个过程。浏览器
var name = "eric"; function say(){ var name = "xu"; console.log(name); } say();//xu
输出“xu”,而不是“eric”,这个咱们也许都很好理解,由于函数内部定义了局部同名变量name,而不会使用全局的name。上面的环境中包含全局变量name
和say
函数;当say执行时,js引擎作了些什么。下面咱们模拟下引擎“偷偷”为咱们作的事。闭包
首先say()执行时会建立一个执行环境,为了形象一些,我这里以三个大括号可视化表示一个执行环境。如:say(){{{...}}}函数
这个执行环境中会自动拥有一个特殊的内部属性[[Scope]]
(为了更好的理解,能够把它想象成若是是全局环境的window,全局环境定义的变量和函数附着在这个变量上自动成为window的属性和方法,这样的一个局部功能“局部内全局对象”。但其实局部的变量和函数会被附着在其活动对象上,活动对象又是做用域链第一个变量对象。)this
函数调用时与执行环境同时建立的就是相应的做用域链
[[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
<script> console.log(name);//undefined var name = 'xu'; </script>
由于咱们的活动对象会自动变为第一个活动对象,因此第一个变量对象就等于活动对象
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开始一句一句解析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这句至关重要,当前这一层找到了,再也不向上一层找了。即在这一层环境中找到了变量name for (var i=0;i<Scope.length;i++){ if (name in Scope[i]){ console.log(Scope[i].name); break; } }
我以为这段代码,能够很是形象的表达了做用域链的查找过程
,
即首先查找第一个变量对象,其实就是函数内部的活动对象,若是找到则不进行下一个变量对象的查找,若是内部函数没有,才会沿着做用域链找下一个值,直到顶层的全局环境。
这就是为何我用数组去模拟做用域链的缘由,由于做用域链能够理解是个有序列表(其实做用域链的本质就是指向变量对象的指针列表)
,查找过程是按顺序查找的。
经过上面的形象化解释,是否是很是好理解做用域和做用域链了呢!!!
咱们都知道在函数执行完毕以后,内部的变量和内部定义的函数会随之销毁,也就是被垃圾回收机制所回收,以下:
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的运行机制和闭包的原理。