JavaScript之作用域和闭包的理解

JavaScript 使用变量的时候必须要声明,一般在使用前声明,也可以在使用后声明。 JavaScript 程序运行的时候,会自动把变量的声明提升到最前面去,所有变量的声明可以在使用后声明。但是不会把赋值提升上去,比如使用后声明的 a=1

目录
  1. 声明提升
  2. 作用域
  3. 执行上下文
  4. 作用域链
  5. 闭包(closure)
  6. 闭包实例
  7. 总结

声明提升#

JavaScript 使用变量的时候必须要声明,一般在使用前声明,也可以在使用后声明。

console.log(a); // undefined
var a = 1;
var b = 2;
console.log(b); // 2
 
f1(); // 10;
// 函数声明
function f1() {
  console.log(10);
}
f2(); // 报错
// 函数表达式
var f2 = function () {
  console.log(20);
};

JavaScript 程序运行的时候,会自动把变量的声明提升到最前面去,所有变量的声明可以在使用后声明。但是不会把赋值提升上去,比如使用后声明的a=1,在使用时不会获取a的值是1,而是undefined,这样没什么意义。建议使用变量还是提前声明比较好。

函数的声明也会提升到最前面,但是函数表达式不会提升,所以后面的 f2 函数调用报错。函数使用后声明有利也有弊,比如在复杂的使用场景中,可以使用函数后再声明,这样也不会报错,但是也可能会忘记声明函数。函数声明和函数表达式有时候是等价的,有时候只能使用表达式,看具体情况使用。

作用域#

JavaScript 的作用域可以分为全局作用域和局部作用域(函数作用域),在全局作用域的变量就是全局变量,而在函数作用域的变量就是局部变量。

var a = 1; // 全局变量
var b = 2; // 全局变量
console.log(a); // 1
 
function f() {
  var b = 20; // 局部变量
  var c = 30; // 局部变量
 
  console.log(a, b); // 1 20
  console.log(c); // 30
}
f();
 
console.log(b); // 还是2
console.log(c); // 报错

全局变量可以被函数所引用,但是局部变量只能在函数内部使用,无法在全局中使用。因为全局变量所在的全局作用域,可以在全局任何地方调用。而局部变量所在的局部作用域,只能在局部作用域中使用。

什么是作用域? 作用域为可访问变量,对象,函数的集合

全局变量在局部作用域中重新被赋值后,就变成了局部变量,所赋的值只在所在的局部作用域中使用,在全局作用域中是无效的。在不同的局部作用域内,变量的值可能不一样。

执行上下文#

在函数中我们经常用的this,在不同的地方this的值也是不一样的,需要根据函数的执行上下文来决定。

function Foo(name) {
  this.name = name;
  console.log(this); // {name: 'hale'}
}
var f = new Foo("hale");
 
var obj = {
  name: "hale",
  sayName: function () {
    console.log(this);
  },
};
obj.sayName(); // {name:'hale', sayName:f}
 
function fn() {
  console.log(this);
}
fn(); // window

this的指向一般是下面的三种情况

  • 对于构造函数,this则指向新创建的对象。
  • 对于对象,this指向调向该对象。
  • 对于普通函数,this始终指向全局对象window(在 nodejs 中是global)。

作用域链#

作用域之间是怎么相关联的呢?是通过作用域链来联系的。

var a = 1;
function f1() {
  var b = 2;
  function f2() {
    var c = 3;
    console.log(a);
    console.log(b);
    console.log(c);
  }
  f2();
}
f1(); // 1 2 3

作用域链的运行过程:

  • 调用 f1 函数后,会运行其中的 f2 函数。
  • 需要打印 a,但是 a 不在 f2 函数作用域中,执行程序会往上一级作用域-f1 函数作用域中,也就是创建了 f2 作用域的作用域中去寻找。但是发现也不在 f1 函数作用域中,继续往上一级的作用域-全局作用域中,a 在全局作用域中找到了,打印 a。
  • 需要打印 b,但是 b 不在 f2 函数作用域中,执行程序会往上一级作用域中找,即在 f1 函数作用域中寻找,b 在 f1 函数作用域中找到了,打印 b。
  • 需要打印 c,c 就在 f2 函数作用域中,一下就找到了,打印 c。

