还没搞懂作用域、执行上下文、变量提升?看这篇就够啦

前言

 📫 大家好,我是南木元元,热爱技术和分享,欢迎大家交流,一起学习进步!

 🍅 个人主页:南木元元


目录

作用域(Scope)

全局作用域

函数作用域

块级作用域

作用域链(Scope Chain)

执行上下文

执行上下文的类型

执行上下文栈

创建执行上下文

(1)创建阶段

(2)执行阶段

作用域与执行上下文区别

解释阶段

执行阶段

变量提升

本质原因

小练习

结语


作用域(Scope)

在JavaScript中,作用域(Scope)是指代码中变量、函数和对象的可访问性范围,决定了变量或函数在何处可以被访问或引用。

JavaScript中有3种类型的作用域,包括:

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

全局作用域

在全局作用域中定义的变量在代码中任何地方都能访问到,常见的就是在最外层函数外面定义一个变量,该变量拥有全局作用域。

var globalVar = "I am global";

function test() {
  console.log(globalVar); // "I am global"
}

test();
console.log(globalVar); // "I am global"

除了上述情形外,还有几种情况拥有全局作用域:

  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域,如窗口名字window.name、页面URL地址window.location等

全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

函数作用域

函数作用域是指变量和函数在函数内部定义时,只在函数内部可见和可访问,函数外部无法访问函数内部的变量和函数(每当调用一个函数时,都会创建一个新的函数作用域)。

function localFunction() {
  var localVar = "I am local";
  console.log(localVar); // "I am local"
}

localFunction();
console.log(localVar); // ReferenceError: localVar is not defined

块级作用域

使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中创建(由一对花括号{}包裹的代码片段),使用var声明的变量不会有块级作用域。

if (true) {
  let blockVar = "I am in block";
  console.log(blockVar); // "I am in block"
}
console.log(blockVar); // ReferenceError: blockVar is not defined

作用域链(Scope Chain)

某个变量在当前作用域中找不到时,就会往它的上⼀级寻找,如果找到全局作用域还没找
到,就放弃寻找(返回 undefined),这种层级关系就是作用域链

作用域链的作用就是保证对变量和函数的有序访问

var globalVar = "I am global";

function outerFunction() {
  var outerVar = "I am outer";
  
  function innerFunction() {
    var innerVar = "I am inner";
    console.log(globalVar); // "I am global"
    console.log(outerVar); // "I am outer"
    console.log(innerVar); // "I am inner"
  }

  innerFunction();
}

outerFunction();

在上述代码中,innerFunction可以访问globalVar和outerVar,因为它们在外部作用域中。

需要注意的是,js采用的是静态作用域(词法作用域),函数的作用域在函数定义时就确定了(这里不再详细展开,推荐阅读JavaScript深入之词法作用域和动态作用域

执行上下文

当JavaScript代码执行一段可执行代码时,会创建对应的执行上下文,执行上下文就是代码在执行过程中的环境信息。

执行上下文的类型

1.全局执行上下文:这是默认的执行上下文,当JavaScript代码开始运行时首先创建。全局执行上下文中的代码在全局作用域中执行,并且只有一个全局执行上下文。

2.函数执行上下文:每当一个函数被调用时,会创建一个新的执行上下文。每个函数都有自己的执行上下文,函数执行上下文在函数调用时被创建,并在函数执行完毕后被销毁。

3.eval函数执行上下文eval函数内部代码会创建一个独立的执行上下文,但不推荐使用eval

执行上下文栈

JavaScript引擎使用执行上下文栈来管理执行上下文。

当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。

示例:

function f(a) {
  console.log(a);
}

function func(a) {
  f(a);
}

func(1);

假设用ESP指针来保存当前的执行状态,在系统栈中会产生如下的过程:

  1. 调用func, 将 func 函数的上下文压栈,ESP指向栈顶。

  2. 执行func,又调用f函数,将 f 函数的上下文压栈,ESP 指针上移。

  3. 执行完 f 函数,将ESP 下移,f函数对应的栈顶空间被回收。

  4. 执行完 func,ESP 下移,func对应的空间被回收。

 图示如下:

创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段。

(1)创建阶段

在 JavaScript 代码执行前,执行上下文将经历创建阶段。主要包括:

  • 创建变量对象(Variable Object, VO)

在ES5中称为变量对象,在ES6及以后称为词法环境组件,这个对象包含了执行环境中所有变量和函数。

对全局执行上下文,变量对象是全局对象;对函数执行上下文,变量对象包含函数的参数、局部变量和函数声明。

提升函数和变量声明:将函数声明提升到当前作用域的顶部,并存储在变量对象中;将变量声明提升到当前作用域的顶部,但不会初始化(初始化在执行阶段进行)。

  • 建立作用域链

创建作用域链,包含当前执行上下文的变量对象和其父级执行上下文的变量对象,直到全局对象。

作用域链本质上是一个指向变量对象的指针列表,作用域链的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

  • 确定this绑定

在全局执行上下文中,this指向全局对象;在函数执行上下文中,this取决于函数的调用方式。

(2)执行阶段

完成对变量的分配,在变量声明阶段被提升的变量,现在会被赋值,最后执行代码。

作用域与执行上下文

JavaScript属于解释型语言,其执行分为解释和执行两个阶段。

解释阶段

  • 词法分析:即分词,它的工作就是将一行行的代码分解成一个个token
  • 语法分析:将上一步生成的token数据,根据一定的语法规则转化为AST
  • 作用域规则确定

执行阶段

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。

作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

变量提升

变量提升是指使用var声明的变量以及函数声明会被提升到作用域顶部, 变量提升的结果,就是可以在变量初始化之前访问该变量(返回undefined),在函数声明前可以调用该函数。

console.log(a); // undefined
var a = 5;
console.log(a); // 5

在上述代码中,尽管console.log(a)在var a = 5之前调用,但代码不会报错,因为在执行代码之前,变量已经被提升。

同样,函数声明也会被提升:

foo(); // "Hello, world!"
function foo() {
  console.log("Hello, world!");
}

上述代码中,即使foo在函数声明之前调用,代码依然可以正常执行。

本质原因

变量提升的本质原因:js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

简单来说就是在执行JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

注意点

1.let和const声明的变量不会被提升。如果在声明前使用它们,会报错。

2.函数声明会被提升,函数表达式不会被提升。

小练习

说出下面代码的执行结果。

var foo = function () {
    console.log("foo1")
}
foo()

var foo = function () {
    console.log("foo2")
}
foo()


function foo() {
    console.log("foo1")
}
foo()

function foo() {
    console.log("foo2")
}
foo()

这是一道关于变量提升的题,具体就不展开了,可以看这道面试题真的很变态吗?

结语

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~ 

 

  • 134
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 109
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 109
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南木元元

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值