前端学习<四>JavaScript基础——23-作用域、变量提升、函数提升

作用域

作用域(Scope)的概念和分类

  • 概念 :作用域是一个变量或函数的作用范围。作用域在函数定义时,就已经确定了。

  • 目的:为了提高程序的可靠性,同时减少命名冲突。

在 JS 中,一共有两种作用域:(ES5 中)

  • 全局作用域:作用于整个 script 标签内部,或者作用于一个独立的 JS 文件。

  • 函数作用域(局部作用域):作用于函数内的代码环境。

全局作用域 和 window 对象

直接编写在 script 标签中的 JS 代码,都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。

在全局作用域中有一个全局对象 window,它代表的是浏览器的窗口,由浏览器创建,我们可以直接使用。相关知识点如下:

  • 创建的变量 都会作为 window 对象的属性保存。比如在全局作用域内写 const a = 100,这里的 a 等价于 window.a。

  • 创建的函数都会作为 window 对象的方法保存。

作用域的访问关系

在内部作用域中可以访问外部作用域的变量,在外部作用域中无法访问到内部作用域的变量。

代码举例:

复制代码
 
复制代码
const a = 'aaa';
 function foo() {
     const b = 'bbb';
     console.log(a); // 打印结果:aaa。说明 内层作用域 可以访问 外层作用域 里的变量
 }
 ​
 foo();
 console.log(b); // 报错:Uncaught ReferenceError: b is not defined。说明 外层作用域 无法访问 内层作用域 里的变量

变量的作用域

根据作用域的不同,变量可以分为两类:全局变量、局部变量。

全局变量:

  • 在全局作用域下声明的变量,叫「全局变量」。在全局作用域的任何一地方,都可以访问这个变量。

  • 在全局作用域下声明的变量是全局变量。

局部变量:

  • 定义在函数作用域的变量,叫「局部变量」。仅限函数内部访问这个变量。

  • 函数的形参也是属于局部变量。

从执行效率来看全局变量和局部变量:

  • 全局变量:只有浏览器关闭时才会被销毁,比较占内存。

  • 局部变量:当其所在的代码块运行结束后,就会被销毁,比较节约内存。

特殊情况:

(1)无论是在函数外还是函数内,变量如果未经声明就赋值(意思是,如果不加var/let/const),这个变量是全局变量。

比如:

复制代码
  // 声明变量时如果不加var/let/const,这个变量是全局变量。且可以被修改。
  function fn() {
    a = 1;
  }
  fn(); // 这行代码必须要写,否则下一行代码执行时会报错:Uncaught ReferenceError: a is not defined
  console.log(a); // 打印结果:1

当然,我们不建议这么用。

(2)如果局部变量和全局变量重名,则在函数内部,变量是以局部变量为准。

作用域的上下级关系

当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错 ReferenceError。

在函数中要访问全局变量可以使用 window 对象。(比如说,全局作用域和函数作用域都定义了变量 a,如果想访问全局变量,可以使用window.a)

全局作用域的预处理

**预处理(预解析)**的概念:JS在解析代码之前,有一个"预处理(预解析)"阶段,将当前 JS 代码中所有变量的定义和函数的定义,放到所有代码的最前面。

(打个比方,学生在学习文言文之前,会扫读整篇文章,做简单的预习。)

这种预解析,也称之为声明提前。

变量的声明提前(变量提升)

使用 var 关键字声明的变量( 比如 var a = 1),会在所有的代码执行之前被声明 (但是不会赋值)。但是如果声明变量时不是用 var 关键字(比如直接写a = 1),则变量不会被声明提前。

举例 1:

复制代码
 console.log(a);
 var a = 123;

打印结果:undefined。注意,打印结果并没有报错,而是 undefined,说明变量 a 被提前声明了,只是尚未被赋值。

举例 2:

复制代码
 console.log(a);
 a = 123; //此时a相当于window.a

程序会报错:Uncaught ReferenceError: a is not defined。

举例 3:

复制代码
 a = 123; //此时a相当于window.a
 console.log(a);

打印结果:123。

举例 4:

复制代码
 foo();
 ​
 function foo() {
     if (false) {
         var i = 123;
     }
     console.log(i);
 }

打印结果:undefined。注意,打印结果并没有报错,而是 undefined。这个例子,再次说明了:变量 i 在函数执行前,就被提前声明了,只是尚未被赋值。

例 4 中, if(false)里面的代码虽然不会被执行,但是整个代码有解析的环节,解析的时候就已经把 变量 i 给提前声明了。

总结:

既然在ES5 中存在变量提升的现象,那么,在实战开发中,为了避免出错,建议先声明一个变量,然后再使用这个变量。

函数的声明提前(函数提升)

函数声明:

使用函数声明的形式创建的函数function foo(){},会被声明提前。

也就是说,整个函数会在所有的代码执行之前就被创建完成。所以,在代码顺序上,我们可以先调用函数,再定义函数。

代码举例:

复制代码
 fn1(); // 虽然 函数 fn1 的定义是在后面,但是因为被提前声明了, 所以此处可以调用函数
 ​
 function fn1() {
     console.log('我是函数 fn1');
 }

