深入理解JavaScript作用域链

JavaScript 每日一篇,记录自学JavaScript语言的点点滴滴。

探究JavaScript作用域

JavaScript 作用域链是如何工作的

JavaScript作为一门面向对象,函数式编程语言,其最大的优势就是你可以将函数的方法组合起来,形成一个链式的方法进行编程。其中,作用域链是JavaScript编程语言中的重要组成部分。

JavaScript分为三种作用域------全局作用域、块作用域和函数作用域。

仔细看下面这段代码,通过分析这段代码,体现JavaScript的全局作用域、块作用域和函数作用域。

javascript 复制代码
'use strict';

function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `You are ${age}, born in ${birthYear}`;
        console.log(output);
    }
    printAge();

    return age;
}

const firstName = 'Jones';
calcAge(2001);

通过运行,可以得出一下结果:

解析下面这段结构:

实际上,JavaScript在被解释或及时编译的一瞬间,它会将相应的函数和全局变量放入栈中进行调用。全局作用域会被放入栈底,方便调用其他函数。而被调用的函数或其他变量会被依次入栈,这样当调用的函数需要某些变量时,引擎会依次自底向上进行搜索。

在JavaScript中,函数属于块作用域,你可以将定义在函数内部的函数看作外层函数的一个变量,外层函数的所有变量和函数都可以被内层函数所调用。

如果此时你调用函数块内部的printAge()函数或age变量,程序会报错,这位这两个变量并非全局变量,而是函数内部的块作用域的变量。

javascript 复制代码
'use strict';

function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `You are ${age}, born in ${birthYear}`;
        console.log(output);
    }
    printAge();

    return age;
}

const firstName = 'Jones';
calcAge(2001);
// printAge(); // Uncaught ReferenceError: printAge is not defined
// console.log(age);   // Uncaught ReferenceError: age is not defined

但作为全局作用域变量firstName,你可以在代码的任何地方进行调用,无论是在函数作用域内,还是在块作用域内。例如在下面的代码中,你完全可以在calcAge()的函数内部的printAge()内部调用全局变量的firstName内部的内容。

javascript 复制代码
'use strict';

function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);
    }
    printAge();

    return age;
}

const firstName = 'Jones';
calcAge(2001);
// printAge(); // Uncaught ReferenceError: printAge is not defined
// console.log(age);   // Uncaught ReferenceError: age is not defined

关于块作用域的使用

在最新的ES6标准中提供了关于JavaScript块作用域的功能。在你使用letconst关键字时,你只有声明此变量的代码块内进行访问,无权在块外使用。例如下面这段代码中我们在if块内声明了一个变量名为str的常量用来存储模版字符串。访问该常量时,你只能在if块作用域 内进行声明。如果你在函数printAge()内进行调用是无效的。

javascript 复制代码
'use strict';

function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);
        }
        console.log(str);
    }
    printAge();

    return age;
}

const firstName = 'Zoe';
calcAge(1994);
// printAge(); // Uncaught ReferenceError: printAge is not defined
// console.log(age);   // Uncaught ReferenceError: age is not defined

在过去老版本的JavaScript中,常常用var关键字声明变量。用var关键字声明的变量是函数作用域。就比如下面这段代码中用var关键字声明的millenial变量,即便在if作用域块内,用var关键字声明的millenial变量也能不受块约束的调用在块的外部。例如在printAge()内部的任何区域。

javascript 复制代码
function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            var millenial = true;   // In the same function, so we can also call this variable.
            // 因为此变量使用var关键字进行变量的声明,具有函数作用域,
            // 因此在同一个函数你仍然可以访问到
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);
        }
        // console.log(str);
        console.log(millenial); // millenial => true
    }
    printAge();

    return age;
}

⚠️注意:当然我们不建议这样做,因为在某些情况下,如使用var关键字声明的循环索引变量很容易在循环运行的过程中出错。另外频繁的使用var关键字也容易对程序后期的维护造成较大困难😷,所以尽可能避免使用var关键字。

有关严格模式下的作用域

在严格模式状态下,函数是块作用域

javascript 复制代码
'use strict';

function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            var millenial = true;   // In the same function, so we can also call this variable.
            // 因为此变量使用var关键字进行变量的声明,具有函数作用域,
            // 因此在同一个函数你仍然可以访问到
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);

            // 在严格模式下,函数是块作用域
            function add(a, b) {
                return a + b;
            }
        }
        // console.log(str);
        console.log(millenial); // millenial => true
        add(2, 3);
    }
    printAge();

    return age;
}

