一、什么是作用域
通俗来讲,作用域是变量存储在哪里,以及怎么取变量的一套规则。
作用域与编译器、引擎配合,在代码运行过程中发挥了至关重要的作用。
比如以下代码:
var a = 2;
function add(b) {
console.log(a + b);
}
add(3);
考虑这三者的分工那就是这样的:
编译器:查看全局作用域中是否有a这个变量,结果没有,于是在全局作用域中定义这个变量。查看全局作用域中是否有add这个函数,结果没有,于是在全局作用域中定义这个函数。然后是add函数作用域,查看函数作用域中是否有b变量,结果没有,于是在add的作用域中定义b这个变量。将处理之后的代码给到引擎。
引擎:询问全局作用域中是否存在a,找到了,于是给a赋值。询问全局作用域中是否存在add,存在了,取值并调用,询问add的作用域中是否存在b,存在,于是将3赋值给b。
然后询问add的作用域是否存在b,存在,于是取到b的值。然后询问add的作用域是否存在a,不存在,再问全局作用域,存在,于是取到a的值。打印两者相加的结果。
在这里add的作用域就涉及到了一个叫 作用域的嵌套 ,当在一个作用域找不到变量的时候,会一直往上层继续寻找,直到全局作用域,无论找没找到都会停止查找的过程。
作用域的嵌套还会引发一种叫“遮蔽效应”,如果在里层作用域里定义了一个变量,那么外层的同名变量就无法访问到。不过全局变量可以通过 window.xx 的方式绕开这种遮蔽
二、作用域模型
作用域大体分为两种模型,一种是词法作用域,这种是JavaScript所采用的模型。另一种是动态作用域,也有很多编程语言使用。
词法作用域是根据代码书写的位置来决定的。在词法分析阶段,会保持作用域不变。(当然也有一些例外,后面细说)
参考以下代码:
function add(b) {
console.log(a + b);
}
var a = 1;
add(2);
在这里有2个作用域:
1. 全局作用域,声明变量有add和a
2. add函数作用域,声明变量有b
在使用了with和eval之后,会改变原本的词法作用域。总的来说,with创建了一个全新的词法作用域,而eval修改了原有的词法作用域
1. with
在这里传入一个对象,然后with会以这个对象创建出一个新的词法作用域,对象的属性就是词法作用域的变量。
with(obj) {
a = 3;
}
在这里传入obj,如果obj原先有a属性,那么会被赋值为3。如果obj原先没有a属性,那么会按照词法作用域的寻找过程,现在obj的作用域里找,再往上全局作用域,在非严格模式下会声明一个全局变量a并赋值为3。
2. eval
eval可以接受字符串代码,然后在运行的时候会生成代码替换原先eval所在的位置,就如同原本代码就写在那里一样
eval("var a = 4");
这里原本词法作用域中没有a变量,用了eval之后词法作用域多了a变量,所以eval修改了词法作用域
这两种影响词法作用域的做法并非不好,首先是因为严格模式并不支持,而且还会引发一系列的性能问题。
JavaScript代码在编译的时候,会采取一些优化的策略,会对代码的词法进行静态分析,根据代码和变量的位置做一些优化,而eval和with破坏了这种策略。eval使得引入的变量和代码变得不确定,with使得传入的对象不可预知
所以在写代码的时候要尽量减少这两种做法。
三、函数作用域
函数作用域有利于避免变量的污染
var a = 3;
function hide() {
var b = 4;
}
console.log(b);
hide创建了一个函数作用域,里面包含了b变量,这样做避免b污染全局变量。
但是通常用一个函数声明来隐藏变量不是很好,因为首先你声明一个函数名,这个函数名就会污染全局,而且你还必须调用函数。所以自然有疑问产生就是,有没有更加方便的,不污染全局又能不必分次调用的方式。
答案是,有的!这就是IIFE(立即执行函数表达式)
IIFE
形如下面的结构:
(function() {
var a = 1;
var b = 2;
})()
这就是立即执行函数表达式。
给function(){}加上个括号,是将函数声明转为函数表达式,而且这个函数是匿名函数,不会污染全局。
转为表达式之后在后面加上括号实现自动调用
四、块级作用域
学习JavaScript,大家应该对JS的块级作用域没什么印象,而实际上有没有这个东西呢?是有的。
1. try/catch
平时用的 try...catch(e) {...} 中,
{...}这里面的内容属于块级的作用域
2. let
ES6开始,let声明的变量都会属于块级作用域。const同样也有块级作用域的概念,但是声明的变量不能更改
那么块级作用域的作用是啥呢?
块级作用域可以避免一些不必要的变量泄露,比如最典型的for循环
for(let i = 0; i < 12; i++) {
console.log(i);
}
如果用var定义而不是let的话,那么i会泄露到全局中,i这个变量主要是用在for循环中,带到全局并没有什么意义,所以用let可以让i避免泄露到全局。