Javascript 中的闭包有什么用

 2024-03-09 02:05:35  阅读 0

一般来说我们知道js是一种弱类型语言。 要声明变量,只需要 var 保留字。 如果在函数中不使用 var 声明变量,该变量将被提升为全局变量并脱离函数作用域,如下所示:

function f(){
 b = 123;
}
f();
console.log(b);//123

此时,相比之前使用var声明的a变量,b变量被提升为全局变量,仍然可以在函数作用域外访问。

由于在函数作用域中不使用 var 声明变量会将该变量提升为全局变量,那么如果在全局作用域中不使用 var 会发生什么情况呢?

//全局下不使用var声明,该变量依然是全局变量
c = "hello scope";
console.log(c);//hello scope
console.log(window.c);//hello scope
//查看c变量的属性
console.log(Object.getOwnPropertyDescriptor(window, 'c'));//Object {value: "hello scope", writable: true, enumerable: true, configurable: true} ,此时c变量可赋值,可列举,可配置
//试着删除c变量
delete c;//true 表示c变量被成功删除
console.log(c);//c is not defined
console.log(window.c);//undefined
//使用var声明后再删除d变量
var d = 1;
console.log(Object.getOwnPropertyDescriptor(window, 'd'));//Object {value: 1, writable: true, enumerable: true, configurable: false} ,此时d变量可赋值,可列举,但不可配置
delete d;//false 表示d变量删除失败
console.log(d);//1 
console.log(window.d);//1

总结起来,有以下规则:

JS 中的作用域链

函数对象与其他对象一样,具有可以通过代码访问的属性和一组只能由引擎访问的内部属性。 内部属性之一是 [[Scope]],由 ECMA-262 标准第三版定义。 此内部属性包含创建函数的范围内的对象集合。 这个集合称为函数的作用域链,它决定了函数可以访问哪些数据。

我们先看一个栗子:

var e = "hello";
function f(){
 e = "scope chain";
 var g = = "good";
}

上述作用域链图如下所示:

当函数执行时,函数f内部会生成一个作用域链。 引擎的内部对象会被放入。外部e变量位于作用域链的第二级,index=1,内部g变量位于作用域链的顶层。 index=0,因此访问 g 变量总是比访问 e 变量快。

关闭

说到范围,我们就不得不谈一下闭包。 那么,什么是闭包呢?

“官方”的解释是:闭包是一个表达式(通常是一个函数),它有很多变量以及这些变量绑定到的环境,因此这些变量也是表达式的一部分。

这是什么意思? 简而言之:

在 ES6 之前,我们通常实现使用闭包的模块。 闭包依赖的结构有一个显着的特点,那就是:函数在词法作用域之外执行。 如下,f2是闭包的关键,它的词法作用Scope是函数f的内部私有作用域,它在f的作用域之外执行。

var h = 1;
function f(){
 var i = 2;
 return function f2(){
 var j = 3 + i + h;
 console.log(j);
 }
}
var ff = f();
ff();//6

由于f2在定义时位于f内部,因此f2可以访问f的内部私有作用域。 这样,通过返回f2,就保证了i变量也可以在f函数之外被访问到。

当f2执行时,变量j位于作用域链的位置,变量i和变量h分别位于作用域链的位置。 因此,j的赋值过程实际上就是沿着作用域链的第二层和第三层找到i和h。 然后将h的值与3相加,最后赋值给j。

浏览器沿着作用域链搜索变量总是需要 CPU 时间。 作用域链的外层(或者离f2越远的变量),浏览器搜索的时间越长,因为需要遍历作用域链的次数越多。 。 所以全局变量()总是需要最多的访问时间。

封闭内的微观世界

如果我们想更深入的理解闭包以及函数f和嵌套函数f2的关系,还需要引入其他几个概念:函数执行环境()、活动对象(调用)、作用域(作用域)、作用域链(作用域)链)。 以函数a从定义到执行的过程为例来说明这些概念。

至此,整个函数f从定义到执行的步骤就完成了。 此时,f将函数f2的引用返回给ff,函数f2的作用域链中包含了函数f的活动对象的引用,这意味着f2可以访问f中定义的所有变量和函数。 函数f2被ff引用,而函数f2又依赖于函数f,所以函数f返回后不会被GC回收。

执行函数f2时,与上述步骤相同。 因此,f2执行时的作用域链包含3个对象:f2的活动对象、f的活动对象、对象,如下图所示:

如图所示,在函数f2中访问变量时,查找顺序为:

综上所述,这一段提到了两个重要的词:函数定义和执行。 文章提到,函数的作用域是在定义函数时确定的,而不是在执行函数时确定的(参见步骤 1 和 3)。 用一段代码来说明这个问题:

function f(x) { 
 var g = function () { return x; }
 return g;
}
var h = f(1);
alert(h());

在此代码中,变量 h 指向 f 中的匿名函数(由 g 返回)。

如果第一个假设为真,则输出值为; 如果第二个假设成立,则输出值为 1。

运行结果证明第二个假设是正确的,说明函数的作用域确实是在函数定义的时候就确定了。

