JS基础知识:变量对象、作用域链和闭包
前言:这段时间一直在消化作用域链和闭包的相关知识。之前看《JS高程》和一些技术博客,对于这些概念的论述多多少少不太清楚或者不太完整,包括一些大神的技术文章。这也给我的学习上造成了一些困惑,这几个概念的理解也是始终处于一个半懂不懂的状态。后来在某公众号看到了维客馆的基础文章,这应该是我所看到的最清楚,最全面,最好懂的文章了。所以我在学习之余决定写一篇文章,总结学到的知识点,用我的理解来阐述,不足之处,见请谅解。
执行上下文(Execution Context)
也叫执行环境,也可以简称“环境”。是JS在执行过程中产生的,当JS执行一段可执行的代码时,就会生成一个叫执行环境的东西。JS中每个函数都会有自己的执行环境,当函数执行时,就生成了它的执行环境,执行上下文会生成函数的作用域。
除了函数有执行环境,还有全局的环境。在JS中,往往不止一个执行环境。
让我们先来看一个栗子
var a=10;
function foo(){
var b=5;
function fn(){
var c=20;
var d=100;
}
fn();
}
foo();
在这个栗子中,包括了三个执行环境:全局环境,foo()执行环境,fn()执行环境;
执行环境的处理机制
在这里我们要了解到执行上下文的第一个特点:内部的环境可以访问外部的环境,而外部的环境无法访问内部的环境。
例如:我们可以在fn()中访问到位于foo()中的b,在全局环境中的a,而在foo()中却无法访问到c或者d。
为什么会这样,这就要了解JS处理代码的一个机制了。
我们知道JS的处理过程是以堆栈的方式来处理,JS引擎会把执行环境一个个放入栈里,然后先放进去的后处理,后放进去的先处理,上面这个栗子,最先被放进栈中的是全局环境,然后是foo(),再是fn(),然后处理完一个拿出一个来,所以我们知道为什么foo()不能访问fn()里的了,因为它已经走了。
执行环境的生命周期
好了,了解完执行环境的的处理方式,我们要说明执行环境的生命周期。执行环境的生命周期分为两个阶段,这两个阶段描述了执行环境在栈里面做了些什么。
创建阶段;执行阶段
创建阶段
执行环境在创建阶段会完成这么几个任务:1.生成变量对象;2.建立作用域链;3.确定this指向
执行阶段
到了执行阶段,会给变量赋值,函数引用,然后还有执行其他的代码。
完成了这两个步骤,执行环境就可以准备出栈,一路走好了。
以上就是执行环境的具体执行内容。上面提到了执行环境在创建阶段会生成变量对象,这也是一个很重要的概念,我们下文会详细论述。
变量对象(variable object)
变量对象是什么呢?《JS高程》是这样说的:“每个执行环境都有与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。”
那变量对象里有些什么东西呢?看下文:
变量对象的内容
在变量对象创建时,经过了这样三个步骤:
生成arguments属性;找到function函数声明,创建属性;找到var变量声明,创建属性
其中值得注意的是:function函数声明的级别比var变量声明的级别要高,所以在实际执行的过程中会先寻找function的声明。
还需要注意的是:在执行环境的执行阶段之前,变量对象中的属性都无法访问,这里还有一个活动对象(activation object)的概念,其实这个概念正是由进入执行阶段的变量对象转化而来。
来看一个栗子:
function foo(){
var a=10;
function fn(){
return5;
}
}
foo();
让我们来看看foo()函数的执行环境:
它会包括三个部分:1.变量对象;2.作用域链;3.this指向对象
创建阶段:
建立arguments找到fn();找到变量a,undefined;
执行阶段:
变量对象变成活动对象;arguments还是它~fn();a=10;
以上就是变量对象的内容了,需要记住这个东西,因为会方便我们了解下文另一个重要的概念:作用域链。
作用域链(scope chain)
什么是作用域链?《JS高程》里的文字是:“作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。”懵不懵逼?反正我第一次看到的时候确实是懵逼了。前面我们说过作用域,那么作用域链是不是就是串在一起的作用域呢?并不是。
作用域和作用域链的关系,作用域是一套通过标识符查找变量的规则。而作用域链则是这套规则这套规则的具体运行。
是不是还是有点懵逼?还是看例子吧:
function foo(){
var a=10;
function fn(){
return5;
}
}
foo();
我们还是用上面的栗子,这次我们只看作用域链,根据规则,在一个函数的执行环境的作用域链上,会依次放入自己的变量对象,父级的变量对象,祖级的变量对象…一直到全局的变量对象。
比如上面这个栗子,fn()的执行环境的作用域链上会有些什么呢?首先是自己的OV,然后是foo()的OV,接着就是全局的OV。而foo()的作用域链则会少一个fn()的OV。(OV是变量对象的缩写)
那这样放有什么好处呢?我们知道“作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。”有序!外层函数不能访问内层函数的变量,而内层能够访问外层。正是有了这个作用域链,通过这个有方向的链,我们可以查找标识符,进而找到变量,才能实现这个特性。
闭包
好了,终于要讲到这个前端小萌新眼里的小boss了。在技术博客和书里翻滚了将将一周,对闭包的各种解释把我搞得精力憔悴,怀疑人生。以至于在写下这段关于闭包的论述时,也是内心忐忑,因为我也不确定我说的是百分之百正确。
先看看《JS高程》说的:“闭包是指有权访问另一个函数作用域中的变量的函数。”
说法是:“当函数可以记住并访问所在的作用域(全局作用域除外)时,就产生了闭包,即使函数是在当前作用域之外执行。”
…
好吧其实我觉得都说的不是太清楚。让我们这样来理解,就是内部函数引用了外部函数的变量对象时,外部函数就是一个闭包。
还是看例子吧。
function foo(){
var a=20;
return
function(){
return a;
}
}
foo()();
在这个栗子中,foo()函数内部返回了一个匿名函数,而匿名函数内部引用了外部函数foo()的变量a,由于作用域链,这个引用是有效的,按照JS的机制,foo()执行完毕后,执行环境会失去引用,内存会销毁,但是由于内部的匿名函数的引用,a会被暂时保存下来,罩着a的就是闭包。
return一个匿名函数时创造一个闭包的最简单的方式,实际上创造闭包十分灵活,再看一个栗子:
var fn =null;function foo(){
var a =2;
function innnerFoo(){
console.log(a);
}
fn = innnerFoo;
}
function bar(){
fn();
}
foo();
bar();// 2
如上,可以看到:通过把innnerFoo()赋值给全局变量fn,内部的函数在当前作用域外执行了,但是这不会影响foo形成了一个闭包。
闭包和两个不同的案例
这两组栗子都是在各种书籍和各种博客上司空见惯了的栗子,其实跟闭包的关系不是很大,但是涉及到了函数相关的知识点,所以在这里写下来。也算是积累。
闭包和变量(见《JS高程》P181)
一个例子
function createFunction(){
var result=newArray();
for(i=0;i<10;i++){
result[i]=function(){
return i;
}
}
return result;
}
alert(createFunction());
这个例子并不会如我们以为的返回从0到9的一串索引值。当我们执行createFunction()时,函数内会return result,而我们注意到result是一个数组,而每一个result[i]呢?它返回的则是一个函数,而不是这个函数的执行结果 i。
所以我们想要返回一串索引值的时候,试着选择result数组的其中一个,再加上圆括号让它执行起来,像这样:
createFunction()[2]()
这样子就能执行了吗?运行起来发现并没有,执行的结果是一串的i,为什么呢?
原因是在执行createFunction()的时候,i的值已经增加到了10,即退出循环的值,而再要执行result内部的匿名函数时,它能获取到的i就只有10了,所以不管引用多少次,i的值都会是10;
那要如何修改才能达到我们的目的呢?
function createFunction(){
var result=[];
for(i=0;i<10;i++){
result[i]=function(num){
returnfunction(){
return num;
};
}(i);
}
return result;
}
alert(createFunction()[2]());
弹出的警告和索引值一模一样。这又是什么原因呢?
我们执行
createFunction()
时,把外部的匿名函数的执行结果赋值给了result,返回的result就是十个函数的数组。
而在这个外部函数里,有一个参数num,由于IIFE(立即执行函数)的缘故,循环过程中的i被赋值给了一个个的num,前后一共保存了10个num,为什么能够保存下来呢?因为内部的匿名函数引用了num。而这外部函数就是一个闭包
接下来,当执行
createFunction()[2]()
时实际上是执行这个数组result的第三项,即:
function(){
return num;
};
这个函数。
num值是多少呢?如前所述,正是对应的i。所以返回的值就能够达到我们的预期了。
实际上,我认为这个例子中更重要的是自执行函数这个概念,正是有了自执行,才能形成多对对多的引用,尽管这个例子里确实存在闭包,不过我认为用这个例子来介绍闭包并不是太恰当。
闭包和this
this也是JS里一个重中之重。我们知道,JS的this十分灵活的,前面已经介绍过,this的指向在函数执行环境建立时确定。函数中的this的指向是一个萌新们的难点,什么时候它是指向全局环境呢?什么时候它又是指向对象呢?注意:此处讨论的是指函数中的this,全局环境下的this一般情况指向window。
结论一:this的指向是在函数被调用的时候确定的
因为当一个函数调用时,一个执行环境就创建了,接着它会执行,这是执行环境的生命周期。所以this的指向是在函数被调用时确定的。
结论二:当函数执行时,如果这个函数是属于某个对象,调用的方式是以对象的方法进行的,那么this的指向就是这个对象,而其他情况,如函数独立调用,则基本是指向全局对象。
PS:实际上这个说法不大准确,当函数独立调用时,在严格模式下,this的指向时undefined,而非严格模式下,则时指向全局对象。
为了更好的说明,让我们看一个例子:
var a =20;
var foo ={
a:10,
getA:function(){
returnthis.a;
}
}
console.log(foo.getA());// 10
var test = foo.getA;
console.log(test());// 20
在上面这个例子中,foo.getA()作为对象方法的调用,指向的自然是这个对象,而test虽然指向和foo.getA相同,但是因为是独立调用,所以在非严格模式下,指向的是全局对象。
除了上面的例子,在《JS高程》中还有一个经典的例子,众多博客文章均有讨论,但是看过之后觉得解释还是不够清楚,至少我没完全理解,这里我将试着用自己的语言来解释。
var name="the window";
varobject={
name:"my object",
getNameFunc:function(){
returnfunction(){
returnthis.name;
};
}
};
alert(object.getNameFunc()());// the window
在这个带有闭包的例子里,我们可以看到object.getNameFunc()执行的返回是一个函数,再加()执行则是一个直接调用了。所以指向的是全局对象。
如果我们想要返回变量对象怎么办呢?
让我们看一段代码:
var name=“the window”;
varobject={
name:"my object",getFunc:function(){
returnthis.name;
}};
alert(object.getFunc());//"my object"```
我去掉了上面例子的闭包,可以看出在方法调用的情况下,this指向的是对象,那么我们只要在闭包能访问到的位置,同时也是在这个方法调用的同一个作用域里设置一个“中转站”就好了,让我们把这个位置的this赋值给一个变量来存储,然后匿名函数调用这个变量时指向的就会是对象而不是全局对象了。
var name="the window";
varobject={
name:"my object",
getFunc:function(){
var that=this;
returnfunction(){
return that;
};
}
};
alert(object.getFunc());
that’s all
闭包的应用
闭包的应用太多了,最重要的一个就是模块模式了。不过说实话,实在还没上路,所以这里就用一个模块的栗子来结尾吧。(强行结尾)
(function(){
var a =10;
var b =20;
function add(num1, num2){
var num1 =!!num1 ? num1 : a;
var num2 =!!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
add(10,20);
我们需要知道的是,所谓模块利用的就是闭包外部无法访问内部,内部却能访问外部的特性,通过引用了指定的公共变量和方法,达到访问私有变量和方法的目的。模块可以保证模块内部的私有方法和变量不被外部变量污染,进而方便更大规模的开发项目。