MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript函数提升与作用域

2021-11-222.2k 阅读

JavaScript函数提升

在JavaScript中,函数提升是一个重要的概念。简单来说,函数提升意味着在代码执行之前,函数声明会被移动到其所在作用域的顶部。这使得我们可以在函数声明之前调用该函数,而不会抛出引用错误。

函数声明提升的示例

// 调用函数在声明之前
sayHello();

function sayHello() {
    console.log('Hello!');
}

在上述代码中,sayHello函数在调用时还未在代码顺序上声明,但依然可以正常执行。这是因为JavaScript引擎在解析代码时,将函数声明提升到了其所在作用域的顶部。

函数表达式与函数声明的区别

与函数声明不同,函数表达式不会被提升。例如:

// 以下代码会抛出ReferenceError
sayGoodbye();

var sayGoodbye = function() {
    console.log('Goodbye!');
};

这里,sayGoodbye被定义为一个函数表达式,它实际上是一个变量的赋值操作。变量声明会被提升,但变量的赋值不会。所以在调用sayGoodbye时,变量已经声明但未赋值,因此会抛出ReferenceError

函数提升的机制

JavaScript引擎在解析代码时,会创建一个执行上下文。对于函数作用域,在创建执行上下文的过程中,会有一个名为“变量对象”(Variable Object)的概念。函数声明会被添加到变量对象中,而变量声明只是在变量对象中创建一个标识符,其赋值操作依然在原代码位置执行。

例如,对于以下代码:

function test() {
    console.log(x);
    var x = 10;
    console.log(x);
}
test();

在函数test的执行上下文中,变量对象首先会包含x的声明(但此时x的值为undefined)。所以第一个console.log(x)会输出undefined,而第二个console.log(x)会输出10,因为在代码执行到var x = 10时,x被赋值了。

JavaScript作用域

作用域决定了变量和函数的可访问范围。JavaScript中有两种主要的作用域类型:全局作用域和函数作用域。

全局作用域

全局作用域是最外层的作用域。在JavaScript代码中,任何未在函数内声明的变量都会处于全局作用域。例如:

var globalVar = 'I am global';

function printGlobal() {
    console.log(globalVar);
}

printGlobal();
console.log(globalVar);

在上述代码中,globalVar在全局作用域中声明,所以在函数printGlobal内部和全局代码中都可以访问到它。

函数作用域

函数作用域是指在函数内部定义的变量和函数,它们只能在该函数内部访问。例如:

function localScope() {
    var localVar = 'I am local';
    console.log(localVar);
}

localScope();
// 以下代码会抛出ReferenceError
console.log(localVar);

这里,localVarlocalScope函数内部声明,所以在函数外部访问它会抛出ReferenceError

块级作用域(ES6之前)

在ES6之前,JavaScript没有真正的块级作用域。像iffor等块语句不会创建新的作用域。例如:

for (var i = 0; i < 5; i++) {
    console.log(i);
}
console.log(i);

在上述代码中,i虽然是在for循环内声明的,但由于for循环块没有自己的作用域,i实际上处于包含它的函数作用域(如果在全局代码中,则是全局作用域)。所以在for循环结束后,依然可以访问到i,此时i的值为5

块级作用域(ES6之后 - let和const)

ES6引入了letconst关键字,它们可以创建块级作用域。例如:

{
    let blockVar = 'I am in block scope';
    console.log(blockVar);
}
// 以下代码会抛出ReferenceError
console.log(blockVar);

在上述代码中,blockVar使用let声明,它只在包含它的块级作用域(这里是花括号内)有效。在块级作用域外部访问blockVar会抛出ReferenceError

同样,const声明的变量也遵循块级作用域规则。例如:

{
    const blockConst = 'Constant in block scope';
    console.log(blockConst);
}
// 以下代码会抛出ReferenceError
console.log(blockConst);

const声明的变量一旦赋值就不能再改变,并且它也只在块级作用域内有效。

作用域链

当在JavaScript中查找一个变量时,引擎会首先在当前作用域中查找。如果找不到,它会向上一级作用域查找,直到找到该变量或者到达全局作用域。这种查找变量的路径形成了一个链条,称为作用域链。

作用域链示例

var outerVar = 'I am outer';

function outerFunction() {
    var innerVar = 'I am inner';

    function innerFunction() {
        console.log(outerVar);
        console.log(innerVar);
    }

    innerFunction();
}

outerFunction();

在上述代码中,innerFunction内部需要访问outerVarinnerVar。当查找outerVar时,在innerFunction的作用域中找不到,于是向上一级作用域(outerFunction的作用域)查找,还是找不到,继续向上到全局作用域找到outerVar。而innerVarouterFunction的作用域中,所以在innerFunction访问innerVar时,沿着作用域链在outerFunction的作用域中找到它。

