深入理解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也能和编译语言一样拥有较快的编译和运行速度。

相关推荐
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
从兄3 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf5 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
薛一半6 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
过期的H2O26 小时前
【H2O2|全栈】JS进阶知识(四)Ajax
开发语言·javascript·ajax
MarcoPage6 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js
你好龙卷风!!!7 小时前
vue3 怎么判断数据列是否包某一列名
前端·javascript·vue.js
shenweihong8 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
巧克力小猫猿9 小时前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js
嚣张农民9 小时前
一文简单看懂Promise实现原理
前端·javascript·面试