JS中的作用域及闭包

时间:2024-03-25 11:06:44

1、JS中的作用域

在 es6 出现之前JS中只有全局作用域函数作用域没有块级作用域,即 JS 在函数体内有自己的作用域,但是如果不是在函数体的话就全部都是全局作用域。比如在 if、for 等有 {} 的结构体,就不会具备自己的作用域,在这些结构体内声明的变量将会是全局变量。由此可能导致一些问题,下面代码示例:

var tmp = new Date();
function f() {
console.log(tmp);
if (false) {  //即使没有运行到下面的代码,该变量声明也被提升了。因为变量声明是在编译阶段就被运行的,而不是代码运行阶段
var tmp = 'hello world';
}
}
f(); // undefined 由于变量提升,导致内层的tmp变量覆盖了外层的tmp变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5 用来计数的循环变量泄露为全局变量。

在 es6 中引入的块级作用域就解决了这些问题。通过 let、const 引入块级作用域后:

function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5 无法访问到块级作用域里的变量
}

1.1、调用函数时,函数内部的变量的作用域

编程语言的作用域规则有动态作用域和词法作用域,词法作用域的指的是函数和变量的作用域由声明时所处的位置决定,JS使用的就是词法作用域。

在JS中,调用函数时,函数内部的变量的作用域由函数声明时所处的位置决定,而不是调用的位置。

var a = 'window'
var f = function (){
console.log(a);
}
var b = function (){
var a = 'else'
f();
}
b(); //输出 'window'

但是 JS 中的 this 指针并不遵守词法作用域,而是取决于函数的调用方式。

2、全局变量和局部变量

JS中的变量只有两种:全局变量和局部变量。函数体外声明的变量,称为全局变量。 函数内部使用 var 声明的变量,称为局部变量。

(网上都说在函数体内声明的变量称为局部变量,那块级作用域中的变量是什么变量?我觉得应该是所有在 {} 结构体内声明的变量都称之为局部变量)

//函数内部可以直接读取全局变量。
var n=999;
function f1(){
alert(n);
}
f1(); // //但是在函数外部无法读取函数内的局部变量,因为函数内部的变量在函数执行完毕以后就会被释放掉
function f1(){
 var n=999;
}
f1();
alert(n); //报错: n is not defined

注意:在函数内部声明变量的时候,一定要使用 var、let、const 命令。否则的话实际上是声明了一个全局变量!

function f1(){
 n=999;
}
f1();
alert(n); //

而要想访问到局部变量,那么就可以使用闭包。

3、闭包的概念

闭包就是能够读取其他函数内部变量的函数。

在 es6之前,JavaScript中的变量的作用域只有两种:全局作用域和函数作用域。函数内部可以直接读取全局变量,但是在函数外部就无法读取函数内的局部变量。要想从外部读取函数内的局部变量就要用到闭包。

function f1() {
n = 999;
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); //
console.log(n); //报错 n is not defined

关于闭包的定义有很多种说法,有定义指闭包是外层函数,也有说闭包是内层函数。

还有一种说法,即指闭包不是某个函数,而是一项技术或者一个特性。函数作用域中的变量在函数执行完成之后就会被垃圾回收,一般情况下访问一个函数作用域中的变量,正常是无法访问的,只能通过特殊的技术或者特性来实现,就是在函数作用域中创建内部函数来实现,这样就不会使得函数执行完成变量被回收,这种技术或者特性应该被称为“闭包”,像是《JavaScript权威指南》打的比方,像是把变量包裹了起来,形象的称为“闭包”。

闭包可以形象地理解成:将一个变量包裹起来了,在函数外部也可以访问该变量。但请注意,闭包只是让我们在函数外部可以使用另一个函数来访问前一个函数内部的变量,但是该变量并不是变成了全局变量,直接访问该变量会报错。

4、闭包的作用

闭包可以用在许多地方。它的最大用处有两个:(1)可以读取函数内部的变量、(2)让这些变量的值始终保持在内存中。

4.1、让这些变量的值始终保持在内存中。

function f1() {
var n = 999;
nAdd = function () {
n += 1
}
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); //
nAdd();
result(); //

在上面的代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。(如果n是一个需要很费时的操作才能得到的值的话就会作用明显,而且可以把局部变量驻留在内存中,避免使用全局变量)

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,nAdd是一个全局变量,这个函数本身也是一个闭包,因为该函数的存在我们可以在函数外部对函数内部的局部变量进行操作。

4.2、减少全局变量的污染

所有的变量声明时如果不加上var等关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量、造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。

闭包有一个作用就是能减少全局变量的使用,因为使用闭包可以在外部访问函数内部的变量,而该变量也只能通过函数访问,并不是一个全局变量。

