从游戏的角度看作用域

作用域是 JavaScript 里的一个非常重要和基础的概念. 很多人认为自己理解了作用域, 但是在遇到闭包时却说不出个所以然, 甚至不能识别出来.

闭包也是个非常重要, 且经常被误解的概念. 然而闭包就是基于作用域书写代码时所产生的自然结果. 倘若抛开作用域讲闭包, 那都是耍流氓. 闭包可以说在平时的代码里随处可见, 但真正让闭包发挥积极作用的做法是隔离作用域、模块函数等.

作用域机制是不能直接查看的, 我们首先模拟一个场景来尽可能的说明作用域这套规则, 然后通过代码片段和开发者工具进行验证.

游戏存档

想必大家都有玩过游戏的经验. 刚开始的时候, 也就是第一关, 难度比较简单. 到了第二关的时候, 就在第一关的基础上加些难缠的角色, 难度相应地加大了. 关卡越是往后, 难缠的角色也就会越来越多.

可在游戏的时候, 由于各种原因, 往往我们不可能一下子通过所有的关卡, 所以游戏提供了存档的功能. 下次再玩的时候可以从存档里续上. 如果不想这样, 完全可以从头玩起.

为什么我们能从存档里直接跳到上次的关卡, 很显然, 这里是有记录存储的. 比如第一关有个场景食人花和海王, 第二关又多了个邪恶人等等. 每个关卡都会记录该关卡新增的角色或场景同时也会存储之前关卡的记录. 这样就保证了不同的存档的独立性, 无论在哪个关卡存档, 下次也定会续上之前的地方. 当然了, 我们也可以回到上一个关卡.

Aquaman
( 海王之雄风&敌人之邪恶)

几个知识点

结合上面的场景, 我们再回头看看以下几个知识点.

  1. 标识符: 变量、函数、属性的名字, 或者函数的参数.

  2. 每个函数都有自己的执行环境. 当执行流进入一个函数时, 函数的环境就会被推入一个环境栈中. 而在函数执行后, 栈将其环境弹出, 把控制权返回之前的执行环境.

  3. 执行环境定义了变量或函数有权访问的其它数据. 每个执行环境都有一个与之关联的变量对象, 环境中定义的所有变量和函数都保存在这个对象中. 某个执行环境中的所有代码执行完毕后, 该环境被销毁, 保存在其中的所有变量和函数定义也随之销毁.

  4. 当代码在一个环境中执行时, 会创建变量对象的一个作用域链.

  5. 作用域链是保证对执行环境有权访问的所有变量和函数的有序访问. 作用域的前端始终都是当前执行的代码所在的变量对象. 如果这个环境是函数, 则将其活动对象作为变量对象. 活动对象在最开始只包含一个变量, 即 arguments 对象. 作用域链中的下一个变量对象来自包含(外部)环境. 全局执行环境的变量对象始终都是作用域链的最后一个对象.

  6. 当某个环境中为了读取或写入而引入一个标识符时, 必须通过搜索来确定该标识符来确定该标识符实际代表什么. 搜索过程从作用域链的前端开始, 向上逐级查询与给定名字匹配的标识符. 如果在局部环境中找到了该标识符, 搜索过程停止, 变量就绪. 如果在局部环境中没有找到该变量名, 则继续沿作用域链向上搜索. 搜索过程将一直追溯到全局环境的变量对象. 如果在全局环境中也没有找到这个标识符, 则意味着该变量尚未声明.

如果我们把以上的几个知识点串起来, 这就是所谓的作用域链规则了. 上图解释一波.( arguments 应该加到变量对象里的, 图中没体现, 疏忽)

图解作用域

Scope Chain

现在我们从最后两行说起,

var outer = outerFn(10);
var inner = outer(10);
复制代码

执行 outer = outerFn(10) 后, outer 拥有了返回函数的引用. outer(10) 在执行的时候它会创建 属于它自己 的作用域链, 这里包含函数所处外部环境的变量对象.

在读取 initial 变量时, 在 Inner 变量对象中没有检索到, 它会沿着作用域链向上搜索, 在 outer 变量对象里找到了该标识符, 搜索过程停止, 变量就绪.

函数在定义的时候就已经决定了之后执行时, 作用域里将包含什么. 这也解释了, 即使我们把定义在函数内部的函数扔在外边执行也能访问到函数内部的变量. 这和内部函数在哪执行没有半毛钱关系.

为什么强调 属于它自己 的呢?

function outer() {
    var num = 0;
    return function inner() {
        return num++;
    }
}
let innerFn_1 = outer();
let a_1 = innerFn_1()
let innerFn_2 = outer();
let a_2 = innerFn_2();

let a_1_1 = innerFn_1();
let a_2_2 = innerFn_2();
复制代码

innerFn_1 和 innerFn_2 都属于自己的作用域链, 而 a_1 和 a_2 则分别在 innerFn_1 和 innerFn_2 上创建了属于自己的作用域链. 所以它们函数里的 num 是属于不同作用域链里的变量. 但对于 a_1 和 a_1_1 来说它们都是基于 innerFn_1, 拥有同一 outer 变量对象, num 自然也是同一个, 所以会累加. 同理 a_2 和 a_2_2.

如果理解了这个, 那么面试常考的一题就小菜一碟了.

for(var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i*1000)
}
复制代码

重点是执行的时候才会创建变量对象的一个作用域链.

来自作用域的彩蛋

闭包是什么?

如果理解了以上的概念, 就会觉得闭包是作用域埋的一个彩蛋, 用的好就是惊喜, 用的不好就成惊吓了.

好了, 扔几个闭包出来巩固一下.

function outer_1() {
    var a = 'hello world';
    function inner() {
        console.log(a)
    }
    outer_2(inner)
}
function outer_2(fn) {
    fn()
}
复制代码

这里也有闭包.

var a = new array(99999999);
function b() {
    console.log(b)
}
b()
window.addEventListener('click', function() {
    console.log('hello world')
})
复制代码

DevTools里直观看闭包

还有开头所说的可以结合开发者工具直观地看一下, 一张动态图解释一切.

devToolsWithScope

猜你喜欢

转载自juejin.im/post/5c3b3c126fb9a049d2364923