作用域
什么是作用域?
作用域
是指针访问某一变量
具有访问权限的代码空间,在JS当中作用域是在函数中体现的,表示可访问某一变量的区域,指代了上下文执行环境;
通俗一点说就是,JS只包含两种作用域:全局作用域
与局部作用域
ps:有的同学问那方法呢,其实方法也是变量,就是声明的语法不一样而已
function fn(){}
var fn = function(){}
全局作用域
在web应用当中,全局作用域就是我们的window对象
,node环境中全局作用域为global对象
var a = '123'
console.log(a) // 123
console.log(window.a) // 123
a = {}
window.a === a // true
局部作用域
js当中只有一种局部作用域,就是函数作用域
,在当前函数执行上下文,只能访问当前函数内部的变量
和嵌套作用域的变量
(作用域链稍后说),当前函数的作用域链是由函数被声明的时候确定的,而不是调用的时候
var a = 'window'
function fn1(){
var a = 'fn1'
console.log(a) // fn1
}
fn1()
块级作用域(ES6)
块级作用域是ES6新增的特性,主要是利用了const和let这样的关键字在声明变量时,会在当前声明的上下文的{}
大括号中,创建一个块级作用域
块级作用域解决了什么问题呢?
假设我们有一个需求,我要让函数每次保存一个循环的值,然后最后再依次打印出来
var callbacks = []
for(var i = 0; i < 10; i++) {
callbacks.push(function(){
console.log(i)
})
}
callbacks.forEach((fn)=>{
fn()
})
// 输出10次10
结果我们发现,因为i是用var声明的,所以是一个全局变量,当10次循环结束后,全局变量已经变为10了,这跟我们的初衷不同,那么我们改用let来试一下
var callbacks = []
for(let i = 0; i < 10; i++) {
callbacks.push(function(){
console.log(i)
})
}
callbacks.forEach((fn)=>{
fn()
})
// 0 1 2 3 4 5 6 7 8 9
这次就和我们的预期一致了,这是为什么呢?
首先每一次循环的时候,let都会为当前{}
内的上下文创建一个新的块级作用域并且声明了变量i再赋值,根据作用域链
的就近原则,i每次会找到离自己最近的作用域当中声明的变量i打印
使用var和let的区别:
变量提升(补充)
这其实是个很大的话题,以后有机会讲可以参考下这篇文章js变量提升
作用域链
js引擎会根据作用域之间的嵌套关系,遵从就近原则,首先在当前作用域当中查找变量的引用,如果没有找到,就像一条锁链逐级向上查询所使用的变量
var a = 'window';
function fn1() {
console.log('fn1:', a); // a -> window
function fn2() {
var a = 'fn2a'
console.log('fn2:', a); // a -> fn2a
function fn3() {
console.log('fn3:', a); // a -> fn2a
}
fn3();
}
fn2();
}
fn1();
// 执行结果:
// fn1: window
// fn2: fn2a
// fn3: fn2a
作用域链:
闭包
什么是闭包?
闭包让你可以在一个内层函数中访问到其外层函数的作用域
function init() {
var name = "window"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
init()
创建了一个局部变量 name
和一个名为 displayName()
的函数。displayName()
是定义在 init()
里的内部函数,并且仅在 init()
函数体内可用。请注意,displayName()
没有自己的局部变量。然而,因为作用域链嵌套的原因它可以访问到外部函数的变量,所以 displayName()
可以使用父函数 init()
中声明的变量 name
。
再看一个例子:
function makeFunc() {
var name = "window";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
运行这段代码的效果和之前 init()
函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName()
在执行前,从外部函数返回。
第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 makeFunc()
执行完毕,你可能会认为 name
变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。
原因在于,JavaScript中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc
是执行 makeFunc
时创建的 displayName
函数实例的引用。displayName
的实例维持了一个对它的词法环境(变量 name
存在于其中)的引用。因此,当 myFunc
被调用时,变量 name
仍然可用,其值 window
就被传递到alert
中。
闭包的原理
总结一下,由于内部函数
引用了外部函数
的变量,内部函数
又被return出后被全局变量myFunc
引用形成了一个引用链,导致js垃圾回收机制无法回收外部函数当中的变量name
,于是形成了一个外部函数
无法被其他人访问,但可以被内部函数
访问的封闭环境,即为闭包
实用的闭包
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。所以总而言之当你需要一个函数访问一个属于他私有的作用域时,就可以使用闭包
闭包的缺点
由于闭包会使函数中的引用的外部变量无法被垃圾回收机制
回收,外部变量仍然占据内存空间,内存消耗很大,所以不能滥用闭包,解决办法是,退出函数之前,将不使用的局部变量删除。