4.3、实现封装

var person = function () {
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
}
}();
console.log(person.name); //直接访问,结果为undefined
console.log(person.getName()); // default
person.setName("abruzzi");
console.log(person.getName()); // abruzzi

上面的代码给对象 person 创建了私有变量 name,又对外提供了获取的方法,对外封装了一个对象并且不可直接访问和操作,只可通过对象定义的方法进行操作,增加了安全性。

上面的代码就是典型的自执行函数和闭包结合使用的示例,立即执行函数和闭包其实并没有什么关系,只是两者会经常结合在一起使用而已,但两者有本质上的区别。两者也有一个共同的优点就是能减少全局变量的使用。

4.4、实现继承

下面定义了Person,它就像一个类,我们new一个 Person 对象,访问它的方法。下面我们定义了Jack,继承自 Person,并添加自己的方法,Jack 继承了 Person。

function Person2() {
var name = "default";
this.age = 12;
return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
}
}; var p = new Person2();
console.log(p.age);
p.setName("Tom");
console.log(p.getName()); var Jack = function () {};
//继承自Person
Jack.prototype = new Person2();
//添加私有方法
Jack.prototype.Say = function () {
console.log("Hello,my name is Jack");
};
var j = new Jack();
j.setName("Jack");
j.Say();
console.log(j.getName());

4.5、闭包在实际开发中的使用

闭包在实际开发中的一般都是用来替代全局变量,避免造成变量污染。

function isFirstLoad(){
var list=[];
return function(option){
if(list.indexOf(option)>=0){ //检测是否存在于现有数组中,有则说明已存在
console.log('已存在')
}else{
list.push(option);
console.log('首次传入'); //没有则返回true,并把这次的数据录入进去
}
}
} var ifl=isFirstLoad();
ifl("zhangsan"); //首次传入
ifl("lisi"); //首次传入
ifl("zhangsan"); //已存在

可以看到,如果外界想访问 list 变量,只能通过我定义的函数isFirstLoad来进行访问。我对想访问 list 的外界只提供了 isFirstLoad 这一个接口。至于怎么操作_list,我已经定义好了,外界能做的就只是使用我的函数,然后传几个不同的参数罢了。并且 list 变量并不是全局变量,所以就避免了变量污染。

闭包的用处在一道面试题中常常能看到:在 setTimeout 中依次输出 0 1 2 3 4

//下面的代码全部输出 5
for (var i=1; i<5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
} //下面会依次输出 0 1 2 3 4
for (var i=1; i<5; i++) {
(function(i) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
})(i)
}

上面的第二个循环中,在外层的 function 里面还包含着 setTimeout 里面的 function 函数,而里面的 function 函数就访问了外层 function 的 i 的值,由此就形成了一个闭包。每次循环时,将 i 的值保存在一个闭包中,当 setTimeout 中定义的操作执行时,就会访问对应闭包保存的 i 值,所以输出 0 1 2 3 4。

5、闭包的危害

5.1、可能导致内存泄露

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露(即己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,将导致程序运行速度减慢)。解决方法是,在退出函数之前,将外部的引用置为 null。

5.2、可能不小心修改掉私有属性

闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,闭包可能会修改掉父函数的私有属性。

6、垃圾回收机制

不再使用的变量也就是生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。

(局部变量应该是只有在函数执行时才会被创建然后占用内存,当函数执行结束之后如果没有闭包引用它的话就会被回收。当函数再次执行时又再次创建然后占用内存)

可以参考:https://blog.csdn.net/zxd10001/article/details/81038533

6.1、JavaScript的内存生命周期

  • 分配所需要的内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放

垃圾回收机制的原理其实很简单:确定变量中哪些还在继续使用的,哪些已经不用的,然后垃圾收集器每隔固定的时间就会清理一下,释放内存。

局部变量在程序执行过程中,会为局部变量分配相应的空间,然后在函数中使用这些变量,如果函数运行结束了,而且在函数之外没有再引用这个变量了,局部变量就没有存在的价值了,因此会被垃圾回收机制回收。在这种情况下,浏览器很容易辨别哪些变量该回收,但是并非所有情况下都这么容易。比如说全局变量。在现代浏览器中,通常使用标记清除策略来辨别及实现垃圾回收。

  • 标记清除

标记清除会给内存中所有的变量都加上标记,然后去掉环境中的变量以及不在环境中但是被环境中变量引用的变量(闭包)的标记。剩下的被标记的就是等待被删除的变量,原因是环境中的变量已经不会再访问到这些变量了。最后垃圾回收器会完成内存清理,销毁那些被标记的值释放内存空间。

参考:https://www.cnblogs.com/yunfeifei/p/4019504.html、  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures