在JavaScript中,Scope(做用域)是一个很是重要的概念,对不少刚接触JavaScript的开发者来讲,这个概念理解起来并不容易。本文的目的就是对JavaScript Scope的知识点作一次梳理,但愿经过本文能帮助你更好的理解Scope。bash
在JavaScript中,Scope通常译为做用域,能够通俗易懂的理解为:做用域限定了在某个特定范围内能够获得的变量、函数和对象等资源。这里注意几个关键点,首先做用域起着限定做用,其次它限定在某个特定范围,超过这个范围是不被容许的,最后它规定了能够获得什么,能够获得包括变量、函数声明和对象等。Scope能够分为Global Scope(全局做用域)和 Local Scope(局部做用域)。函数
若是一个变量定义在任何函数或花括号{}
以外,那么这个变量就处于Global Scope之中,这个变量称为全局变量。性能
// global scope
let name = '老王'
function alertName() {
alert(name)
}
复制代码
定义在Global Scope中的变量,能够在代码中的任何位置使用。尽管这样看起来很酷,可是仍是不建议这么作。看下面的代码。优化
代码片断一:ui
// global scope
let name = '老王'
let name = '老李' // Uncaught SyntaxError: Identifier 'name' has already been declared
复制代码
代码片断二:spa
// global scope
var name = '老王'
var name = '老李'
console.log(name) // 老李
复制代码
代码片断一出现了报错,经过let
或者const
定义了一个变量,而后再次定义同名变量,则会提示报错信息,这是由于产生了命名冲突。尽管代码片断二没有出现报错,可是经过var
定义了一个变量,而后再次定义同名变量,则变量的值被覆盖了。code
事实上,定义在Global Scope中的变量和函数越多,产生命名冲突的风险也就越大。因而可知,定义变量的时候应该尽量的定义在Local Scope中,而非Global Scope中,避免污染全局命名空间。对象
若是一个变量定义在函数或者花括号{}
之中,那么其只能被一部分特定的代码所使用,咱们能够认为它处于Local Scope中,这个变量能够称为局部变量。ip
Local Scope又包括Function Scope(函数做用域)和Block Scope(块级做用域)。先来看Function Scope。内存
在很长一段时间里,在JavaScript中声明一个变量,只能经过var
来声明,在函数中经过var
声明的变量是做用在Function Scope之中的。
// global scope
function showName() {
// function scope
var name = '老王'
if (true) {
console.log(name)
}
}
function getName() {
// function scope
return name
}
console.log(name) // undefined
showName() // 老王
getName() // undefined
复制代码
经过以上代码能够发现,在getName这个函数中,没有声明name
变量,可是它也没法获取showName
中的name
变量,这是由于在没有任何关系的函数之间,函数的Function Scope之间是相互隔离的。同时也能够发如今Global Scope中也是没法获取showName
中的name
变量的,这是由于name
变量被限定在Function Scope之中。既然Global Scope没法获取Function Scope中的变量,那么Function Scope可不能够获取Global Scope中的变量呢?
// global scope
function showName() {
// function scope
if (true) {
console.log(name)
}
}
var name = '老王'
console.log(name) // 老王
showName() // 老王
复制代码
经过以上代码可知,在JavaScript中,子做用域能够获取父级做用域中的变量和函数声明,反之则不行。
在JavaScript中,有一个特别的现象,在函数开始执行的阶段,JavaScript Engine(JavaScript引擎)会将经过var
声明的变量提高到函数的最顶端(其实函数的声明也会),这就是所谓的Hoisting。
// global scope
name = '老王'
age = 40
var name
var age
复制代码
以上代码等价于
// global scope
var name;
var age;
name = '老王';
age = 40;
复制代码
值得注意的是变量提高只会提高变量的声明,变量的赋值并不会提高,变量的赋值仍要等到代码执行到赋值语句的位置才会赋值。
// function scope
console.log(name) // undefined
name = '老王'
console.log(name) // 老王
var name
console.log(name) // 老王
复制代码
还有一种状况是须要避免出现的,那就是若是定义在函数中的变量没有使用var
或者let
和const
声明,在非严格模式(use strict
)下,变量将提高为全局变量。
// global scope
function showName() {
console.log(name)
name = '老王'
console.log(name)
}
showName() // 老王, 老王
console.log(name) // 老王
name = '老李'
showName() // 老李, 老王
console.log(name) // 老李
复制代码
这样name
变量将能够被随意修改,可能会产品意想不到的bug。
在ES6出现以前,JavaScript并无严格意义上的Block Scope,这让不少有其余语言开发经验的开发者感到很困惑。除了不推荐使用的eval
和with
,只有try/catch
中的catch
块拥有Block Scope。
try {
alert(age)
var name = '老王'
} catch(err) {
console.log(err) // ReferenceError: age is not defined
}
console.log(name) // 老王
console.log(err) // Uncaught ReferenceError: err is not defined
复制代码
因而可知,err
变量只限定在catch
的Block Scope之中,而try
则没有本身Block Scope。
ES6的发布为广大JavaScript开发者带来了全新的变量声明方式,let
和const
,其中let
和var
同样,都是用来声明变量,而const
则是用来声明常亮,顾名思义,其值是不可改变的。这里要重点说明的是,let
和const
是做用于Block Scope的。
// global scope
function showName() {
// function scope
var name1 = '老王'
if (true) {
// block scope
let name2 = '老李'
const name3 = '老赵'
console.log(name3) // 老赵
}
console.log(name1) // 老王
console.log(name2) // ReferenceError: name2 is not defined
}
showName()
复制代码
经过以上代码发现,在Block Scope以外,console.log
是没法获取name2
和name3
的,这就是Block Scope的限制。
JavaScript是自带Garbage collection(垃圾回收)机制的,咱们实际开发过程不用特别关心内存回收的问题,由于JavaScript Engine已经帮咱们处理好了。但在开发大型应用的时候,性能问题会逐渐凸显,关于性能,其实涉及到不少方面,这里不一一细说,其中有一点是咱们能够利用Block Scope来提升垃圾回收的效率。
既然在Block Scope以外,代码是没法获取其中的变量的,Garbage collection一旦发现变量或对象没有被引用,就会将内存及时的回收以备他用。因此利用这个机制,咱们能够优化部分代码的书写方式。
// global scope
var name = '老王'
{
// block scope
let age = 40
console.log(age) // 40
}
console.log(name) // 老王
复制代码
经过加上花括号,来建立一个Block Scope,一旦代码执行结束,Block Scope中的变量将没法获取,那么它就会被及时回收。
接下来咱们来看看实际运用中Function Scope和Block Scope的区别。 代码片断一:
// global scope
for(var i = 0; i < 10; i++){
setTimeout(function(){
console.log(i);
},100);
}
复制代码
代码片断二:
// global scope
for(let i = 0; i < 10; i++){
setTimeout(function(){
console.log(i);
},100);
}
复制代码
运行代码片断一,将会打印出10个10。运行代码片断二,会打印出0、一、二、三、四、五、六、七、八、9。这说明了var
和let
是的不一样的,var
声明的变量会提高,是做用于Global Scope或Function Scope中的,在代码片断一的setTimeout
开始执行的时候for
循环已经运行结束了,这个时候i
的值是10,因此打印出了10个10。而let
是做用于Block Scope的,每次循环声明的i
只在当前的Block Scope中有效,因此每次打印出的i
都不相同。
Lexical Scope又称为Static Scope(静态做用域),通俗点说就是你在写代码的时候,某个变量的做用域就已经肯定好了,你能够经过直接看代码就可以分析出变量的值。
// global scope
function getName() {
// function scope
var name = '老王'
return function () {
// function scope
console.log(name)
}
}
let showName = getName()
function getUser() {
// function scope
var name = '老李'
showName()
}
console.log(showName()) // 老王
getUser() // 老王
复制代码
当代码执行到getName
内部的console.log(name)
时候,无论外部是如何调用的,都是经过向上查找到var name = '老王'
这条声明赋值语句,从而获取name
变量的值。因此说在函数还未执行以前,就能够根据Static Scope找到变量对应的值,且这种关系是肯定的,不会发生改变,这就是词法做用域。
既然有Static Scope,那么也有Dynamic Scope(动态做用域)。动态做用域意味着在代码执行阶段,变量的取值是会根据做用域的不一样而发生变化。
// global scope
function user1() {
// function scope
var name = '老王'
getName()
}
function user2() {
// function scope
var name = '老李'
getName()
}
function getName() {
console.log(name)
}
user1() // undefined
user2() // undefined
复制代码
看以上代码,getName
中并无声明name
变量,因此执行结果都为undefined
。若是JavaScript支持动态做用域,那么user1()
的执行结果将是老王
,user2()
的执行结果将是老李
。事实上,JavaScript并无Dynamic Scope,它只有Lexical Scope,它们的本质区别是,Lexical Scope是做用于代码编写阶段,关心的是函数在哪声明;而Dynamic Scope是做用于代码执行阶段,关心的是函数在哪被调用。
var
定义的变量做用于Function Scope,而且会出现Hoisting
现象。let
和const
定义的变量做用于Block Scope,在Block Scope以外没法引用。理解Scope的相关概念对学好JavaScript很是重要,但愿经过以上的梳理,能让你对JavaScript Scope有更加清晰的认识。若是有什么解释的不清楚的或者理解有误的地方,欢迎留言交流。