关于JavaScript的作用域,你真的掌握了吗?

510次阅读
没有评论

关于JavaScript的作用域,你真的掌握了吗?
我们先看一段代码

var a = 10;
function foo() {
 console.log(a);
}
function bar() {
 var a = 20;
 foo();
}
bar();

如果你认为这段代码最后会打印20,那么说明你还未掌握javascript的作用域知识,需要加深学习一下。
我们知道,变量的作用域(scope)是程序源代码中一个区域,在这个区域内变量有定。在ES6之前的JavaScript中,声明变量的唯一方式是使用var关键字,无法声明常量。关于作用域,有些人说 JavaScript 有 2 个作用域,也有人说 3 个,还有人说是 4 个! 其实,JavaScript有两个主要作用域(Global、Local),而 Local 作用域由另外 2 个作用域组成(Function、Block)
那么作用域有什么用?我们先看一下下面的代码

//代码1
let i = 0;
for (i = 0; i <= 10; i++) {
  console.log(i);
}
console.log(i); 
//代码2
for (let i = 0; i <= 10; i++) {
 console.log(i); 
}
console.log(i); 

在代码1中,i最后的值为11,但是在代码2中,i最后会报not defined错误而不是1,如果我们把代码改为下面这样,

//代码3
for (var i = 0; i <= 10; i++) {
  console.log(i);
}
console.log(i); 

此时就不会报错了
通过对比,我认为作用域主要有2个作用

  • 出于内存原因,为方便更好的利用内存,javascript也是有垃圾回收机制(GC)的,当我们用let创建一个变量,GC就会自动在变量用完时及时删除它
  • 为了代码安全性,什么可以在当前范围内访问,什么不能访问,在整个应用程序中每个变量都可随时访问是很糟糕的

    全局作用域

    您需要知道的是,函数外部的变量是在全局范围内声明的,只需记住一件事。如果您使用“var”在全局范围内声明变量,它将被分配给 window 对象,但如果您使用“let”声明它,则不会分配给它。

    
    var a = 5;
    let b = 2; // Global variable
    function doSomething() {
    //Local Scope
    return a + b; //Global variable can be accessed anywhere
    }

console.log(window.a); // 5
console.log(window.b); // undefined


# 局部作用域(**Local Scopes)**
局部作用域有两种:

## Function Scope
函数作用域:每次创建函数时,都会为其创建一个新作用域,该函数中声明的所有变量都可以被其中的所有代码访问,并且一旦结束,它们就不再可访问

## Block Scope
块作用域Block Scope是在ECMAScript6引入的,ECMAScript6 中引入了 let 和 const,现在我们可以在循环中声明变量,**并且仅使用 let 或 const 进行 if 语句,并且在块结束后,使用 GC 将变量从内存中清除**。


# 词法环境(Lexical environment)
词法环境是指代码{}的一个隔离区域,定义为代码中的**新本地环境**,当 JavaScript 引擎为函数或块创建新的执行上下文时,**它会创建一个词法环境来存储在执行阶段定义的变量**
根据 ECMAScript 规范 262 (8.1): > _A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code
词法环境是一种规范类型,用于基于 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联_ 要搞懂词法环境,我们先要了解一下**作用域链** ## **作用域链**(Scope Chain) 当我们有一组嵌套的函数并且每个函数都有自己的环境时,如果我们将执行以下代码: ```javascript let a = 7; let b = 10; const multiplier = () => { let a = 5; return a * b; }; multiplier(); // 50

JS引擎将首先在本地作用域中搜索变量a和b,它将在其中找到变量“a”,但变量“b”不存在,因此将在父作用域中搜索它,并从那里使用它。
这称为作用域链,它有助于解析变量
关于JavaScript的作用域,你真的掌握了吗?
对照上图再看一下下面的代码

// 在全局作用域中定义fullName:
const fullName = "Oluwatobi Sofela";

// 嵌套函数包含2个fullName变量
function profile() {
  const fullName = "Tobi Sho";
  function sayName() {
    const fullName = "Oluwa Sofe";
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

console.log(profile())

这段代码将打印“Oluwa Sofe”,因为在作用域链中,fullName在sayName的词法环境中被找到。

词法环境如何工作

当 JavaScript 引擎为函数或块创建新的执行上下文时,它会创建一个词法环境来存储在执行阶段定义的变量
关于JavaScript的作用域,你真的掌握了吗?
让我们看看当我们的代码执行时会发生什么,如上图所示:
创建全局执行上下文并将其放置在执行堆栈的底部。
当调用 bar 时,将创建一个新的 bar 执行上下文并将其置于全局执行上下文之上。
当 bar 调用嵌套函数 foo 时,会创建一个新的 foo 执行上下文,并将其放置在 bar 执行上下文之上。
当 foo 返回时,它的上下文被从堆栈中弹出,并且 flow 返回到 bar 上下文。
一旦 bar 执行完成,流程将返回到全局上下文,最后,堆栈被清空。
执行堆栈以 LIFO 数据结构方式工作。它等待最顶层的执行上下文返回,然后再执行下面的上下文
每个执行上下文有两个阶段

  • 创建阶段:首先我们将变量和参数以及函数声明存储在 词法环境 组件中作为初始存储“由于作用域提升,var 声明的变量被初始化为未定义的值”。而此时的this指向这个环境
  • 执行阶段:分配值并使用词法环境, 解析绑定变量和函数

我们回到文章开头的那段代码

var a = 10;
function foo() {
 console.log(a);
}
function bar() {
 var a = 20;
 foo();
}
bar(); 

代码最终会打印10,为什么呢?这是因为在执行foo函数时它已经捕获了全局的“a = 10”
关于JavaScript的作用域,你真的掌握了吗?

总结

  • 使用var声明的变量不具有块作用域。这种变量的作用域仅限 于包含函数的函数体,无论它们在函数中嵌套的层次有多深。
  • 如果在函数体外部使用var,则会声明一个全局变量。
  • 与通过let声明的变量不同,使用var多次声明同名变量是合 法的。而且由于var变量具有函数作用域而不是块作用域,这种重新声明实际上是很常见的。
  • var声明的一个最不同寻常的特性是作用域提升。在使用var声明变量时,该声明会被提高(或提升)到包含函数译注1的顶部。但变量的初始化仍然在代码所在位置完 成,只有变量的定义转移到了函数顶部。
  • 全局作用域是 JavaScript 作用域链的最后一个作用域,即全局作用域是查找的终点
正文完
 

公众号