做用域是JS中一个很基础可是很重要的概念,面试中也常常出现,本文会详细深刻的讲解这个概念及其余相关的概念,包括声明提高,块级做用域,做用域链及做用域链延长等问题。javascript
第一个问题就是咱们要弄清楚什么是做用域,这不是JS独有的概念,而是编程领域中通用的一个概念。咱们如下面这个语句为例:前端
let x = 1;
这一个简单的语句其实包含了几个基本的概念:java
- 变量(variable):这里x就是一个变量,是用来指代一个值的符号。
- 值(value):就是具体的数据,能够是数字,字符串,对象等。这里
1
就是一个值。- 变量绑定(name binding):就是变量和值之间创建对应关系,
x = 1
就是将变量x
和1
联系起来了。- 做用域(scope):做用域就是变量绑定(name binding)的有效范围。就是说在这个做用域中,这个变量绑定是有效的,出了这个做用域变量绑定就无效了。
就整个编程领域而言的话,做用域又分为静态做用域和动态做用域两类。git
静态做用域又叫词法做用域,JS就是静态做用域,好比以下代码:github
let x = 10; function f() { return x; } function g() { let x = 20; return f(); } console.log(g()); // 10
上述代码中,函数f
返回的x
是外层定义的x
,也就是10
,咱们调用g
的时候,虽然g
里面也有个变量x
,可是在这里咱们并无用它,用的是f
里面的x
。也就是说咱们调用一个函数时,若是这个函数的变量没有在函数中定义,就去定义该函数的地方查找,这种查找关系在咱们代码写出来的时候其实就肯定了,因此叫静态做用域。这是一段很简单的代码,你们都知道输出是10
,难道还能输出20
?还真有输出20的,那就是动态做用域了!面试
Perl语言就采用的动态做用域,仍是上面那个代码逻辑,换成Perl语言是这样:编程
$x = 10; sub f { return $x; } sub g { local $x = 20; return f(); } print g();
上述代码的输出就是20
,你们能够用Perl跑下看看,这就是动态做用域。所谓动态做用域就是咱们调用一个函数时,若是这个函数的变量没有在函数中定义,就去调用该函数的地方查找。由于一个函数可能会在多个地方被调用,每次调用的时候变量的值可能都不同,因此叫动态做用域。动态做用域的变量值在运行前难以肯定,复杂度更高,因此目前主流的都是静态做用域,好比JS,C,C++,Java这些都是静态做用域。json
在ES6以前,咱们申明变量都是使用var
,使用var
申明的变量都是函数做用域,即在函数体内可见,这会带来的一个问题就是申明提早。异步
var x = 1; function f() { console.log(x); var x = 2; } f();
上述代码的输出是undefined
,由于函数f
里面的变量x
使用var
申明,因此他其实在整个函数f
可见,也就是说,他的声明至关于提早到了f
的最顶部,可是赋值仍是在运行的x = 2
时进行,因此在var x = 2;
上面打印x
就是undefined
,上面的代码其实等价于:函数
var x = 1; function f() { var x console.log(x); x = 2; } f();
看下面这个代码:
function f() { x(); function x() { console.log(1); } } f();
上述代码x()
调用是能够成功的,由于函数的声明也会提早到当前函数的最前面,也就是说,上面函数x
会提早到f
的最顶部执行,上面代码等价于:
function f() { function x() { console.log(1); } x(); } f();
可是有一点须要注意,上面的x
函数若是换成函数表达式就不行了:
function f() { x(); var x = function() { console.log(1); } } f();
这样写会报错Uncaught TypeError: x is not a function
。由于这里的x
其实就是一个普通变量,只是它的值是一个函数,它虽然会提早到当前函数的最顶部申明,可是就像前面讲的,这时候他的值是undefined
,将undefined
当成函数调用,确定就是TypeError
。
既然变量申明和函数申明都会提早,那谁的优先级更高呢?答案是函数申明的优先级更高!看以下代码:
var x = 1; function x() {} console.log(typeof x); // number
上述代码咱们申明了一个变量x
和一个函数x
,他们拥有一样的名字。最终输出来的typeof
是number
,说明函数申明的优先级更高,x
变量先被申明为一个函数,而后被申明为一个变量,由于名字同样,后申明的覆盖了先申明的,因此输出是number
。
前面的申明提早不太符合人们正常的思惟习惯,对JS不太熟悉的初学者若是不了解这个机制,可能会常常遇到各类TypeError
,写出来的代码也可能隐含各类BUG。为了解决这个问题,ES6引入了块级做用域。块级做用域就是指变量在指定的代码块里面才能访问,也就是一对{}
中能够访问,在外面没法访问。为了区分以前的var
,块级做用域使用let
和const
声明,let
申明变量,const
申明常量。看以下代码:
function f() { let y = 1; if(true) { var x = 2; let y = 2; } console.log(x); // 2 console.log(y); // 1 } f();
上述代码咱们在函数体里面用let
申明了一个y
,这时候他的做用域就是整个函数,而后又有了一个if
,这个if
里面用var
申明了一个x
,用let
又申明了一个y
,由于var
是函数做用域,因此在if
外面也能够访问到这个x
,打印出来就是2,if
里面的那个y
由于是let
申明的,因此他是块级做用域,也就是只在if
里面生效,若是在外面打印y
,会拿到最开始那个y
,也就是1.
块级做用域在同一个块中是不容许重复申明的,好比:
var a = 1; let a = 2;
这个会直接报错Uncaught SyntaxError: Identifier 'a' has already been declared
。
可是若是你都用var
申明就不会报错:
var a = 1; var a = 2;
常常看到有文章说: 用let
和const
申明的变量不会提高。其实这种说法是不许确的,好比下面代码:
var x = 1; if(true) { console.log(x); let x = 2; }
上述代码会报错Uncaught ReferenceError: Cannot access 'x' before initialization
。若是let
申明的x
没有变量提高,那咱们在他前面console
应该拿到外层var
定义的x
才对。可是如今却报错了,说明执行器在if
这个块里面实际上是提早知道了下面有一个let
申明的x
的,因此说变量彻底不提高是不许确的。只是提高后的行为跟var
不同,var
是读到一个undefined
,而块级做用域的提高行为是会制造一个暂时性死区(temporal dead zone, TDZ)。暂时性死区的现象就是在块级顶部到变量正式申明这块区域去访问这个变量的话,直接报错,这个是ES6规范规定的。
下面这种问题咱们也常常遇到,在一个循环中调用异步函数,指望是每次调用都拿到对应的循环变量,可是最终拿到的倒是最后的循环变量:
for(var i = 0; i < 3; i++) { setTimeout(() => { console.log(i) }) }
上述代码咱们指望的是输出0,1,2
,可是最终输出的倒是三个3
,这是由于setTimeout
是异步代码,会在下次事件循环执行,而i++
倒是同步代码,而所有执行完,等到setTimeout
执行时,i++
已经执行完了,此时i
已是3了。之前为了解决这个问题,咱们通常采用自执行函数:
for(var i = 0; i < 3; i++) { (function(i) { setTimeout(() => { console.log(i) }) })(i) }
如今有了let
咱们直接将var
改为let
就能够了:
for(let i = 0; i < 3; i++) { setTimeout(() => { console.log(i) }) }
这种写法也适用于for...in
和for...of
循环:
let obj = { x: 1, y: 2, z: 3 } for(let k in obj){ setTimeout(() => { console.log(obj[k]) }) }
那能不能使用const
来申明循环变量呢?对于for(const i = 0; i < 3; i++)
来讲,const i = 0
是没问题的,可是i++
确定就报错了,因此这个循环会运行一次,而后就报错了。对于for...in
和for...of
循环,使用const
声明是没问题的。
let obj = { x: 1, y: 2, z: 3 } for(const k in obj){ setTimeout(() => { console.log(obj[k]) }) }
在最外层(全局做用域)使用var
申明变量,该变量会成为全局对象的属性,若是全局对象恰好有同名属性,就会被覆盖。
var JSON = 'json'; console.log(window.JSON); // JSON被覆盖了,输出'json'
而使用let
申明变量则没有这个问题:
let JSON = 'json'; console.log(window.JSON); // JSON没有被覆盖,仍是以前那个对象
上面这么多点其实都是let
和const
对之前的var
进行的改进,若是咱们的开发环境支持ES6,咱们就应该使用let
和const
,而不是var
。
做用域链实际上是一个很简单的概念,当咱们使用一个变量时,先在当前做用域查找,若是没找到就去他外层做用域查找,若是尚未,就再继续往外找,一直找到全局做用域,若是最终都没找到,就报错。好比以下代码:
let x = 1; function f() { function f1() { console.log(x); } f1(); } f();
这段代码在f1
中输出了x
,因此他会在f1
中查找这个变量,固然没找到,而后去f
中找,仍是没找到,再往上去全局做用域找,这下找到了。这个查找链条就是做用域链。
前面那个例子的做用域链上其实有三个对象:
f1做用域 -> f做用域 -> 全局做用域
大部分状况都是这样的,做用域链有多长主要看它当前嵌套的层数,可是有些语句能够在做用域链的前端临时增长一个变量对象,这个变量对象在代码执行完后移除,这就是做用域延长了。可以致使做用域延长的语句有两种:try...catch
的catch
块和with
语句。
这实际上是咱们一直在用的一个特殊状况:
let x = 1; try { x = x + y; } catch(e) { console.log(e); }
上述代码try
里面咱们用到了一个没有申明的变量y
,因此会报错,而后走到catch
,catch
会往做用域链最前面添加一个变量e
,这是当前的错误对象,咱们能够经过这个变量来访问到错误对象,这其实就至关于做用域链延长了。这个变量e
会在catch
块执行完后被销毁。
with
语句能够操做做用域链,能够手动将某个对象添加到做用域链最前面,查找变量时,优先去这个对象查找,with
块执行完后,做用域链会恢复到正常状态。
function f(obj, x) { with(obj) { console.log(x); // 1 } console.log(x); // 2 } f({x: 1}, 2);
上述代码,with
里面输出的x
优先去obj
找,至关于手动在做用域链最前面添加了obj
这个对象,因此输出的x
是1。with
外面仍是正常的做用域链,因此输出的x
仍然是2。须要注意的是with
语句里面的做用域链要执行时才能肯定,引擎没办法优化,因此严格模式下是禁止使用with
的。
var
变量会进行申明提早,在赋值前能够访问到这个变量,值是undefined
。var
高。var
的函数表达式其实就是一个var
变量,在赋值前调用至关于undefined
(),会直接报错。let
和const
是块级做用域,有效范围是一对{}
。var
不同,块级做用域里面的“变量提高”会造成“暂时性死区”,在申明前访问会直接报错。let
和const
能够很方便的解决循环中异步调用参数不对的问题。let
和const
在全局做用域申明的变量不会成为全局对象的属性,var
会。try...catch
的catch
块会延长做用域链,往最前面添加一个错误对象。with
语句能够手动往做用域链最前面添加一个对象,可是严格模式下不可用。let
和const
,不要用var
。文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges