Skip to content
TOC

下面代码打印什么?

js
function bar() {
  console.log(myName);
}
function foo() {
  var myName = ' 极客邦 ';
  bar();
}
var myName = ' 极客时间 ';
foo();

答案是:极客时间。而不是极客帮。

作用域链

从作用域链的角度出发,bar 里面没有 myName 所以会往上找,但是->

为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域。和函数是怎么调用的没有关系。

那么,根据作用域链 + 词法作用域 下面的代码输出什么?

js
function bar() {
  var myName = ' 极客世界 ';
  let test1 = 100;
  if (1) {
    let myName = 'Chrome 浏览器 ';
    console.log(test);
  }
}
function foo() {
  var myName = ' 极客邦 ';
  let test = 2;
  {
    let test = 3;
    bar();
  }
}
var myName = ' 极客时间 ';
let myAge = 10;
let test = 1;
foo(); // 答案是:1

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包

你也可以通过“开发者工具”来看看闭包的情况,打开 Chrome 的“开发者工具”,在 bar 函数任意地方打上断点,然后刷新页面,可以看到如下内容:

从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。

闭包的垃圾回收

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

所以,如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

思考题

下面代码打印什么?

js
var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(myName);
  },
};
function foo() {
  let myName = ' 极客时间 ';
  return bar.printName;
}
let myName = ' 极客邦 ';
let _printName = foo();
_printName(); // ?
bar.printName(); // 极客邦

我知道 bar.printName(); 打印的是极客邦,以为_printName();打印的是极客时间,然而结果还是极客邦。

分析:

相信你已经知道了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是“极客邦”。这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。

不过按照常理来说,调用 bar.printName 方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来了 this。

  • bar 不是一个函数,因此 bar 当中的 printName 其实是一个全局声明的函数,bar 当中的 myName 只是对象的一个属性,也和 printName 没有联系,如果要产生联系,需要使用 this 关键字,表示这里的 myName 是对象的一个属性,不然的话,printName 会通过词法作用域链去到其声明的环境,也就是全局,去找 myName

  • foo 函数返回的 printName 是全局声明的函数,因此和 foo 当中定义的变量也没有任何联系,这个时候 foo 函数返回 printName 并不会产生闭包

如果改成下面的方式:

js
var bar = function () {
  let printName = function () {
    console.log(myName);
  };
  return printName;
};

function foo() {
  let myName = ' 极客时间 ';
  return bar;
}

let myName = ' 极客邦 ';
let _printName = foo();

_printName()();
bar()();

答案还都是极客邦。

总结

js 中同名变量如何取值?

首先确定词法环境,是取得局部变量还是全局变量。(词法环境)

范围搞清楚了之后,再到此范围确定变量提升的问题。(作用域链)

往外找是因为你知道作用域链,往哪个外面找是因为词法环境。

Released under the CC BY-NC-ND 3.0