前言
最近在重刷面试题,想根据自己看的文章或者书总结一系列的知识点。常见的面试题有很多,但如果不细细的追究他背后考的什么,也只是背诵而已。文章中如果有错误欢迎指正。有什么观点也欢迎评论交流。
啥是作用域
在wikipedia中,作用域是这样被定义的:
作用域是名字与实体的绑定保持有效的那部分计算机程序
直白的讲作用域实际上是一套规则,他是用来确定在何处以及如何查找变量。对于变量赋值,作用域会使用LHS查询。对于获取变量的值,使用的是RHS查询。这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域根据标识符进行变量查找。[1]
不同的语言中,作用域的模型可能不太一样(静态或者动态)。js中使用的是静态作用域也就是词法作用域。
词法作用域
定义
词法作用域就是定义在词法阶段的作用域,也就是你在写代码时候块和变量的位置来决定。也就是说无论函数在哪里被调用,也无论他被怎样的调用,词法作用域只在声明时候的位置来决定。
如果说词法环境和执行上下文是和js引擎息息相关的,那么作用域这个观念其实和语言没有特别大的联系。他更像一种约定的规则。如果我们把作用域想成一栋楼的话,整个楼就是全局作用域,每户人家就是全局中的函数作用域或者块作用域。如果你在楼道里,你是不能拿到每户人家的冰箱的食物的。如果你在某户人机里面,你可以使用公共区域的冰箱,但是如果冰箱在某个房间内(在某个作用域里)你还是拿不到食物。
对于代码片段:
function foo(a){
var b = 2;
function bar(){};
var c = 3;
}
复制代码
foo的作用域中包括的标识符有a,b, bar,c。因此如果在全局作用域中输出b的话,就会抛出错误:b is not defined
那么,问题来了,我就偏偏想修改作用域怎么办?(以下方法较危险,建议未成年作用域不要随意模仿)
修改作用域
js中提供了两种不建议使用的修改作用域的方式: with
和eval
.
- Eval: eval(str)接受一个字符串为参数,调用了这个函数,就能假装你传入的代码字符串好像就是书写的时候就在那里一样。
- With:with接受一个参数,将这个传入的参数处理为一个完全隔离的词法作用域。
这两种方法的区别就在于,eval是修改了传入代码的词法作用域,with则是为传递的参数凭空创造了一个全新的词法作用域。(vue中在拿到ast之后,生成代码的时候就是用了with将词法作用域限制在传入的this中) (说白了就是闪电侠和蚁人的区别:))
函数作用域
函数作用域的含义是指:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。也就是说如果代码片段如果被函数作用域包裹的话,就实现将他们"隐藏"起来了。这种隐藏机制很好的实现了规避冲突。
IIFE就是很好的利用了函数作用域的一种表达式。常见的IIFE用途包括:[1]
- 保护变量不被污染
- 解决undefined默认值被覆盖导致的异常
- 倒置代码的运行顺序
// 保护变量不被污染
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log(a);// 2 保护的好好的耶
// undefined被改成别的了。。。
undefined = true;
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
// 倒置执行顺序
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
复制代码
块作用域
除了js之外很多编程语言都支持块级作用域(es6之前)。但如果深究的话,块级作用域还是暗搓搓的存在着的。
-
with
上面说了,with从对象中创建出的作用域尽在with声明中而非外部作用域中有效
-
try/catch
在catch分句中会创建一个块级作用域,声明的变量仅仅在catch内部有效。
-
let & const
es6中引入了let和const关键字,let可以将变量绑定到所在的任意作用域中。
一道常见的面试题引发的思考
下面的代码片段输出啥?怎么才能输出1,2,3,4,5?
for(var i = 1; i<=5;i++){
setTimeout(function timer(){
console.log(i)
},0);
}
复制代码
这道面试题大家已经都看烂了,都知道5个6。但是我觉得这道题相关的知识点有两个,一个是setTimeout的执行时间,一个是解决方案所应用的原理。
- js引擎执行任务采用的是事件循环机制,每次执行的就是宏观的任务队列就是macrotask组成的队列,每个宏观任务由一个一个的microtask组成。setTimeout是宿主环境的异步宏任务,所以会在for循环结束之后才会执行。这也就是为什么setTimeout会在i为6之后才会去读取
- 怎么才能把每个循环的i给保留住呢,我们常见的解法比如说let,或者IIFE都是应用了作用域的原理,将console时候的i 限制在作用域中。
上面提及的函数作用域,就是iife能正常输出的原理。以及将i作为第三个参数传入,也是应用的函数作用域。 而let的解决方式就是应用的块级作用域。
正经解法:
- let
for(let i = 1; i<=5;i++){
setTimeout(function timer(){
console.log(i)
},0);
}
复制代码
- IIFE
for(var i = 1; i<=5;i++){
;(function(i){
setTimeout(function timer(){
console.log(i)
},0);
})(i)
}
复制代码
- setTimeout第三个参数
for(var i = 1; i<=5;i++){
setTimeout(function timer(i){
console.log(i)
},0,i);
}
复制代码
不正经解法
那么根据上面所提到的块级作用域,其实还可以有以下的骚操作解法。但是这些都是不规范的,适合秀一手,告诉面试官你知道这个知识点,但是一定要将利弊讲出来,不然容易被吊打。
- try/catch
for(let i = 1; i<=5;i++){
try{
throw i;
}
catch(i){
setTimeout(function timer(){console.log(i)},0);
}
}
复制代码
- with
for(let i = 1; i<=5;i++){
with({i}){
setTimeout(function timer(){
console.log(i)
},0);
}
}
复制代码
再次重申一遍:秀操作不规范,面试后两行泪。
总结
作用域是一个通常和别的知识点结合的考点,他和词法环境息息相关,他和引擎和编译也息息相关。他的根本只是一种规则。
[1] you-dont-know-javascript scope & closure