什么是作用域

作用域是代码运行时某些特定的部分中变量、函数和对象的可访问性,换句话说,作用域决定了代码块中变量和其他资源的可见性

作用域共有两种工作模型

  • 词法作用域(静态作用域)
  • 动态作用域

词法作用域

词法作用域也被称为静态作用域,就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。JS作用域采用的就是词法作用域

举个例子:

var a = 2
function foo() {var b = 2 console.log(a + b)
}
foo() // 4

foo函数定义是就已经确定了它能拿到哪些变量,如果foo函数中有a,则直接拿去计算,否则会往上一层去寻找。

例子2:

var a = 2
function bar() {console.log(a)
}
function foo() {var a = 3bar()
}
foo() // 2

bar只会从它定义的位置往上查找,即找到a2

动态作用域

函数的作用域在函数调用时候确定,bash便是动态作用域

将以下代码存为shell执行最终结果输出为:3

#!/bin/bash
a=2
foo(){echo $a
}
bar(){a=3foo
}
bar

Javascript中的作用域

JavaScript中作用域分为两类

  • 全局作用域
  • 局部作用域

全局作用域

整个JavaScript文档就是一个全局作用,变量定义在函数之外,那么变量就是全局范围的

var a = 1

可以在任意其他范围内访问和更改

var a = 1console.log(a) // 1function bar() {console.log(a)
}bar() // 1

局部作用域/函数作用域

函数内定义的变量就在局部作用域内,变量绑定到函数,每个函数都有不同的作用域,并且在其他函数中是不可访问的

// 全局作用域
function foo() {// 局部作用域 var a = 1console.log(a) // 1
}
// 全局作用域
function bar () {// 局部作用域 console.log(a) //  Uncaught ReferenceError: a is not defined
}

作用域细分

嵌套作用域

当一个块或函数嵌套在另一个或函数中时,就发生了作用域嵌套,因此在当前作用域中无法找到某个变量,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或抵达最外层的作用域为止

function foo(a) {console.log(a + b)
}
var b = 2
foo(2) // 4

foob是未定义的,便会往上一层作用域查找,找到后停止查找。

作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域可以定义同名的标识符,这叫“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)

例:

var a = 2
function foo() {var a = 3function bar () {console.log(a)}return bar;
}
foo()() // 3

块作用域

与函数不同、ifswitchwhilefor这样的语句不会创建新的作用域,会形成块作用域。如果使用var定义,最终会属于外部作用域

if (true){var a = 1
}
if (false) {var b = 2
}
console.log(a) // 1
console.log(b) // undefined 在块作用域中, 当使用var声明时,写在哪里最终都会属于外部作用域for (var i = 0;i < 10;i++){}
console.log(i) // 10

使用try catchwith语句会创建块作用域

try {undefined() // 抛出一个错误
} catch (err) {console.log(err) // 正常执行
}
console.log(err) // Uncaught ReferenceError: err is not defined

ES6中的letconst关键字支持在块语句内声明局部作用域

if (true){var a = 1let b = 2const c = 3
}
console.log(a) // 1
console.log(b) // Uncaught ReferenceError: b is not defined
console.log(c) // Uncaught ReferenceError: c is not defined

也可以进行显示的创建

if (true) {var a = 1{let b = 2const c = 3}
}
console.log(a) // 1
console.log(b) // Uncaught ReferenceError: b is not defined
console.log(c) // Uncaught ReferenceError: c is not defined

利用块作用域进行垃圾回收

function process(data) {}
var someReallyBigData = { ... }
process(someReallyBigData)
var btn = document.getElementById("my_button")
btn.addEventListener("click", function click(evt) {console.log("button clicked")
})

click函数的点击回调并不需要someReallyBigData变量。理论上当process(..)执行后,在内存中占用大量空间的数据结构就可以被回收了。但是,由于click函数形成了一个覆盖整个作用域的闭包JavaScript引擎极有可能依然保存着这个结构

使用块作用域可以解决这个问题

function process(data) {}
{let someReallyBigData = { ... }process(someReallyBigData)
}var btn = document.getElementById("my_button")
btn.addEventListener("click", function click(evt) {console.log("button clicked")
})

提升

声明从他们在代码中出现的位置被移动到最上面,这个过程叫做变量提升

变量的提升

看以下代码

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

导致上面结果的原因是引擎会在解释Javascript代码前首先对其进行编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来