const firstName = 'Zoe';
calcAge(1994);

但如果取消严格模式,函数也可以在外部作用域被调用

javascript 复制代码
function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            var millenial = true;   // In the same function, so we can also call this variable.
            // 因为此变量使用var关键字进行变量的声明,具有函数作用域,
            // 因此在同一个函数你仍然可以访问到
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);

            // 在严格模式下,函数是块作用域
            function add(a, b) {
                return a + b;
            }
        }
        // console.log(str);
        console.log(millenial); // millenial => true
        console.log(add(2, 3)); // call the add function
    }
    printAge();

    return age;
}

但如果此时打开严格模式,此函数将无法访问

JavaScript中的就近原则

下面这段代码中我们有两个名称为firstName的变量。但是JavaScript在编译的过程中会遵循就近原则------只应用同一变量名中寻找当前作用域最近的变量。例如在全局作用域下有firstName变量,但在块作用域下的函数printAge()下的if块内也有firstName变量,此时,引擎会优先引入if块内的firstName变量。因此结果会显示"李琳",而不是全局作用域下的"Zoe"。

javascript 复制代码
function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        const output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            var millenial = true;   // In the same function, so we can also call this variable.
            // 因为此变量使用var关键字进行变量的声明,具有函数作用域,
            // 因此在同一个函数你仍然可以访问到
            const firstName = '李琳';
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);

            // 在严格模式下,函数是块作用域
            function add(a, b) {
                return a + b;
            }
        }
        // console.log(str);
        console.log(millenial); // millenial => true
        // console.log(add(2, 3)); // call the add function
    }
    printAge();

    return age;
}

const firstName = 'Zoe';
calcAge(1994);

尽管有两个相同的firstName变量,但是因为这两个变量在不同的作用域上,所以尽管他们有相同的变量名称,但实际上它们是两个完全不同的变量。以后在分析V8引擎对JavaScript的解释和编译策略时会提到这一点。

javascript 复制代码
function calcAge(birthYear) {
    const age = 2037 - birthYear;
    // console.log(firstName);

    function printAge() {
        let output = `${firstName}, you are ${age}, born in ${birthYear}`;
        console.log(output);

        if (birthYear >= 1981 && birthYear <= 1996) {
            var millenial = true;   // In the same function, so we can also call this variable.
            // 因为此变量使用var关键字进行变量的声明,具有函数作用域,
            // 因此在同一个函数你仍然可以访问到
            const firstName = '李琳';
            const str = `哦,你是千禧一代啊!Oh, and you're a millenial ${firstName}`;
            console.log(str);

            // 在严格模式下,函数是块作用域
            function add(a, b) {
                return a + b;
            }

            output = `新的输出!`;
        }
        // console.log(str);
        console.log(millenial); // millenial => true
        // console.log(add(2, 3)); // call the add function
    }
    printAge();

    return age;
}

另外,需要注意的是,随着JavaScript编程语言在后端的大量使用,目前JavaScript已经脱离了解释,改为了即时编译或解释➕编译的混合编译手段,即JIT(Just in time)即时编译策略。这使得JavaScript也能和编译语言一样拥有较快的编译和运行速度。

相关推荐
前端小咸鱼一条1 小时前
React组件化的封装
前端·javascript·react.js
随便起的名字也被占用1 小时前
leaflet中绘制轨迹线的大量轨迹点,解决大量 marker 绑定 tooltip 同时显示导致的性能问题
前端·javascript·vue.js·leaflet
JuneXcy2 小时前
11.Layout-Pinia优化重复请求
前端·javascript·css
天下无贼!2 小时前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
PineappleCoder3 小时前
JS 作用域链拆解:变量查找的 “俄罗斯套娃” 规则
前端·javascript·面试
知识分享小能手3 小时前
Vue3 学习教程,从入门到精通,Vue3 中使用 Axios 进行 Ajax 请求的语法知识点与案例代码(23)
前端·javascript·vue.js·学习·ajax·vue·vue3
533_3 小时前
[echarts] 更新数据
前端·javascript·echarts
讨厌吃蛋黄酥3 小时前
利用Mock实现前后端联调的解决方案
前端·javascript·后端
zzywxc7873 小时前
在处理大数据列表渲染时,React 虚拟列表是提升性能的关键技术,但在实际实现中常遇到渲染抖动和滚动定位偏移等问题。
前端·javascript·人工智能·深度学习·react.js·重构·ecmascript
_Kayo_10 小时前
VUE2 学习笔记14 nextTick、过渡与动画
javascript·笔记·学习