乐趣区

作用域和闭包深入理解

作用域和闭包

作用域

高程 92 页

执行环境 是 JavaScript 中最为重要的一个概 念。

每个 执行环境 都有一个 与之关联的 变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

每个函数 都有自己的 执行环境

当代码在一个函数中执行时,会创建 变量对象 的一个 作用域链(scope chain)。

将函数的 活动对象 (activation object)作为 变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。

作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

Javascript 权威指南

函数的变量的作用域是在函数定义时确定的。

作用域在函数定义时确定很重要
这里一共有 执行环境 , 变量对象 , 函数 , 活动对象 几个专有名词

只提到函数情况 , 就不考虑全局环境了 , 一句话 :

每个函数有自己的执行环境 , 函数的活动对象作为执行环境的变量对象

作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

下面是举例

var color = "blue"; 
 
function changeColor(){     
    var anotherColor = "red"; 
    function swapColors(){    
        var tempColor = anotherColor;   
        anotherColor = color;  
        color = tempColor;
        debugger;
        // 这里可以访问 color、anotherColor 和 tempColor     
    } 
 
        // 这里可以访问 color 和 anotherColor,但不能访问 tempColor             
    swapColors();} 
 
// 这里只能访问 color changeColor();

可以到浏览器控制台运行一下 .

我们看看 debugger 时 的 scoped

我们再加一层看看

var color = "blue"; 
 
function changeColor(){     
    var anotherColor = "red"; 
    function swapColors(){    
        var tempColor = anotherColor;   
        anotherColor = color;  
        color = tempColor;
        function consoleColors(){console.log(anotherColor);
            console.log(tempColor);
            debugger;
        }
        // 这里可以访问 color、anotherColor 和 tempColor
        consoleColors();} 
 
        // 这里可以访问 color 和 anotherColor,但不能访问 tempColor             
    swapColors();} 
 
// 这里只能访问 color changeColor();

我们看到它保存了两个作用域链上父级和父级的父级 (下面称为父父级) 的变量(下文称闭包变量) , 并且是引用的变量才保存 , 也就是如果没有引用父级的变量 , 但是引用了父父级的变量 , 那么不会生成父级的变量对象而是生成父父级的变量对象 .

注意我上面说的是引用而不是保存 , 也就是说它并非是静态传递的 , 下面会提到 .

闭包(Closure)

高程 196 页

闭包是指有权访问另一个函数作用域中的变量的函数。

另一个指哪一个 ?

创建闭包的常见方式,就是在一个函数内部创建另一个函数

**
上面举的例子 , 就是在函数内部创建另一个函数 , 所以它生成了闭包 . 所以我们暂时把另一个当成上级就是了.

有权访问上级函数作用域中的变量的函数是闭包

我们甚至可以说一个函数 (在函数中定义 && 引用到了这个函数的变量) 它就是闭包 .

这是充分条件而非并必要条件 拓展会讲到特殊情况

Javascript 权威指南

函数的变量的作用域是在函数定义时确定的。

理解闭包的关键:作用域是定义时上下文确定 , 而非依赖调用时的上下文
为什么会生成闭包变量 (指作用域讲解中调试图的红框的 Closure)? 因为 函数的作用域是定义时就确定了, 但是函数并不知道自己将会在哪里调用 , 它可能在父函数中调用 , 它可能在父函数外被调用 , 所以 设计者就让它生成闭包变量来保存作用域链 , 保证它当在外部调用时 , 它就可以直接找到定义时状态的变量 . 当然无论是否是在外部调用的 , 他们都会生成闭包变量 , 他们都是闭包 .  

但是讨论不在父级作为返回值返回的闭包在父函数执行完毕后是没有意义的(刚刚在作用域链讲解示例就是这种情况) . 因为父级函数执行完毕之后 , 一般来说 , 父级的作用域链就被销毁了 ,  外界访问不到闭包 , 自然也会被回收 .

但是返回值为函数时会有不同

