几乎所有的编程语言都有一个基本功能,就是能够存储变量的值,并且能在之后对这个值进行访问和修改。
那这些变量存储在哪里?怎么找到它?因为只有找到它才能对它进行访问和修改。
简单来说,作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)。
那么问题来了,究竟在哪里设置这些作用域的规则呢?怎样设置?
首先,我们要知道,一段代码在执行之前会经历三个步骤,统称为“编译”。
这个过程会将字符串分解成有意义的代码块,这些代码块称为词法单元。
var a = 1;
// 这段代码会被分解为五个词法单元:
var 、 a 、 = 、 1 、 ;
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表语法结构的树。这个树称为“抽象语法树(AST)”
这个过程是将AST转换为可执行的代码。
简单来说,用某种方法可以将
var a = 2;
的抽象语法树(AST)转化为一组机器指令,
指令用来创建一个叫作a的变量,并将一个值2存在a中
在这个过程中,有3个重要的角色:
所以,看似简单的一段代码 var a = 1; 编译器是怎么处理的呢?
var a = 1;
那么,引擎是如何查找变量的?
引擎会为变量 a 进行LHS查询(左侧)。另外一个叫RHS查询(右侧)
简单来说,LHS查询就是试图找到变量的容器本身(比如a);而RHS查询就是查询某个变量的值(比如1)
总结:作用域就是根据名称查找变量的一套规则。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
function add(a) {
console.log(a + b)
}
var b = 2;
add(1) // 3
在add()内部对b进行RHS查询,发现查询不到,但可以在上一级作用域(这里是全局作用域)中查询到。
怎么区分LHS和RHS查询?思考以下代码
function add(a) {
// 对b进行RHS查询 无法找到(未声明)
console.log(a + b) // 对变量b来说,取值操作
b = a // 对变量b来说,赋值操作
}
add(1) // ReferenceError: b is not defined
function add(a) {
// 对b进行LHS查询,无法找到,会自动创建一个全局变量window.b(非严格模式)
b = a // 对变量b来说,赋值操作
console.log(a + b)// 对变量b来说,取值操作
}
add(1) // 2
总结:如果查找变量的目的是赋值,则进行LHS查询;如果是取值,则进行RHS查询
作用域有两种主要的工作模型。第一种最为普遍,也是重点,叫作词法作用域,另一种叫作动态作用域(几乎不用)
简单来说,词法作用域就是定义在词法阶段的作用域(通俗易懂的说,就是在写代码时变量或者函数声明的位置)。
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2) // 2, 4, 12
变量查找的过程:首先从最内部的作用域(即bar函数)的作用域开始查找,引擎无法找到变量a,因此会到上一级作用域(foo函数)中继续查找,在这里找到了变量a,因此引擎使用了这个引用。变量b同理,对于变量c来说,引擎在bar函数中的作用域就找到了它。
注意:作用域查找会在找到第一个匹配的变量(标识符)时停止查找。
简单来说,函数作用域是指,属于这个函数的全部变量都可以在这个函数范围内使用及复用(复用:即在嵌套的其他作用域中也可以使用)。
var a = 1
// 定义一个函数包裹代码块,形成函数作用域
function foo() {
var a = 2
console.log(a) // 2
}
foo()
console.log(a) // 1
你会觉得,如果我要使用函数作用域,那么我必须定义一个foo函数,这让全局作用域多了个函数,污染了全局作用域,且必须执行一次该函数才能运行其中的代码块。
那有没有一种办法,可以让我不污染全局作用域(即不定义新的具名函数),且函数可以自动执行呢?
你一定想到了,IIFE(立即执行函数)
var a = 1;
(function foo() {
var a = 2
console.log(a) // 2
})()
console.log(a) // 1
这种写法,实际上不是一个函数声明,而是一个函数表达式。要区分这两者,最简单的方法就是看function关键字是否出现在第一个位置(第一个词),如果是,那么是函数声明,否则是一个函数表达式。
尽管你可能没写过块作用域的代码,但你一定对下面的代码块很熟悉:
for(var i = 0; i < 5; i++) {
console.log(i)
}
我们在for循环的头部定义了变量i,是因为想在for循环内部的上下文中使用i,而忽略了最重要的一点:i会被绑定在外部作用域(即全局作用域中)。
ES6改变了这种情况,引入let关键字,提供另一种声明变量的方式。
{
let a = 2;
console.log(a) // 2
}
console.log(a) // ReferenceError: a is not defined
讨论一下之前的for循环
for(let i = 0; i < 5; i++) {
console.log(i)
}
console.log(i) // ReferenceError: i is not defined
这里,for循环头部的i绑定在循环内部,其实它在每一次循环中,对i进行了重新赋值。
{
let j;
for(let j = 0; j < 5; j++) {
let i = j // 每次循环重新赋值
console.log(i)
}
j++
}
console.log(i) // ReferenceError: i is not defined
小知识:其实在ES6之前,使用try/catch结构(在catch分句中)也有块作用域
先有鸡(声明)还是先有蛋(赋值)?
简单来说,一个作用域中,包括变量和函数在内的所有声明都会在任何代码被执行前首先被 “移动” 到作用域的最顶端,这个过程就叫作提升。
a = 2
var a
console.log(a) // 2
// 引擎解析:
var a
a = 2
console.log(a) // 2
console.log(a) // undefined
var a = 2
//引擎解析:
var a
console.log(a) // undefined
a = 2
可以发现,当JavaScript看到 var a = 2; 时,会分成两个阶段,编译阶段和执行阶段。
编译阶段:定义声明,var a
执行阶段: 赋值声明,a = 2
结论:先有蛋(声明),后有鸡(赋值)。
函数和变量都会提升,但函数会首先被提升,然后是变量。
foo() // 2
var foo = 1
function foo() {
console.log(2)
}
foo = function() {
console.log(3)
}
// 引擎解析:
function foo() {...}
foo()
foo = function() {...}
多个同名函数,后面的会覆盖前面的函数
foo() // 3
var foo = 1
function foo() {
console.log(2)
}
function foo() {
console.log(3)
}
提升不受条件判断控制
foo() // 2
if (true) {
function foo() {
console.log(1)
}
} else {
function foo() {
console.log(2)
}
}
注意:尽量避免普通的var声明和函数声明混合在一起使用。
秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它。
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2 快看啊,这就是闭包!!!
函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()本身当作一个值类型进行传递。
正常情况下,当foo()执行后,foo()内部的作用域都会被销毁(引擎的垃圾回收机制),而闭包的“神奇”之处就是可以阻止这件事请的发生。事实上foo()内部的作用域依然存在,不然bar()里面无法访问到foo()作用域内的变量a
foo()执行后,bar()依然持有该作用域的引用,而这个引用就叫作闭包。
总结:无论何时何地,如果将函数当作值类型进行传递,你就会看到闭包在这些函数中的应用(定时器,ajax请求,事件监听器...)。
我相信你懂了!
回顾一下之前提到的for循环
for(var i = 0; i < 10; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
期望:每秒依次打印1、2、3、4、5...9
结果:每秒打印的都是10
稍稍改进一下代码(利用IIFE)
for(var i = 0; i < 10; i++) {
(function(i) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
})(i)
}
问题解决!对了,我们差点忘了let关键字
for(var i = 0; i < 10; i++) {
let j = i // 闭包的块作用域
setTimeout(function timer() {
console.log(j)
}, j * 1000)
}
还记得吗?之前有提到,for循环头部的let声明在每次迭代都会重新声明赋值,而且每个迭代都会使用上一个迭代结束的值来进行这次值的初始化。
最终版:
for(let i = 0; i < 10; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
好了,现在你肯定懂了!
总结:当函数可以记住并访问所在的词法作用域,即使函数是在当前的词法作用域之外执行,就产生了闭包。
如果你坚持看到了这里,我替你感到高兴,因为你已经掌握了JavaScript中的作用域和闭包,这些知识都是进阶必备的,如果有不理解的,花时间多看几遍,相信你一定可以掌握其中的精髓。
都到这儿了!
点个关注再走呗!!
本文转载自微信公众号「SH的全栈笔记」,作者SH。转载本文请联系SH的全栈笔记公...
前言 项目开发中不管是前台还是后台都会遇到烦人的null,数据库表中字段允许空值...
idea官方推送了2020.2.4版本的更新,那么大家最关心的问题来了,之前激活idea202...
大家好,我是狂聊君。 今天来聊一聊 Mysql 缓存池原理。 提纲附上,话不多说,直...
来源:DeepenStudy 漏洞文件:js.asp % Dimoblog setoblog=newclass_sys oblog.a...
问题:我们在做flex的开发中,如果用到别人搭建好的框架,而别人的server名称往...
CKeditor,以前叫FCKeditor,已经使用过好多年了,功能自然没的说。最近升级到3....
本文转载自微信公众号「SQL数据库」,作者丶平凡世界 。转载本文请联系开发公众...
本文实例讲述了AJAX+Servlet实现的数据处理显示功能。分享给大家供大家参考,具...
在Flash Player 10.1及以上版本中,adobe新增了全局错误处理程序UncaughtErrorEv...