什么是作用域
作用域是代码运行时某些特定的部分中变量、函数和对象的可访问性,换句话说,作用域决定了代码块中变量和其他资源的可见性
作用域共有两种工作模型
- 词法作用域(静态作用域)
- 动态作用域
词法作用域
词法作用域也被称为静态作用域,就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。
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 = 3
bar()
}
foo() // 2
bar
只会从它定义的位置往上查找,即找到a
为2
动态作用域
函数的作用域在函数调用时候确定,
bash
便是动态作用域
将以下代码存为shell
执行最终结果输出为:3
#!/bin/bash
a=2
foo(){
echo $a
}
bar(){
a=3
foo
}
bar
Javascript中的作用域
在JavaScript
中作用域分为两类
- 全局作用域
- 局部作用域
全局作用域
整个JavaScript文档就是一个全局作用,变量定义在函数之外,那么变量就是全局范围的
var a = 1
可以在任意其他范围内访问和更改
var a = 1
console.log(a) // 1
function bar() {
console.log(a)
}
bar() // 1
局部作用域/函数作用域
函数内定义的变量就在局部作用域内,变量绑定到函数,每个函数都有不同的作用域,并且在其他函数中是不可访问的
// 全局作用域
function foo() {
// 局部作用域
var a = 1
console.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
在foo
中b
是未定义的,便会往上一层作用域查找,找到后停止查找。
作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域可以定义同名的标识符,这叫“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)
例:
var a = 2
function foo() {
var a = 3
function bar () {
console.log(a)
}
return bar;
}
foo()() // 3
块作用域
与函数不同、if
、switch
、while
、for
这样的语句不会创建新的作用域,会形成块作用域。如果使用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 catch
、with
语句会创建块作用域
try {
undefined() // 抛出一个错误
} catch (err) {
console.log(err) // 正常执行
}
console.log(err) // Uncaught ReferenceError: err is not defined
ES6中的let
和const
关键字支持在块语句内声明局部作用域
if (true){
var a = 1
let b = 2
const 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 = 2
const 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) // undefined
var 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() // 1
foo = function() {
console.log(2)
}
var foo
虽然出现在function foo()...
的声明之前,但它是重复的声明,直接被忽略。因为函数声明会被提升到普通变量之前。
注意:后面的函数声明可以覆盖前面的
例:
foo(); // 3
function 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()
的this
为window
foo()
的执行环境为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhY5XUnQ-1639723468249)(evernotecid://328B33FE-ED00-4B09-8BC9-557510243583/appyinxiangcom/33490301/ENResource/p120)]
2. 执行阶段
在执行阶段,变量对象中的值会被赋值,代码最终被执行
作用域链和自由变量
作用域
各个作用域的嵌套关系组成了一条作用域链,当一个代码在执行环境中执时,会创建变量对象的一个作用域链。
例:
function foo () {
var a = 1
function bar() {
var a = 2
}
}
该例中
bar
函数的作用域链为bar
>foo
>window
(全局)
foo
保存的作用域链为foo
>window
(全局)
使用作用域链主要是进行标识符(变量和函数)的查询,标识符(变量和函数)解析就是沿着作用域链一级一级地所搜标识符的过程,而作用域链就是保证对变量和函数的有序访问
自由变量
什么是自由变量
在当前作用域中存在,但并未当前作用域中声明的变量
例:
var b = 2;
function foo() {
var a = 1
function bar() {
var c = b + a
}
}
上例中
b
和a
在bar
作用域中未声明,所以a
和b
为自由变量
一旦出现自由变量,就肯定会有作用域链,再根据作用域链查找机制,查找到对应的变量。
结语
作用域和作用域链作为Javacript
的基础是非常重要的一环,搞懂了这个对学习闭包有很大的帮助。
参考文档
JavaScript高级程序设计
你不知道的JavaScript
Understanding Scope in JavaScrip