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

相关推荐
麒麟而非淇淋21 分钟前
AJAX 入门 day1
前端·javascript·ajax
2401_8581205323 分钟前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢28 分钟前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写2 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
快乐牌刀片882 小时前
web - JavaScript
开发语言·前端·javascript
秋雨凉人心2 小时前
call,apply,bind在实际工作中可以使用的场景
javascript
哪 吒3 小时前
华为OD机试 - 第 K 个字母在原来字符串的索引(Python/JS/C/C++ 2024 E卷 100分)
javascript·python·华为od
(⊙o⊙)~哦3 小时前
JavaScript match() 方法
开发语言·javascript·ecmascript
花下的晚风3 小时前
Vue实用操作篇-1-第一个 Vue 程序
前端·javascript·vue.js
18资源4 小时前
H5白色大方图形ui设计公司网站HTML模板源码
前端·javascript·html