面试必问题闭包、作用域、作用域链,这些知识点其实都是相互关联对应的。
1,作用域概念
当面试时闻到这个问道什么是作用域时,可简短回答:作用域指定了程序中变量的生命周期和适用范围。
在es6以前,js的作用域只有 函数作用域和全局作用域,es6里新增了块级作用域。
作用域链:由于作用存在着嵌套(比如函数嵌套另一个函数),所以js引擎在查找变量时会先查找当前作用域内,如果查找不到会查找外层作用域内是否含有,直到查找到全局作用域。这就形成了作用域链的概念。
接下来我们详细来讲解。
2,理解作用域
js引擎在执行js代码之前,编译器会先对js代码进行词法分析、语法分析和代码生成。(具体这部分的内容可以查看资料,简单了解下。)。
编辑器在词法分析会把一段代码分解成词法单元,然后把词法单元(token)解析成树结构,而在代码生成式时遇到变量,会把变量存在当前环境作用域内并生成引擎可以执行的代码。引擎在执行代码时会先去作用域中查当前变量是否存在,之后进行下一步操作。
比如遇到var a=1;这段程序时。
1,编译阶段,编辑器遇到var a的时候会先进行变量提升,把a放到当前执行环境的作用域内。
2,引擎执行阶段 遇到a=1;会先去作用域内查,是否有变量a,如果有,就把1赋值给它。
3,理解作用域链
function f1(a){
var c=1
console.log(a+b+c)
}
var b=2;
f1(1);//1
遇到上诉代码时,整个流程如下,
1,编译阶段,编译器遇到function和var时,会先获取这些变量的定义,把b,和f1函数放入全局作用域内。
2,引擎执行代码时,
1)遇到b=2,在作用域内查找变量b,并将2赋值给b。
2)遇到f1(1)时,会进入f1函数的执行上下文,进行执行f1,当执行到console.log时,调用栈的环境如下图所示。
如上图所示,我们在执行console.log( a + b +c )时,引擎查找b时,会现在f1函数的作用域范围内查找,找不到就会去全局作用里查找。在全局作用域里,找到b=2。这就是所谓的作用域链式查找。
4,作用域链形成规则
这里又会有一个问题,怎么样才算作用域的嵌套呢?形成的是作用域链呢。比如
function f2() {
console.log(myName)
}
function f1() {
var myName = "f1变量"
f2()
}
var myName = "全局变量"
f1() //全局变量
如上诉代码,我们执行后发现f2虽然嵌入在f1函数里,但是f2里的myName取值并不是f1中的myName,而是全局的变量myName。
这里涉及到一个概念:作用域链是由词法作用域决定的,而词法作用域就是指作用域由代码中函数声明的位置来决定的,所以词法作用域也成为静态作用域。(也可以理解为在词法生成阶段就已经决定了作用域的位置。也就决定了作用域链)
(词法作用域-----编辑器的第一个工作阶段是词法化,词法化阶段会对源代码中每个字符逐个检查,如果有状态的解析,还会赋予这个词一定的意义。这也是词法作用域名称的来历)
其实每个作用域里都有一个outer指向它的上一级作用域,这个指向是按照代码书写位置来决定的。
当执行到console.log(myname),调用栈信息如下图所示
如图所示,outer指针指向的是上一级作用域,是由代码书写位置来决定的。也就是说作用域链由代码书写位置决定,和函数调用没有关系。
一般来说词法作用域由代码书写位置决定的,不过也有两种机制会在运行时改变作用域链,eval和with,不过这两个都不建议用。这里我们就不讨论了。
作用域由代码书写位置决定的好处是,可以在编辑阶段进行一些优化。
5,es6的块级作用域
在es6之前,只有全局作用域和函数作用域的概念,在es6中新增了块级作用域。在es6中由{}包括的都属于块级作用域。(不过var在块级作用域里没有任何意义,let和const有用。)
块级作用域一般属于全局作用域或者函数作用域里。
function f2() {
{
let myName="f2块级作用域"
}
{
let m="2"
console.log("f2-2=",myName)
}
}
function f1() {
var myName = "f1变量"
f2()
}
var myName = "全局变量"
f1() //f2-2=全局变量
如上诉代码所示,输出结果:f2-2=全局变量;当执行到 console.log(“f2-2=”,myName)时,调用栈如下图所示。
如上图所示,当执行console时,会在当前块级作用域查找,如果查找不到就去函数的整体环境变量里查找,如果没有就通过作用域链继续查找上一级。查找顺序也就是图中1、2、3、4.