函数表达式:

使用函数表达式创建的函数const foo = function(){},不会被声明提前,所以不能在声明前调用。

很好理解,因为此时只是变量 foo 被提升了,且值为 undefined,并没有把 function(){} 赋值给 foo。

所以,下面的例子会报错:

复制代码
 // 不会报错,可以正常执行函数,正常打印结果
 fun1();
 ​
 // 此时 fun2 相当于 undefined。执行时会报错:Uncaught ReferenceError: Cannot access 'fun2' before initialization
 fun2();
 ​
 // 函数声明,会被提前声明
 function fun1() {
   console.log('我是 fun1 函数');
 }
 ​
 // 函数表达式,不会被提前声明
 const fun2 = function () {
   console.log('我是 fun12 函数');
 };

函数提升优先于变量提升

在JS的规则中,函数提升优先于变量提升。来看看下面这段代码,你认为打印结果应该如何:(这是一道经典面试题)

复制代码
fun(); // 打印 B

// 变量提升
var fun = function () {
  console.log('A');
};

// 函数提升
function fun() {
  console.log('B');
}

fun(); // 打印 A

打印结果:

复制代码
B
A

当声明被提前后,上方代码的实际顺序可以这样理解:(把它当成伪代码理解即可)

复制代码
/*伪代码*/

// 函数提升
function fun() {
  console.log('B');
}

var fun = undefined;

fun(); // 打印 B

fun = function () {
  console.log('A');
};

fun(); // 打印A

当然,上方代码是ES5写法,如果把 var 改成ES6中的 let/const,代码会报错Uncaught SyntaxError: Identifier 'fun' has already been declared。也就是说,ES6中不需要关心谁优先提升的问题了。

函数作用域的预处理

上一段讲的是全局作用域中的声明提前。在函数作用域中,也有声明提前的现象:

  • 函数中,使用 var 关键字声明的变量,会在函数中所有代码执行之前被提前声明。

  • 函数中,没有 var 声明的变量都是全局变量,且并不会被提前声明。

举例:

复制代码
var a = 1;

function foo() {
    console.log(a);
    a = 2; // 此处的a相当于window.a
}

foo();
console.log(a); //打印结果是2

上方代码中,执行 foo() 后,函数里面的打印结果是1。如果去掉第一行代码,执行 foo() 后,函数里面的打印结果是Uncaught ReferenceError: a is not defined。

补充:定义形参就相当于在函数作用域中声明了变量。举例如下:

复制代码
function fun(e) {
    // 这个函数中,因为有了形参 e,此时相当于在函数内部的第一行代码里,写了 var e;
    console.log(e);
}

fun(); //打印结果为 undefined
fun(123); //打印结果为123

ES5中没有块级作用域

在其他编程语言中(如 Java、C#等),存在块级作用域,由{}包括起来。比如在 Java 语言中,if 语句里创建的变量,只能在 if 语句内部使用:

复制代码
if(true){
    int num = 123;
    system.out.print(num); // 123
}
system.out.print(num); // 报错

但是,在 ES5 中没有块级作用域。举例如下:

复制代码
if (true) {
    var num = 123;
    console.log(num); //123
}

console.log(num); //123(可以正常打印)

作用域链

先来认识函数的嵌套:

  • 只要是代码,就至少有一个作用域

  • 函数内部有局部作用域

  • 如果函数内部还嵌套了函数,那么在这个作用域中就又诞生了另一个作用域。

基于上面几条内容,我们可以得出作用域链的概念。

作用域链 :在嵌套函数中,变量会从内到外逐层寻找它的定义(查找时,采用就近原则)。也就是说,采用的是链式查找的方式来决定取哪个值,这种结构称之为作用域链。

代码举例:

复制代码
var num = 10;

function fn() {
    // 外部函数
    var num = 20;

    function fun() {
        // 内部函数
        console.log(num);
    }
    fun();
}
fn();

打印结果:20。

写在最后:希望大家可以点个关注点个赞,这对up真的很重要!谢谢!
相关推荐
xxwl5851 分钟前
RabbitMQ 学习笔记
笔记·学习·rabbitmq
计算机魔术师19 分钟前
DeepSeek 据报道接近完成至少 800 亿元融资,腾讯与宁德时代参与
前端
可乐ea39 分钟前
多模型编排的三层:框架、模型路由与提供商路由
前端·网络·人工智能·ai智能体·多智能体协作·多模型编排·大模型路由
李日华大战鸡红2 小时前
滑膜观测器(学习记录)
stm32·单片机·学习·matlab·机器人
青柠之夏cc2 小时前
前端拖拽功能原生实现,不引入拖拽库完成业务
开发语言·前端·python
liangshanbo12153 小时前
前端大文件切片上传怎么做?
前端
传奇开心果编程3 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第5课 网络与数据层
android·网络·学习·ui·ios·kotlin·composer
Sand(ContextGate)3 小时前
Python Agent 测试实战:测试与评估,让 Agent 像传统软件一样可交付
前端·javascript·python·microsoft·ai
广州华水科技3 小时前
单北斗GNSS变形监测系统在城市安全与地质灾害中的应用前景
前端