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。this

为何会这样,这就要了解JS处理代码的一个机制了。code

咱们知道JS的处理过程是以堆栈的方式来处理,JS引擎会把执行环境一个个放入栈里,而后先放进去的后处理,后放进去的先处理,上面这个栗子,最早被放进栈中的是全局环境,而后是foo(),再是fn(),而后处理完一个拿出一个来,因此咱们知道为何foo()不能访问fn()里的了,由于它已经走了。对象

执行环境的生命周期

好了,了解完执行环境的的处理方式,咱们要说明执行环境的生命周期。
执行环境的生命周期分为两个阶段,这两个阶段描述了执行环境在栈里面作了些什么。教程

  1. 建立阶段;
  2. 执行阶段

建立阶段

执行环境在建立阶段会完成这么几个任务:1.生成变量对象;2.创建做用域链;3.肯定this指向索引

执行阶段

到了执行阶段,会给变量赋值,函数引用,而后还有执行其余的代码。

完成了这两个步骤,执行环境就能够准备出栈,一路走好了。

以上就是执行环境的具体执行内容。上面提到了执行环境在建立阶段会生成变量对象,这也是一个很重要的概念,咱们下文会详细论述。

变量对象(variable object)

变量对象是什么呢?《JS高程》是这样说的:“每一个执行环境都有与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。”

那变量对象里有些什么东西呢?看下文:

变量对象的内容

在变量对象建立时,通过了这样三个步骤:

  1. 生成arguments属性;
  2. 找到function函数声明,建立属性;
  3. 找到var变量声明,建立属性

其中值得注意的是:function函数声明的级别比var变量声明的级别要高,因此在实际执行的过程当中会先寻找function的声明。

还须要注意的是:在执行环境的执行阶段以前,变量对象中的属性都没法访问,这里还有一个活动对象(activation object)的概念,其实这个概念正是由进入执行阶段的变量对象转化而来。

来看一个栗子:

function foo(){
  var a=10;
  function fn(){
    return 5;
  }
}
foo();

让咱们来看看foo()函数的执行环境:

它会包括三个部分:1.变量对象;2.做用域链;3.this指向对象

建立阶段:

  1. 创建arguments
  2. 找到fn();
  3. 找到变量a,undefined;

执行阶段:

  1. 变量对象变成活动对象;
  2. arguments仍是它~
  3. fn();
  4. a=10;

以上就是变量对象的内容了,须要记住这个东西,由于会方便咱们了解下文另外一个重要的概念:做用域链。

做用域链(scope chain)

什么是做用域链?《JS高程》里的文字是:“做用域链的用途,是保证对执行环境有权访问的全部变量和函数的有序访问。”懵不懵逼?反正我第一次看到的时候确实是懵逼了。前面咱们说过做用域,那么做用域链是否是就是串在一块儿的做用域呢?并非。

做用域和做用域链的关系,用@波同窗的话说,做用域是一套经过标识符查找变量的规则。而做用域链则是这套规则这套规则的具体运行。

是否是仍是有点懵逼?仍是看例子吧:

function foo(){
  var a=10;
  function fn(){
    return 5;
  }
}
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=new Array();
    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){
            return function(){
              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 () {
        return this.a;
    }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

在上面这个例子中,foo.getA()做为对象方法的调用,指向的天然是这个对象,而test虽然指向和foo.getA相同,可是由于是独立调用,因此在非严格模式下,指向的是全局对象。

除了上面的例子,在《JS高程》中还有一个经典的例子,众多博客文章均有讨论,可是看过以后以为解释仍是不够清楚,至少我没彻底理解,这里我将试着用本身的语言来解释。
var name="the window";
var object={
    name:"my object",    
    getNameFunc:function(){
        return function(){
            return this.name;
        };
    }
};
    
alert(object.getNameFunc()());   // the window

在这个带有闭包的例子里,咱们能够看到object.getNameFunc()执行的返回是一个函数,再加()执行则是一个直接调用了。因此指向的是全局对象。

若是咱们想要返回变量对象怎么办呢?

让咱们看一段代码:

var name="the window";

var object={
name:"my object",
getFunc:function(){
        return this.name;
}
};
alert(object.getFunc());   //"my object"```

我去掉了上面例子的闭包,能够看出在方法调用的状况下,this指向的是对象,那么咱们只要在闭包能访问到的位置,同时也是在这个方法调用的同一个做用域里设置一个“中转站”就行了,让咱们把这个位置的this赋值给一个变量来存储,而后匿名函数调用这个变量时指向的就会是对象而不是全局对象了。

var name="the window";
    
    var object={
        name:"my object",
        getFunc:function(){
            var that=this;
            return function(){
                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);

咱们须要知道的是,所谓模块利用的就是闭包外部没法访问内部,内部却能访问外部的特性,经过引用了指定的公共变量和方法,达到访问私有变量和方法的目的。模块能够保证模块内部的私有方法和变量不被外部变量污染,进而方便更大规模的开发项目。

so,这篇文就到这里辣,写了一个下午,最最最要感谢的是@波同窗,正是读了他出色的教程,才能让我对JS的理解更深一点,他的每一篇技术文章都是很是用心的,事实上,我以为个人论述仍然不够系统清晰,想要了解得更清晰的朋友能够去简书搜索@波同窗阅读他写得技术文章,好了,就这样,债见
相关文章
相关标签/搜索