JS 函数作用域与块级作用域的混淆与澄清

各位靓仔靓女们,晚上好!我是你们今晚的JS讲师,咱们今儿个聊聊JavaScript里容易让人头大的一个话题:函数作用域和块级作用域。这俩玩意儿,要是搞不清楚,写出来的代码就像没穿裤衩一样,到处都是bug,风一吹就凉飕飕的。

开场白:作用域是个啥?

想象一下,你是一个小区物业,负责管理小区里的资源。作用域,就类似于你的管理范围。你能管到哪些楼,哪些住户,取决于你的权限。在JavaScript里,作用域决定了你的变量和函数在哪些地方可以被访问。

简单来说,作用域就是一套规则,定义了变量和表达式在代码中的可见性和生命周期。

第一幕:函数作用域的辉煌与落寞

在ES6(也就是ECMAScript 2015)之前,JavaScript只有两种作用域:全局作用域和函数作用域。

  • 全局作用域: 就像整个地球,你在哪儿都能看到它。在函数外部声明的变量,就拥有全局作用域,可以在任何地方被访问。

    var globalVar = "我是全局变量";
    
    function sayHello() {
      console.log(globalVar); // 可以在函数内部访问
    }
    
    sayHello(); // 输出: 我是全局变量
    console.log(globalVar); // 输出: 我是全局变量
  • 函数作用域: 就像你的卧室,只有你才能进去。在函数内部声明的变量,就拥有函数作用域,只能在该函数内部被访问。

    function myFunction() {
      var localVar = "我是局部变量";
      console.log(localVar); // 可以在函数内部访问
    }
    
    myFunction(); // 输出: 我是局部变量
    // console.log(localVar); // 报错:localVar is not defined (函数外部无法访问)

函数作用域的特点是:变量在函数内部声明,只能在函数内部使用。这在一定程度上避免了变量名冲突的问题。但是,它也带来了一些问题。

问题一:var 的变量提升(Hoisting)

var声明的变量,会发生变量提升。也就是说,JavaScript引擎会在代码执行之前,先把var声明的变量提到作用域的顶部。但是,赋值操作不会被提升。

function hoistingExample() {
  console.log(myVar); // 输出: undefined (而不是报错)
  var myVar = "Hello, hoisting!";
  console.log(myVar); // 输出: Hello, hoisting!
}

hoistingExample();

这段代码之所以不会报错,是因为var myVar; 被提升到了函数顶部,相当于:

function hoistingExample() {
  var myVar; // 变量声明被提升
  console.log(myVar); // 输出: undefined
  myVar = "Hello, hoisting!"; // 赋值操作没有被提升
  console.log(myVar); // 输出: Hello, hoisting!
}

hoistingExample();

这很容易让人迷惑,尤其是在大型项目中,如果不小心使用了未声明的变量,可能会导致意想不到的bug。

问题二:var 没有块级作用域

这是var最让人头疼的地方。在if语句、for循环等块级结构中声明的var变量,仍然会泄漏到函数作用域中。

function varLeakage() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 输出: 10 (即使x是在if语句中声明的)
}

varLeakage();

在这个例子中,x是在if语句中声明的,但它仍然可以在if语句外部被访问。这是因为var没有块级作用域,它会将变量提升到函数作用域的顶部。

第二幕:块级作用域的横空出世

为了解决var的这些问题,ES6引入了letconst,它们都具有块级作用域。

  • 块级作用域: 就像你家的某个房间,只有在这个房间里才能使用。在{} 内部声明的变量,就拥有块级作用域,只能在该代码块内部被访问。

    function blockScopeExample() {
      if (true) {
        let y = 20;
        const z = 30;
        console.log(y); // 输出: 20
        console.log(z); // 输出: 30
      }
      // console.log(y); // 报错:y is not defined (块外部无法访问)
      // console.log(z); // 报错:z is not defined (块外部无法访问)
    }
    
    blockScopeExample();

    在这个例子中,yz是在if语句中声明的,它们只能在if语句内部被访问。在if语句外部访问它们,会导致错误。

