深入底层探析JS作用域

前置知识点

(本文讨论的皆为非严格模式情况)

了解编译原理

我们都知道一段代码在执行之前都会先进行编译,编译分为三个步骤 词法分析、解析和代码生成。

  1. 词法分析 : 拿 var a = 2 这行代码作分析,其会被分解成 var、a、=、2 这几个词法单元。空格是否会被当作词法单元取决于空格在这门编程语言中是否有意义,而等号两边的空格是一定没有意义的。

  2. 解析: 将词法单元解析成一个逐级嵌套的程序语法结构树 --也叫抽象语法树。

  3. 代码生成 : 将抽象语法树转换为可执行代码。虽然最后生成出来依旧是 var a = 2这行代码,但在JS引擎看来这跟最初用户在编辑器上写下来的就是不一样的。

了解有效标识符

标识符指开发人员为变量、属性、函数、参数声明的名字,标识符不能是JS的关键字或保留字。而有效标识符则是在某一个域中有作用的标识符

以下面代码举例,a, b(变量声明), bar(函数声明) 是foo的有效标识符,而bar中的有效标识符只有c

scss 复制代码
function foo(a) {
    var b = 2
    function bar(c) {
        console.log(a + b + c);   // 6  
    }
    bar(3)
}

foo(1)

作用域

作用域[[scope]]是函数身上的隐式属性,用于存储函数中的有效标识符(或者说存储运行时执行上下文的集合),我们无法访问,是专门给js引擎访问的

以上那份代码中有三个域:全局的域、foo的域、bar的域,这些域都叫作用域。

在ES5及之前 ,JS中主要有全局作用域函数作用域两种。全局作用域中的变量或函数可以在整个程序中访问,函数作用域中的变量或函数只能在声明它们的函数体内访问。

由以上那份代码可知:内层作用域是可以访问外层作用域的 ,因为bar函数作用域可以访问到foo函数作用域中的a 和 b。但外层作用域是无法访问到内层作用域的,看下面的代码:

scss 复制代码
function foo() {
    var a = 1
}
foo()

console.log(a);  // error 全局无法访问到foo函数中声明的a

底层逻辑

为什么内层作用域可以访问外层作用域,反之不行呢?

因为JS引擎在执行一段代码前会创建一个调用栈,首先全局执行上下文入栈,里面存放全局中的有效标识符;然后函数执行上下文入栈,里面存放函数中的有效标识符,在执行函数时,查找变量从栈顶开始从上往下找,找到就返回,找不到就报错。

示例一:(上面那段代码)

示例二: 嵌套作用域的"遮蔽效应"------当内部作用域和外部作用域中有同名变量时,内部作用域中的变量会优先被使用

javascript 复制代码
var a = 1
function foo() {
    var a = 2
    console.log(a);    // 2
}

foo()

作用域链

作用域是执行期上下文对象的集合,这种集合呈链式连接,我们把这种链关系称之为作用域链。它描述了变量在何处以及如何被查找和解析的机制:

当js引擎需要查找一个变量时,它会从当前执行上下文的作用域链顶端开始,逐级向外层访问,直到全局作用域。如果在作用域链中找到了变量,就返回该变量;如果没有找到,则返回 undefined

词法作用域:

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域只由函数被声明时所处的位置决定

了解自执行函数

写法一: 在函数包含在一对()内部,并在末尾加上另外一个对()。第一对括号将函数变成一个表达式,第二对括号执行了这个函数。

javascript 复制代码
(function () {
    // 函数体
})()   

写法二:函数前加感叹号,没有一对括号包裹

javascript 复制代码
!function () {
    // 函数体
}();

块级作用域

自ES6起,引入了 letconst 关键字,它们可以在任意代码块(如 {} 中)创建块级作用域,使得变量在块级作用域内有效,而不会污染外部作用域。

let 会和 {} 形成块级作用域,而var不会

css 复制代码
for (var i = 0; i < 10; i++) {
    // ...
}
console.log(i);  // 10   
css 复制代码
for (let i = 0; i < 10; i++) {

}
console.log(i);  // error:i is not defined

let VS var

  1. let 会和 {} 形成块级作用域
  2. var 存在声明提升,let不存在 (了解声明提升可读探析js引擎的预编译机制 - 掘金 (juejin.cn)一文)
  3. var 可以重复声明变量,let不可以

let VS const

除了 const声明的变量不能访问外,其他语法一模一样

欺骗词法

1. eval

允许执行一个代码字符串,将原本不属于这里的代码变成就像天生就定义在了这里一样

示例: 执行eval(str)后,会在foo函数内部创建一个变量b,并遮蔽了外部(全局)作用域中的同名变量

css 复制代码
function foo(str, a) {
    eval(str)  // 将字符串变成代码 var b = 3
    console.log(a, b);    // 1 3
}
var b = 2
foo('var b = 3', 1)  

2. with

用于修改一个对象中的属性值,但如果修改的属性在原对象中不存在,那么该属性就会被泄露到全局。

示例一:

ini 复制代码
 var obj = {
     a: 1,
     b: 2,
     c: 3
 }
 with (obj) {
     a = 3
     b = 4
     c = 5
 }
 console.log(obj);   // { a: 3, b: 4, c: 5 }

示例二: with会为obj创建一个单独的词法作用域,内部的var声明不会被限制在这个域中。这个示例中 o2对象内部没有a这个属性,foo和全局作用域中也没有找到标识符a,因此当a = 2执行时,自动创建了一个全局变量(非严格模式)。

scss 复制代码
function foo(obj) {
    with (obj) {  
        a = 2
    }  
}

var o2 = {
    b: 3
}

foo(o2)
console.log(o2.a);  // undefined
console.log(a); // 2

注意! 这两个机制的副作用是JS引擎无法在编译时对作用域查找进行优化,这将导致代码运行变慢,一般不要去使用它们。

认真读完后相信你已经对js作用域有了更加深刻的理解,阅读过程中若发现有任何不足或亮点,欢迎大家在评论区指出交流,我们共同进步~

相关推荐
虾球xz10 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇15 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒19 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
余炜yw27 分钟前
【LSTM实战】跨越千年,赋诗成文:用LSTM重现唐诗的韵律与情感
人工智能·rnn·深度学习
小镇程序员35 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐37 分钟前
前端图像处理(一)
前端
莫叫石榴姐44 分钟前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘
程序猿阿伟44 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
96771 小时前
对抗样本存在的原因
深度学习
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript