2022/01/愿世间再无人不懂作用域/index

作用域是啥

作用域几乎是最基本的功能了,可以用来存储变量,方便我们之后对这些变量进行访问或修改。

675456E07FB54B9C2F705A5CF7A196CE.gif

我们举个简单的栗子🌰来方便理解:

1
var nubility = 'Hello Varlet!'

我们来解析一下浏览器拿到这串代码后做的事情

  • 编译器会查看作用域中是否已经存在一个nubility。如果有了,那么编译器会忽略var声明继续进行编译;否则将会要求作用域声明一个变量nubility

  • 接下来编译器会生成用来处理nubility = 'Hello Varlet!'赋值操作的代码并交给引擎

  • 引擎拿到代码后会先询问作用域中是否存在nubility这个变量。如果有,那么引擎就会用这个变量,否则则会继续上上一层作用域中继续查找该变量

  • 如果作用域中找到了nubility这个变量,那么就会'Hello Varlet!'赋值给它,否则引擎将会抛出一个异常

1
VM418:1 Uncaught ReferenceError: nubility is not defined

词法作用域

我们先介绍一下词法作用域这个命名的来历。

编译器拿到我们编写的源代码后先将代码进行词法化,这个过程中会对源代码中的字符进行检查,如果是有状态的解析过程则会赋予单词语义。(也成为静态作用域)

一句话总结:词法作用域就是定义在词法阶段的作用域。

语义化就是,在我们编写代码时由我们的变量和块级作用域写在哪里决定的。

举个🌰

image.png

  • 黄色的框包含全局并且只有一个标识符a

  • 蓝色的框为a创建的作用域,包含三个标识符namebauthor

  • 红色部分包含b创建的作用域,其中只有一个score标识符

从这边我们可以看出这些对应的框由其作用域块代码的边写决定的,向下层逐级包含。

作用域的结构以及互相之间的位置关系可以给引擎提供用于查找标识符的位置信息。

我们一起来解析一下上面的代码

  • 引擎在执行console.log(author,name,score)时会查找author,name,score这三个变量的引用

  • 先在b的作用域中查找,引擎在这里无法找到author,name,所以会向上一级a的作用域中继续查找

  • a的作用域中,引擎找到了authorname,就使用了这里的引用

总结一下:作用域查找会在找到第一个匹配的标识符时停止查找

在多级嵌套的作用域中,可以定义同名标识符,作用域的查找从运行时所在的最内层向外查找,找到第一个匹配的标识符为止。

⚠️⚠️⚠️⚠️⚠️⚠️

其实这也不是绝对的

895625EE5560EB2873470B4A254502F9.gif

😳😳😳可恶啊居然还有?先摆个烂歇会~

C4D4506256984E0951AE70EF2D39C7AF.gif

ok,时间到😋摆烂结束。接下来我们聊聊欺骗词法

js中有两种机制可以在运行时修改词法作用域,一个是eval另一个是with。社区中普遍认为这种机制会导致性能下降,因此我们不做过多介绍,本篇中仅以eval为例进行分析。

先介绍一下eval的功能:可以接收一个字符串作为参数,并将其中的内容作为书写在程序该位置的代码。

举个🌰:

1
2
3
4
5
6
7
function a(str){
eval(str)
console.log(num)
}
var num = 1

a('var num = 2')

eval调用中的var num = 2,会被当做在a的作用域中来处理,对a的词法作用域进行了修改,从而遮蔽了外部的同名变量num,因此引擎在执行console.log时只会在a内部找到这个num.

函数作用域

函数作用域是指:属于这个函数的全部变量都可以在整个函数的的范围内使用以及复用(包括嵌套的子作用域)。

在软件设计中,我们遵循最小暴露原则。也就是说我们可以使用内嵌作用域来对一些变量和函数进行私有化,从而避免污染全局。

举个🌰

1
2
3
4
5
6
7
8
9
10
11
12
function a(num){
b = num + c(num)
console.log(b)
}

var b

function c(num){
return 2*num
}

a(2)

在这个例子中,变量b和函数c都应该是实现a具体操作的私有内容,放在全局的话可能会被有意或无意的被修改成非预期的方式,因此更合理的方式是将bc隐藏咋函数a的内部.

1
2
3
4
5
6
7
8
9
10
11
12
function a(num){
var b

function c(num){
return 2*num
}

b = num + c(num)
console.log(b)
}

a(2)

块级作用域

块级作用域是用来最小暴露原则进行拓展的工具,将代码从在函数内隐藏信息拓展为在块内隐藏信息。

举一个常见的例子

1
2
3
for(var i = 0;i<996;i++){
console.log(i)
}

i其实只在for的循环内部使用,但是却污染到了整个函数作用域中,我们如果使用块级作用域可以使其只能在for的循环内部使用,这对保证变量不会被混乱的复用,以及提升代码的可维护性有很大的益处。

try/catch

ES3规范中try/catchcatch分句会创建一个块级作用域,其中声明的变量只在catch生效

1
2
3
4
5
6
7
8
try{
undefined() //随便执行一个能让他出错的
}
catch(e){
console.log(e) //能正常执行
}

console.log(e) //VM20919:8 Uncaught ReferenceError: e is not defined

let

ES6中提供了let关键字,可以将变量绑定到任意的作用域,通常是定义改变量所在的{...}中。

也就是说,let关键字可以为其声明的变量隐式创建块作用域。

const

除了let以外ES6还引入了const,同样可以用来创建块作用域变量,但其值时固定的。之后任何修改值的操作都会抛出错误。

image.png

最后

以上就是本人对于作用域的一些见解,如有任何问题或建议,欢迎留言讨论!

顺便推荐一下本人参与的开源项目 varlet 欢迎大家star pr

F1854D82AA4684E53C033B8186B17A42.jpg

文章作者: Joker
文章链接: https://qytayh.github.io/2022/01/%E6%84%BF%E4%B8%96%E9%97%B4%E5%86%8D%E6%97%A0%E4%BA%BA%E4%B8%8D%E6%87%82%E4%BD%9C%E7%94%A8%E5%9F%9F/index/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Joker's Blog