闭包可能导致IE浏览器内存泄漏

我们先看一个栗子:

function f(){
 var div = document.createElement("div"); 
 div.onclick = function(){
 return false;
 }
}

上面div的点击事件是一个闭包。 由于这个闭包的存在,f函数内部的div变量对DOM元素的引用将一直存在。

在早期的IE浏览器中(IE9之前),js对象和DOM对象使用不同的垃圾收集方法。 DOM 对象使用计数垃圾收集机制。 只要匿名函数(比如事件)存在,DOM对象的引用至少为1,所以它所占用的内存永远不会被破坏。

有趣的是,不同的IE版本会导致不同的现象:

总结一下,闭包的优点是:共享函数作用域,可以方便地开放一些接口或者变量供外部使用;

注意:由于闭包可能会导致函数中的变量长期保存在内存中,从而消耗大量内存并影响页面性能,因此不能滥用,并且可能会导致 IE 浏览时出现内存泄漏。 解决办法是先退出函数,删除所有未使用的局部变量。

for循环问题分析

我们先来看看一开始的for循环问题。 添加匿名函数后,for循环内的变量将位于匿名函数的局部范围内。 此时访问属性或者访问i属性只需要在匿名函数的作用域内进行查找即可。 是的,因此查询效率大大提高(测试数据发现提高了200倍以上)。

使用匿名函数后,不仅作用域查询速度更快,而且作用域内的变量也与外界隔离,避免了像i这样的变量影响后续代码。 可谓一石二鸟。

步入范围陷阱

现在让我们来探讨一个经典的范围陷阱。

var div = document.getElementsByTagName("div");
for(var i=0,len=div.length;i

上面代码的本意是在每次点击div时打印div的索引,但实际上打印的是len的值。 我们来分析一下原因。

单击 div 时,将执行 .log(i) 语句。 显然 i 变量不在单击事件的本地范围内。 浏览器会沿着作用域链寻找 i 变量,在哪里,也就是 for 循环开始的地方,这里定义了一个 i 变量,而 js 没有块作用域,所以 i 变量会for循环块执行完后不会被破坏,而i的最后一次增量使得i = len,因此浏览器在作用域链索引index=1处停止并返回i的值,即len的值。

为了解决这个问题,我们就对症下药,从范围入手,改变点击事件的局部范围,如下:

var div = document.getElementsByTagName("div");
for(var i=0,len=div.length;i

由于点击事件是由闭包包裹的,并且闭包是自执行的,因此闭包中的n变量的值每次都是不同的。 当点击div时,浏览器会沿着作用域链搜索n变量,最终会在闭包中找到n变量。 n 变量,并打印出 div 的索引。

这个范围

前面我们学习了作用域链、闭包等基础知识。 现在我们来谈谈这个神秘的范围。

熟悉OOP的开发人员都知道,这是对对象实例的引用,并且始终指向对象实例。 然而在js的世界里,this随着它的执行环境的变化而变化,它始终指向它所在方法的对象。 如下,

function f(){
 alert(this);
}
var o = {};
o.func = f;
f();//[object Window]
o.func();//[object Object]
console.log(f===window.f);//true

当f单独执行时,其内部this指向对象,但是当f成为o对象的属性func时,this指向o对象,而f === .f,所以它们实际上都指向this所在的方法位于。 目的。

下面我们来应用一下

Array.prototype.slice.call([1,2,3],1);//[2,3],正确用法
Array.prototype.slice([1,2,3],1);//[], 错误用法,此时slice内部this仍然指向Array.prototype
var slice = Array.prototype.slice;
slice([1,2,3],1);//Uncaught TypeError: Array.prototype.slice called on null or undefined
//此时slice内部this指向的是window对象,离开了原来的Array.prototype对象作用域,故报错~~

综上所述,使用时只需要注意一件事:

this 始终指向它所在方法的对象。

有声明

说到作用域链,就不得不说到with语句。 with 语句可用于临时更改作用域,并将语句中的对象添加到作用域的顶部。

语法:with (){}

例如:

var k = {name:"daicy"};
with(k){
 console.log(name);//daicy
}
console.log(name);//undefined

with 语句用于对象 k。 第一级作用域是k对象的内部作用域,因此可以直接打印name的值。 with 之外的语句不受此影响。

我们再看一个栗子:

var l = [1,2,3];
with(l) {
 console.log(map(function(i){
 return i*i;
 }));//[1,4,9]
}

在这个例子中,with语句用于数组,因此当调用map()方法时,解释器会检查该方法是否是本地函数。 如果不是,它会检查伪对象l是否是该对象的方法,而map是否是Array对象的方法。 数组l继承了这个方法,所以可以正确执行。

注意:with语句很容易引起歧义。 由于需要强制改变作用域链,因此会带来更多的CPU消耗。 建议谨慎使用with语句。

我将在这里分享闭包的用处。 希望以上内容能够对大家有所帮助,可以学到更多的知识。 如果您觉得文章不错,可以分享出去,让更多的人看到。

标签: 闭包 作用域 变量

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码