作用域链
- 当一个函数被调用的时候,都会创建一个执行环境以及相应的作用域链
- 使用arguments和其他命名参数初始化这个函数的活动对象;位于作用域链的第一位
- 外部函数的活动对象位于作用域链第二位···最后一位是全局执行环境(全局变量对象)
- 全局变量对象始终存在
- 局部活动对象仅在函数执行时存在
- 一般情况下,函数执行后,本地活动对象被销毁;内存中仅保存全局作用域
function compare (a, b) {
if (a < b){
return -1
}else if (a > b) {
return 1
}
return 0
}
var result = compare(5, 10)
上图即展示了,compare()函数执行时的作用域链:
compare()内部活动变量位于作用域链第一位- 外部变量对象,位于第二位;此处外部活动对象即为全局变量对象
- 当
compare()函数执行完成,内部活动对象即被销毁,全局依然保存
但是,闭包不同于普通函数。
闭包
闭包的作用域链
当一个函数返回另外一个函数(闭包)时,返回的这个函数会将包含它的函数的活动对象加入其作用域链。当然,我们先达成共识:每个函数都有其作用域链。
那么闭包的作用域链就有(从内到外):闭包本身的活动变量->闭包包含函数(返回闭包的那个函数)的活动变量->全局变量对象
function createComparisonFunction(propertyName) {
return function (obj1, obj2) {
var val1 = obj1[propertyName]
var val2 = obj2[propertyName]
if (val1 > val2) {
return -1
}else if (val1 < val2) {
return 1
}
return 0
}
}
// 创建函数
var compareNames= createComparisonFunction('name')
// 调用函数
var result = compareName({name: 'aa'},{name: 'nn'})
// 解除对匿名函数的引用,释放内存
compareNames = null
通过下图我们可以看出
- 每个函数调用时都产生一个作用域链
compareNames()调用时,其作用域链会包括(由内到外):闭包自己的活动变量->闭包的包含函数的活动变量->全局变量对象- 闭包包含函数
createComparisonFunction()(外部函数|返回闭包的那个函数)调用完成之后,其执行环境的作用域链会被销毁,但其活动对象不会被销毁,因为··· - 因为闭包的作用域链还在,仍然在引用这个活动对象,那么闭包的作用域链何时销毁
compareNames = null时,解除对匿名函数的应用,此时闭包的执行环境的作用域链被销毁
闭包可能带来的问题
变量的问题
在之前的阅读中,我们知道闭包的作用域链会保存其包含函数的活动对象,那么可以这么说:闭包保存着其包含函数的变量的最终值。那么问题来了:
// 一道常见的面试题
function countFns () {
var fns = []
for (var i = 0; i < 10; i++) {
fns[i] = function () {
console.log(i)
}
}
return fns
}
// 得到函数数组
var theFns = countFns()
theFns[0]() // 我们的期望输出是0,但结果却是10
我们再来体会一下上面那句话,闭包的作用域链包括其包含函数的活动对象,保存着其包含函数变量的最终值。因为,在本题中,闭包中的i最后为10,所以theFns中所有函数输出都是10。那么如何解决这个问题呢?let?yes,let是其中一种
解决方法一
// 使用let局部变量
function countFns () {
let fns = []
for (let i = 0; i < 10; i++) {
// 其实在这里声明了一个隐藏作用域 let i = i(循环的i)
fns[i] = function () {
console.log(i)
}
}
return fns
}
// 得到函数数组
let theFns = countFns()
theFns[0]() // 0,这回就对了
theFns[1]() // 1,这回就对了
theFns[2]() // 2,这回就对了
乍一看觉得不应该和var声明一样嘛?!其实不然,事实上会在循环体(大括号)内再有个隐藏作用域let i = i,所以函数数组中每个函数都有一个不同的i。所以输出自然会不同。详情参考方方老师这篇文章:我用了两个月的时间才理解 let
解决方法二
解决方法二其实告诉了我们一点:ES5中如何创建局部变量
先来整理思路,ES5中使用局部变量要用到匿名立即执行函数,我们要得到的数组的每个元素都是一个函数,可以用闭包。确认这两点,可以coding了
function countFns () {
var fns = []
for (var i = 0; i < 10; i++) {
fns[i] = (function (num) {
return function () {
console.log(num)
}
})(i)
}
return fns
}
为什么匿名立即执行函数可以创建局部变量:在匿名函数中定义的任何变量,都会在执行结束后立即销毁。
this对象的问题
非严格模式下,全局变量this指的是Window对象(浏览器环境下)。而在闭包使用this则指向当前匿名函数中this。
下面看一个栗子:在这个例子中,obj.sayName()返回一个函数,此函数的执行对象不是obj而是Window:我们可以这么理解var temp = obj.sayName(),然后再执行temp()函数。那么闭包中的this很明显就是全局作用域中的this,所以输出全局变量name。
var name = 'The Window'
var obj = {
name: 'The obj',
sayName () {
return function () {
console.log(this.name)
}
}
}
obj.sayName()() // The Window
arguments和this存在同样的问题,每个函数调用时都会自动取得两个特殊变量:this和arguments。内部函数搜索这两个值时,只会搜索到其内部活动对象即止。
本文中示例引用自:Javascript高级程序设计