深入理解Javascript作用域和作用域链

什么是作用域

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

作用域共有两种工作模型

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

词法作用域也被称为静态作用域,就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。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只会从它定义的位置往上查找,即找到a2

动态作用域

函数的作用域在函数调用时候确定,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

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

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

例:

var a = 2
function foo() {
    var a = 3
    function 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 = 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()thiswindow
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
    }
}

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

结语

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

参考文档

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


千羽的博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值