create by db on 2021-1-12 16:47:32
Recently revised in 2021-1-26 14:50:43闲时要有吃紧的心思,忙时要有悠闲的趣味
前言
作为一个前端工程师,javaScript 应该是我们赖以生存的本事了。那么,你知道所谓的 javaScript 的三座大山是什么吗?
对!那就是我们刚学习 js 时老师所强调的:
-
原型和原型链
-
作用域和闭包
-
异步和单线程
下面我们就来爬上第二座大山——作用域和闭包,去领略一下吧。
求票
本人正在参与掘金2020年度人气创作者榜单排名,希望各位小伙伴帮我投票,2021年会给大家带来更多优质的文章,感谢大家。
正文
返回目录 >
阮一峰老师说:要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
作用域
作用域是程序源代码中定义变量的区域。它规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
举个栗子:
// 全局作用域
if (true) {
var personName = 'zhangsan'
}
// 函数作用域
function fn() {
var personAge = 18
console.log(personAge) // 18
}
fn()
console.log(personName) // zhangsan
console.log(personAge) // personAge is not defined
上面的 personName 就被暴露到了全局了,而 personAge 则只能再函数 fn 内部可以访问,在 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 personName = 'zhangsan'
console.log(personName) // zhangsan
}
console.log(personName) // personName is not defined
从上可以看出,let定义的personName是在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 personName = 'default'
return {
getName: function () {
return personName
},
setName: function (newName) {
personName = newName
},
}
})()
- 实现类和继承
代码如下:
function Person() {
var personName = 'default'
return {
getName: function () {
return personName
},
setName: function (newName) {
personName = 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 personName is Jack')
}
var j = new Jack()
j.setName('Jack')
j.Say()
alert(j.getName()) //Jack
闭包的优缺点
优点:
- 函数作为返回值,缓存数据
- 可以让这些局部变量隐藏起来。保存在内存中,不被 GC 回收,实现变量数据共享。
- 函数作为参数传递
- 利用闭包特性完成柯里化(通过将多个参数换成一个参数,每次运行返回新函数的技术),详见详解 JS 函数柯里化
缺点:
- 内存消耗
-
由于闭包会使得函数中的变量都被保存在内存中,无法被销毁,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。
-
解决方法是:在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。
- 如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
总结
关于作用域和闭包,就先说这些了。好好学习,天天向上。
路漫漫其修远兮,与诸君共勉。
参考文献:
路漫漫其修远兮,与诸君共勉。
后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址
文档协议
db 的文档库 由 db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。