letconst的区别在于:

  • let声明的变量可以被重新赋值。
  • const声明的变量必须在声明时赋值,并且不能被重新赋值(但如果const声明的是对象或数组,对象或数组内部的属性或元素是可以修改的)。

letconst 的特性对比:

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升
重复声明 允许 不允许 不允许
是否可重新赋值 可以 可以 不可以

第三幕:实战演练,区分与应用

为了更好地理解函数作用域和块级作用域,我们来看几个实战例子。

例子一:循环中的变量

// 使用var
function loopWithVar() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log("var:", i); // var: 5  var: 5  var: 5  var: 5  var: 5
    }, 100);
  }
}

loopWithVar();

// 使用let
function loopWithLet() {
  for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log("let:", i); // let: 0  let: 1  let: 2  let: 3  let: 4
    }, 100);
  }
}

loopWithLet();

为什么使用var时,输出的都是5? 这是因为var i 在整个 loopWithVar 函数中只有一个,当循环结束后,i 的值变成了 5。而 setTimeout 中的函数是异步执行的,当它们执行时,i 的值已经是 5 了。

而使用let时,每次循环都会创建一个新的i变量,每个setTimeout函数都闭包了对应的i值,所以输出的是0到4。

例子二:避免变量污染

var message = "Hello, global!";

function outerFunction() {
  var message = "Hello, outer!";

  function innerFunction() {
    let message = "Hello, inner!";
    console.log(message); // 输出: Hello, inner!
  }

  innerFunction();
  console.log(message); // 输出: Hello, outer!
}

outerFunction();
console.log(message); // 输出: Hello, global!

在这个例子中,我们有三个message变量,分别位于全局作用域、outerFunction作用域和innerFunction作用域。由于let具有块级作用域,innerFunction中的message变量不会影响到outerFunction中的message变量。

例子三:const 的使用场景

const 通常用于声明那些在程序运行期间不会改变的变量,例如配置信息、常量等。

const API_URL = "https://api.example.com/v1/";
const PI = 3.1415926;

// API_URL = "https://api.example.com/v2/"; // 报错:Assignment to constant variable.

const user = {
  name: "Alice",
  age: 30
};

user.age = 31; // 可以修改对象内部的属性
console.log(user.age); // 输出: 31

总结与建议

  • 拥抱 letconst 尽量使用 letconst 来声明变量,避免 var 带来的问题。
  • 理解作用域链: JavaScript 引擎会沿着作用域链查找变量,直到找到为止。
  • 注意变量提升: 虽然 letconst 不会发生变量提升,但仍然要注意变量的声明位置,避免出现意外的错误。
  • 代码规范: 遵循良好的代码规范,可以有效地避免作用域相关的问题。例如,在函数顶部声明所有变量,避免在块级结构中声明变量。
  • 实践出真知: 多写代码,多调试,才能真正理解作用域的概念。

常见问题解答

  • 闭包和作用域有什么关系? 闭包是指函数可以访问并记住其词法作用域(定义时的作用域),即使该函数在其词法作用域之外执行。闭包是作用域的自然结果,它使得函数可以访问其创建时的上下文。
  • 什么时候应该使用 var,什么时候应该使用 letconst 一般来说,应该避免使用 var。只有在需要兼容老版本的浏览器时,才可能需要使用 var。在现代 JavaScript 开发中,应该优先使用 letconst
  • 如何避免作用域污染? 尽量使用 letconst,避免在全局作用域中声明变量,将代码模块化,使用立即执行函数表达式(IIFE)等。

结束语

掌握函数作用域和块级作用域是成为一名合格的JavaScript开发者的必备技能。希望通过今天的讲解,能够帮助大家更好地理解这两个概念,写出更健壮、更易维护的代码。记住,好的代码就像一件艺术品,需要精雕细琢,而理解作用域就是你手中的雕刻刀。 各位,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注