作用域基础和调用栈
前言
作用域和调用栈是程序设计中两个重要的概念,它们共同影响着程序的执行流程和数据访问。作用域定义了变量的可见性和生命周期,而调用栈则通过管理执行上下文来确保在函数调用和返回时能够正确地访问和操作这些变量。了解这两个概念及其关系对于编写高效、可靠和可维护的代码至关重要。
一,什么是作用域
在JavaScript的编程世界中,作用域([[scope]])是一个至关重要的概念。它定义了变量、函数和表达式在代码中的可见性和生命周期。当我们谈论变量时,我们不仅仅是在谈论它们存储的值,更是在谈论这些值在何处、何时以及如何被访问和修改。作用域正是这个“何处”和“如何”的关键所在。
在接下来的篇幅中,我们将首先介绍作用域的基本概念,包括全局作用域、局部作用域以及函数作用域。然后,我们将深入探讨块级作用域(ES6引入)和变量提升等话题,帮助你更全面地理解JavaScript中的作用域机制。
二,作用域的规则
在了解作用域之前我们先牢记作用域的规则:
内层作用域可以访问外层作用域,外层作用域不能访问内层作用域; 在全局作用域定义的变量影响不到局部作用域,局部作用域可以访问外部作用域的变量。把变量都隐藏到函数的内部这个行为,就是大家常说的封装。
三,作用域的类型
- 全局作用域
全局作用域是指在整个代码执行过程中都有效的作用域,它是最外层的作用域。在全局作用域中定义的变量和函数都是全局可见的,可以在程序的任何地方调用和访问。
- 函数作用域
在编程中,函数的作用域是指函数内部声明的变量、参数和函数自身的可访问范围。当一个函数被定义时,它创建了一个局部作用域,这个作用域包含了函数体内部的所有代码。
通俗点来讲就是如图所示
整个大框框就是window,他就是全局作用域;函数的域可以嵌套,最里面的函数可以依次访问外部作用域。图中的bar函数他访问了foo函数中的内部定义的局部变量b和作用域中的a,因为abc参数都能成功访问,最后a+b+c才能被成功打印出来
- 块级作用域
块级作用域(Block Scope)是指在一组大括号{}
内定义的变量、函数或类。在JavaScript中,使用let
或const
关键字声明的变量具有块级作用域。(通俗讲就是{}
配上let ``const
)它们只在最近的封闭代码块(例如大括号 {}
内部)中有效。
在ES6之前是没有块级作用域的,他的出现其实是为了解决var
的声明提升问题,举个例子
console.log(a);
var a = 1
因为var
存在声明提升,实际上代码的执行如下:
var a
console.log(a);
a = 1
所以上面代码执行会出现一个undefined
结果。因为var
关键字声明的变量会经历一个“提升”的过程。这意味着无论 var
声明出现在代码的什么位置,它都会被“提升”到其所在作用域的最顶部。但是,只有声明会被提升,赋值操作仍然保留在原来的位置。这导致了很多麻烦。于是let
和const
给代码提供了许多便利。我们来看下面这两串代码的不同:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000)
}
//结果10个10
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000)
}
//结果0123456789
为什么会出现这样的结果呢?还是因为var
将i声明到了全局作用域导致每一次循环的i都没有改变。由于 var
的函数作用域特性,所有的 setTimeout
回调函数都会引用同一个 i
变量,并且该变量是外部函数中的 i
;当您使用 let
而不是 var
在 for
循环中声明变量 i
时,每个迭代都会为 i
创建一个新的块级作用域绑定。这意味着每次循环时,i
的当前值都会被捕获在每次迭代的 setTimeout
回调函数的闭包中。
这么看let确实带来了许多便利,但同时我们也要注意避免暂时性死区的问题
暂时性死区
暂时性死区是指从变量声明开始(let
或 const
)到初始化完成之前的区域。在这个区域内,不能引用这个变量,否则会导致一个引用错误。以下是因为 let
声明在 console.log(a)
之后,但在同一个作用域内导致的错误。
if (1) { console.log(a); // 尝试访问在声明之前的 a,但 a 还没有被声明
let a = 2; // 声明并初始化变量 a }
要解决这个问题就必须保证在引用 a
之前确保它已经被声明和初始化,所以我们可以在全局声明一个a
加以改正。
四,var
vs let
经过我们的总结,vas和let存在以下不同:
- var存在声明提升(全局对象属性,在在浏览器中是
window
对象,在Node.js
中是global对象),let不存在 - let会和{}形成块级作用域
- var可以重复声明变量,let不可以
- let在块级作用域中因为没有声明提升导致访问不到变量会造成暂时性死区
五,栈
栈的基本概念
栈(Stack)是一种特殊的线性数据结构,它遵循后进先出的原则进行数据的存储和访问。栈中的数据元素被称为栈帧或栈项,它们被依次压入(push)到栈的顶部,并从栈的顶部依次弹出(pop)。只能在栈的顶部增加或者删除元素
修改属性的方法
数组
arr.push()
尾部增加
arr.pop()
尾部删除
arr.unshift()
头部增加
arr.shift()
头部删除
arr.splice(1,)
中间插入
使用splice()
的规则:
let arr = [1, 2, 3, 4, 5]; // 删除从索引1开始的2个元素
let removed = arr.splice(1, 2);
console.log(arr); // [1, 4, 5]
console.log(removed); // [2, 3] // 从索引1开始添加元素
arr.splice(1, 0, 'a', 'b', 'c');
console.log(arr); // [1, 'a', 'b', 'c', 4, 5]
栈
只有arr.push()
尾部增加arr.pop()
尾部删除两个方法
调用栈
在JavaScript中,调用栈(Call Stack)是一个非常重要的概念,它负责管理函数的调用和返回。当一个函数被调用时,它会被添加到调用栈的顶部,并且开始执行。当函数执行完毕或者遇到返回语句时,它会被从调用栈中移除,控制权返回给调用它的函数。
六,作用域和调用栈的关系
当函数被调用并添加到调用栈时,该函数会创建它自己的作用域链。作用域链是一个包含函数自身的作用域、其父级作用域以及一直延伸到全局作用域的对象链。这个作用域链在函数执行期间是固定的,并且可以被函数内部的代码所访问。
每次函数执行时(即函数在调用栈中时),都会使用该函数的作用域链来解析变量和函数引用。
总结:作用域是在编写代码时确定的,它决定了变量和函数的可访问性。而调用栈是在运行时管理的,它跟踪函数执行的顺序。当函数执行时,它使用自己的作用域链来解析变量和函数引用。这两个概念在函数执行过程中是相互关联的。调用栈用于维护上下文对象。
七,拓展
欺骗词法作用域
eval()
将原本不属于这里的代码变在这里,在后面学习时用于打造框架时常用
with(){}
用于修改一个对象中的属性值,但如果修改的属性在原对象中不存在,那么属性就会被泄露到全局
自执行函数
是JavaScript中的一种技术,它允许你定义一个函数,并且该函数定义完成后会立即被调用。这种函数不会在任何地方被调用,除了它自己的定义。自执行函数常用于创建局部作用域,避免变量污染全局作用域。
转载自:https://juejin.cn/post/7366449497062817803