浅谈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 相关文章推荐
JS创建优美的页面滑动块效果 - Glider.js
Sep 27 Javascript
JQUERY 对象与DOM对象之两者相互间的转换
Apr 27 Javascript
Raphael带文本标签可拖动的图形实现代码
Feb 20 Javascript
jQuery随便控制任意div隐藏的方法
Jun 28 Javascript
Javascript排序算法之合并排序(归并排序)的2个例子
Apr 04 Javascript
JavaScript实现简单的数字倒计时
May 15 Javascript
Bootstrap免费字体和图标网站(值得收藏)
Mar 16 Javascript
vue中mint-ui环境搭建详细介绍
Apr 06 Javascript
vue 指定组件缓存实例详解
Apr 01 Javascript
对layer弹出框中icon数字参数的说明介绍
Sep 04 Javascript
jQuery实现点击滚动到指定元素上的方法分析
Mar 19 jQuery
Javascript生成器(Generator)的介绍与使用
Jan 31 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
PHP序列号生成函数和字符串替换函数代码
2012/06/07 PHP
smarty中英文多编码字符截取乱码问题解决方法
2014/10/28 PHP
php实现的简单检验登陆类
2015/06/18 PHP
php版微信小店API二次开发及使用示例
2016/11/12 PHP
PHP通过调用新浪API生成t.cn格式短网址链接的方法详解
2019/02/20 PHP
PHP实现通过二维数组键值获取一维键名操作示例
2019/10/11 PHP
用Javascript评估用户输入密码的强度(Knockout版)
2011/11/30 Javascript
jQuery中addClass()方法用法实例
2015/01/05 Javascript
JS仿iGoogle自定义首页模块拖拽特效的方法
2015/02/13 Javascript
javascript中的五种基本数据类型
2015/08/26 Javascript
推荐阅读的js快速判断IE浏览器(兼容IE10与IE11)
2015/12/13 Javascript
浅析jQuery Ajax请求参数和返回数据的处理
2016/02/24 Javascript
jquery属性,遍历,HTML操作方法详解
2016/09/17 Javascript
jQuery无缝轮播图代码
2016/12/22 Javascript
js上下视差滚动简单实现代码
2017/03/07 Javascript
详解JavaScript数组过滤相同元素的5种方法
2017/05/23 Javascript
基于canvas实现手写签名(vue)
2020/05/21 Javascript
[01:08:32]DOTA2-DPC中国联赛 正赛 DLG vs PHOENIX BO3 第二场 1月18日
2021/03/11 DOTA
Python实现简单网页图片抓取完整代码实例
2017/12/15 Python
python操作mysql代码总结
2018/06/01 Python
python利用小波分析进行特征提取的实例
2019/01/09 Python
详解Python的三种可变参数
2019/05/08 Python
关于Python3 lambda函数的深入浅出
2019/11/27 Python
Python opencv相机标定实现原理及步骤详解
2020/04/09 Python
python+opencv3.4.0 实现HOG+SVM行人检测的示例代码
2021/01/28 Python
图解CSS3制作圆环形进度条的实例教程
2016/05/26 HTML / CSS
介绍一下如何利用路径遍历进行攻击及如何防范
2014/01/19 面试题
护士毕业生自荐信
2014/02/07 职场文书
捐资助学倡议书
2014/04/15 职场文书
个人租房协议书
2014/11/28 职场文书
2014年企业党支部工作总结
2014/12/04 职场文书
2015年收银工作总结范文
2015/04/01 职场文书
2019垃圾分类宣传口号汇总
2019/08/16 职场文书
pytorch显存一直变大的解决方案
2021/04/08 Python
css3 利用transform-origin 实现圆点分布在大圆上布局及旋转特效
2021/04/29 HTML / CSS
如何解决flex文本溢出问题小结
2022/07/15 HTML / CSS