一、什么是作用域?
作用域指一个变量的作用范围。
每个作用域都是一个独立的地盘,目的就是为了保证当前作用域内的变量不会外泄,且不会和其他作用域中的同名变量冲突。
在JavaScript中的作用域类型
- 全局作用域
- 函数作用域(局部作用域)
- 块级作用域(let/const ES6新增)
二、作用域详解
2.1 全局作用域
写在<script>标签内的最外层变量和函数都是全局作用域。当页面打开时全局作用域会自动创建,而当页面关闭时就会销毁。所有全局作用域对象都可以通过window对象的属性进行访问。
//示例:
<script>console.log(window);var a = 1;function aa(){console.log("aa")
}var person = {name:"张三"};window.aa();
console.log(window.a);
console.log(window.person.name);</script>
2.2 函数作用域
· 定义于函数内部的变量或函数。也称为局部作用域。在函数调用时被创建,函数执行完毕后自动销毁。函数每调用一次都会创建一个新的函数作用域,他们之间是相互独立的。
· 函数作用域中可以访问全局作用域的变量和函数,反之则不行。
· 在函数内部访问一个变量或函数时,会现在当前作用域中查找。如果没找到,就到上一级的作用域中查找,直到全局作用域。
· 函数内部也相当于一个小的全局作用域,所以定义在函数内部的变量和函数也存在变量提升。
//示例:
<script>var name = "张三";
function aa(){var name = "李四";console.log(name) //李四console.log(window.name) //张三
}
aa();</script>
2.3 块级作用域(ES6)
带“{}”的基本都为块级作用域,如:if(){ }、for(){ }、catch(err){}。var和function没有块级作用域的概念,只有let和const有块级作用域概念。
//示例:
<script>if(true){let a = 10;console.log(a) //10
} for(let i=0;i<=0;i++){console.log(i) //0
}console.log(a) //ReferenceError: a is not defined
console.log(i) //ReferenceError: i is not defined</script>
三、什么是作用域链?
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。因此,在当前作用域中没有找到某个变量时,引擎就会在外层嵌套的作用域中继续查询,直到找到该变量。如果抵达最外层的作用域(全局作用域)还找不到,就会抛出ReferenceError异常。
四、 作用域原理
1.1 编译过程
作用域的内部原理分为5个阶段
- 编译阶段 (编译器将代码分解成词法单元并将词法单元解析成一个数结构AST)
- 执行阶段 (编译器询问作用域中是否存在某变量,不存在就创建,并为引擎生成运行时所需要的代码)
- 查询阶段(引擎询问作用域中是否存在某变量,如果存在引擎就会使用该变量并为其赋值,不存在就继续向上查找)
- 嵌套阶段(当前作用域不存在某变量会继续向上查询)
- 异常阶段(直到顶层作用域window也没有找到某变量,就抛出异常:ReferenceError)
例:var a = 2;的执行过程
首先引擎会认为这里有两个完成不同的声明,一个由编译器在编译时处理(var a),另一个则由引擎在运行时处理( = 2)。
- 编译器会进行两步操作:
语法分析阶段:
1>将这段代码分解成词法单元
2>将词法单元解析成一个数结构。
代码执行阶段:
1>遇到var a,编译器会询问作用域是否已经有一个名称为a的变量存在于同一作用域的集合中。如果是,编译器会忽略该声明继续进行编译。否则它会要求作用域在当前作用域的集合中声明一个新的变量,命名为a。
2>接下来编译器会为引擎生成运行时需要的代码,这些代码被用来处理a = 2这个赋值操作- 引擎运行时会首先询问作用域,在当前作用域集合中是否存在一个 叫做a的变量。如果存在,引擎就会使用该变量,如果否,引擎会继续在上一层作用域中查找。
- 如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(变量未声明过时),然后在运行时引擎会在作用域中查找该变量,如果能找到就给它赋值。
1.2 详解LHS和RHS
LHS和RHS的含义为“赋值操作的左侧或右侧”,但是并不意味着就是“=”赋值操作符的左侧或右侧。赋值操作还有其他几种形式,因此在概念上最好将其理解为:
- LHS:(查询变量的容器本身,目的是:为某个变量赋值set)
- RHS:(查询变量的源值,目的是:获取某个变量的值get)
例:
console.log( a ); //这里的a没有赋予任何值,所以需要查找并取得a的值,所以是RHS查询。
a = 2; //这里的a则是LHS查询,因为我们并不关心当前的值是什么,只是要为 = 2这个赋值操作找到一个目标。
总结:LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的变量),就会向上级作用域继续查找目标变量,这样每次上升一级作用域,最后抵达全局作用域(顶层),无论找到或没找到都将停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)
五、词法作用域
词法作用域就是定义在词法阶段的作用域。是根据代码中变量和块作用域的位置所决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况,eval和with特殊)。
在下列示例中,编译器在词法(分词)阶段会创建三个不同的作用域,他们三个作用域是严格包含的
/*foo方法为全局作用域在foo这个函数作用域中又存在a、b、bar在bar函数作用域中存在c
*/function foo(a) {var b = a * 2;function bar(c) {console.log( a, b, c );}bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
六、变量提升
在作用域原理中,第一阶段也就是编译阶段有一部分工作就是找到所有的变量和函数在内的所有声明,并用合适的作用域将他们关联起来。这个处理工作是在代码执行阶段之前就处理完成的。
在JavaScript中,var a=2; js引擎会将其看成两个声明:var a和a=2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
函数声明>变量声明 函数声明和变量声明都会被提升,但是函数会首先被提升。
七、立即执行函数(IIFE)
(function foo(){console.log("我被执行了")
})();//或者
(function(){ console.log("我被执行了")
}());//或者
(function IIFE(){ console.log("我被执行了")
})()。
由于函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()就可以立即执行这个函数,比如(function foo(){ ... })()。第一个()将函数变成表达式,第二个()执行了这个函数。
IIFE用法1:把他们当作函数调用并传递参数进去。
<script>var a = 10;
(function IIFE(global){var a = 3;console.log(a) //3console.log(global.a) //10
})(window)</script>
IIFE用法2: 倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。
<script>/*函数表达式 def 定义在片段的第二部分,然后当作参数(这个参数也叫作 def)被传递进IIFE 函数定义的第一部分中。最后,参数 def(也就是传递进去的函数)被调用,并将window 传入当作 global 参数的值。
*/var a = 10;
(function IIFE(def){def(window)
})(function def(global){var a = 3;console.log(a) //3console.log(global.a) //2
})</script>
七、作用域闭包
函数内部可以访问所以外部作用域中的变量,但是反之则不行,这就形成了闭包,闭包在我们的代码中其实很常见,比如在定时器、事件监听器、ajax请求或者任何其他的异步(或者同步)任务重,只要使用了回调函数,实际上就是在使用闭包。