本文深入解析JavaScript中的函数表达式与闭包机制。首先对比函数声明与表达式定义的差异,剖析函数声明提升导致的常见调用错误及解决方案。接着通过示例阐述闭包的定义、形成原因及其对内存的影响,最后探讨匿名函数中this指向的特殊现象,帮助开发者理解闭包在内存保留与对象方法调用中的实际应用。
JavaScript函数表达式与闭包基础
JavaScript支持函数闭包的核心原因在于其作为函数式语言的特性,即函数需要能够保存数据。具体而言,这种数据保存指的是函数运行结束后,其内部变量的值依然被保留。这是函数式语言的重要特征之一。
回顾前文介绍的三种函数定义方式。
函数声明语法定义:function fun(num) { return num; } 函数表达式定义:var fun = function (num) { return num; } Function构造函数:var fun = new Function("num", "num", "return num");
函数定义与调用常见错误
在深入分析闭包之前,我们先探讨定义和调用函数时容易出现的错误。
示例1:
sayHi(); //错误:函数还不存在
var sayHi = function () {
alert("test");
};
示例2:
if (true) {
function sayHi() {
alert("1");
}
} else {
function sayHi() {
alert("2");
}
}
sayHi();//打印结果并不是我们想要的
示例3:
var fun1 = function fun2() {
alert("test");
}
fun2();//错误:函数还不存在
在示例1中,函数声明式语法定义之前调用函数会报错。解决方案有两种:
1. 若使用函数表达式定义,必须在表达式定义完成后进行调用。
var sayHi = function () {
alert("test");
};
sayHi()
2. 使用函数声明式。浏览器引擎会执行函数声明提升,在所有代码执行前先读取并处理函数声明。
sayHi();
function sayHi () {
alert("test");
};
在示例2中,预期结果应为打印1,但实际结果是打印2。
if (true) {
function sayHi() {
alert("1");
}
} else {
function sayHi() {
alert("2");
}
}
sayHi();//打印结果并不是我们想要的
原因在于函数声明提升。浏览器在预解析阶段不会判断if条件,而是直接解析第二个函数定义,从而覆盖了第一个定义。
解决方案:
var sayHi;
if (true) {
sayHi = function () {
alert("1");
}
} else {
sayHi = function () {
alert("2");
}
}
sayHi();
在示例3中,发现只能使用fun1()调用,而不能使用fun2()调用。
对于这一现象,作者目前尚无确切理解,也未找到相关权威资料解释。
因为1: function fun3() { }; 等效于 var fun3 = function fun3() { }; 如图:
因此,只能使用fun1()调用,而不能使用fun2()调用。
此处作者仍存疑问,若有大神知晓原因,望告知。
既然fun2在外部无法调用,为何在函数内部可以调用?尽管在调试器中仍无法获取fun1。
闭包的定义与内存管理
通过上述三个示例进行热身后,我们继续今天的主题“闭包”。
1. 什么是闭包?
定义:有权访问另一个函数作用域中变量的函数。
我们先从一个示例函数开始:
示例1:
function fun() {
var a = "张三";
}
fun();//在我们执行完后,变量a就被标记为销毁了
示例2:
function fun() {
var a = "张三";
return function () {
alert("test");
}
}
var f = fun();//同样,在我们执行完后,变量a就被标记为销毁了
示例3:
function fun() {
var a = "张三";
return function () {
alert(a);
}
}
var f = fun();//【现在情况发生变化了,如果a被销毁,显然f被调用的话就不能访问到变量a的值了】
f();//【然后变量a的值正常的被访问到了】
//这就是闭包,当函数A 返回的函数B 里面使用到了函数A的变量,那么函数B就使用了闭包。
示例:
function fun() {
var a = "张三";
return function () {
alert(a);
}
}
var f = fun();//【现在情况发生变化了,如果a被销毁,显然f被调用的话就不能访问到变量a的值了】
f();//【然后变量a的值正常的被访问到了】
显然,滥用闭包会增加内存占用。因此,除非特殊情况,否则尽量避免使用闭包。若必须使用,记得手动设置空引用以释放内存,例如:f = null;。
图解说明(不了解作用域链的同学请先阅读前文“作用域和作用域链”):
匿名函数与this指向问题
2. 什么是匿名函数?(仅解释此概念)
例如:(即没有名字的函数)。
关于对象中函数的返回值是匿名函数时,this指向的怪异现象。
讲解前请保持清醒,避免越看越迷糊。若感到困惑,可直接忽略以下内容。
var name1 = "张三";
var obj = {
name1: "李四",
fun2: function () {
alert(this.name1);
},
fun3: function () {
return function () {
alert(this.name1);
}
}
}
obj.fun2(); // 打印结果"李四",符合预期。 obj.fun3()(); // 因为返回的是一个函数,所以需要再加一对()来调用。打印结果是"张三",出乎意料。 // 令人不解的是,this为何指向了全局? 我们之前讲过“哪个对象点出来的方法,this就是哪个对象”,那么 obj.fun3()() 打印的是“张三”,意味着this指向了全局作用域。
我们看看下面的示例,或许能明白原因。
var name1 = "张三";
var obj = {
name1: "李四",
fun2: function () {
alert(this.name1);
},
fun3: function () {
return function () {
alert(this.name1);
}
}
}
//obj.fun3()();
var obj2 = {};
obj2.name1 = "test";
obj2.fun = obj.fun3();
obj2.fun();//打印结果"test",再次证明了“哪个对象点出来的方法,this就是哪个对象”.
var name1 = "张三";
var obj = {
name1: "李四",
fun2: function () {
alert(this.name1);
},
fun3: function () {
return function () {
alert(this.name1);
}
}
}
//obj.fun3()();
var obj2 = {};
obj2.name1 = "test";
obj2.fun = obj.fun3();
obj2.fun();//打印结果"test",再次证明了“哪个对象点出来的方法,this就是哪个对象”.
分解 obj.fun3()():先执行 obj.fun3() 返回一个匿名函数,该函数处于window作用域,接着调用时this便指向了window。(此解释略显牵强,暂作如此理解。)
闭包形成的原因:内存释放问题。
通常,函数执行完毕后,局部活动对象会被销毁,内存中仅保留全局作用域。但闭包的情况不同。
闭包的活动对象依然保存在内存中。如上例所示,函数调用返回后,变量i属于活动对象,其栈区尚未释放。当调用c()时,变量i的作用域链从b()->a()->全局寻找var i声明所在,找到var i=1;后在闭包内执行++i,最终输出值为2。
以上所述是小编给大家分享的JavaScript基础篇(6)之函数表达式闭包,希望大家喜欢。