最近遇到React Hook的过时闭包问题,重温了下红宝书,这里整理之前积累的一些基础。
函数声明:
function say(){
xxxx
}
函数表达式:
赋值变量的方式可以使用匿名函数(lambda函数)
var say = function(){ xxx }
注意函数声明在编译期间把声明提前,函数表达式不会。
这两个概念很重要:
当某个函数被调用时,会创建一个执行环境及相应的作用域链。
然后,使用arguments和其他命名参数的值来初始化函数的活动对象。
每个执行环境都有一个表示变量的对象,变量对象,变量对象是活动对象的具体的每个表示变量的对于下部分
作用域链本质上是一个指针列表,指向变量对象的指针列表,只是引用实际不包含变量对象(可看下图)
在作用域链中,外部函数的活动对象始终处于内部的上一位,直至为作用域链终点的全局执行环境。
全局环境的变量对象始终存在,像函数声明的函数的局部环境的变量对象,只有在函数执行的过程中存在。
举个栗子:
function compare(v1,v2) {
xxx
}
var res = compare(1,2);
这里的说明直接看红宝书,红宝书讲的很好:
创建函数的时候,就会预先创建包含全局变量对象的作用域链,但只有调用的时候,才会创建执行环境,并复制Scope中的作用域链,才会产生活动对象。
这样我们可以解释这个现象:
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
}
return increment;
}
let inc = createIncrement(1);
inc(); //1
// 匿名函数销毁,才会销毁value
// inc = null;
inc(); //2
inc是createIncrement返回的increment函数,createIncrement在执行完毕后,其活动对象不会被销毁,也就是value不会被销毁,因为increment(也可改为匿名函数)的作用域链仍然在引用这个活动对象。当createIncrement函数返回后(被赋值给inc),其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中。除非increment即inc被销毁,createIncrement的活动对象才会被销毁,被销毁之前,value在内存中都是一个变量对象,所以才会一直被累加。
销毁很简单,根据垃圾回收的计数引用机制,inc = null即可。
由于闭包会携带包含它的函数的作用域,就比如上例中的increment包含了createIncrement变量对象,因此闭包会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。但现在V8很智能,会尝试回收闭包占用的内存,下次有机会整理下这里的内容。
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
}
}
return result;
}
var res = createFunctions();
// 10 10 10 10 10
console.log(res[0](), res[1](), res[2](), res[3](), res[4]());
js最经典的入门题,当初刚开始接触js的时候也在社区里问过这个问题,现在结合红宝书的闭包相关概念理论基础来回答这个问题。
createFunctions函数最终返回一个函数数组,期望函数数组返回循环中不同的变量i。但是注意,这里的i是在
createFunctions的作用域中,当createFunctions声明的时候,创建包括全局变量对象的作用域链已经保存在scope属性中,当调用createFunctions的时候,执行环境被创建,并复制scope对象的属性,连接起作用域链。result函数表达式同理,由于result函数只是声明,最后的每个result函数的作用域链中都是保存着createFunctions的活动对象,而i即是活动对象中的一个变量对象。当createFunctions返回后,即执行到
return result
这条语句,i的值即是10,result函数的函数的作用域链此时在1优先级的位置(参考上面2标题的图)连接
着createFunctions的作用域,且所有函数都引用指向createFunctions作用域中保存变量i的同一个变量对象。i在createFunctions返回后值已经是10了。
经典的解决办法:
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
return num;
}
})(i)
}
return result;
}
var res = createFunctions();
// 解释在:
console.log(res[0](), res[1](), res[2](), res[3](), res[4]());
给result函数中返回num的函数外再加一个匿名函数,这样我们并没有直接把闭包复制给result数组,而是把匿名函数执行的结果赋给数组,这个匿名函数有个参数值也是要返回的值,函数参数时按照值传递的,这样就将变量i当前值复制给参数num。而在这个匿名函数中,又创建并返回了一个访问num的闭包(函数中嵌套函数即产生闭包,且num是外部函数作用域的变量对象,那么内部函数即是访问num的闭包)。因为执行了10次,创建了10个不同执行环境的闭包(起码在匿名函数层的作用域链中的变量对象的i值是不同的),结果就是result数组中每个函数都创建拥有自己num变量的一个副本。
(此处不谈ES6的做法。)
理解了上面的执行环境,this就好理解,只用记住,this对象是运行时基于函数的执行环境绑定的。
一个需要注意的是每个函数在调用的时候都会自动获取两个特殊变量: this和arguments。但内部函数在搜索这两个变量时,只会搜索到其活动对象位置,因此永远不可能直接访问外部函数中的两个变量。
因此在:
var name = 'window';
var obj = {
name: 'obj',
getName: function () {
return function () {
return this.name;
}
}
}
console.log(obj.getName()());
最内部的匿名函数并没有取得外部作用于的this对象。所以最经典的做法就是把外部作用域中的this对象保存在一个闭包能够访问到的变量里,让闭包通过该变量访问对象:
var name = 'window';
var obj = {
name: 'obj',
getName: function () {
var that = this;
return function () {
return that.name;
}
}
}
console.log(obj.getName()());