新手秒懂 - 做用域 & 做用域链

前言

根据上篇关于 新手秒懂 - 高逼格解释变量提高 的文章中说明了,在生成执行上下文的建立阶段,生成变量对象后会创建做用域链。那咱们接下里就看看做用域和做用域链究竟是个啥子玩意。javascript

做用域

做用域是一套规则, 用于肯定在何处以及如何查找变量(标识符)。(说白了就是你写代码的那块旮旯里,来肯定你以后怎么查找变量,简单粗暴。。)前端

词法做用域 & 动态做用域

  • 词法做用域: 函数的做用域在函数定义的时候就决定了。(javascript 采用的是静态做用域)
  • 动态做用域: 函数的做用域是在函数调用的时候才决定的。

简单的例子表述一下:java

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); // 1
复制代码

我将以最简单的大白话告诉您发生了啥: foo函数执行 -> 查询value值(没有) -> 向上查找(var value = 1), so, 打印 1es6

静态做用域,只看定义时的位置,就像你跟别人作了邻居,哪天你老婆吵架了跑出去了,不在家里。你只要去外面找就好了,别人家就算也有老婆,但确定不是你要找的老婆啊对不对??函数

函数做用域 & 块级做用域

  • 函数做用域: 已声明函数的形式, 将内部代码"隐藏"起来。从而造成函数做用域
  • 块级做用域: 从 ES3 开始,try/catch 结构在 catch 分句中具备块做用域。在 ES6 中引入了 let/const 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if{ .. } 块的变量,而且将变量添加到这个块 中。以下例所示:
var foo = true;
if (foo) {
    let bar = foo * 2;
    bar = something( bar ); 
    console.log( bar );
 }
console.log( bar ); // ReferenceError
复制代码

匿名函数表达式 & IIFE

之前刚入门的时候被人问到一个问题:请问,当即执行函数表达式的做用是什么??post

白痴的我居然把匿名函数和 IIFE(当即执行函数表达式) 认为是同一个东西。ui

  • 匿名函数表达式: 顾名思义,就是没有名字标识的函数表达式(注意: 函数声明则不能够省略函数名)
  • IIFE: 最多见的用法其实就是使用了匿名函数表达式并最后加入(),让它当即执行。
var a = 2;
(function () {
  var a = 3;
  console.log( a ); // 3
  // 匿名函数表达式内及是块级做用域
})();
console.log( a ); // 2
复制代码

而它的做用主要包括几点:spa

  1. 避免命名冲突
  2. 减小内存占用
  3. 产生块级做用域,形成做用域隔离。关于为啥要存在块级做用域,请参照《ECMAScript 6 入门》

做用域链

定义

当查找变量的时候,会先从当前上下文的变量对象中查找,若是没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫作做用域链。code

形象一点就是:对象

做用域链就像一栋楼,当前做用域在一楼,全局做用域在顶楼,就是一直往上找你要用的变量。

而编译器的查找方式有两种:

  • 若是目的是获取变量的值,就会使用 RHS 查询, 不成功的RHS引用会致使抛出 ReferenceError 异常。 // console.log(a) --> VM130:1 Uncaught ReferenceError: a is not defined
  • 赋值操做符会致使 LHS 查询, 不成功的 LHS 引用会致使自动隐式地建立一个全局变量(非严格模式下),该变量使用 LHS 引用的目标做为标识符,或者抛 出 ReferenceError 异常(严格模式下)。 // a = 1

深刻理解

由于javascript 是静态做用域,函数的做用域在函数定义的时候就决定了。

这是由于函数有一个内部属性 [[scope]],当函数建立的时候,就会保存全部父变量对象到其中,你能够理解 [[scope]] 就是全部父变量对象的层级链,可是注意:[[scope]] 并不表明完整的做用域链!(意思就是在函数建立时就能够拿到父级的变量对象VO)

文字比较难理解不要紧,我们以一个例子说明

function foo() {
    function bar() {
    ...
    }
}
复制代码

函数建立时, 各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
]; // 父级对象的 AO/VO(表示变量对象),俺的上篇文章提到过
复制代码

而以后函数激活, 变量对象就会添加到做用链的前端

做用域链 = [AO].concat([[Scope]]);
复制代码

下面咱们结合示例具体说说实现过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
复制代码

执行过程以下:

1.checkscope 函数被建立,保存做用域链到 内部属性 [[scope]]

checkscope.[[scope]] = [
    globalContext.VO // 建立时就能够获取父变量对象(静态做用域)
];
复制代码

2.执行 checkscope 函数,建立 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

执行上下文栈:当执行一个函数的时候,就会建立一个执行上下文,而且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

ECStack = [ // 执行上下文栈
    checkscopeContext, // checkscope上下文
    globalContext // 全局上下文
];
复制代码

3.checkscope 函数启动(不执行函数内的请求)。开始作准备工做,第一步:复制函数[[scope]]属性建立做用域链

checkscopeContext = {
    做用域链: checkscope.[[scope]], //上面提到的建立时生成的[[scope]]
}
复制代码

4.第二步:用 arguments 建立活动对象,随后初始化活动对象,加入形参、函数声明、变量声明(生成变量对象的的几个过程)

checkscopeContext = {
    VO: { // 变量对象
        arguments: {
            length: 0
        },
        scope2: undefined
    }
}
复制代码

5.第三步:将活动对象压入 checkscope 做用域链顶端

checkscopeContext = {
    VO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    做用域链: [VO, [[Scope]]]
}
复制代码

6.准备工做作完,开始执行函数,随着函数的执行,修改 AO (活动变量,变量对象的执行阶段)的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope' // 函数执行后 scope2 获取到值
    },
    做用域链: [AO, [[Scope]]]
}
复制代码

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];
复制代码

结尾

这篇主要分享的是做用域相关知识,感受大体了解就差很少了,写的都是我本身的浅薄理解,有错误的地方欢迎指出,对于变量对象不了解的小伙伴请参照个人上篇文章 新手秒懂 - 高逼格解释变量提高,仍是一句话,努力,奋斗💪💪

参考文献

《你不知道的JavaScript(上卷)》

JavaScript深刻之做用域链

相关文章
相关标签/搜索