2

声明:此文的所有语法适用于ES5, 因为ES6引入了块级作用域,所以此文的很多说法放在ES6的环境下,是错误的。

首先给js的作用域这个话题打标签:2,var, 全局变量,局部变量,函数,undefined, 作用域提升,赋值不会提升,ReferenceError, 同名覆盖。
打完标签之后,我们来说跟作用域有关的几条铁打的规则:
1: JS的作用域有2种:全局作用域,函数作用域。

把作用域想象成一个房间,而{}是房间的门。门上装了一个猫眼,所以房间里面可以看清楚外面,但是外面却看不见里面。
在JAVA或者C里面,大括号可能会出现的情况有两种:
    1: 一个function定义的时候
    2: 一个块定义(比如if,for, while)的时候.
所以此时的作用域有三种类型:
    1: 全局作用域,
    2: 函数作用域
    3: 块级作用域。

但是在JS里面,虽然{}出现的情况也有两种,但是只有function的{}才起到栅栏的作用。

2: 声明在全局作用域里的变量是全局变量,声明在函数里面的变量是局部变量

3: 怎么创造一个全局变量和局部变量?

创造全局变量的方法有两种:

   1: 在全局作用域内用var定义: var a;
   2: 声明一个变量,不带var(无论是在全局,在函数里面还是在一个块里面):b

创造局部变量的方法只有一种:

   在函数体里面,带var声明一个变量: function func(){var b;}

3: 使用一个没有声明过的变量,会得到一个ReferenceError。无论什么情况下。
4: 不同作用域内,同名的变量,越小的作用域的变量会覆盖越大的作用域的。

4: 作用域提升:变量在声明之前就可以引用了!
这个不是和第三点矛盾了吗?其实并没有。它背后的真正原理是:并不是作用域被提升(我们前面说了,一个变量的作用域会被框在一对栅栏{}里面,一旦这个栅栏确定了,那这个作用域是不可能变化的),其实是变量的‘声明’在其作用域里面被放到任何代码之前(当然包括引用它的代码之前)。看一段代码:

var scope = 'global';
function func(){
    console.log(scope); //输出‘undefined’,而不是‘global’
    var scope = 'local';
    console.log(scope); //输出‘local’
}

看到第一个console,可能以为会输出‘global’, 因为通过猫眼可以看见外面的变量。但是,一旦我们进到一个函数体里面,遇到任何的变量的引用,首先要先在当前的房间里面找,只有在当前的房间里面找不到时,才到父层去找。那为什么是‘undefined呢?其实以上的代码等价于:

var scope = 'global';
function func(){
    var scope; //变量的声明会提升到最前面,但是赋值并不会,所以此刻scope的值还只是undefined.
    console.log(scope); //输出‘undefined’,而不是‘global’
    scope = 'local'; //赋值在这里完成
    console.log(scope); //输出‘local’
}

============闭包的分割线===========
1: 什么是闭包?
我看过看多不同的书,对必包的定义都不一样,而且就算是我知道了闭包的定义,对我真正理解它的工作原理还是没有什么用。所以,我就不去纠结它到底是什么,我接下来只关注它是怎么工作的。
2: 什么时候会形成闭包?
以下内容非原创,这里只是我自己的一个学习笔记。我看了http://www.jianshu.com/p/7312...(在这里感谢作者),跟着文章里面的例子(代码根据自己的喜好改了一些)走一遍:
1: When? 闭包出现的时刻?


function foo() {
    var a = 2;
    function baz() { 
        console.log( a );
    }
    return baz;
}
var fn = foo()
fn();//2

图片描述

在断点的过程中,运行完第9行代码的时候,调试窗口里并没有出现任何闭包;直到我运行了第10行代码,跳到第5行的时候(也就是baz这个方法被调用的时候),调试窗口出现了闭包(Closure)。并且可以看到说foo是closure, 它包含一个变量a,值为2。

结论1:虽然很多书上说闭包跟函数定义的时候的作用域有关,跟它执行时候的作用域无关,但是它在浏览器里面出现的时机却是在执行的时候。

2 How? 闭包出现的条件?

function foo() {
    var a = 2;
    function baz(m) { 
        console.log(m);
    }
    return baz;
}
var fn = foo()
fn(20); //20

图片描述

很多地方都说在方法里面定义方法就会形成闭包,但是在这里例子里面,我执行完第10行的代码,调试窗口并没有出现任何闭包。和例1的差别在于,baz没有引用变量a.

结论2:一个方法,一定要引用其父层方法(非自己方法内部的变量)的变量才会形成闭包。

但是闭包的形成是不是一定要执行到引用了父层作用域变量的方法才出现呢?看下面一个例子:

3: 当父层方法里有不只一个方法

function foo() {
    var a = 10;
    var b = 30;

    function fn1() {
        return a;
    }

    function fn2() {
        return 20;
    }

    return fn2;
}

var fn = foo();
fn();



在调试的过程中,

当我执行到17行然后跳进其方法体(第10行)的时候,调试窗口出现了Closure, 并且它有一个变量a,
值为10。其实这个例子说明了两个事情:

结论3: 在执行一个定义在方法里面的方法时,即使它的方法体自己没有引用父层变量,但是只要有任何兄弟方法引用了,那就会形成闭包。闭包里面保存的变量只有被方法引用了的变量(这个例子里,闭包里只有a,并没有b)。

4: 当不是兄弟方法,而是子方法引用了父级变量,会发生什么情况呢?

function foo() {
    var a = 10;
    var b = 30;

    function fn1() {
        var c = 20;
        function fn2(){
            return a;
        }
        return fn2;
    }

    return fn1;
}

var fn1 = foo();
var fn2 = fn1();
fn2();

图片描述

fn1方法体内没有引用任何的父层变量,但是它的子方法fn2引用了变量a。在调试的过程中,当执行到f1的时候(执行到第6行,这时候可以看到变量c都还是'undefined'),Closure就已经出现了,并不像之前那样一定要等到执行fn2.最后看一个例子:

5: 当不是直系子方法,而是侄子方法引用了父级变量

function foo() {
    var a = 10;
    var b = 30;

    function fn1() {
        var c = 20;
        function fn2(){
            return a;
        }
        return fn2;
    }
    function fn3(){
        return 40;
    }

    return fn3;
}
    
var fn3 = foo();
fn3();
    

图片描述

当执行到调用fn3的时候,Chosure出现了。所以闭包到底是在什么时候形成呢?在我们前面的结论里面有提到说方法执行的时候,但是经过后面的这几个例子说明并不一定非得是执行的时候,而且JavaScript是基于词法作用域的语言(变量的作用域在其定义的时候就已经确定了,不依赖于执行时的环境),所以我倾向于闭包(Closure)是在方法定义的时候就形成了的。那最后来说说What的问题:闭包到底是什么?
1: 闭包是一个作用域。(鉴于在Chrome的调试窗口,Closure是放在Scope下面的)
2: 那闭包这个作用域是个什么范围:被后代方法(子方法,孙子方法。。。)所引用的变量所在的作用域(也就是这个变量的作用域。如我们上面的例子里面,所有的Closure的作用域都是函数foo())。
3: 闭包里面有什么:只包含被后代方法所引用了的变量,并不包含这个变量的兄弟姐妹。
4: 上面的所有例子都只引用了变量,换成方法,也都是同样的结果。
真的是最后一个例子了:

图片描述

在这个例子里,因为fn3()引用了其父函数的变量a,所以Closure里面有了一个变量a;又因为fn1()引用了fn3,所以Closure里面有了变量fn3。


nanaistaken
586 声望43 粉丝