JavaScript系列之做用域和做用域链

在上一篇《JavaScript系列之变量对象》中,咱们已经知道一个执行上下文的数据(函数的形参、函数及变量声明)做为属性储存在变量对象中。javascript

此外,咱们也知道每次进入上下文时都会建立变量对象并填充初始值,而且值会在代码执行阶段进行更新,如今就对执行上下文作更深一步的了解。java

先来回顾一下关于执行上下文的三个阶段生命周期:git

本章将专门介绍与执行上下文建立阶段直接相关的另外一个细节——做用域链github

做用域(Scope)

在介绍做用域链前,有必要先来了解一下被称为做用域(Scope)的特性,那什么是做用域呢?编程

做用域就是在运行时代码中不一样部分中函数和变量的可访问性。可能这句话并不太好理解,咱们先来看段代码:数组

function fn() {
    var inVariable = "inner variable";
    console.log(inVariable); // inner variable
}

fn(); 
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
复制代码

从上面的代码中咱们能够很直观地体会做用域的概念,变量inVariable在全局做用域没有声明,因此在全局做用域下直接取值会报错。因此咱们能够这样理解:做用域就像一个地头蛇,个人地盘我作主,让属于本身域内的变量不会轻易外泄出去。也就是说做用域最大的用处就是隔离变量,不一样做用域下同名变量不会有冲突。这几句话总比前面那句好理解多了吧。函数

关于JavaScript 中的做用域类型,ES6 以前 JavaScript 并无块级做用域,只有全局做用域和函数做用域。ES6的到来,为咱们提供了‘块级做用域’,可经过新增命令let和const来体现:ui

  • 全局做用域—变量能够随处访问
  • 函数做用域—变量能够在定义它们的函数的边界内访问
  • 块级做用域—变量能够在定义它们的块中访问,块由 { 和 } 分隔

全局做用域和函数做用域

const global = 'global scoped'

function fn() {
    const global = 'function scoped';
    console.log(global); // function scoped
}

fn();
console.log(global); // global scoped
复制代码

从上面例子能够看出全局做用域和函数做用域的做用范围,即便全局变量在函数内部分配了不一样的值,它也只保留在同一函数的边界内,互相并不影响,咱们也不会因使用相同的变量名而出错。再来看个例子加深理解:spa

const global = 'global scoped'
const anotherGlobal = 'also global scoped'

function fn() {
    const global = 'function scoped'
    console.log(global) // function scoped
    const scoped = 'also function scoped'

    function inner() {
        console.log(scoped); // also function scoped
        console.log(anotherGlobal) // also global scoped
    }

    inner();
}

console.log(global); // global scoped
console.log(anotherGlobal); // also global scoped

fn();
inner(); // Uncaught ReferenceError: inner is not defined
复制代码

在这里咱们能够看到 inner() 函数能够访问在其父函数中声明的变量—fn()。每当咱们须要函数内部的变量时,引擎将首先在当前函数做用域内查找它。若是它没有当前函数做用域内找到它,它将继续上升,向上一级查找,直到它找到全局做用域内的变量,若是找不到变量,咱们将获得一个ReferenceError。格外注意函数内层做用域能够访问外层做用域的变量,反之则不行调试

除了上面所讲的最外层函数外面定义的变量拥有全局做用域,全局做用域还有一种特殊的出现场合:就是全部末声明直接赋值的变量将自动声明为拥有全局做用域的变量

function fn() {
    variable = "undeclared variable";
    var inVariable = "inner variable";
}

fn();
console.log(variable); // undeclared variable
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
复制代码

全局做用域有个弊端:若是咱们写了不少行 JavaScript 代码,变量定义都没有用函数包括,那么它们就所有都在全局做用域中,这样就会污染全局命名空间,容易引发命名冲突。同时意外的全局变量还会引发内存泄漏,因此在编程时,尽可能避免全局变量的使用,以便后期更快地调试。

还有值得注意的是:块语句(大括号“{}”中间的语句),如 ifswitch 条件语句或 forwhile 循环语句,不像函数,它们不会建立一个新的做用域。在块语句中定义的变量将保留在它们已经存在的做用域中。好比:

if (true) {
    // 'if' 条件语句块不会建立一个新的做用域
    var name = 'miqilin'; // name 依然在全局做用域中
}

console.log(name); // miqilin
复制代码

JS 的初学者常常须要花点时间才能习惯变量提高,而若是不理解这种特有行为,就可能致使bug出现 。正由于如此, ES6 引入了块级做用域,让变量的生命周期更加可控。

