词法作用域
词法作用域就是定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用,函数内的函数也可以访问其外部嵌套的函数作用域的变量,但外部在通常情况下无法访问函数内定义的变量,函数会最小限度地暴露自己内部的实现。
函数声明? 函数表达式?
一个比较容易混淆的点是函数声明和函数表达式。让我们来看下面的一个例子,下面哪一个是函数声明,哪一个是函数表达式呢?
// func A
function funcA() {
// code here
}
// func B
(function funcB(){
// code here
})()
// func C
var funcC = function(){ ... }
区分函数声明和表达式的最简单的方式,是看 function 关键字出现在声明中的位置,如果是在生命中的第一个词,则这个是一个函数声明,如果不是则是一个函数表达式。记住,是整个声明的位置,而不是单看一行代码的位置噢。
⭐️ 那区分这两者的意义在哪里呢?为什么需要区分这二者呢?
这个会涉及到啊他们所绑定的名字标识符会绑定在哪里:
- 函数表达式
(function(){})()的名字标识符被限制在了函数表达式自身函数中,而非作用域。这样意味着函数名(变量标识符)被隐藏在自身之中而不会污染外部作用域。 - 函数声明
function(){}绑定在其声明所在的作用域之中。
比较一下上面代码例子中的 funcA and funcB, 当我们调用 funcA 只需要直接在声明作用域调用即可,即我们直接 funcA 即可。但是 funcB 其实是一个立刻执行表达式(IIFE), 我们只能在 {…}() 里面去调用 funcB,直接在外部作用域调用是不行的。 IIFE 这也是避免非必要污染外部作用域的方式之一。
那 funcA 和 funcC 又有何区别呢? funcC 也是一个函数表达式。 我们应该都听说过 JavaScript 的 hoisting 机制,函数和变量都会被提升到作用域顶部。思考一下下述代码,都可以正常运行么?
funcA()
funcC()
// func A
function funcA() {
// code here
}
// func C
var funcC = function(){ ... }
答案是 funcA 可以正常运行,而 funcC 不行。原因在于,JavaScript 遇到 funcA 认为这是一个函数声明,则直接将 funcA 提示到顶部,所以 funcA 可以。 而当遇到 funcC 时,认为这是一个表达式,那只会提升 funcC 这个变量,而赋值则会被留在原地,所以执行到 funcC 时,会提示 TypeError: funcC is not a function 。这里还有一个小细节,函数和变量虽说都会被提升,但函数会被首先提升,其次才是变量。
如果不理解这部分 hoisting 可以看看我之前写到的 关于 let & const 文章 ,里面提及了 hoisting 机制,希望有帮助 :>
作用域闭包
闭包无处不在。我们只需要去识别和拥抱它即可。
闭包这个概念其实也是在学习 JavaScript 中比较让人头疼的部分。何为闭包,怎么识别出闭包?
先抛出闭包的定义,**函数可以记住并访问其所在的词法作用域,并在词法作用域以外执行,**闭包就产生了。从这个定义来讲,闭包是词法作用域书写过程中自然产生的结果。看看下面的例子:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var closure = foo();
closure(); // 本来存在于 foo 里的 bar,记住了它的词法作用域,并在外面执行
在执行了 foo() 之后,将在 foo 里面的 bar 返回,而 bar 记住了原来的词法作用域,并在外部作用域中,通过不同的标识符,对其词法作用域进行访问,它在自己所定义的词法作用域以外的地方执行。
原本在执行了 foo() 之后, foo() 的内部作用域都应该被销毁。而 closure 的成功调用,这说明了内部作用域(即 bar 的词法作用域)依旧存在,没有被回收销毁。
这是因为其作用域依旧存在被引用的情况,同时由于 bar 的声明位置,它拥有覆盖 foo() 的内部作用域,使得这个作用域能够一直存活。 bar 本身持有了这个作用域的引用,而这个引用就被称之为闭包。
无论通过什么手段,只要将内部函数传递到其所在的词法作用域以外,它都会持有对原始定义的作用域的引用。闭包可以使得函数可以继续访问定义时的词法作用域,无论这个函数在哪里执行,都会使用到闭包。 现在,可以理解了“闭包是词法作用域的自然产生结果”这句话了吧。
块级作用域
早在 ES3 时,就已经有了 try…catch… 中,catch 的块级作用域。ES3 规范中,为 try/catch 中的 catch 分句会创建一个块级作用域,其声明的变量只有在 catch 中才有效果。
之后在 ES6 之后,新定义的 let、const 才让块级作用域真正走到台前。在声明中任意位置,都可以使用 {…} 为 let or const 创建一个用于绑定的块。
{…} 包裹生成一个块级作用域这个比较通俗易懂,我们来看一个比较有意思的 case —— for 循环。
for 循环
一个大家都比较熟知的面试题,for 循环,下面的题目最后会打印出什么呢?
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
这个题目我们被问到都快应激了,毫不犹豫 5 5 5 5 5。 如果我们要打印出 0、1、2、3、4 应该怎么改呢!再次应激,把 var 改为 let;存一个 i 的值;用 IIFE 把 i 传进去等等,这里面牵扯到了闭包、作用域、变量声明等等。
// IIFE
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i); // 0, 1, ...., 9
}, 500);
})(i);
}
这里有个比较有意思的角度是,每次迭代,其实都形成了一个新的块级作用域。for 循环的头部不仅仅将 i 绑定到 for 循环块中,事实是它将 i 每一次迭代时绑定都进行了重新绑定,确保使用上次循环迭代结束时的值进行重新赋值。
小结
这篇简单介绍了 JavaScript 中几种作用域:
- 词法作用域: 和书写时声明的位置有关。
- 函数作用域: 函数声明内部形成了一个作用域,外部无法直接访问内部,而内部却可以访问外部变量。
- 块级作用域: 在一个块内形成了一个作用域,目前和
letorconst关联比较紧密。
下一篇文章,将会承接本篇内容,引出最让我头疼的 this 及所谓的动态作用域。