在闭包从父级中被返回后,它的作用域链被初始化为包含父级函数的活动对象和全局变量对象

因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当父级函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,父级的活动对象才会被销毁.

js 引擎知道返回了一个闭包 , 它就不会销毁闭包中引用到的父级变量 . 
所以闭包一般指下面的情况  将函数作为返回值给外界接收使用的情况

function createComparisonFunction(propertyName) {return function(object1, object2){var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if (value1 < value2){return -1;} 
            else if (value1 > value2){return 1;} 
            else {return 0;}
        };
    }

它引用了父级的变量(参数也是变量) , 并作为父级的返回值返回给外层接收 .

闭包中变量的问题

刚刚说到闭包只是引用变量 , 而非按值传递

即闭包只能取得包含函数中任何变量的 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量

比如以下函数

function createFunctions(){var result = new Array(); 
 
    for (var i=0; i < 10; i++){result[i] = function(){return i;};
    } 
     return result;
 }

它返回了一个闭包数组 , 这个函数原意认为 , 每个闭包都应该保存 i 应该是从 0 -9 ; 但是实际上他们都只保存了 10 ; 因为他们引用的是同一个 i ;

拓展: 深刻理解 es6 函数默认值作用域和其中生成中的闭包

阮一峰 es6 入门: 函数的拓展 – 默认值的作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

Javascript 权威指南

函数的变量的作用域是在函数定义时确定的。

函数的变量的作用域是在函数定义时确定的 , 很重要 . 

var x = 1;
function foo(x, y = function() {x = 2;}) {
  var x = 3;
  y();
  console.log(x);
}

这个函数会打印出什么 ? 按照常识我们会认为它打印出了 2 ; 按常识 , 在这个上下文中 ,  作用域链会往上找 , x 应该指向的是var x = 3 的 x ; 被  y()  后改为了 2 ;

但是它打印出了 3 而非 2;

奥秘就在开头 , 如果设置默认值时 , 就会生成一个单独的作用域 就好像是这样子

function(){
    let x;
    let y = function(){x = 2;}
}

它类似于一个函数 (下面称作用域伪函数), 生成了一个作用域 , 它与 foo 函数是平行的 , 而非包含关系

所以 y 是 这个作用域中的闭包 , 我们可以打上 debugger 来看看

var x = 1;
function foo(x, y = function() {x = 2;debugger;}) {
  var x = 3;
  y();
  console.log(x);
}

注意 x = 2 ; y()把父级作用域中的 x 由 undefined 变为了 2 ;

同时 y 也非常特殊 , 它并非是被 return 而保存下来的 , 它是编译器内部把 y 的地址赋给了参数(如果你没有输入)

为什么他们是平行的 以下两个例子说明

// 仍然是上面的函数
y(5);
// x 已经不取默认值

//5
//Closure(foo)
    x:2

我们传入 x 的参数 , 很显然编辑器并不考虑有没有传参数 , 传了几个参数 , 只要你设置了其中一个默认值 它就会在执行函数前一步执行作用域伪函数: 声明所有参数变量 有默认值则赋值 无默认值则是 undefined ;

如果你传入了某个参数 , 它就抛弃这个值 ; 如果你没有传入某个参数 , 它就把这个值赋给这个参数 ;

函数是引用变量 , 所以这也是没有 return 却可以使用一个闭包的原因

下面是第二个例子

var x = 1;
function foo(y = function() {x = 2;}) {
  var x = 3;
  y();
  console.log(x);
}
console.log(x);

这次我们没有定义 x 了 .

会发生什么?

A. y()时寻找到 foo()内部的 x 变量并赋值

B.y()时寻找到外部的 x 变量并赋值

C. 定义时报错

D.y()执行时报错

答案是

B
解析: 虽然在某个内部函数被调用但是它还是 沿定义时作用域链向上找变量 , 所以 没有 x 参数 , 它就找到了全局变量的 x . 

//3
//2

这个例子就是与 foo 函数是作用域平行的最好证明 .

退出移动版