闭包和函数作用域链

前言

闭包,是javascript中重要的一个概念,在这里做个总结。

一、什么是闭包

我们以一个最简单的例子来看下什么是闭包。

function A() {
  function B() {
    console.log("Hello Closure!");
  }

  return B;
}
var c = A();
c();//Hello Closure!

让我们简单分析一下它和普通函数有什么不同:
1、定义了一个普通函数A;
2、在A中定义了普通函数B;
3、在A中返回B(确切的讲,在A中返回B的引用);
4、执行A(),把A的返回结果赋值给变量C;
5、执行C()
把这些操作总结下就是:函数A的内部函数B被函数A外的一个变量C引用,即当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

二、闭包的作用

举个例子,如果HTML中有如下面相同的四个a元素,现在要给它们指定事件处理程序,使它们在用户单击时报告自己在页面中的顺序。

<a href="#">点我测试</a> 
<a href="#">点我测试</a> 
<a href="#">点我测试</a> 
<a href="#">点我测试</a>

很多人会掉入陷阱中,写下如下代码

function badClosureExample() {
  var as = document.querySelectorAll('a');
  for (var i = 0; i < 4; i++) {
    as[i].onclick = function () {
      alert('单击第' + i + '个');
    }
  }
}

badClosureExample();

但是运行结果却是单击任何任意一个链接全都是”单击第4个”,因为在badClosureExample()函数中指定给element.onclick的事件处理程序,
也就是onclick那个匿名函数是在badClosureExample()函数运行完成后(用户单击链接时)才被调用的。而调用时,需要对变量i求值,解析
程序首先会在事件处理程序内部查找,但i没有定义。然后,又到badClosureExample()函数中查找,此时有定义,但i的值是4(只有i大于4才
会停止执行for循环)。解决方案如下

function badClosureExample() {
  var as = document.querySelectorAll('a');
  for (var i = 0; i < 4; i++) {
    (function (i) {
      as[i].onclick = function () {
        alert('单击第' + i + '个');
      }
    })(i);
  }
}

badClosureExample();

再举个例子(返回对象):

(function (document) {
  var viewport;
  var obj = {
    init: function (id) {
      viewport = document.querySelector("#" + id);
    },
    addChild: function (child) {
      viewport.appendChild(child);
    },
    removeChild: function (child) {
      viewport.removeChild(child);
    }
  };
  window.jView = obj;
})(document);

在这段代码中似乎看到了闭包的影子,但匿名函数中没有任何返回值,似乎不具备闭包的条件。不过把obj赋值给了window全局对象的一个变量
jView,即全局变量jView引用了obj,而obj对象中的函数又引用了匿名函数中的变量viewport,因此f中的viewport不会被GC回收,会一直
保存到内存中,所以这种写法满足闭包的条件。
再举个例子(对象创建):

function a() {
  var n = 0;
  this.inc = function () {
    n++;
    console.log(n);
  };
}
var c = new a();
c.inc();  //1
c.inc();  //2

这里inc函数访问了构造函数a里面的变量n,所以形成了一个闭包。

三、函数作用域链

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?

先举个例子,这是一道非常典型的JS闭包问题。其中嵌套了三层fun函数,搞清楚每层fun的函数是那个fun函数尤为重要。

创建函数的几种方式

1、声明函数

function fn1(){}

2、创建匿名函数表达式

var fn1=function (){};

3、创建具名函数表达式

var fn1=function xxcanghai(){};

创建一个变量,内容为一个带有名称的函数,具名函数表达式的函数名只能在创建函数内部使用,即采用此种方法创建的函数在函数外层只能使用
fn1不能使用xxcanghai的函数名。xxcanghai的命名只能在创建的函数内部使用
4、Function构造函数,此种方法创建的是匿名函数。
5、自执行函数

(function(){alert(1);})();
(function fn1(){alert(1);})();

6、其他创建函数的方法
比如采用eval,setTimeout,setInterval等非常用方法,属于非标准方法,这里不做过多展开。
自执行函数属于上述的“函数表达式”,规则相同。

函数作用域链的问题

在说第三个fun函数之前需要先说下,在函数表达式内部能不能访问存放当前函数的变量。

var o = {
  fn: function () {
    console.log(fn);
  }
};
o.fn();//fn is not defined

var fn = function () {
  console.log(fn);
};
fn();//[Function: fn]

从上面两个例子可以看出非对象内部的函数表达式内,可以访问到存放当前函数的变量;在对象内部的不能访问到。原因也非常简单,因为函数作
用域链的问题,采用非对象内部的函数表达式是在外部创建了一个fn变量,函数内部当然可以在内部寻找不到fn后向上沿作用域查找fn,而在创
建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。
所以综上所述,可以得知,最内层的return出去的fun函数不是第二层fun函数,是最外层的fun函数。所以,三个fun函数的关系也理清楚了,
第一个等于第三个,他们都不等于第二个。

第一行

第一个fun(0)是在调用第一层fun函数。
第二个fun(1)是在调用前一个fun的返回值的fun函数,
所以:第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数。所以结果是undefined、0、0、0

第二行

第一次调用第一层fun(0)时,o为undefined;
第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以
o为0;
第三次调用fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun(1,0)所以
n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1;
第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2;
即最终答案:undefined,0,1,2

第三行

第一次调用第一层fun(0)时,o为undefined;
第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以
o为0;
第三次调用fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;
第四次调用fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1;
即最终答案:undefined,0,1,1

四、垃圾回收原理及内存泄漏

垃圾回收

1、在javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收;
2、如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。在js中使用闭包,往往会给javascript的垃圾回收
器制造难题。尤其是遇到对象间复杂的循环引用时,垃圾回收的判断逻辑非常复杂,搞不好就有内存泄漏的危险。

内存泄漏

内存泄漏是内存占用很大吗?不是,即使1Byte的内存也叫内存泄漏
程序中提示”内存不足”不是内存泄漏,一般是某些原因导致”内存溢出”
内存泄漏是堆区,栈区不会内存泄漏
跳转页面后,内存泄漏仍然存在,直到关闭浏览器
window对象是DOM对象吗?否,window对象参与的循环引用不会导致内存泄漏
内存泄漏后果不是很严重,但过多DOM操作会导致网页执行缓慢

### JavaScript 闭包作用域链详解 #### ### 1. 作用域链的概念及工作原理 JavaScript作用域链是确保对执行环境有权访问的所有变量函数的有序访问的一种机制。当一个函数被创建时,它会捕获其定义时的作用域,并将其存储为作用域链的一部分[^4]。作用域链采用链式查找的方式,一层一层向上查找,先查找外面的嵌套的函数是否有所需内容,找到就输出相应的结果,如果没有再向上查找,直到查到全局作用域[^2]。 例如: ```javascript var a = 1; function outerFunction() { var b = 2; function innerFunction() { var c = 3; console.log(a + b + c); // 输出 6 } innerFunction(); } outerFunction(); ``` 在上述代码中,`innerFunction` 的作用域链包含三个对象:`innerFunction` 自身的变量对象、外层的 `outerFunction` 的变量对象以及全局变量对象[^3]。 #### ### 2. 闭包的定义及用法 闭包是指一个函数能够记住并访问它的词法作用域,即使这个函数是在它的词法作用域之外被执行。这通常发生在内部函数引用了外部函数的变量或参数时。 以下是一个闭包的例子: ```javascript function createCounter() { var count = 0; // 外部函数的局部变量 return function() { count += 1; // 内部函数访问外部函数的变量 return count; }; } const counter = createCounter(); console.log(counter()); // 输出 1 console.log(counter()); // 输出 2 ``` 在这个例子中,返回的匿名函数形成了一个闭包,因为它记住了 `createCounter` 函数作用域链,即使 `createCounter` 已经执行完毕[^5]。 #### ### 3. 闭包的应用场景 闭包常用于以下几个方面: - **模拟私有变量**:通过闭包可以创建只在特定函数内部可见的变量。 ```javascript function createPerson(name) { return { getName: function() { return name; // 访问外部函数的变量 } }; } const person = createPerson("Alice"); console.log(person.getName()); // 输出 "Alice" ``` - **数据封装**:闭包可以用来封装数据,防止外部直接访问修改。 - **回调函数**:在事件处理程序或异步操作中使用闭包保存状态。 #### ### 4. 闭包与垃圾回收的关系 由于闭包会持有对外部变量的引用,因此可能导致内存泄漏问题。如果闭包不再被使用,但仍然保持对某些大对象的引用,这些对象可能无法被垃圾回收机制释放[^3]。 #### ### 5. 注意事项 - 避免在循环中创建闭包,因为每个迭代都会创建一个新的闭包实例,可能导致意外的行为。 - 使用闭包时应注意性能问题,尤其是在需要频繁创建销毁闭包的情况下。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值