在 f2 函数作用域中,a、b 是自由变量。在 f1 函数作用域中,a 是自由变量。打印 a 的过程中,从 f2 作用域 -> f1 作用域 -> 全局作用域中,一级一级往上寻找,就形成了一条“作用域链”。

闭包(closure)#

前面学习了作用域和作用链,知道了局部作用域可以访问全局作用域的变量,但是反过来就不行,在全局作用域中无法访问局部变量。其实,可以通过闭包来访问。

function fn() {
  var a = 1;
  return function () {
    console.log(a);
  };
}
 
var f = fn();
f(); // 1

从上面我们已经知道了局部作用域的变量是只有在作用域内部使用,外面是无法使用的。但是上面的代码显示在全局作用域中,还是可以访问到局部作用域的变量的。

变量a是函数 fn 内部的变量,它的值是 1 。而变量f是一个全局变量,它被赋予了fn函数调用的值,即 fn 里面的匿名函数。然后再调用f,输出了a的值 1,这相当于我们访问到了局部变量a

其实 fn 函数就是一个最简单的闭包

这个例子比较简单,我们再看下面的例子:

function fn() {
  var a = 0;
  a++;
  console.log(a);
}
 
fn(); // 1
fn(); // 1
fn(); // 1

我们发现调用函数 fn 3 次后,输出的值都是 1,那是因为a是局部变量,根据 js 的垃圾回收机制,函数执行之后会把局部变量的值销毁掉。如果变量a拿到外面,变成全局变量的话,a的值会一直存在。

var a = 0;
function fn() {
  a++;
  console.log(a);
}
 
fn(); // 1
fn(); // 2
fn(); // 3

a变成全局变量后,因为全局变量是一直存在,所以调用函数fn后,输出的值是累加的,也就是变量a的值在累加。

那有没有一种方法,使得a是局部变量,但是a可以和全局变量一样,一直存在着呢?而不会再函数调用后就销毁。答案是肯定的,使用闭包。

function fn() {
  var a = 0;
  return function () {
    a++;
    console.log(a);
  };
}
 
var f = fn();
 
f(); // 1
f(); // 2
f(); // 3

使用闭包后,连续调用函数f后,输出的值是累加的,说明局部变量a没有被销毁,而是一直存在着,它的值在累加。当执行var f = fn()后,f 是可以访问到 fn()被调用时产生的环境的,而局部变量a一直存在这个环境里。既然局部变量所在的环境可以被外界访问到,那么这个局部变量就不会被垃圾回收机制回收,也就是不会被销毁了,它相当于是一个全局变量了。

从上面我们可以看出闭包有几个重要特征:

  1. 返回函数
  2. 可以访问到局部变量
  3. 局部变量一直存在着

什么是闭包(closure)呢?貌似现在还没有一个明确的定义。个人理解是:闭包指可以读取其他函数作用域变量的函数,并且一直保存里面局部变量的值。

闭包实例#

// 查询是否是新用户
function isNew() {
  var list = new Array();
  return function (id) {
    if (list.indexOf(id) > -1) {
      return false;
    } else {
      list.push(id);
      return true;
    }
  };
}
 
var isNewId = isNew();
console.log(isNewId(10)); // true
console.log(isNewId(10)); // false
console.log(isNewId(20)); // true
console.log(isNewId(20)); // false

上面代码中,全局变量isNewId引用了isNew(),局部变量list没有被回收,list里面的值保存在内存里。

总结#

闭包有两个非常重要的特性:

  • 访问其他的函数内部的变量值。
  • 闭包里面的变量会始终保存着。

闭包的两个特性有非常好的应用。比如:封装一些不需要暴露的全局变量,还可以把局部变量的值保存起来长期使用等等。但是,也不能滥用闭包,因为闭包的值会保存在内存中,会消耗内存,造成网页性能问题。