前言
闭包是个容易老马失蹄的概念,让我们先从案例入手来了解闭包是什么。
案例
案例一
case 1.1
这是个真实案例
var greet = function (props) {
return (name) => console.log(props.content, name);
};
class Greeter {
constructor(props) {
this.props = props;
}
task = greet(this.props);
}
const instance = new Greeter({ content: "hello" });
instance.task("jack");
instance.props.content = "hi";
instance.task("jack");
猜一下会输出什么?
实际上,这段程序会抛出TypeError的错误
调用栈
观察现象
观看调用栈可知,Greeter的方法task在类声明时,就已经被赋值了一个闭包,而这个闭包内部的props尚未赋值。
当new 实例后,即使this.props发生改变,也不会影响到闭包内部的props。
case 1.2
function interview(){
setTimeout(()=>{
throw new Error('aaa');
},0)
}
function task(){
try{
interview();
throw new Error('b');
}catch(e){
console.log(e);
}
}
task();
结果
调用栈
在setTimeout的回调函数里的error,因为调用时是在另一个调用栈中,实际上并没有被在task里的那个try catch包裹,所以这个错误没有被捕获。
案例二
var greet = function (props) {
return () => console.log(props.content);
};
var props = { content: "hello" };
var task = greet(props);
task();
props.content = "hi";
task();
调用栈
结果
观察现象
这个案例与案例一不同的地方在于,props一开始已经被定义了。
当props发生改变时,闭包内部的props发生了变化。
可以推断的是,这个闭包内部的props保存的是外部那个props的引用,所以才会有这个效果。
变形
var greet = function (props) {
return () => console.log(props.content);
};
var props = { content: "hello" };
var task = greet(props);
task();
props = { content: "hi" };
task();
输出
观察现象
这个结果佐证了上面说的。确切来说,闭包保存的是值的引用而不是值本身。
即,当我重新对外部props赋值,对闭包内所存的那个旧props引用不影响。
案例三
那么案例一可以变形为
var greet = function (props) {
return (name) => console.log(props.content, name);
};
class Greeter {
constructor(props) {
this.props = props;
this.task = greet(this.props);
}
}
const instance = new Greeter({content: "hello"});
instance.task("jack");
instance.props.content = "hi";
instance.task("jack");
输出
小结
当入参为引用数据类型时,闭包内部维护的是传入的值的引用。
案例四
入参为基本数据类型时
var greet = function (props) {
return (name) => console.log(props, name);
};
var props = "hello";
var task = greet(props);
task("Jack");
props = "hi";
task("Jack");
输出
作用域
观察现象
闭包内部维护了一个同名的局部变量,保存的是第一次调用时传入的值,作用域为函数内部。
小结
- 对于引用数据类型,闭包保存的是值的引用。
- 对于基础数据类型,闭包保存的是值本身。
概念
闭包是什么?
首先,要有闭包肯定得有函数。
函数与作用域链
作用域链
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象
普通函数
function compare(value1, value2) {
if (value1 < value2) {
return - 1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(1, 2);
普通函数的作用域
观察现象
- 在执行到compare时,持有this、value1、value2这三个局部变量。
为什么说value1、value2是局部变量?简单说来,就是在compare执行时,首先在函数顶部声明了var value1=value1;var value2=value2;
。 -
变形
加了一个内部变量
可以看到在local声明未赋值时,赋值过的value1、value2就出现在作用域内了
- 作用域链除了这些个局部变量之外,还有一个this。到现在作用域链这个概念还是挺抽象的,没有办法一管窥全豹。
作用域图例
来源:JavaScript高级程序设计(以下序号与图中相匹)
-
在创建
创建时[[scope]]compare()
函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]
属性中。
-
当调用
调用时compare作用域compare()
函数时,会为函数创造一个执行环境,然后通过复制函数的[[scope]]
属性中的对象构建起执行环境的作用域链。
也就是说,全局变量对象是在执行函数时复制了创建函数时就已经确定好的那些变量对象。
-
此后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。
- 对于这个例子来说,其作用域链包含两个变量对象:本地活动对象和全局变量对象。
闭包 Closure
function createComparisonFunction(propertyName) {
return function compare(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
var compareNames = createComparisonFunction("name"); // 创建函数
var result = compareNames({ name: "jack" }, { name: "tony" }); // 调用函数
compareNames = null; // 释放内存
作用域图例
来源:JavaScript高级程序设计步骤
-
创建compare时
创建时 -
创建compare后
- 当
compare
被返回之后,他的作用域链被初始化为包含createComparisonFunction()
函数的活动对象和全局变量对象。
执行完创建之后 -
createComparisonFunction
执行完了,但他的活动对象不会被销毁。因为返回的那个compare
作用域链仍然在引用这个活动对象。
即,当createComparisonFunction
返回之后,其执行环境的作用域链会被销毁,但他的活动对象仍然存在内存中。
- 执行compare时
createComparisonFunction
的活动对象propertyName
被存在了compare
[[scope]]
属性的Closure
里。
函数执行的时候,从Closure
内访问propertyName
。
执行中
作用域
变形1
把他改成文章最开始的那个例子,如果Closure
内的变量是引用类型会怎样
function createComparisonFunction(referObj) {
return function compare(object1, object2) {
var value1 = object1[referObj.value];
var value2 = object2[referObj.value];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
var referObj = { value: "name" };
var compareNames = createComparisonFunction(referObj); // 创建函数
referObj.value = "gender";
var result = compareNames(
{ name: "jack", gender: "man" },
{ name: "tony", gender: "woman" }
); // 调用函数
compareNames = null;
修改传入的referObj后
可以看到修改传入的
referObj
后,闭包所持有的那个referObj
也发生了变化,印证了小结里的结论。
变形2
如果不是修改而是直接变更referObj
的值
可以看到Closure持有的referObj仍然是最开始的那一个
总结
对于闭包来说:
创建函数时
函数的[[scope]]
属性拥有两个引用:一个是Global
、一个是Closure
函数执行时
作用域链如下
作用域链
闭包内所存活动对象
值类型存值,引用类型存引用。
网友评论