浅谈JavaScript作用域和闭包


Posted in Javascript onSeptember 18, 2017

作用域和闭包在JavaScript里非常重要。但是在我最初学习JavaScript的时候,却很难理解。这篇文章会用一些例子帮你理解它们。

我们先从作用域开始。

作用域

JavaScript的作用域限定了你可以访问哪些变量。有两种作用域:全局作用域,局部作用域。

全局作用域

在所有函数声明或者大括号之外定义的变量,都在全局作用域里。

不过这个规则只在浏览器中运行的JavaScript里有效。如果你在Node.js里,那么全局作用域里的变量就不一样了,不过这篇文章不讨论Node.js。

`const globalVariable = 'some value'`

一旦你声明了一个全局变量,那么你在任何地方都可以使用它,包括函数内部。

const hello = 'Hello CSS-Tricks Reader!'

function sayHello () {
 console.log(hello)
}

console.log(hello) // 'Hello CSS-Tricks Reader!'
sayHello() // 'Hello CSS-Tricks Reader!'

尽管你可以在全局作用域定义变量,但我们并不推荐这样做。因为可能会引起命名冲突,两个或更多的变量使用相同的变量名。如果你在定义变量时使用了const或者let,那么在命名有冲突时,你就会收到错误提示。这是不可取的。

// Don't do this!
let thing = 'something'
let thing = 'something else' // Error, thing has already been declared

如果你定义变量时使用的是var,那第二次定义会覆盖第一次定义。这也会让代码更难调试,也是不可取的。

// Don't do this!
var thing = 'something'
var thing = 'something else' // perhaps somewhere totally different in your code
console.log(thing) // 'something else'

所以,你应该尽量使用局部变量,而不是全局变量

局部作用域

在你代码某一个具体范围内使用的变量都可以在局部作用域内定义。这就是局部变量。

JavaScript里有两种局部作用域:函数作用域和块级作用域。

我们从函数作用域开始。

函数作用域

当你在函数里定义一个变量时,它在函数内任何地方都可以使用。在函数之外,你就无法访问它了。

比如下面这个例子,在sayHello函数内的hello变量:

function sayHello () {
 const hello = 'Hello CSS-Tricks Reader!'
 console.log(hello)
}

sayHello() // 'Hello CSS-Tricks Reader!'
console.log(hello) // Error, hello is not defined

块级作用域

你在使用大括号时,声明了一个const或者let的变量时,你就只能在大括号内部使用这一变量。

在下例中,hello只能在大括号内使用。

{
 const hello = 'Hello CSS-Tricks Reader!'
 console.log(hello) // 'Hello CSS-Tricks Reader!'
}

console.log(hello) // Error, hello is not defined

块级作用域是函数作用域的子集,因为函数是需要用大括号定义的,(除非你明确使用return语句和箭头函数)。

函数提升和作用域

当使用function定义时,这个函数都会被提升到当前作用域的顶部。因此,下面的代码是等效的:

// This is the same as the one below
sayHello()
function sayHello () {
 console.log('Hello CSS-Tricks Reader!')
}

// This is the same as the code above
function sayHello () {
 console.log('Hello CSS-Tricks Reader!')
}
sayHello()

使用函数表达式定义时,函数就不会被提升到变量作用域的顶部。

sayHello() // Error, sayHello is not defined
const sayHello = function () {
 console.log(aFunction)
}

因为这里有两个变量,函数提升可能会导致混乱,因此就不会生效。所以一定要在使用函数之前定义函数。

函数不能访问其他函数的作用域

在分别定义的不同的函数时,虽然可以在一个函数里调用一个函数,但一个函数依然不能访问其他函数的作用域内部。

下面这例,second就不能访问firstFunctionVariable这一变量。

function first () {
 const firstFunctionVariable = `I'm part of first`
}

function second () {
 first()
 console.log(firstFunctionVariable) // Error, firstFunctionVariable is not defined
}

嵌套作用域

如果在函数内部又定义了函数,那么内层函数可以访问外层函数的变量,但反过来则不行。这样的效果就是词法作用域。

外层函数并不能访问内部函数的变量。

function outerFunction () {
 const outer = `I'm the outer function!`

 function innerFunction() {
  const inner = `I'm the inner function!`
  console.log(outer) // I'm the outer function!
 }

 console.log(inner) // Error, inner is not defined
}

如果把作用域的机制可视化,你可以想象有一个双向镜(单面透视玻璃)。你能从里面看到外面,但是外面的人不能看到你。

浅谈JavaScript作用域和闭包

