前端进阶(第二期)- 作用域闭包笔记

577 阅读8分钟

原文地址:

2-1 从作用域链谈闭包

2-2 JavaScript深入之闭包

2-3 深入javascript——作用域和闭包

JavaScript Closures Explained by Mailing a Package

2-1 从作用域链谈闭包

原文地址

闭包的概念

闭包是指有权访问另外一个函数作用域中的变量的函数 --- 红宝书

关键于两点:

  • 是一个函数
  • 能访问另外一个函数的作用域中的变量

晦涩难懂有木有? 个人感觉文中的例子描述的非常好,特此摘录:

function getName() {
  var name = "美女的名字";
  console.log(name);     //"美女的名字"
}
function displayName() {
    console.log(name);  //报错
}

外部访问不到函数作用域中的变量。但是为了得到美女的名字,不死心的单身汪把代码作了一些修改便得逞了,如下:

function getName() {
  var name = "美女的名字";
  function displayName() {
    console.log(name);   
  }
  return displayName;
}
var 美女 = getName();  
美女()  //"美女的名字"

这时的‘美女’是一个闭包了,单身汪想怎么玩就怎么玩了(邪恶脸--)。

  • 闭包可以访问当前函数以外的变量
  • 即使外部函数已经返回,闭包仍能访问外部函数定义的变量
  • 闭包可以更换外部变量的值

作用域链

                            ---为什么闭包就能访问外部函数的变量呢?

首先回顾下上期学习的 --- 执行上下文

Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

作用域链:当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。

作用域链原型继承 查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError

那些图例还是没怎么搞懂,我需要回去再翻翻我的红宝书!

闭包的作用需要在看一下这一篇文章

2-2 JavaScript深入之闭包

原文地址

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

什么是自由变量?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

ECMAScript中,闭包指的是:

从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

从实践角度:以下函数才算是闭包: 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 在代码中引用了自由变量

举个例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

了解具体的执行过程, 我们知道 f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

2-3 深入javascript——作用域和闭包

原文地址

作用域

  1. 变量声明提前
  2. 没有块级作用域

作用域链

作用域链的逐级查找,也会影响到程序的性能,变量作用域链越长对性能影响越大,这也是我们尽量避免使用全局变量的一个主要原因。

闭包

  • 基础概念(略)
  • 闭包中的变量

在使用闭包时,由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。

例子1:

//该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题
function timeManage() {
   for (var i = 0; i < 5; i++) {
       setTimeout(function() {
           console.log(i);
       },1000)
   };
}
//调用timeManage输出都是5

例子2:

function createClosure(){
   var result = [];
   for (var i = 0; i < 5; i++) {
       result[i] = function(){
           return i;
       }
   }
   return result;
}
//调用timeManage输出仍都是5

以上两个例子可以看出闭包在带有循环的内部函数使用时存在的问题:因为每个函数的作用域链中都保存着对外部函数(timeManage、createClosure)的活跃对象,因此,他们都引用着同一变量i,当外部函数返回时,此时的i值为5,所以内部的每个函数i的值也为5。

可以通过 匿名包裹器 (匿名自执行函数表达式)来强制返回预期的结果:

function timeManage() {
   for (var i = 0; i < 5; i++) {
       (function(num) {
           setTimeout(function() {
               console.log(num);
           }, 1000);
       })(i);
   }
}

或者 在闭包匿名函数中再返回一个匿名函数赋值 :

function timeManage() {
    for (var i = 0; i < 10; i++) {
        setTimeout((function(e) {
            return function() {
                console.log(e);
            }
        })(i), 1000)
    }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = function(num) {
            return function() {
                console.log(num);
            }
        }(i);
    }
    return result;
}
//1, 2, 3, 4, 5

无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

  • 闭包中的 this

由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        return function(){
            return this.scope;
        }
    }
}

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建thisarguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。

解决办法 :

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        var that = this;
        return function(){
            return that.scope;
        }
    }
}

-内存与性能

由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:

 function bindEvent(){
    var target = document.getElementById("elem");
    target.onclick = function(){
        console.log(target.name);
    }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:

  function bindEvent(){
    var target = document.getElementById("elem");
    var name = target.name;
    target.onclick = function(){
        console.log(name);
    }
    target = null;
 }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。