作用域和闭包
作用域
高程92页
执行环境是 JavaScript中最为重要的一个概 念。 每个执行环境都有一个 与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中.
每个函数都有自己的执行环境
当代码在一个函数中执行时,会创建变量对象的一个作用域链(scope chain)。
将函数的活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。
作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
执行环境 , 变量对象 , 函数 , 活动对象 几个专有名词 , 不考虑全局函数的情况下 .
- 每个函数有自己的执行环境 , 函数的活动对象作为执行环境的变量对象.作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
示例
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
debugger;
// 这里可以访问 color、anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors();
}
// 这里只能访问 color changeColor();
可以到浏览器控制台运行一下 .
我们看看debugger时 的 scoped
我们再加一层看看
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
function consoleColors(){
console.log(anotherColor);
console.log(tempColor);
debugger;
}
// 这里可以访问 color、anotherColor 和 tempColor
consoleColors();
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors();
}
// 这里只能访问 color changeColor();

它保存了两个作用域链上上级的变量(下文称闭包变量) , 并且是引用的变量才保存 , 也就是如果没有引用父级的变量 , 但是引用了上级的变量 , 那么不会生成父级的变量对象而是生成上级的变量对象 .
注意是引用而不是保存或者按值传递 , 也就是说它并非是静态的 , 下面会提到它的用途和误用点.
闭包(Closure)
高程196页
闭包是指有权访问另一个函数作用域中的变量的函数。
另一个指哪一个 ?
创建闭包的常见方式,就是在一个函数内部创建另一个函数
作用域示例中,在A函数内部创建B函数函数 , B就是闭包 。
我们得出闭包的必要条件如下
- A函数内部创建B函数函数
- B引用A中得变量
Javascript权威指南
函数的变量的作用域是在函数定义时确定的。
理解闭包的关键:作用域是定义时上下文确定 , 而非依赖调用时的上下文
为什么会生成闭包变量(指作用域讲解中调试图的红框的Closure)? 因为函数的作用域是定义时就确定了, 但是函数并不知道自己将会在哪里调用 , 它可能在父函数中调用 , 它可能在父函数外被调用 , 所以设计者就让它生成闭包变量来保存作用域链 , 保证它当在外部调用时 , 它就可以直接找到定义时状态的变量 . 当然无论是否是在外部调用的 , 他们都会生成闭包变量 , 他们都是闭包 .
但是讨论不在父级外被引用的闭包在父函数执行完毕后是没有意义的(刚刚在作用域链讲解示例就是这种情况) . 因为父级函数执行完毕之后 , 一般来说 , 父级的作用域链就被销毁了 , 外界访问不到闭包 , 自然也会被回收 .(这里涉及回收问题 , 可能会有误)
但是返回值为函数时会有不同
在闭包从父级中被返回后,它的作用域链被初始化为包含父级函数的活动对象和全局变量对象
因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当父级函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,父级的活动对象才会被销毁.
js引擎知道外界引用了一个闭包 , 它就不会销毁闭包中的父级变量 .
所以闭包一般指下面的情况 将函数作为返回值给外界接收使用的情况
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
}
else if (value1 > value2){
return 1;
}
else {
return 0;
}
};
}
它引用了父级的变量(参数也是变量) , 并作为父级的返回值返回给外层接收 .
闭包其实无处不在 , 除了 return
一个闭包是你刻意使用闭包之外 , 其实一般的回调函数都是闭包 , 再举一个例子:
es6中最常用的就是箭头函数了 , 除了它简洁的特性之外 , 还有一个很大的作用就是绑定this
//阮一峰 es6入门 函数的拓展--箭头函数
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,
实际原因是箭头函数根本没有自己的this,
导致内部的this就是外层代码块的this。
正是因为它没有this,所以也就不能用作构造函数。
所以,箭头函数转成 ES5 的代码如下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面代码中,转换后的 ES5 版本清楚地说明了,
箭头函数里面根本没有自己的this,
而是引用外层的this。
ES5版本的保存this就是用到了闭包 , 这个函数被传入定时器 , 显然它将在外部被调用(最外层环境调用这个函数) , 因为作用域固定 , 所以变量_this依旧会被找到 .
回调函数肯定是脱离了原函数环境 , 所以它也是闭包 .
高程中说 , 理解作用域是理解闭包的关键 . 其实不然 , 理解闭包才是理解作用域的关键 , 因为一个函数的作用域就是通过一个个闭包变量(上级变量对象)堆起来的 .
闭包中变量的问题
刚刚说到闭包只是引用变量 , 而非按值传递
即闭包只能取得包含函数中任何变量的 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量
比如以下函数
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
它返回了一个闭包数组 , 这个函数原意认为 , 每个闭包都应该保存 i 应该是从0-9 ; 但是实际上他们都只保存了10 ; 因为他们引用的是同一个 i ;
它由闭包特性引起 , 同时 , 也可以通过闭包的特性解决 .
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = (function(i){
return function(){
return i;
})()
};
}
return result;
}
我们创建了一个函数 , 这个函数返回一个闭包 . 我们将 i 传递入父函数 , 注意函数参数的传递是按值传递 , 所以闭包报保存了当时父函数的值 . 不过现在let
声明来声明i已经很方便地解决了这个问题 .
拓展:深刻理解es6函数默认值作用域和其中生成中的闭包
阮一峰es6入门:函数的拓展--默认值的作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
Javascript权威指南
函数的变量的作用域是在函数定义时确定的。
**函数的变量的作用域是在函数定义时确定的 , 很重要 . **
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
这个函数会打印出什么 ? 按照常识我们会认为它打印出了 2 ;按常识 , 在这个上下文中 , 作用域链会往上找 , x 应该指向的是var x = 3
的 x ; 被 y()
后改为了 2 ;
但是它打印出了 3 而非 2;
奥秘就在开头 , 如果设置默认值时 , 就会生成一个单独的作用域 就好像是这样子
function(){
let x;
let y = function(){
x = 2;
}
}
它类似于一个函数 (下面称作用域伪函数), 生成了一个作用域 , 它与foo函数是平行的 , 而非包含关系
所以 y 是 这个作用域中的闭包 , 我们可以打上debugger来看看
var x = 1;
function foo(x, y = function() { x = 2;debugger; }) {
var x = 3;
y();
console.log(x);
}

注意x = 2 ; y()把父级作用域中的 x 由undefined 变为了 2 ;
同时y也非常特殊 , 它并非是被 return 而保存下来的 , 它是编译器内部把y的地址赋给了参数(如果你没有输入)
为什么他们是平行的 以下两个例子说明
//仍然是上面的函数
y(5);
//x已经不取默认值
//5
//Closure(foo)
x:2
我们传入x的参数 , 很显然编辑器并不考虑有没有传参数 , 传了几个参数 , 只要你设置了其中一个默认值 它就会在执行函数前一步执行作用域伪函数: 声明所有参数变量 有默认值则赋值 无默认值则是undefined ;
如果你传入了某个参数 , 它就抛弃这个值 ; 如果你没有传入某个参数 , 它就把这个值赋给这个参数 ;
函数是引用变量 , 所以这也是没有return却可以使用一个闭包的原因
下面是第二个例子
var x = 1;
function foo(y = function() { x = 2;}) {
var x = 3;
y();
console.log(x);
}
console.log(x);
这次我们没有定义 x 了 .
会发生什么?
A. y()时寻找到foo()内部的x变量并赋值
B.y()时寻找到外部的x变量并赋值
C.定义时报错
D.y()执行时报错
答案是
B
解析:虽然在某个内部函数被调用但是它还是沿定义时作用域链向上找变量 , 所以 没有x参数 , 它就找到了全局变量的x .
//3
//2
这个例子就是与foo函数是作用域平行的最好证明 .