函数作用域就像是双向镜一样。你可以从里面向外看,但是外面看不到你。

嵌套的作用域也是相似的机制,只是相当于有更多的双向镜。

浅谈JavaScript作用域和闭包

多层函数就意味着多个双向镜。

理解前面关于作用域的部分,你就能理解闭包是什么了。

闭包

你在一个函数内新建另一个函数时,就相当于创建了一个闭包。内层函数就是闭包。通常情况下,为了能够使得外部函数的内部变量可以访问,一般都会返回这个闭包。

function outerFunction () {
 const outer = `I see the outer variable!`

 function innerFunction() {
  console.log(outer)
 }

 return innerFunction
}

outerFunction()() // I see the outer variable!

因为内部函数是返回值,因此你可以简化函数声明的部分:

function outerFunction () {
 const outer = `I see the outer variable!`

 return function innerFunction() {
  console.log(outer)
 }
}

outerFunction()() // I see the outer variable!

因为闭包可以访问外层函数的变量,因此他们通常有两种用途:

  1. 减少副作用
  2. 创建私有变量

使用闭包控制副作用

当你在函数返回值时执行某些操作时,通常会发生一些副作用。副作用在很多情况下都会发生,比如Ajax调用,超时处理,或者哪怕是console.log的输出语句:

function (x) {
 console.log('A console.log is a side effect!')
}

当你使用闭包来控制副作用时,你实际上是需要考虑哪些可能会混淆代码工作流程的部分,比如Ajax或者超时。

要把事情说清楚,还是看例子比较方便:

比如说你要给为你朋友庆生,做一个蛋糕。做这个蛋糕可能花1秒钟的时间,所以你写了一个函数记录在一秒钟以后,记录做完蛋糕这件事。

为了让代码简短易读,我使用了ES6的箭头函数:

function makeCake() {
 setTimeout(_ => console.log(`Made a cake`, 1000)
 )
}

如你所见,做蛋糕带来了一个副作用:一次延时。

更进一步,比如说你想让你的朋友能选择蛋糕的口味。那么你就给做蛋糕makeCake这个函数加了一个参数。

function makeCake(flavor) {
 setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}

因此当你调用这个函数时,一秒后这个新口味的蛋糕就做好了。

makeCake('banana')
// Made a banana cake!

但这里的问题是,你并不想立刻知道蛋糕的味道。你只需要知道时间到了,蛋糕做好了就行。

要解决这个问题,你可以写一个prepareCake的功能,保存蛋糕的口味。然后,在返回在内部调用prepareCake的闭包makeCake

从这里开始,你就可以在你需要的时调用,蛋糕也会在一秒后立刻做好。

function prepareCake (flavor) {
 return function () {
  setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
 }
}

const makeCakeLater = prepareCake('banana')

// And later in your code...
makeCakeLater()
// Made a banana cake!

这就是使用闭包减少副作用:你可以创建一个任你驱使的内层闭包。

私有变量和闭包

前面已经说过,函数内的变量,在函数外部是不能访问的既然不能访问,那么它们就可以称作私有变量。

然而,有时候你确实是需要访问私有变量的。这时候就需要闭包的帮助了。

function secret (secretCode) {
 return {
  saySecretCode () {
   console.log(secretCode)
  }
 }
}

const theSecret = secret('CSS Tricks is amazing')
theSecret.saySecretCode()
// 'CSS Tricks is amazing'

这个例子里的saySecretCode函数,就在原函数外暴露了secretCode这一变量。因此,它也被成为特权函数。

使用DevTools调试

Chrome和Firefox的开发者工具都使我们能很方便的调试在当前作用域内可以访问的各种变量一般有两种方法。

第一种方法是在代码里使用debugger关键词。这能让浏览器里运行的JavaScript的暂停,以便调试。

下面是prepareCake的例子:

function prepareCake (flavor) {
 // Adding debugger
 debugger
 return function () {
  setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
 }
}

const makeCakeLater = prepareCake('banana')

打开Chrome的开发者工具,定位到Source页下(或者是Firefox的Debugger页),你就能看到可以访问的变量了。

浅谈JavaScript作用域和闭包

使用debugger调试prepareCake的作用域。

你也可以把debugger关键词放在闭包内部。注意对比变量的作用域:

function prepareCake (flavor) {
 return function () {
  // Adding debugger
  debugger
  setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
 }
}

const makeCakeLater = prepareCake('banana')

浅谈JavaScript作用域和闭包

调试闭包内部作用域

第二种方式是直接在代码相应位置加断点,点击对应的行数就可以了。

