相关学习推荐:javascript视频教程
前言
闭包 永远都是前端开发者绕不过去的一个坎,不管你喜欢与否,在工作和面试中,都会遇到。每个人对闭包的理解都不尽相同,这里笔者谈谈自身对闭包的理解。(如果与您的理解有出入,请以您自己为准 )
如何定义闭包
在给出定义之前,不妨看看别人是如何定义闭包的:
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包” — JavaScript权威指南(第六版)
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。 — JavaScript高级程序设计(第三版)
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 — 你不知道的JavaScript(上卷)
虽然上面的几段话描述起来并不一样,但是您细细品味后还是能找出一些共同点。其中最重要的是不同作用域之间的联系。当然了,您可以直接引用上面的定义(毕竟上面几个定义还是比较权威的),这里笔者比较喜欢最后一段的定义,同时力推《你不知道的JavaScript(上卷)》这本书,值得反复细读。
闭包涉及哪些知识点
光给出定义是远远不够的,还必须探讨内部涉及了哪些知识点。下面是笔者认为有用到的知识点。
作用域与作用域链
嗯,其实笔者知道你们都想到了这点(不会吧,不会有人没想到这点吧)。既然大家都了解作用域。这里就简单描述一下,过一下场即可。
作用域:根据名称查找变量的一套规则。分为三种类型:全局作用域;函数作用域;块作用域。
需要注意的是块作用域,ES6新增的规范。 在花括号{}
里面使用let,const
定义的变量,都会绑定到该作用范围内,花括号以外的地方无法访问。注意:在花括号开始 到 let变量声明之前,存在暂时性死区
(该点不在本文讨论范围)。
作用域链:当不同的作用域 (混~淆~在~一~起~ 呸,不小心出戏了) 圈套在一起时,就形成了作用域链。注意的是,查找方向是从内到外的。
为什么作用域的查找方向是从内到外的呢?这是个很有趣的问题。个人觉得是跟js执行函数的入栈方式决定的(感觉有点偏题了,有兴趣的小伙伴可以去查一下资料)。
词法作用域
函数之所以 可以访问另一个函数作用域的变量(或者说记住当前的作用域并在当前以外的地方访问)的关键点
就是词法作用域
在起作用。这一点很重要,但不是所有人都知道这个知识点,这里简单探讨一下。
在编程界中,存在两种作用域工作模式,一种是被大多数编程语言所采用的
词法作用域
;另一种就是与其相反的动态作用域
(这个不在本文的讨论范围)。
词法作用域: 变量和块的作用域 在 您编写代码的阶段 就已经确定好了,不会随着调用的对象或者地方的不同而改变(感觉跟this相反)。
要不,举个栗子看看吧:
let a = 1; function fn(){ let a = 2; function fn2(){ console.log(a); } return fn2; } let fn3 = fn(); fn3();
从上面的定义可以知道,fn
是一个闭包函数,fn3
拿到了fn2
的指针地址,当fn3
执行的时候,其实是执行fn2
,而里面的a
变量,根据作用域链的查找规则,找到的是fn
作用域内的变量a
,所以最终的输出是2,不是1。(可以看下图)
题外话,如何欺骗词法作用域?
虽然词法作用域是静态的,但依然有办法可以欺骗它,达到动态的效果。
第一种方法是使用eval. eval可以把字符串解析成一个脚本来运行,由于在词法分析阶段,无法预测eval运行的脚本,所以不会对其进行优化分析。
第二种方法是with. with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。with本身比较难掌握,使用不当容易出现意外情况(如下例子),不推荐使用 -.-
function Fn(obj){ with(obj){ a = 2; } } var o1 = { a:1 } var o2 = { b:1 } Fn(o1); console.log(o1.a); //2 Fn(o2); console.log(o2.a); //undefined; console.log(a); //2 a被泄漏到全局里面去了 // 这是with的一个副作用, 如果当前词法作用域没有该属性,会在全局创建一个
闭包能干啥?
闭包的使用场景可多了,平时使用的插件或者框架,基本上都有闭包的身影,可能您没留意过罢了。下面笔者列举一些比较常见的场景。
模拟私有变量和方法,进一步来说可以是模拟模块化
;目前常用的AMD,CommonJS等模块规范,都是利用闭包的思想;
柯里化函数或者偏函数;利用闭包可以把参数分成多次传参。如下面代码:
// 柯里化函数 function currying(fn){ var allArgs = []; function bindCurry(){ var args = [].slice.call(arguments); allArgs = allArgs.concat(args); return bindCurry; } bindCurry.toString = function(){ return fn.apply(null, allArgs); }; return bindCurry; }
实现防抖或者节流函数;
实现缓存结果(记忆化)的辅助函数:
// 该方法适合缓存结果不易改变的函数 const memorize = fn => { let memorized = false; let result = undefined; return (...args) => { if (memorized) { return result; } else { result = fn.apply(null,args); memorized = true; fn = undefined; return result; } }; };
如何区分闭包?
说了那么多,我怎么知道自己写的代码是不是闭包呢?先不说新手,有些代码的确隐藏的深,老鸟不仔细看也可能发现不了。
那有没有方法可以帮助我们区分一个函数是不是闭包呢?答案是肯定的,要学会善于利用周边的工具资源,比如浏览器。
打开常用的浏览器(chrome或者其他),在要验证的代码中打上debugger断点,然后看控制台,在scope里面的Closure(闭包)里面是否有该函数(如下图)。
闭包真的会导致内存泄漏?
答案是有可能。内存泄漏的原因在于垃圾回收(GC)无法释放变量的内存,导致运行一段时候后,可用内存越来越少,最终出现内存泄漏的情况。常见的内存泄漏场景有4种:全局变量;闭包引用;DOM事件绑定;不合理使用缓存。其中,闭包导致内存泄漏都是比较隐蔽的,用肉眼查看代码判断是比较难,我们可用借助chrome浏览器的Memory标签栏工具来调试。由于篇幅问题,不展开说明了,有兴趣自己去了解一下如何使用。
想了解更多编程学习,敬请关注php培训栏目!