当编译器看到var a = 2时,会将其看成两个声明var a;
a = 2;第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行
所以上述代码会以以下形式处理

var a;
a = 2;
console.log(a)
var a;
console.log(a)
a = 2 // 留在原地

上述整个过程就是提升,总的来说先有声明后有赋值

注意:只有声明本身会被提升,而赋值或其他运行逻辑会留在原地

函数的提升

看以下代码

foo()function foo() {console.log(a) // undefinedvar a = 2;
}

foo函数的声明被提升了,因为第一行可以正常调用执行,上述代码可以理解为下面这种形式

function foo() {var a;console.log(a)a = 2
}foo()

值得注意的是函数表达式不会被提升

foo() // Uncaught TypeError: foo is not a function
var foo = function bar() {}

上述代码可以理解为以下形式

var foo;
foo()
foo = function() {var bar = ...self...}

函数优先

函数声明和变量声明都会被提升。函数会首先被提升,然后才是变量。

例:

foo() // 1
var foo; // 声明变量
function foo() {console.log(1)
} // 声明函数foo = function() {cosnole.log(2)
}

结果会输出1,如果是变量先提升则会出现TypeError
可以转成以下形式理解:

function foo() {console.log(1)
}foo() // 1foo = function() {console.log(2)
}

var foo虽然出现在function foo()...的声明之前,但它是重复的声明,直接被忽略。因为函数声明会被提升到普通变量之前。

注意:后面的函数声明可以覆盖前面的
例:

foo(); // 3function foo() {console.log(1)
}var foo = function() {console.log(2)
}function foo() {console.log(3)
}

尽量避免写出这种代码

执行环境(执行上下文)

执行环境(执行上下文)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中

全局执行环境

全局执行环境在WEB浏览器中就是window对象,因此所有的全局变量和函数都是作为window的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境知道应用程序退出 - 关闭网页或浏览器时才会被销毁

函数执行环境

每一个函数都有一个执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。在函数执行之后,栈将其环境弹出,控制权返回给之前的执行环境,浏览器始终执行位于堆栈顶部的执行环境

执行环境有两个阶段

1. 创建阶段

当函数倍调用但其代码尚未执行时

  • 创建变量(活动)对象
  • 创建作用域链
  • 设置上下文,即this

变量对象

也被称为活动对象,包含执行环境中的定义的所有变量、函数和其他声明,当一个函数被调用,解释器会扫描它所有的资源包括函数参数、变量和其他声明。变量对象最开始值包含arguments对象

'variableObject': {}

作用域链

作用域链实在变量对象之后创建的。作用域链本身包含变量对象。作用域链是保证对执行环境有权访问的所有变量和函数的有序访问

'scopeChain': {}

把执行环境抽象成一个对象:

executionContextObject = {'variableObject': {},'scopeChain': {},'this': {}
}

例:

function foo () {var a = 1;function bar () {var b = 1;}bar()
}

