JavaScript深入之作用域和作用域链与闭包

523 阅读4分钟

一、作用域

任何程序设计语言都有都有作用域的概念,简单的说,作用域就是变量和函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域分为全局作用域和局部作用域两种。

1.全局作用域 (Global Scope)

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说一下几种情形拥有全局作用域:

(1)最外层函数和在最外层函数外面定义的变量拥有全局作用域,比如:

var globalValue = `global value`;

function checkGlobal() {
    var localValue = `local value`;
    console.log(localValue);                // "local value"
    console.log(globalValue);               // "global value"
}

console.log(globalValue);                   // "global value"
console.log(checkGlobal);                   // "global value"
console.log(localValue);                    // "Uncaught ReferenceError: localValue is not defined"

(2)所有未定义直接赋值的变量自动声明为全局作用域,比如:

function checkGlobal() {
    var localValue = 'local value';
    globalValue = 'global value';
    console.log(localValue, globalValue);    // 'local value' 'globalValue'
}

console.log(globalValue);                    // 'globalValue'
console.log(localValue);                     // "Uncaught ReferenceError: localValue is not defined"

(3)所有window对象的属性

一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。

2.局部作用域(Local Scope)

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。

(1)函数作用域

就是指声明在函数内部的变量,

(2)块级作用域

块级作用域可通过letconst声明,声明后的变量再指定块级作用域外无法被访问。

3.词法作用域和动态作用域

(1)词法作用域

词法作用域也是静态作用域,在JavaScript中采用的就是词法作用域。词法作用域就是定义在词法阶段的作用域。就是一段代码,在它执行之前就已经确定了它的作用域,简单来说就是在执行之前就确定了它可以应用哪些地方的作用域(变量)。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。因此当词法分析器处理代码时会保持作用域不变(大部分情况是这样) 比如:

var a = 1;

function foo() {
    console.log(a);
}

function bar() {
    var a = 2;
    foo();
}

bar();      //  1
(2)动态作用域

动态作用域即是与词法作用域相反的。

借用上述的例子可以知道:

它依然会像采用词法作用域的形式执行函数,唯一不一样的地方在于:在执行foo()函数时,他不会向外一层查找a,而是从调用的函数作用域中查找,所以最后的结果输出为2

二、作用域链

JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。

通俗的讲,就是当所需要的变量在所在的作用域中查找不到的时候,它会一层一层向上查找,直到找到全局作用域还没有找到的时候,就会放弃查找。这种一层一层的关系就是作用域链。

三、闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包。即使函数是在当前词法作用域之外执行。

function foo() {
    var a = 1;
    return function() {
        console.log(a);
    }
}

var bar = foo();
bar();

从上面的代码可以知道: foo()函数的执行结果返回给bar,而此时由于变量a还在使用,因而没有被销毁,然后执行bar()函数。这样,我们就能在外部作用域访问到函数内部作用域的变量。这个就是闭包。

闭包的形成条件:

  • 函数嵌套
  • 内部函数引用外部函数的局部变量

闭包的作用:

  • 可以读取函数内部的变量
  • 可以使变量的值长期保存在内存中,生命周期比较长。
  • 可用来实现JS模块(JQuery库等)

JS模块是具有特定功能的JS文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含多个方法的对象或函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。

闭包的特性:

  • 每个函数都是闭包,函数能够记住自己定义时所处的作用域,函数走到了哪,定义时的作用域就到了哪。
  • 内存泄漏

内存泄漏就是一个对象在你不需要它的时候仍然存在。所以不能滥用闭包。当我们使用完闭包后,应该将引用变量置为null

function outer(){
    var num = 0;
    return function add(){
        num++;
        console.log(num);
    };
 }
var func1 = outer();
func1();              // 1
func1();              // 2  [没有被释放,一直被占用]
var func2 = outer();
func2();              // 1  [重新引用函数时,闭包是新的]
func2();              // 2

面试题:面试官:说说作用域和闭包吧