你不知道的作用域

288 阅读4分钟

如果你是一个前端工程师,那么JavaScript的作用域你一定不会陌生。

你知道的可能是 JavaScript中有

  • 传统的函数级作用域 和 全局作用域
  • ES6 let const 的块级作用域

但是还有一个你不知道的块级作用域的存在,今天我们就来扒一扒这些鲜为人知的小秘密

我们先来看看下面这个段代码,请思考一下结果是什么。

function fn() {
    var foo = 1;
    if (true) {
        foo = 2;
        function foo() {}
        foo = 3;
        console.log(foo);
    }
    console.log(foo); 
}

fn();

想必你心中已经有了结果

正确答案是3 2

你答对了吗?答案结果是否有些意外呢?

好了 我们来看看为什么会出现这个答案

首先我们需要先了解以下基本知识

声明提前

声明提前指的是JS引擎在执行之前对代码进行的预解析(为了提高执行效率) 具体的来说就是使用(var)声明变量和(function)声明的函数正预编译阶段将其提升到了作用域的顶部

全局变量声明

// 全局变量在声明时将其提升到全局作用域的顶部
console.log(foo);  // undefined
var foo = 'test';


// 上面代码相当于
var foo;
console.log(foo);
foo = 'test';

函数作用域变量声明

// 函数内的变量在声明是将其提升到函数作用域的顶部
function fn(){
    console.log(foo); // undefined
    var foo = 5;
}
fn();
console.log(foo); // err: foo is not defined

// 不同于全局变量的是 在函数中声明的变量仅提升到函数作用域的顶部

函数声明

console.log(fn);  // function fn

function fn(){
    // dosomething
}

// 函数声明提升到了作用域顶部

函数表达式声明

console.log(fn);// undefined

var fn = function(){}

// 此处应为fn是使用var关键字声明所以按照变量声明提升的情况 变量提升不赋值 则结果为undefined

函数块级作用域

通过下面代码我们可以知道 变量的声明是没有块级作用域的,if语句块中声明的变量foo会提升到全局作用域并初始化值为undefined

console.log(foo); // undefined

if(true){
    var foo = 'test';
}

我们再看看函数的情况

console.log(foo); // undefined

if(true){
    console.log(foo); // function foo
    function foo(){}
}
console.log(foo);  // function foo

上面这个例子告诉我们 函数foo提升到了if语句块的顶部,但是没有提升到全局作用域的顶部。但全局作用域中存有一个名为foo的变量 在代码执行后同步成了函数foo

同步?为什么会有同步?我们来看看观察上帝视角的神器 ———— 断点调试

我这边监听了 foo 变量和 window.foo 大家请注意一下它的变化

同时我们也关注一下 Scope 作用域

我们看到Scope中只有全局作用域 foo和window.foo的只都是undefined

img

代码执行到if语句块中 我们再Scope中又发现了一个新的块级作用域 当前块级作用域foo的值被赋值为一个函数(函数提升) 而全局作用域中的foo依旧是undefined

img

代码继续往后执行 执行函数的赋值 block作用域依然存在 我把几个变量的值使用箭头进行了对应

img

神奇的地方来了 在函数执行赋值后 块级作用域消失 而全局变量的foo同步成了刚才块级作用域的foo

img

回归原题

我们对foo变量进行隔行输出 看看结果

function fn() {
    // 由于此处最高级别的作用域为 fn函数作用域( 此处不涉及全局作用域以及全局对象window )
    // 我们将注释中的foo 命名为 fn.foo 和 block.foo
    console.log(foo); // undefined ( fn.foo = undefined )
    var foo = 1; // 定义变量 
    console.log(foo); // 1 ( fn.foo = 1 )
    if (true) {
        console.log(foo); // function foo ( block.foo = function )  函数声明提升 
        foo = 2;
        console.log(foo); // 2 ( block.foo = 2 )  ( fn.foo = 1 )

        function foo() {} // 执行函数赋值 将block的foo同步到父级作用域fn  (fn.foo = 2)
        console.log(foo); // 2 ( block.foo = 2) (fn.foo = 2)
        foo = 3;
        console.log(foo); // 3 ( block.foo = 3) (fn.foo = 2)
    }
    console.log(foo); // 2 (fn.foo = 2)
}

fn();

结合上面断点测试的结果大家可以发现,函数在代码块中声明会提前到块级作用域顶部,预编译结束后开始执行代码 在执行阶段任然会执行函数的赋值操作,其实是函数赋值的第二次执行(第一次在预编译阶段) 第二次的赋值执行的意义是确认当前 函数/全局 作用域能准确的检索到函数 所以讲函数同步到了 当前的函数或全局作用域中。

好了本次解析就到这里,还有不明白的小伙伴可以copy代码去进行断点测试,相信很快你能理解其中奥秘。 关注我 学更多前端知识。下次再见。