块级做用域

在ES6中,咱们获得了两个新的变量声明关键字 - letconst。它们和var之间的主要区别在于,使用ES6关键字声明的变量是块做用域,这意味着它们仅在它们定义的代码块中可用。块级做用域在以下状况被建立:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上能够用 let 来代替 var 进行变量声明,但会将变量的做用域限制在当前代码块中。块级做用域有如下几个特色:

  • 声明变量不会提高到代码块顶部

let/const建立的变量不会像使用var声明的变量那样被提高到顶部,所以你须要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。好比:

cosole.log(name); // Uncaught ReferenceError: cosole is not defined
const name = "miqilin";
复制代码

因此确保代码没有引用错误的一种方法是确保只使用letconst进行变量声明。

  • 禁止重复声明

若是一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符再进行 let 声明就会抛出错误。好比:

var count = 10;
let count = 20; // Uncaught SyntaxError: Identifier 'count' has already been declared
复制代码

上面例子中count 变量被先后声明了两次:第一次使用 var ,另外一次使用 let 。由于 let 不能在同一做用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。但若是在嵌套的做用域内使用 let 声明一个同名的新变量,则不会抛出错误:

var count = 10;
// 不会抛出错误
if (condition) {
let count = 20;
// 其余代码
}
复制代码
  • 循环中的绑定块做用域的妙用

开发者可能最但愿实现for循环的块级做用域了,由于能够把声明的计数器变量限制在循环内,好比:

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i);
// ReferenceError: i is not defined
复制代码

上面代码中,由于用let声明计数器i,只在for循环体内有效,因此在循环体外引用就会报错。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
复制代码

上面代码中,变量ivar命令声明的,在全局范围内都有效,因此全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,全部数组a的成员里面的i,指向的都是同一个i,致使运行时输出的是最后一轮的i的值,也就是 10。

若是换使用let,声明的变量仅在块级做用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
复制代码

上面代码中,变量ilet声明的,当前的i只在本轮循环有效,因此每一次循环的i其实都是一个新的变量,因此最后输出的是6。你可能会问,若是每一轮循环的变量i都是从新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是由于 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父做用域,而循环体内部是一个单独的子做用域。

for (let i = 0; i < 5; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
// abc
// abc
复制代码

上面代码正确运行,输出了 5 次abc。这代表函数内部的变量i与循环变量i不在同一个做用域,有各自单独的做用域。

做用域链(Scope Chain)

上面用一大篇幅来说解做用域,其实在里面就有涉及到做用域链的知识了。简单的来讲,当查找变量的时候,会先从当前上下文的变量对象中查找,若是没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫作做用域链。看下面一个例子:

function a() {
    
    function b() {
        console.log(myVar);
    }

    var myVar = 2;
    b();
}

var myVar = 1;
a(); // 2
b(); // Uncaught ReferenceError: b is not defined
复制代码

最后加以执行a()b(),这时候咱们会发现两件事:

1.执行a()会获得2的结果:之因此会有这样的结果,是由于当咱们执行function a里面的function b时,由于在function b里面它找不到myVar这个变量,所以它开始往它的外层去搜寻,而这时候它的父级做用域是function a,在function a里面它便找到了myVar = 2,所以它就再也不往外部环境 (myVar = 1)去找了,直接返回了2这样的结果。

2.b()会获得b is not defined的结果:之因此b会是not defined(记得是not defined不是undefined哦!),是由于这时候在最外层的全局上下文(global execution context)中,找不到function b

而从b() --> a() --> global execution context这样的链,就称为做用域链(Scope Chain)

若是咱们把function a里面对于myVar的声明拿掉的话,它才会继续往外层搜寻myVar,直到找到全局做用域中的声明myVar = 1,这时候才会返回1的结果。

function a() {
    
    function b() {
        console.log(myVar);
    }

    //var myVar = 2;
    b();
}

var myVar = 1;
a(); // 1
复制代码

若是咱们更进一步的把全局做用域中,对于myVar的声明也拿掉,那么如今在全局做用域中也找不到myVar这个变量了,也就是说,在这整个做用域链中都找不到myVar,所以可想而知,最后的结果是not defined

function a() {
    
    function b() {
        console.log(myVar);
    }

    //var myVar = 2;
    b();
}

//var myVar = 1;
a(); // Uncaught ReferenceError: myVar is not defined
复制代码

若是以为文章对你有些许帮助,欢迎在个人GitHub博客点赞和关注,感激涕零!

相关文章
相关标签/搜索