在创建时
foo()的活动对象为:arguments,a,bar
foo()的作用域链为:foo > window
foo()thiswindow
foo()的执行环境为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhY5XUnQ-1639723468249)(evernotecid://328B33FE-ED00-4B09-8BC9-557510243583/appyinxiangcom/33490301/ENResource/p120)]

2. 执行阶段

在执行阶段,变量对象中的值会被赋值,代码最终被执行

作用域链和自由变量

作用域

各个作用域的嵌套关系组成了一条作用域链,当一个代码在执行环境中执时,会创建变量对象的一个作用域链。

例:


function foo () {var a = 1function bar() {var a = 2}
}

该例中
bar函数的作用域链为bar>foo>window(全局)
foo保存的作用域链为foo>window(全局)

使用作用域链主要是进行标识符(变量和函数)的查询,标识符(变量和函数)解析就是沿着作用域链一级一级地所搜标识符的过程,而作用域链就是保证对变量和函数的有序访问

自由变量

什么是自由变量

在当前作用域中存在,但并未当前作用域中声明的变量

例:

var b = 2;
function foo() {var a = 1function bar() {var c =  b + a}
}

上例中
babar作用域中未声明,所以ab为自由变量
一旦出现自由变量,就肯定会有作用域链,再根据作用域链查找机制,查找到对应的变量。

结语

作用域和作用域链作为Javacript的基础是非常重要的一环,搞懂了这个对学习闭包有很大的帮助。

参考文档

JavaScript高级程序设计
你不知道的JavaScript
Understanding Scope in JavaScrip


千羽的博客

深入理解Javascript作用域和作用域链相关推荐

  1. 深入理解JavaScript的变量作用域

    在学习JavaScript的变量作用域之前,我们应当明确几点: a.JavaScript的变量作用域是基于其特有的作用域链的. b.JavaScript没有块级作用域. c.函数中声明的变量在整个函数 ...

  2. 深入理解JavaScript的变量作用域(转)

    在学习JavaScript的变量作用域之前,我们应当明确几点: a.JavaScript的变量作用域是基于其特有的作用域链的. b.JavaScript没有块级作用域. c.函数中声明的变量在整个函数 ...

  3. 深入理解JavaScript的变量作用域(转载Rain Man之作)

    在学习JavaScript的变量作用域之前,我们应当明确几点: JavaScript的变量作用域是基于其特有的作用域链的. JavaScript没有块级作用域. 函数中声明的变量在整个函数中都有定义. ...

  4. 真丶深入理解 JavaScript 原型和原型链(二):原型和原型链

    原文地址: https://www.jeremyjone.com/745/,转载请注明. 上一篇文章已经总结了关于原型的两个属性,那么接下来所有原型和原型链,乃至后面的继承,都与这两个属性有关系. 原 ...

  5. 深入理解javascript原型和原型链

    文章目录 构造函数 1. new一个新对象的过程,发生了什么? 2. 手写new函数 3. 构造函数上的方法 原型 1. 什么是原型? 2. 原型的作用是什么? 3. 原型中this的指向是什么? 函 ...

  6. JavaScript中的作用域,闭包和上下文

    深入理解JavaScript中的作用域和上下文 很多语言当中都会有作用域的概念,它会给我们带来便利,偶尔也会有烦恼,只有清楚地理解和掌握了它,才能更好地为我所用,今天就带来这么一篇文章供大家参考. 介 ...

  7. 深入理解javascript原型和闭包(2)——函数和对象的关系

    上文(理解javascript原型和作用域系列(1)--一切都是对象)已经提到,函数就是对象的一种,因为通过instanceof函数可以判断. var fn = function () { }; co ...

  8. JavaScript 开发进阶:理解 JavaScript 作用域和作用域链(上)

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望 ...

  9. JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望 ...

最新文章

  1. python运维开发之第十一天(RabbitMQ,redis)
  2. JVM调优:开启/关闭TLAB和逃逸分析耗时对比
  3. c++求平均值_云顶之弈S4:六射手娱乐吃鸡!人均刮痧王,每个人都是主C
  4. 关于Android 传感器坐标与读数的进一步讨论
  5. 《Java程序设计》第2周学习总结
  6. 聚类分析(三)Mini Batch KMeans算法
  7. 吃了一辈子土豆,原来这么多吃法还治病,真要好好学学!
  8. 「人物特写」国产手机操作系统突围任重而道远,创新才是关键
  9. python灰度处理_python 简单图像处理(14) 灰度图腐蚀和膨胀,开运算、闭运算...
  10. 使用静态库的一些问题 -all_load
  11. 在阿帕奇服务器布置文件,Apache文件列表服务器美化 index of /
  12. 串口控件MSCOMM的注册方法(使用MSCOMM串口控件程序的运行问题)
  13. 为什么阿里云域名解析48小时还没有生效?
  14. 快手“老”矣,尚能饭否?
  15. python 文字识别 tesseract_Python_文字识别引擎试用:tesseract-ocr
  16. MySQL源代码的海洋中游弋 初探MySQL之SQL执行过程 [转]
  17. python的时间转换datetime和pd.to_datetime
  18. unity3D游戏开发十之粒子系统
  19. Kafka知识总结之Broker原理总结
  20. .net4.0注册到IIS ,重新注册IIS ,iis注册

热门文章

  1. 实验三+087+饶慧敏
  2. 网上订房系统的设计实现
  3. Android-App性能测试工具GT的使用方法
  4. Python通过手肘法实现k_means聚类
  5. 深度linux系统和win7,国产操作系统Deepin Linux(深度系统)安装体验-深度win7
  6. SDCMS1.31调用指定栏目信息的代码大全及调用方法
  7. JVM之方法调用-分派
  8. leetcode题目:求一个非负数n的平方根
  9. qt去掉文件路径中的最后一个路径内容
  10. 北京理工大学计算机基本介绍