[JavaScript] 执行上下文(执行环境) 变量对象 作用域链 闭包 this 小结

执行上下文(执行环境)

介绍

执行环境( execution context,为简单起见,有时也称为“环境”)是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象( variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。

每个函数都有自己的执行环境,称为局部执行环境

当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。 ECMAScript 程序中的执行流正是由这个方便的机制控制着。 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。

当代码在一个环境中执行时,会创建变量对象的一个作用域链( scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象( activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

通过下面的例子来进一步解释上面介绍的内容:

var global_a = 10
function foo(a) {
    var foo_a = 12;
    function bar(temp) {
        var bar_a = 14
        console.log(global_a)
        console.log(foo_a)
        console.log(bar_a)
        console.log(temp)
    }
    var foo_b = 3;
    bar(foo_b);
}
console.log('hhh')
foo(1);

通过上面的例子来解释执行上下文的产生:首先JavaScript引擎在执行一段代码前会先进行预处理,执行上下文就是在预处理阶段创建的。使用var定义的变量以及使用函数声明定义的函数都会被预处理(也就是我们常常提到的变量提升)。

1.第一步

开始执行全局代码前,JS引擎创建全局执行上下文(全局执行环境), 记作globalEC,每一个全局执行上下文有一个与之相关联的变量对象VO, 把全局执行上下文的变量对象记作globalVO,环境中定义的所有变量和函数都保存在这个对象中;
此外,全局执行上下文中还保存了作用域链,也就是scope,作用域链可以看作一个单向链表,全局作用域中保存了一个元素,即全局执行上下文的变量对象。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

// 全局执行上下文可以看作这样的一个对象
globalEC = {
    VO: { // 把这个对象记作globalVO
        global_a: undefined,
        foo: refenrence to foo func,
    },
    scope: [globalVO] // 作用域链
}

JS引擎还做了一件重要的事件:在创建 foo()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数对象的[[Scope]]属性中。

foo.[[scope]] = [globalVO]

在开始执行前,全局上下文环境就会被推入环境栈中。

全局上下文预处理好后,开始一句一句的执行代码,最后的全局执行上下文变量如下

// 全局执行上下文可以看作这样的一个对象
globalEC = {
    VO: { // 把这个对象记作globalVO
        global_a: 10,
        foo: refenrence to foo func,
    },
    scope: [globalVO] // 作用域链
}

2.第二步

当调用 foo()函数时,在代码开始执行前,仍然会进行预处理,会为foo函数创建一个函数执行环境,然后通过复制foo函数的[[Scope]]属性中的对象构建起foo函数执行环境的作用域链。此后,又有一个新的活动对象(在此作为变量对象使用)被创建并被推入函数执行环境作用域链的前端。对于这个例子中 foo()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。

// foo函数执行上下文可以看作这样的一个对象
fooEC = {
    VO: { // 把这个对象记作fooVO
        foo_a: undefined,
        bar: refenrence to bar func,
        foo_b: undefined
    },
    scope: [fooVO, globalVO] // 作用域链
}

JS引擎还做了一件重要的事件:在创建 bar()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数对象的[[Scope]]属性中。

bar.[[scope]] = [fooVO, globalVO]

在开始执行前,foo函数上下文环境就会被推入环境栈中

全局上下文预处理好后,开始一句一句的执行代码,最后的全局执行上下文变量如下

// foo函数执行上下文可以看作这样的一个对象
fooEC = {
    VO: { // 把这个对象记作fooVO
        foo_a: 12,
        bar: refenrence to bar func,
        foo_b: 3
    },
    scope: [fooVO, globalVO] // 作用域链
}

3.第三步

当调用 bar()函数时,在代码开始执行前,仍然会进行预处理,会为bar函数创建一个函数执行环境,然后通过复制bar函数的[[Scope]]属性中的对象构建起bar函数执行环境的作用域链。此后,又有一个新的活动对象(在此作为变量对象使用)被创建并被推入函数执行环境作用域链的前端。对于这个例子中 bar()函数的执行环境而言,其作用域链中包含三个变量对象。

// bar函数执行上下文可以看作这样的一个对象
barEC = {
    VO: { // 把这个对象记作barVO
        bar_a: undefined
    },
    scope: [barVO, fooVO, globalVO] // 作用域链
}

JS引擎还做了一件重要的事件:在创建 bar()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数对象的[[Scope]]属性中。

bar.[[scope]] = [barVO, fooVO, globalVO]

在开始执行前,foo函数上下文环境就会被推入环境栈中

全局上下文预处理好后,开始一句一句的执行代码,最后的全局执行上下文变量如下

// bar函数执行上下文可以看作这样的一个对象
barEC = {
    VO: { // 把这个对象记作barVO
        bar_a: 14
    },
    scope: [barVO, fooVO, globalVO] // 作用域链
}

总结

JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以 JavaScript 引擎会先解析创建全局执行上下文,然后将全局执行上下文压栈。然后当执行流进入一个函数时,会先解析创建函数的执行上下文,然后将它的执行上下文压栈。而在函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文。注意,全局执行上下文会一直留在栈底,直到整个应用结束。全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止。也就是从当前环境的变量对象开始查找,如果没有,则到外部环境的变量对象中查找,直到全局变量对象为止。这也是我们常常理解的:作用域只与函数定义时的位置有关,因为函数在创建时就确定了他的[[scope]]属性(也就是作用域)。

  1. 全局上下文阶段,创建全局对象。
  2. 将全局对象压入作用域链
  3. 为全局对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)
  4. 每一个函数上下文阶段,复制函数的[[Scope]]属性,创建作用域链
  5. 创建活动对象,并用 arguments 创建活动对象
  6. 将活动对象压入当前上下文中的作用域链
  7. 为活动对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

例子

<body>
<!--
1. 依次输出什么?
  global begin:undefined
  foo() begin:1
  foo() begin:2
  foo() begin:3
  foo() end:3
  foo() end:2
  foo() end:1
  global end:1
2. 整个过程中产生了几个执行上下文?
  5个(调用4次foo,产生4个局部上下文和1个全局上下文)
-->
<script type="text/javascript">
  console.log('global begin: '+ i);//global begin: undefined
  var i = 1;
  foo(1);
  function foo(i) {
    if (i == 4) {
      return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);//递归调用:在函数内部调用自己
    console.log('foo() end:' + i);
  }
  console.log('global end: ' + i);//global end: 1
</script>
</body>

变量对象

在讲解执行上下文的时候提到,每个执行环境都有一个与之关联的变量对象( variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。

全局上下文中的变量对象

相信大家都已经猜到了,全局上下文中的变量对象就是全局对象,因为它们拥有的特征太像了。

我们先来看一下什么是全局对象:

全局对象(global object)是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。全局对象不是任何对象的属性,所以它没有名称。全局对象上预定义了全局的属性、函数、对象、构造函数以便你开发使用。

比如说:

1.全局属性:比如undefined、Infinity以及NaN。
2.全局对象:比如Math、JSON和Number
3.全局函数:比如isNaN()、isFinite()、parseInt()和eval()等。
4.全局构造器(constructor),也即全局类。比如Date()、RegExp()、String()、Object()和Array()等。

js运行时内置了一个Global对象。这个Global对象跟运行环境有关。在浏览器运行环境中。Global就是window对象。在nodejs中。Global对象是global对象。当你在浏览器环境中,直接使用一个未经定义的变量,例如foo=123;那么foo这个变量自动声明为全局变量。变量引用自动挂载到了Global对象,即window对象上。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。但通常不必用这种方式引用全局对象,因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

我们再来看一下全局上下文中的变量对象的特点:

全局上下文中的变量对象在执行所有代码之前创建。

全局上下文中的变量对象一直存在,直到程序结束。

全局上下文中的变量对象保存了全局上下文中所有的变量和函数声明

全局变量对象位于作用域链的顶端

其实对照起来一看,我们就会明白为什么会说全局上下文中的变量对象就是全局对象了。但全局对象除了含有变量对象的特点外,它还做了其他事,那就是在初始化时会将 Math 、 String 、Date、parseInt 等作为自身属性,这也是为什么我们可以访问使用这些函数和属性的原因。

其实我们只要记住全局上下文中的变量对象就是全局对象就行了。

函数上下文中的变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象,它包括如下属性:

  • callee — 指向当前函数的引用
  • length — 真正传递的参数个数
  • properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。

注意,arguments 代表的是真正传入函数的参数列表(不受参数个数限制),和函数参数是分开的。

作用域链

作用域链也就是前面提到的每个执行环境中的scope属性。

当代码在一个环境中执行时,会创建变量对象的一个作用域链( scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象( activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。也就是说一个函数在定义的时候其作用域就已经决定了。

例一

<script type="text/javascript">
    /*
        作用域:
        -指一个变量的作用范围
        -在js中一共有两种作用域
        全局作用域:
            直接编写在script标签中的代码
            全局作用域在页面打开时创建,在页面关闭时销毁
            在全局作用域中,有一个全局对象window,我们可以直接使用
                它代表的是浏览器的窗口,它由浏览器创建我们可以直接使用
            在全局作用域中,创建的变量都会作为window对象的属性保存
                创建的函数都会作为window对象的方法
            全局作用域中的变量在页面任何位置都能访问到
        局部(函数)作用域:
            调用函数时创建函数作用域,函数结束时,摧毁
            每调用一次,都会创建一个新的,它们之间相互独立
            在函数作用域中可以访问到全局作用域的变量
            在全局作用域中不能被访问到函数作用域中的变量
            当在函数作用域中操作一个变量时,他会现在自身作用域中寻找,如果有就用
            如果没有就去上一级作用域中寻找,直到全局作用域
            如果一直没找到,就报错了
            在函数中想使用全局变量,window.a
            在函数作用域中也有声明提前(var变量 和 函数声明函数)
        */
    var b="我是全局作用域b";
    function fun2(){
        var b="我是函数作用域b";
        console.log(b);//我是函数作用域b
        console.log(window.b);//我是全局作用域b
    }
    fun2();
    window.fun2();
    
    var c=10;
    function fun3() {
        //在函数中,不是用var变量声明的变量都会成为全局变量
        //除非它是一个形参,那么他就是一个函数变量
        c=5;//全局变量去改变外面的c
        d=100;//声明一个全局变量
    }
    fun3();
    console.log(c);//5
    console.log(d);//100 输出全局变量d
    
    var e=10;
    //定义形参就相当于在函数作用域中声明了一个变量e
    function fun4(e){
        alert(e);
    }
    fun4();//undefined          
</script>

面试题1

<script type="text/javascript">
  /*
   问题: 结果输出多少?
   */
  var x = 10;
  function fn() {
    console.log(x);
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);// 10
</script>

  • 简单的理解上面的面试题:在第11行调用f函数的时候,f函数实际就是全局函数fn的引用,fn的作用域链在定义的时候就已经确定了,fn函数中的x就是全局变量x,所以结果为10。
  • 严谨的来理解:fn函数在预处理时,身上添加了[[scope]]属性,fn.[[scope]] = [globalVO],进入fn的执行环境后,执行环境中有作用域链scope,scope=[fnVO, globalVO],打印x的时候,先在局部变量对象fnVO上寻找,没有找到,去全局变量对象globalVO上找,所以打印出10。

面试题2

/*
   输出情况?
   */
var fn = function () {
    /*
    先在fn函数作用域寻找fn,没有找到
    然后去全局寻找,就找到了
  */
    console.log(fn); // 打印出fn函数
};
fn();

面试题3

var obj = {
    fn2: function () {
        /*
        首先在fn2函数作用域找fn2,找不到
        然后去全局作用域寻找,
        注意:全局只有obj.fn2,而没有fn2,所以是以下结果 
      */
        console.log(this.fn2); // this就是调用该方法的对象,也就是obj
        //console.log(fn2);//fn2 is not defined
    }
};
obj.fn2();

闭包

mdn对闭包的介绍

  • 一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • JavaScript中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。
  • 闭包是指有权访问另一个函数作用域中的变量的函数。

理解1

理解了执行上下文的概念后,我们知道:无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同 。

// 将函数作为另一个函数的返回值
function fn1() {
    var a = 2;
    function fn2() {
        a++;
        console.log(a);
    }
    return fn2;
}
var f = fn1(); 
f(); // 打印3 
f(); // 打印4

通过上面的例子来说明闭包的概念:

在函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 fn1()函数内部定义的fn2函数的作用域链中,实际上将会包含外部函数 fn1()的活动对象。

一个函数,它能访问另一个函数作用域中的变量。这就是闭包。

// fn1的执行上下文
fn1EC = {
    VO: { // 把这个对象记作fn1VO
        a: 2,
        fn2: reference to func fn2
    },
    scope: [fn1VO, globalVO] // 作用域链
}
// fn2函数在创建时被添加了一个[[scope]]属性
fn2.[[scope]] = [fn1VO, globalVO]
// fn2的执行上下文
fn2EC = {
    VO: { // 把这个对象记作fn2VO
    },
    scope: [fn2VO, globalVO] // 作用域链
}

在fn2从 fn1()中被返回后,它的作用域链被初始化为包含fn1()函数的活动对象和全局变量对象。这样,fn2函数就可以访问在fn1()中定义的所有变量。更为重要的是, fn2()函数在执行完毕后,其活动对象也不会被销毁,因为fn2的作用域链仍然在引用这个活动对象。换句话说,当 fn1()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到fn2被销毁后, fn1()的活动对象才会被销毁 。

要销毁的话,只需要执行下面的语句

fn1 = null

我们将fn2函数作为fn1函数的返回值。这样我们调用fn1的时候就会得到一个返回值f,f实际上就是fn2函数的引用。当我们调用f函数的时候,控制台会打印一个3,也就是a变量加1后的结果。

按照执行上下文的知识我们知道a变量是fn1执行上下文相关的变量对象中的变量,我们每次调用f函数的时候,都会将这个变量+1,所以分别打印出了3和4.

理解2

function fn1() {
  var a = 2;
  function fn2() {
    a++;
    console.log(a);
  }
  return fn2;
}

var f = fn1();
f(); // 打印3
f(); // 打印4
var g = fn1(); // 会创建一个新的闭包
g(); // 打印3

分析上面例子的打印结果,fn1被调用了两次,所以先后产生了两次fn1的执行上下文环境,也就是有两个相关的变量对象,所以g()打印了3

闭包的生命周期

<!--
1. 产生: 在嵌套的内部函数的定义执行完时就产生了(不是在调用)
2. 死亡: 在嵌套的内部函数成为垃圾对象时
-->
<script type="text/javascript">
  function fun1() {
    // 在js引擎预处理阶段:产生闭包
    var a = 3;
    function fun2() {
      a++;
      console.log(a);
    }
    return fun2;
  }
  var f = fun1();
  f();
  f();
  f = null // 此时闭包对象死亡,没有变量引用内部函数了,(内部函数和fun1的变量对象将会被垃圾回收机制回收)
</script>

闭包的缺点

<body>
<!--
1. 缺点
  * 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
  * 容易造成内存泄露
2. 解决
  * 能不用闭包就不用
  * 及时释放
-->
<script type="text/javascript">
  function fn1 () {
    var arr = new Array[10000];
    function fn2 () {
      console.log(arr.length);
    }
    return fn2;
  }
  var f = fn1();
  f();
  f = null;//解决方法:让内部函数成为垃圾对象-》回收闭包
</script>

闭包的副作用以及解决方案

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。 别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。

<body>
    <button>点我</button>
    <button>点我</button>
    <button>点我</button>
    <script>
      var btns = document.getElementsByTagName('button');
      for(var i=0,length=btns.length; i<length; i++) {
          var btn = btns[i];
          btn.onclick = function () {
              alert('第'+(i)+'个');
          };       
      }
      // 结果: 点击第一个按钮弹出‘第3个’ 点击第二个按钮弹出‘第3个’ 点击第三个按钮弹出‘第3个’
      </script>
</body>

这里的脚本为3个按钮添加了点击事件。表面上看,似乎每个事件处理函数都应该返自己的索引值,即位置 0 的函数返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个按钮点击后都会弹出3。因为每个函数的作用域链中都 保 存 着 全局上下文 的 变量 对 象 , 所 以 它 们 引 用 的 都 是 同 一 个 变 量 i 。 当for循环执行完后后,变量 i 的值是 3。

全局上下文的变量对象如下

globalVO = {
    i: 3,
    btns: 集合
    btn: xxx,
    还有很多其他变量...
}

但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。

<body>
    <button>点我</button>
    <button>点我</button>
    <button>点我</button>
    <script>
      var btns = document.getElementsByTagName('button');
      for(var i=0,length=btns.length; i<length; i++) {
          var btn = btns[i];
          (function (num) {
            btn.onclick = function () {
              alert('第'+(num)+'个');
            };
          })(i);            
      }
      // 结果: 点击第一个按钮弹出‘第0个’ 点击第二个按钮弹出‘第1个’ 点击第三个按钮弹出‘第2个’
      </script>
</body>

在这个版本中,我们定义了一个匿名函数。在调用每个匿名函数时,我们传入了变量 i。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给匿名函数的形参num。而在这个匿名函数内部,又创建了一个访问 num 的闭包。这样一来,就可以弹出不同的数值了。

也就是我们添加的匿名函数会被调用3次,因此会有3个变量对象,3个按钮绑定的事件处理函数分别引用了三个变量对象的i

应用:利用闭包将方法私有

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1); // 引用changeBy
    },
    decrement: function() {
      changeBy(-1); // 引用changeBy
    },
    value: function() {
      return privateCounter; // 引用privateCounter
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

词法环境包括privateCounter和changeBy。该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这就有点类似于Java,将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

应用-利用闭包封装js模块

  • 定义一个js插件(模块)coolModule.js

    /**
     * 自定义模块1
     */
    function coolModule() {
      //私有的数据
      var msg = 'i am msg';
      var names = ['linlin', 'daling', 'xiaoli'];
      //私有的操作数据的函数
      function doSomething() {
        console.log('doSomething()'+msg.toUpperCase());
      }
      function doOtherthing() {
        console.log(names.join(' '));
      }
      //向外暴露包含多个方法的对象
      // return doSomething;
      return {
        doSomething: doSomething,
        doOtherthing: doOtherthing
      };
    }
    
  • 使用js插件

    <body>
    <!--
    闭包的应用2: 定义JS模块
      * 具有特定功能的js文件
      * 将所有的数据和功能都封装在一个函数内部(私有的)
      * 只向外暴露一个包含n个方法的对象或函数
      * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
    -->
    <script type="text/javascript" src="coolModule.js"></script>
    <script type="text/javascript">
      var module = coolModule();
      // module();
      module.doSomething();
      module.doOtherthing();
    </script>
    </body>
    

应用-利用闭包封装js模块2

  • 定义个js模块coolModule2.js

    使用立即执行匿名函数的形式来定义,那这个插件引入后会立即执行一次(而且只能执行这一次),通过向window对象挂载一个属性来向外暴露,外部通过window.coolModule2即可访问到我们暴露的方法。

    /**
     * 自定义模块2
     */
    (function (window) {
      //私有的数据
      var msg = 'i am msg';
      var names = ['linlin', 'daling', 'xiaoli'];
      //操作数据的函数
      function a() {
        console.log(msg.toUpperCase())
      }
      function b() {
        console.log(names.join(' '))
      }
      //向外暴露
      window.coolModule2 =  {
        doSomething: a,
        doOtherthing: b
      };
    })(window);
    
  • 使用这个插件

    <body>
    <!--
    闭包的应用2 : 定义JS模块
      * 具有特定功能的js文件
      * 将所有的数据和功能都封装在一个函数内部(私有的)
      * 只向外暴露一个包信n个方法的对象或函数
      * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
    -->
    <script type="text/javascript" src="coolModule2.js"></script>
    <script type="text/javascript">
      coolModule2.doSomething()
      coolModule2.doOtherthing()
    </script>
    </body>
    

面试题1

<script type="text/javascript">
  /*
   说说它们的输出情况
   */
  //代码片段一 没有闭包
  var name = "The Window";
  var object = {
    name: "My Object",
    getNameFunc: function () {
      return function () {
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());  //The Window

  //代码片段二 有闭包 内部函数引用了外部函数的this
  var name2 = "The Window";
  var object2 = {
    name2: "My Object",
    getNameFunc: function () {
      var that = this;
      return function () {
        return that.name2;
      };
    }
  };
  alert(object2.getNameFunc()()); //My Object
</script>

面试题2

<script type="text/javascript">
  /*
   说说它们的输出情况
   */
  function fun(n, o) {
    console.log(o);
    return {
      fun: function (m) {
        return fun(m, n);
      }
    }
  }
  var a = fun(0);//undefined 产生了闭包 且没有消失,因为有a指向了这个对象
  a.fun(1);//0 fun(1,0) 这里也产生了一次闭包,但是没有变量接收 又消失了
  a.fun(2);//0 fun(2,0)
  a.fun(3);//0 fun(3,0)

  //fun(0,undefined) fun(1,0) fun(2,1) fun(3,2)
  var b = fun(0).fun(1).fun(2).fun(3); //undefined,0,1,2

  // fun(0,undefined) fun(1,0)
  var c = fun(0).fun(1);// undefined,0
  c.fun(2);//1 fun(2,1)
  c.fun(3); //1 fun(3,1)
</script>

this

与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别。

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。this 不能在执行期间被赋值,并且在每次函数被调用时 this 的值也可能会不同。ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES2015 引入了箭头函数,箭头函数不提供自身的 this 绑定(this 的值将保持为闭合词法上下文的值)。

更详细的参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

看下面的代码

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        console.log(this.name) // "My Object"
        return function(){
        	return this.name;
        };
    }
};
alert(object.getNameFunc()()); //"The Window"

为什么alert(object.getNameFunc()()); 打印出了The Window"呢?

每个函数在被调用时都会自动取得两个特殊变量: this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量 。所以不会进入getNameFunc获取this变量。

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中, this 等于 window,而当函数被作为某个对象的方法调用时, this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        console.log(this.name) // "My Object"
        var that = this;
        return function(){
        	return that.name;
        };
    }
};
alert(object.getNameFunc()()); //"The Window"

参考文档

《闭包》
《学习Javascript闭包(Closure)》
《JavaScript 论代码执行上下文》
《深入理解JavaScript系列(12):变量对象(Variable Object)》
《JavaScript深入之变量对象》
《JavaScript 全局对象》
《JAVASCRIPT关于全局对象》
《JavaScript 的 this 原理-阮一峰》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值