浅谈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 相关文章推荐
JavaScript之HTMLCollection接口代码
Apr 27 Javascript
jquery入门——事件机制之事件中的冒泡现象示例解释
Sep 12 Javascript
Jquery操作cookie记住用户名
Mar 29 Javascript
js放大镜放大购物图片效果
Jan 18 Javascript
JS正则获取HTML元素的方法
Mar 31 Javascript
详解Vue-cli 创建的项目如何跨域请求
May 18 Javascript
jQuery实现的淡入淡出与滑入滑出效果示例
Apr 18 jQuery
实例讲解Vue.js中router传参
Apr 22 Javascript
关于jquery中attr()和prop()方法的区别
May 28 jQuery
浅谈Node.js 中间件模式
Jun 12 Javascript
详解基于Wepy开发小程序插件(推荐)
Aug 01 Javascript
Vue 封装防刷新考试倒计时组件的实现
Jun 05 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
使用Curl进行抓取远程内容时url中文编码问题示例探讨
2013/10/29 PHP
php站内搜索关键词变亮的实现方法
2014/12/30 PHP
基于ThinkPHP+uploadify+upload+PHPExcel 无刷新导入数据
2015/09/23 PHP
PHP检测链接是否存在的代码实例分享
2016/05/06 PHP
javascript Prototype 对象扩展
2009/05/15 Javascript
IE8 中使用加速器(Activities)
2010/05/14 Javascript
js猜数字小游戏的简单实现代码
2013/07/02 Javascript
JS中eval函数的使用示例
2013/07/21 Javascript
在Google 地图上实现做的标记相连接
2015/01/05 Javascript
IE6兼容透明背景图片及解决方案
2015/08/19 Javascript
JS代码实现table数据分页效果
2016/05/26 Javascript
Bootstrap Table表格一直加载(load)不了数据的快速解决方法
2016/09/17 Javascript
Javascript 链式作用域详细介绍
2017/02/23 Javascript
浅谈nodejs中的类定义和继承的套路
2017/07/26 NodeJs
创建echart多个联动的示例代码
2018/11/23 Javascript
Vue基础学习之项目整合及优化
2019/06/02 Javascript
vue跳转同一个组件,参数不同,页面接收值只接收一次的解决方法
2019/11/05 Javascript
Node.js API详解之 os模块用法实例分析
2020/05/06 Javascript
微信小程序自定义弹出层效果
2020/05/26 Javascript
vue 动态生成拓扑图的示例
2021/01/03 Vue.js
[03:09]2014DOTA2国际邀请赛 Mushi前队友送上祝福
2014/07/12 DOTA
Python fileinput模块使用实例
2015/05/28 Python
Linux下python3.6.1环境配置教程
2018/09/26 Python
python和mysql交互操作实例详解【基于pymysql库】
2019/06/04 Python
Python 网络编程之UDP发送接收数据功能示例【基于socket套接字】
2019/10/11 Python
Python Selenium安装及环境配置的实现
2020/03/17 Python
阿玛尼美国官方网站:Armani.com
2016/11/25 全球购物
捷克浴室和厨房设备购物网站:SIKO
2018/08/11 全球购物
面向对象编程OOP的优点
2013/01/22 面试题
什么是托管函数?托管函数有什么用?
2014/06/15 面试题
人力资源专业推荐信
2013/11/29 职场文书
计算机专业职业规划
2014/02/28 职场文书
志愿者爱心公益活动策划方案
2014/09/15 职场文书
2014向国旗敬礼网上签名活动总结
2014/09/27 职场文书
学术会议邀请函
2015/01/30 职场文书
win10如何开启ahci模式?win10开启ahci模式详细操作教程
2022/07/23 数码科技