(JavaScript)变量作用域

嵌套函数

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

词法环境

一.变量的环境
  1. 每个运行的函数、代码块、整个脚本都有一个被称为词法环境的内部的关联对象。

  2. 词法环境包含:
    ①环境记录:一个存储所有 局部变量 作为其 属性 (包括一些其他信息,例如 this 的值)的 对象
    ②对 外部词法环境 的引用,与外部代码相关联。

  3. 全局词法环境
    与整个脚本相关联

//①环境记录:phrase: "Hello"
//②外部语法环境:null(全局词法环境没有外部引用)
let phrase = "Hello";
alert(phrase);
  1. 变量在词法环境中的定义过程
    在这里插入图片描述
    1.phrase:< uninitialized >
    当脚本开始运行,词法环境预先填充了所有声明的变量。
    最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let 声明前,不能引用它。几乎就像变量不存在一样。
    2.然后 let phrase 定义出现了。它尚未被赋值,因此它的值为 undefined。从这一刻起,我们就可以使用变量了。
    3.phrase 被赋予了一个值。
    4.phrase 的值被修改。
二、 函数声明

当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
在这里插入图片描述
正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)…。

三.内部和外部的词法环境在这里插入图片描述

say函数词法环境:包含属性name:"John"
全局词法环境:包含say: function
phrase:"Hello"

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

四.返回函数
//每次makeCounter()调用的开始,都会创建一个新的词法环境对象
//包含makeCounter词法环境和全局词法环境
function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}
//makeCounter返回一个函数,仅仅是返回,并没有运行
//返回该函数的同时创建该函数的词法环境(所有的函数在“诞生”时都会记住创建它们的词法环境)
//包括环境记录和外部词法环境的引用
//因为外部词法环境(makeCounter)有该函数的引用,根据JavaScript垃圾回收机制,外部词法环境(makeCounter)不会消失,也就是该函数的引用指针不会指向null,外部 let count = 0 会被保存下来
//let counter1 = function(){
//	return count++;
//};
let counter1 = makeCounter();
//运行的时候counter1的时候,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于之前保存的外部词法环境。在当前环境中找不到count变量,根据外部词法环境的引用可以找到 count = 0
counter1();

在变量所在的词法环境中更新变量

function makeCounter() {
    let count = 0;
    return function () {
        return count++;
    };
}
let counter = makeCounter();
//每次调用的时候,会为该调用创建一个新的词法变量,但是这些新的词法变量的外部环境变量都指向的都是同一个makeCounter,所以呈现递增
alert(counter());    //0
alert(counter());    //1
alert(counter());    //2
alert(counter());    //3

//counter1是通过makeCounter的又一次调用创建的
//和counter不属于同一个
let counter1 = makeCounter();
alert(counter1());    //0
alert(counter1());    //1
alert(counter1());    //2
alert(counter1());    //3

垃圾收集

如果词法环境没有任何引用,那么该环境将会被删除,词法环境仅在可达时才会被保留在内存中

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // 当 g 函数存在时,该值会被保留在内存中

g = null; // ……现在内存被清理了

实际开发中的优化:

但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。

在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。

function f() {
  let value = Math.random();

  function g() {
    //在函数g中并没有使用外部的变量
    //JavaScript引擎试图优化,将外部环境f删除
    debugger; // 在 Console 中:输入 alert(value); No such variable!
  }

  return g;
}

let g = f();
g();
let value = "Surprise!";

function f() {
  let value = "the closest value";

  function g() {
    debugger; // 在 console 中:输入 alert(value); Surprise!
  }

  return g;
}

let g = f();
//首先在g环境中寻找,没有找到
//接着去f环境中找,f环境被删了
//最后去全局环境中找,找到了
g();

=====================================

练习

一、

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // 会显示Pete

//解答:
//sayHi的外部词法环境是全局环境
//全局环境中保存了name = "John"
//在sayHi()函数调用之前,全局环境中保存的name值被换成了Pete
//所以调用sayHi(),首先在sayHi环境中找,没有找到,之后去全局环境中找,这时name已经被换了,所以是Pete

二、

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();   //执行结果是什么
//答案:error。
//函数 sayHi 是在 if 内声明的,所以它只存在于 if 中。外部是没有 sayHi 的。

三、

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

//执行结果是error
//程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let 语句。
//换句话说,一个变量从技术的角度来讲是存在的,但是在 let 之前还不能使用。
//变量暂时无法使用的区域(从代码块的开始到 let)有时被称为“死区”。

四、

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // 创建一个 shooter 函数,
      alert( i ); // 应该显示其编号
    };
    shooters.push(shooter); // 将此 shooter 函数添加到数组中
    i++;
  }

  // ……返回 shooters 数组
  return shooters;
}

let army = makeArmy();

// ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3...
army[0](); // 编号为 0 的 shooter 显示的是 10
army[1](); // 编号为 1 的 shooter 显示的是 10
army[2](); // 10,其他的也是这样。

让我们检查一下 makeArmy 内部到底发生了什么,那样答案就显而易见了。

1.它创建了一个空数组 shooters:

let shooters = [];

2.在循环中,通过 shooters.push(function) 用函数填充它。

每个元素都是函数,所以数组看起来是这样的:

shooters = [
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); },
  function () { alert(i); }
];

4.调用这个函数的时候,i的值已经变为10,alert(i)时去本环境中找到i为10,所以输出结果相同

修改:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
      let j = i;
      let shooter = function() { // shooter 函数
        alert( j ); // 应该显示它自己的编号
      };
    shooters.push(shooter);
    i++;
  }

  return shooters;
}

let army = makeArmy();

// 现在代码正确运行了
army[0](); // 0
army[5](); // 5

调用函数的时候,去本环境中去找j,由于是let,每次的j都不一样,所以代码正确运行了

如果我们一开始使用for循环,也可以避免这样的问题

function makeArmy() {

  let shooters = [];

  for(let i = 0; i < 10; i++) {
    let shooter = function() { // shooter 函数
      alert( i ); // 应该显示它自己的编号
    };
    shooters.push(shooter);
  }

  return shooters;
}

let army = makeArmy();

army[0](); // 0
army[5](); // 5

参考自:
变量作用域,闭包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值