浅谈JavaScript作用域和闭包

通过断点调试作用域

总结一下

闭包和作用域并不是那么难懂。一旦你使用双向镜的思维去理解,它们就非常简单了。

当你在函数里声明一个变量时,你只能在函数内访问。这些变量的作用域就被限制在函数里了。

如果你在一个函数内又定义了内部函数,那么这个内部函数就被称作闭包。它仍可以访问外部函数的作用域。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Google AJAX 搜索 API实现代码
Nov 17 Javascript
js自动下载文件到本地的实现代码
Apr 28 Javascript
JS限制Textarea文本域字符个数的具体实现
Aug 02 Javascript
js enter键激发事件实例代码
Aug 17 Javascript
基于Javascript实现文件实时加载进度的方法
Oct 12 Javascript
ES6新特性之解构、参数、模块和记号用法示例
Apr 01 Javascript
jQueryUI Sortable 应用Demo(分享)
Sep 07 jQuery
js仿微信抢红包功能
Sep 25 Javascript
Vue-CLI项目中路由传参的方式详解
Sep 01 Javascript
vue实现百度搜索功能
Dec 28 Javascript
vue中是怎样监听数组变化的
Oct 24 Javascript
nuxt静态部署打包相对路径操作
Nov 06 Javascript
详解用函数式编程对JavaScript进行断舍离
Sep 18 #Javascript
深入浅析JavaScript中的RegExp对象
Sep 18 #Javascript
JavaScript 数组的进化与性能分析
Sep 18 #Javascript
JavaScript实现HTML5游戏断线自动重连的方法
Sep 18 #Javascript
Node.JS中快速扫描端口并发现局域网内的Web服务器地址(80)
Sep 18 #Javascript
BetterScroll 在移动端滚动场景的应用
Sep 18 #Javascript
Windows下Node.js安装及环境配置方法
Sep 18 #Javascript
You might like
推荐文章系统(一)
2006/10/09 PHP
PHP Error与Logging函数的深入理解
2013/06/03 PHP
destoon设置自定义搜索的方法
2014/06/21 PHP
PHP使用stream_context_create()模拟POST/GET请求的方法
2016/04/02 PHP
详解thinkphp实现excel数据的导入导出(附完整案例)
2016/12/29 PHP
js 代码集(学习js的朋友可以看下)
2009/07/22 Javascript
js对象内部访问this修饰的成员函数示例
2014/04/27 Javascript
使用jquery+CSS实现控制打印样式
2014/12/31 Javascript
每天一篇javascript学习小结(Boolean对象)
2015/11/12 Javascript
微信小程序仿微信运动步数排行(交互)
2018/07/13 Javascript
angular实现input输入监听的示例
2018/08/31 Javascript
基于node+vue实现简单的WebSocket聊天功能
2020/02/01 Javascript
JavaScript变量Dom对象的所有属性
2020/04/30 Javascript
[01:14:10]2014 DOTA2国际邀请赛中国区预选赛 SPD-GAMING VS Orenda
2014/05/22 DOTA
[04:45]上海特级锦标赛主赛事第三日TOP10
2016/03/05 DOTA
python使用post提交数据到远程url的方法
2015/04/29 Python
Python常见格式化字符串方法小结【百分号与format方法】
2016/09/18 Python
python中实现k-means聚类算法详解
2017/11/11 Python
Python面向对象之类和实例用法分析
2019/06/08 Python
python Django中models进行模糊查询的示例
2019/07/18 Python
Django+Uwsgi+Nginx如何实现生产环境部署
2020/07/31 Python
Python+OpenCV图像处理——打印图片属性、设置存储路径、调用摄像头
2020/10/22 Python
web页面录屏实现
2019/02/12 HTML / CSS
您的健身减肥和健康饮食专家:vitafy
2017/06/06 全球购物
阿迪达斯印尼官方网站:adidas印尼
2020/02/10 全球购物
JAVA招聘远程笔试题
2015/07/23 面试题
毕业生在校学习的自我评价分享
2013/10/08 职场文书
20岁生日感言
2014/01/13 职场文书
廉洁自律承诺书
2014/03/27 职场文书
机关班子查摆问题及整改措施
2014/10/28 职场文书
2015年度企业工作总结
2015/05/21 职场文书
新娘父亲婚礼致辞
2015/07/27 职场文书
课文《燕子》教学反思
2016/02/17 职场文书
幼儿园教学反思范文
2016/03/02 职场文书
党风廉政建设心得体会
2019/05/21 职场文书
2019年消防宣传标语集锦
2019/11/21 职场文书