@JS 三座大山之作用域和闭包
目录
-
前言
-
正文
-
总结
前言
作为一个前端工程师,javaScript 应该是我们赖以生存的本事了。那么,你知道所谓的 javaScript 的三座大山是什么吗?
对!那就是我们刚学习 js 时老师所强调的:
-
原型和原型链
-
作用域和闭包
-
异步和单线程
下面我们就来爬上第二座大山——作用域和闭包,去领略一下吧。
求票
本人正在参与掘金2020年度人气创作者榜单排名,希望各位小伙伴帮我投票,2021年会给大家带来更多优质的文章,感谢大家。
正文
阮一峰老师说:要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
作用域
作用域是程序源代码中定义变量的区域。它规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
举个栗子:
if (true) {
var name = 'zhangsan'
}
console.log(name) // zhangsan
从上面的例子可以体会到作用域的概念,作用域就是一个独立的地盘,让变量不会外泄、暴露出去。
上面的 name
就被暴露出去了,因为,在 ES6 到来之前,javaScript 中只有全局作用域和函数作用域
全局作用域
全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。
拥有全局作用域的变量叫做全局变量,他们在代码中任何地方都能访问到的,谁都可以对其更改,这样的坏处就是很容易撞车、冲突。
举个栗子:
// 张三写的代码中
var data = { a: 100 }
// 李四写的代码中
var data = { x: true }
console.log(data) // { x: true }
函数作用域
我们将使用var
将变量声明在函数内部(function(){....}
),就形成了函数作用域。
像这样,定义在函数内部的变量叫做局部变量,局部变量只能在它被调用的作用域范围内进行读和写的操作,对该函数外部来说,局部变量是不可见的,当然也不可更改。
举个栗子:
function fn() {
var a = 200
console.log('fn', a)
}
fn() // fn 200
console.log('global', a) // Error: a is not defined
这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()
中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。
块级作用域
现在我们有了 ES6, ES6定义了let
和const
,他们可以保证外层块不受内层块的影响。即内层块形成了一个块级作用域({}
)。
举个栗子:
if (true) {
let name = 'zhangsan'
}
console.log(name) // undefined,因为let定义的name是在if这个块级作用域
作用域链
首先认识一下什么叫做自由变量 。
自由变量
如我在全局中定义了一个变量a
,然后我在函数中使用了这个a
,这个a
就可以称之为自由变量
。可以这样理解,凡是跨了自己的作用域的变量都叫自由变量。
举个栗子:
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn() // 100 200
如上代码中,console.log(a)
要得到 a
变量,但是在当前的作用域中没有定义 a
(可对比一下 b
)。 a
就是自由变量
。
那么问题来了,a
在当前作用域没有定义,他又是如何打印出来的呢?
没错,向父级作用域寻找。
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。
举个栗子:
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺作用域链向父作用域找
console.log(b) // 自由变量,顺作用域链向父作用域找,找到全局作用域
console.log(c) // 本作用域的变量
console.log(d) // 没有定义,找到全局作用域找不到返回错误
}
F2()
}
F1() // 100 200 300 Error:d is not defined
闭包
了解了作用域
和作用域链
,我们就可以看看闭包
了
什么是闭包
在 JavaScript 中,根据变量作用域的规则,内部函数总是可以访问其外部函数中声明的变量。
当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
简单来说:
- 在函数 A 中还有函数 B,函数 B 调用了函数 A 中的变量,那么函数 B 就称为函数 A 的闭包。
通俗来讲,JS 所有的 function 都是一个闭包。
举个栗子:
function foo() {
let num = 0
return function () {
num++
console.log(num)
}
}
const f = foo()
var num = 100
f() // 1
f() // 2
自由变量将从作用域链中去寻找,所以自由变量num
找到函数foo
中就找到了,于是num
的值是1
;
函数foo
执行时创建了一个内部函数,这个内部函数作为返回值,以某种方式保留下来(num
一直存在),所以每次调用num
都会 + 1。这里就用了闭包。
闭包的用途场景
也许你会疑惑,闭包就这?这有啥用?
- 匿名自执行函数
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在函数执行完后会立刻释放资源,关键是不污染全局对象。
代码如下:
;(function () {
var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
today = new Date(),
msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate()
alert(msg)
})()
- 结果缓存
我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。
闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。 代码如下:
var CachedSearchBox = (function () {
var cache = {},
count = []
return {
attachSearchBox: function (dsid) {
if (dsid in cache) {
//如果结果在缓存中
return cache[dsid] //直接返回缓存中的对象
}
var fsb = new uikit.webctrl.SearchBox(dsid) //新建
cache[dsid] = fsb //更新缓存
if (count.length > 100) {
//保正缓存的大小<=100
delete cache[count.shift()]
}
return fsb
},
clearSearchBox: function (dsid) {
if (dsid in cache) {
cache[dsid].clearSelection()
}
},
}
})()
- 封装
代码如下:
var person = (function () {
//变量作用域为函数内部,外部无法访问
var name = 'default'
return {
getName: function () {
return name
},
setName: function (newName) {
name = newName
},
}
})()
- 实现类和继承
代码如下:
function Person() {
var name = 'default'
return {
getName: function () {
return name
},
setName: function (newName) {
name = newName
},
}
}
var p = new Person()
p.setName('Tom')
alert(p.getName()) //Tom
var Jack = function () {}
//继承自Person
Jack.prototype = new Person()
//添加私有方法
Jack.prototype.Say = function () {
alert('Hello,my name is Jack')
}
var j = new Jack()
j.setName('Jack')
j.Say()
alert(j.getName()) //Jack
闭包的优缺点
优点:
- 函数作为返回值,缓存数据
- 可以让这些局部变量隐藏起来。保存在内存中,不被 GC 回收,实现变量数据共享。
- 函数作为参数传递
- 利用闭包特性完成柯里化(通过将多个参数换成一个参数,每次运行返回新函数的技术),详见详解 JS 函数柯里化
缺点:
- 内存消耗
-
由于闭包会使得函数中的变量都被保存在内存中,无法被销毁,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。
-
解决方法是:在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。
- 如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
总结
关于作用域和闭包,就先说这些了。好好学习,天天向上。
路漫漫其修远兮,与诸君共勉。
参考文献:
-
jsliang 求职系列 - 03 - 闭包与柯里化 | 掘金-jsliang
-
js 中闭包的理解、用途场景、优缺点及解决办法 | 简书-sdcV
路漫漫其修远兮,与诸君共勉。
后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址
文档协议
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!