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

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

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

词法作用域 & 动态作用域

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

简单的例子表述一下:

var value = 1;

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

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

bar(); // 1

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

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

函数作用域 & 块级作用域

  • 函数作用域: 已声明函数的形式, 将内部代码"隐藏"起来。从而形成函数作用域

  • 级作用域: 从 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
以前刚入门的时候被人问到一个问题:请问,立即执行函数表达式的作用是什么??

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

  • 匿名函数表达式: 顾名思义,就是没有名字标识的函数表达式(注意: 函数声明则不可以省略函数名)

  • IIFE:最常见的用法其实就是使用了匿名函数表达式并最后加入(),让它立即执行。

    var a = 2;
     (function () {
       var a = 3;
       console.log( a ); // 3
       // 匿名函数表达式内及是块级作用域
     })();
     console.log( a ); // 2
    

而它的作用主要包括几点:

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

作用域链

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

形象一点就是:

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

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

  • 如果目的是获取变量的值,就会使用 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(上卷)》

我的前端学习交流群562862926,2019前端开发全套资料开发工具欢迎大家加群领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值