作用域链与闭包

闭包是JavaScript中一个强大的特性,它与作用域链密切相关。闭包是指一个函数能够访问并记住其外部作用域的变量,即使该函数在外部作用域之外执行。例如:

function outer() {
    var outerVar = 'I am outer for closure';

    function inner() {
        console.log(outerVar);
    }

    return inner;
}

var closureFunction = outer();
closureFunction();

在上述代码中,outer函数返回了inner函数。当outer函数执行完毕后,其作用域本应被销毁,但由于inner函数形成了闭包,它记住了outerVar变量。所以当closureFunction(即inner函数)被调用时,依然可以访问到outerVar

闭包的应用场景

闭包在很多场景下都非常有用。例如,实现模块模式:

var myModule = (function() {
    var privateVar = 'This is private';

    function privateFunction() {
        console.log('This is a private function');
    }

    return {
        publicFunction: function() {
            privateFunction();
            console.log(privateVar);
        }
    };
})();

myModule.publicFunction();

在上述代码中,通过闭包,privateVarprivateFunction对于外部代码是不可直接访问的,只有通过publicFunction才能间接访问它们,从而实现了模块的封装。

函数提升与作用域的关系

函数提升是在作用域的基础上进行的。函数声明会被提升到其所在作用域的顶部。例如:

function outer() {
    saySomething();

    function saySomething() {
        console.log('Inside outer function');
    }
}

outer();

在上述代码中,saySomething函数声明被提升到了outer函数作用域的顶部,所以在函数内部可以在声明之前调用它。

嵌套函数与作用域和函数提升

当存在嵌套函数时,情况会变得更加复杂。例如:

function outer() {
    function inner() {
        console.log('Inner function');
    }

    return inner;
}

var innerFunc = outer();
innerFunc();

这里,inner函数在outer函数内部声明,它可以访问outer函数作用域内的变量和函数。同时,inner函数的声明也会被提升到outer函数作用域的顶部。

函数提升与块级作用域

在ES6引入块级作用域后,函数提升在块级作用域内的行为有所变化。在严格模式下,块级作用域内的函数声明不再被提升到包含它的函数作用域或全局作用域顶部,而是仅在块级作用域内提升。例如:

function testBlockFunction() {
    'use strict';
    console.log(func); // 输出undefined
    {
        function func() {
            console.log('Inside block');
        }
    }
    console.log(func); // 输出undefined
}

testBlockFunction();

在上述代码中,由于处于严格模式,func函数声明仅在块级作用域内提升,所以在块级作用域外部,func变量是undefined

实际应用中的注意事项

在实际开发中,理解函数提升与作用域对于编写健壮的JavaScript代码至关重要。

避免变量污染

由于全局作用域中的变量可以被任何地方访问,过度使用全局变量容易导致变量污染。例如:

// file1.js
var globalVar = 'Value from file1';

// file2.js
var globalVar = 'Value from file2';

如果这两个文件在同一个页面中加载,后定义的globalVar会覆盖前一个,可能导致难以调试的错误。因此,应尽量减少全局变量的使用,将变量限制在尽可能小的作用域内。

合理使用闭包

虽然闭包很强大,但如果滥用也会带来问题。例如,闭包会导致内存泄漏,因为闭包会保持对外部作用域变量的引用,使得这些变量无法被垃圾回收机制回收。例如:

function createClosure() {
    var largeArray = new Array(1000000).fill(1);
    function inner() {
        console.log(largeArray.length);
    }
    return inner;
}

var closure = createClosure();
// 即使createClosure函数执行完毕,largeArray依然不能被回收

在上述代码中,inner函数形成的闭包保持了对largeArray的引用,使得largeArray无法被垃圾回收,可能导致内存占用过高。

注意函数提升的影响

在编写代码时,要注意函数提升可能带来的影响。例如,函数声明提升可能会导致代码逻辑看起来不清晰。例如:

// 看起来逻辑不清晰
callFunction();

function callFunction() {
    console.log('Function called');
}

为了提高代码的可读性,建议在调用函数之前声明函数,即使JavaScript允许在声明之前调用。

总结

JavaScript的函数提升与作用域是其核心概念。函数提升使得函数声明可以在代码执行之前被移动到其所在作用域的顶部,这为我们在代码组织上提供了一定的灵活性,但也可能带来一些理解上的困难。作用域决定了变量和函数的可访问范围,包括全局作用域、函数作用域以及ES6引入的块级作用域。作用域链则是变量查找的路径,闭包是基于作用域链的一个强大特性,它允许函数记住并访问其外部作用域的变量。在实际开发中,合理利用函数提升与作用域,避免变量污染和闭包滥用等问题,对于编写高质量、可维护的JavaScript代码至关重要。我们需要不断地实践和理解这些概念,以更好地掌握JavaScript这门语言。