本文是《后端程序员的 JavaScript 之旅 - 作用域链、执行上下文与闭包》的学习笔记,有兴趣的朋友可以去阅读以下原文。
JavaScript 采用词法作用域(lexical scoping)
先来解释这句话,JavaScript 采用词法作用域(lexical scoping),函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定。猜猜下面代码的运行结果
var scope = 'global scope';
function foo() {
var scope = 'local scope';
return function () {
return scope;
}
}
var f = foo();
f()// 返回 "local scope"
用多了脚本语言,搞得我已经忘了,按照传统的格式严密的C语言,Java语言的思维逻辑应该如何解读这段代码。好吧,以C语言的严谨逻辑理解好像是这样的吧,return function(){}返回的是一个函数指针,然后在外面用f()调用了这个函数,然后函数查找当前函数作用上下文有没有scope,没有发现scope,所以返回了全局scope变量的值‘global scope’。好像是这样的吧,我也搞不太清楚了(欢迎探讨)。可是这里JavaScript返回的却是local scope。这里面涉及了一个”高大上“的概念——闭包。函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定。这句话说实话有点难懂或者说抽象。我想通俗且具象的来解释一下JavaScript闭包的实现机制。
首先,在JavaScript中一切皆对象,函数也是对象,每个函数都有自己的属性。你可以这样考虑,当你写这样一段代码function foo(){}的时候其实是创建了一个对象而并不是定义了一个函数。有了这个概念很多东西就很好理解了。那么当你创建了一个函数对象,这个函数对象里都有什么呢?我先讲与闭包相关的属性,没错它就叫<Closure>属性,是一个有序列表,列表中装的就是“作用域”,它们的排列顺序是当前函数作用域——上层函数作用域——全局作用域。所以说当函数想访问一个变量的时候它先查找当前作用域,然后查找上层函数作用域直到全局作用域。而这一切都在在这个函数的自身的<Closure>属性中查找,而这些属性在“函数定义”的时候(实际上是函数对象的创建)就已经设置到函数对象里了(它在函数对象里,却不能用JS代码来访问,这个属于只有浏览器才能操作的属性),所以说函数执行依赖的变量作用域是由函数定义的时候决定的。
我这里插播一段对函数对象笼统解释。函数对象比普通对象多了 prototype和<Closure>属性,用构造函数生成的普通对象的_proto_指向它的构造函数的prototype属性,prototype属性里默认包含了constructor属性(即函数本身)和_proto_(Object类型)。<Closure>属性是一个列表,里面的元素指向了当前函数所有上层函数的作用空间,变量查询的时候由近及远,直到最上层的Window是对象(也就是全局作用与)。
现在重新解释一下上面的代码
//定义全局变量,属于Window作用域
var scope = 'global scope';
//创建一个叫foo的函数对象它<Closure>属性里面有一个foo作用域
function foo() {
//在foo作用域里创建scope变量
var scope = 'local scope';
//创建一个匿名函数对象,它的<Closure>属性里除了有自己的作用域,还有foo和Window的作用域
return function () {
return scope;
}
}
//调用foo函数foo函数返回它内部创建的匿名函数对象
var f = foo();
//执行匿名函数,获得scope变量,这个函数自己的作用域里没有scope,它就找上层函数对象foo的作用域,发现里面有一个scope变量,于是返回
f()
思考一下下面的代码,想一想如何改写最里面的匿名函数,让程序可以访问到"my scope"
var scope = 'global scope';
function foo() {
var scope = 'local scope';
function innerFun(){
var scope = 'inner scope'
return function(){
this.getScope = function (){
return this.scope;
}
return scope;
};
}
return new innerFun();
}
// var ff = new foo();
var f = foo();
console.log(f()); // 返回 "local scope"
f.scope = "my scope";
console.log(f());
console.log(getScope())
输出结果
inner scope
inner scope
global scope
不得不说,用JavaScript写的代码给人一种谜一般的感觉。我基本已经被绕晕了。不过用上面我写的概念分析这段代码还是没问题的。这里有一个新的关键字new我决定再开一篇文章总结。没想到光写一个词法作用域就写了这么多。《后端程序员的 JavaScript 之旅 - 作用域链、执行上下文与闭包》中提到的其他概念我再开一片文章分析吧。欢迎大家热烈讨论
对不住大家了,我这篇文章上面的解释可能会导致一些误解,在此更正一下。当一个函数配定义的时候,在这个函数内部被声明的局部变量并没有被写入<Closure>属性,函数被执行的时候局部变量才会被创建,被写入<Closure>的只有定义当前函数的上层函数的局部变量,一直到全局变量(更准确的说应该是变量的引用)。也就是说上面的代码我的注释是有错误的。我重新解释一下,对比我之前注释,大家可以加深对这个问题的理解。
//定义全局变量,属于Window作用域
var scope = 'global scope';
//创建一个叫foo的函数对象它<Closure>属性里面没有任何属性
function foo() {
//在foo作用域里创建scope变量
var scope = 'local scope';
//创建一个匿名函数对象,它的<Closure>属性有foo作用域中的局部变量scope的引用
return function () {
return scope;
}
}
//调用foo函数foo函数返回它内部创建的匿名函数对象
var f = foo();
//执行匿名函数,这个函数自己并没有定义scope变量,它就到<Closure>里找上层函数对象foo的作用域,发现里面有一个scope变量,于是返回
f()
所以说函数内部局部变量是在函数执行的时候被创建的,而函数对象闭包属性中的变量是在函数定义时被它的上层函数创建,并赋给这个函数对象的<Closure>属性。
这里面又引出了另一个问题,没有闭包概念的编程语言,函数内部定义局部变量后,它们会被存储到一个函数栈中,当函数调用结束,这些局部变量会出栈,永久销毁,无法用任何方式访问。简单来说JavaScript的函数局部变量也差不多,当函数调用结束,这些局部变量会被标注成垃圾等待垃圾回收,无法被访问到。但是有了闭包这个概念就不一样了,如果在一个函数的内部定义另一个函数,它会把自己的局部变量的引用传递给它定义的函数对象,当这个子函数对象被返回给外部的时候,即使当前函数执行完毕,它的局部变量的引用仍然在“活在”某一个函数对象里,所以这些的局部变量不会被垃圾回收,这就是闭包的核心概念,既函数可以访问它上层定义它的函数的局部变量。由此我们可知,闭包的滥用,会导致应该回收的内存(函数作用域范围内的局部变量)得不到回收,从而会导致内存泄露。当然,现在的的计算机内存如此之大,想写出一个能把浏览器内存都吃没的程序也不是那么容易的,所以日常编程中也没有哪个前端程序员会考虑内存泄露这个问题。
网友评论