深入理解JavaScript中的作用域和上下文
介绍
JavaScript中有一个被称为作用域(Scope)的特性。虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,我会尽我所能用最简单的方式来解释作用域。理解作用域将使你的代码脱颖而出,减少错误,并帮助您使用它强大的设计模式。
什么是作用域(Scope)?
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
为什么说作用域是最小访问原则?
那么,为什么要限制变量的可见性呢,为什么你的变量不是在代码的任何地方都可用呢?一个优点是作用域为您的代码提供了一定程度的安全性。计算机安全的一个常见原则是用户应该一次只能访问他们需要的东西。
想象一下计算机管理员。由于他们对公司的系统有很多控制权限,因此向他们授予超级管理员权限就好了。他们都可以完全访问系统,一切工作顺利。但突然发生了一些坏事,你的系统感染了恶意病毒。现在你不知道谁犯的错误?你意识到应该授予普通用户权限,并且只在需要时授予超级访问权限。这将帮助您跟踪更改,并记录谁拥有什么帐户。这被称为最小访问原则。看起来很直观?这个原则也适用于编程语言设计,在大多数编程语言中被称为作用域,包括我们接下来要研究的 JavaScript 。
当你继续在你的编程旅程,您将意识到,您的代码的作用域有助于提高效率,帮助跟踪错误并修复它们。作用域还解决了命名问题,在不同作用域中变量名称可以相同。记住不要将作用域与上下文混淆。它们的特性不同。
JavaScript中的作用域
在JavaScript中有两种类型的作用域:
- 全局作用域
- 局部作用域(也叫本地作用域)
定义在函数内部的变量具有局部作用域,而定义在函数外部的变量具有全局范围内。每个函数在被调用时都会创建一个新的作用域。
全局作用域
当您开始在文档中编写JavaScript时,您已经在全局作用域中了。全局作用域贯穿整个javascript文档。如果变量在函数之外定义,则变量处于全局作用域内。
- // 默认全局作用域
- var name = 'Hammad';
在全局作用域内的变量可以在任何其他作用域内访问和修改。
- var name = 'Hammad';
- console.log(name); // logs 'Hammad'
- function logName() {
- console.log(name); // 'name' 可以在这里和其他任何地方被访问
- }
- logName(); // logs 'Hammad'
局部作用域
函数内定义的变量在局部(本地)作用域中。而且个函数被调用时都具有不同的作用域。这意味着具有相同名称的变量可以在不同的函数中使用。这是因为这些变量被绑定到它们各自具有不同作用域的相应函数,并且在其他函数中不可访问。
- // Global Scope
- function someFunction() {
- // Local Scope #1
- function someOtherFunction() {
- // Local Scope #2
- }
- }
- // Global Scope
- function anotherFunction() {
- // Local Scope #3
- }
- // Global Scope
块语句
块语句,如 if
和 switch
条件语句或 for
和 while
循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
- if (true) {
- // 'if' 条件语句块不会创建一个新的作用域
- var name = 'Hammad'; // name 依然在全局作用域中
- }
- console.log(name); // logs 'Hammad'
ECMAScript 6 引入了 let
和 const
关键字。可以使用这些关键字来代替 var
关键字。
- var name = 'Hammad';
- let likes = 'Coding';
- const skills = 'Javascript and PHP';
与 var
关键字相反,let
和 const
关键字支持在局部(本地)作用域的块语句中声明。
- if (true) {
- // 'if' 条件语句块不会创建一个新的作用域
- // name 在全局作用域中,因为通过 'var' 关键字定义
- var name = 'Hammad';
- // likes 在局部(本地)作用域中,因为通过 'let' 关键字定义
- let likes = 'Coding';
- // skills 在局部(本地)作用域中,因为通过 'const' 关键字定义
- const skills = 'JavaScript and PHP';
- }
- console.log(name); // logs 'Hammad'
- console.log(likes); // Uncaught ReferenceError: likes is not defined
- console.log(skills); // Uncaught ReferenceError: skills is not defined
只要您的应用程序生活,全球作用域就会生存。 只要您的函数被调用并执行,局部(本地)作用域就会存在。
上下文
许多开发人员经常混淆 作用域(scope) 和 上下文(context),很多误解为它们是相同的概念。但事实并非如此。作用域(scope)我们上面已经讨论过了,而上下文(context)是用来指定代码某些特定部分中 this
的值。作用域(scope) 是指变量的可访问性,上下文(context)是指 this
在同一作用域内的值。我们也可以使用函数方法来改变上下文,将在稍后讨论。 在全局作用域(scope)中上下文中始终是Window
对象。(注:取决于JavaScript 的宿主换环境,在浏览器中在全局作用域(scope)中上下文中始终是Window
对象。在Node.js中在全局作用域(scope)中上下文中始终是Global
对象)
- // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
- console.log(this);
- function logFunction() {
- console.log(this);
- }
- // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
- // 因为 logFunction() 不是一个对象的属性
- logFunction();
如果作用域在对象的方法中,则上下文将是该方法所属的对象。
- class User {
- logName() {
- console.log(this);
- }
- }
- (new User).logName(); // logs User {}
(new User).logName() 是一种将对象存储在变量中然后调用logName
函数的简单方法。在这里,您不需要创建一个新的变量。
您会注意到,如果您使用 new
关键字调用函数,则上下文的值会有所不同。然后将上下文设置为被调用函数的实例。考虑上面的示例,通过 new
关键字调用的函数。
- function logFunction() {
- console.log(this);
- }
- new logFunction(); // logs logFunction {}
当在严格模式(Strict Mode)中调用函数时,上下文将默认为 undefined
。
执行期上下文(Execution Context)
注:这部分解释建议先查看这篇文章,更加通俗易懂,https://www.html.cn/archives/7262
上面我们了解了作用域和上下文,为了消除混乱,特别需要注意的是,执行期上下文中的上下文这个词语是指作用域而不是上下文。这是一个奇怪的命名约定,但由于JavaScipt规范,我们必须链接他们这间的联系。
JavaScript是一种单线程语言,因此它一次只能执行一个任务。其余的任务在执行期上下文中排队。正如我刚才所说,当 JavaScript 解释器开始执行代码时,上下文(作用域)默认设置为全局。这个全局上下文附加到执行期上下文中,实际上是启动执行期上下文的第一个上下文。
之后,每个函数调用(启用)将其上下文附加到执行期上下文中。当另一个函数在该函数或其他地方被调用时,会发生同样的事情。
每个函数都会创建自己的执行期上下文。
一旦浏览器完成了该上下文中的代码,那么该上下文将从执行期上下文中销毁,并且执行期上下文中的当前上下文的状态将被传送到父级上下文中。 浏览器总是执行堆栈顶部的执行期上下文(这实际上是代码中最深层次的作用域)。
无论有多少个函数上下文,但是全局上下文只有一个。
执行期上下文有创建和代码执行的两个阶段。
创建阶段
第一阶段是创建阶段,当一个函数被调用但是其代码还没有被执行的时。 在创建阶段主要做的三件事情是:
- 创建变量(激活)对象
- 创建作用域链
- 设置上下文(context)的值( `this` )
变量对象
变量对象,也称为激活对象,包含在执行期上下文中定义的所有变量,函数和其他声明。当调用函数时,解析器扫描它所有的资源,包括函数参数,变量和其他声明。包装成一个单一的对象,即变量对象。
- 'variableObject': {
- // 包含函数参数,内部变量和函数声明
- }
作用域链
在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量或其他任何资源为止。作用域链可以简单地定义为包含其自身执行上下文的变量对象的对象,以及其父级对象的所有其他执行期上下文,一个具有很多其他对象的对象。
- 'scopeChain': {
- // 包含自己的变量对象和父级执行上下文的其他变量对象
- }
执行期上下文对象
执行期上下文可以表示为一个抽象对象,如下所示:
- executionContextObject = {
- 'scopeChain': {}, // 包含自己的变量对象和父级执行上下文的其他变量对象
- 'variableObject': {}, // 包含函数参数,内部变量和函数声明
- 'this': valueOfThis
- }
代码执行阶段
在执行期上下文的第二阶段,即代码执行阶段,分配其他值并最终执行代码。
词法作用域
词法作用域意味着在一组嵌套的函数中,内部函数可以访问其父级作用域中的变量和其他资源。这意味着子函数在词法作用域上绑定到他们父级的执行期上下文。词法作用域有时也被称为静态作用域。
- function grandfather() {
- var name = 'Hammad';
- // likes 在这里不可以被访问
- function parent() {
- // name 在这里可以被访问
- // likes 在这里不可以被访问
- function child() {
- // 作用域链最深层
- // name 在这里也可以被访问
- var likes = 'Coding';
- }
- }
- }
你会注意到词法作用域向内传递的,意味着 name
可以通过它的子级期执行期上下文访问。但是,但是它不能向其父对象反向传递,意味着变量 likes
不能被其父对象访问。这也告诉我们,在不同执行上下文中具有相同名称的变量从执行堆栈的顶部到底部获得优先级。在最内层函数(执行堆栈的最上层上下文)中,具有类似于另一变量的名称的变量将具有较高优先级。
闭包(?Closures)
注:这部分解释建议先查看这篇文章,更加通俗易懂,https://www.html.cn/archives/7262
闭包的概念与我们在上面讲的词法作用域密切相关。 当内部函数尝试访问其外部函数的作用域链,即在直接词法作用域之外的变量时,会创建一个闭包。 闭包包含自己的作用域链,父级的作用域链和全局作用域。
闭包不仅可以访问其外部函数中定义的变量,还可以访问外部函数的参数。
即使函数返回后,闭包也可以访问其外部函数的变量。这允许返回的函数保持对外部函数所有资源的访问。
当从函数返回内部函数时,当您尝试调用外部函数时,不会调用返回的函数。您必须首先将外部函数的调用保存在单独的变量中,然后将该变量调用为函数。考虑这个例子:
- function greet() {
- name = 'Hammad';
- return function () {
- console.log('Hi ' + name);
- }
- }
- greet(); // 什么都没发生,没有错误
- // 从 greet() 中返回的函数保存到 greetLetter 变量中
- greetLetter = greet();
- // 调用 greetLetter 相当于调用从 greet() 函数中返回的函数
- greetLetter(); // logs 'Hi Hammad'
这里要注意的是,greetLetter
函数即使在返回后也可以访问 greet
函数的 name
变量。 有一种方法不需要分配一个变量来访问 greet
函数返回的函数,即通过使用两次括号 ()
,即 ()()
来调用,就是这样:
- function greet() {
- name = 'Hammad';
- return function () {
- console.log('Hi ' + name);
- }
- }
- greet()(); // logs 'Hi Hammad'
公共作用域和私有作用域
在许多其他编程语言中,您可以使用公共,私有和受保护的作用域来设置类的属性和方法的可见性。考虑使用PHP语言的这个例子:
- // Public Scope
- public $property;
- public function method() {
- // ...
- }
- // Private Sccpe
- private $property;
- private function method() {
- // ...
- }
- // Protected Scope
- protected $property;
- protected function method() {
- // ...
- }
来自公共(全局)作用域的封装函数使他们免受脆弱的攻击。但是在JavaScript中,没有公共或私有作用域。幸好,我们可以使用闭包来模拟此功能。为了保持一切与全局分离,我们必须首先将我们的函数封装在如下所示的函数中:
- (function () {
- // 私有作用域 private scope
- })();
函数末尾的括号会告知解析器在没有调用的情况下一旦读取完成就立即执行它。(注:这其实叫立即执行函数表达式)我们可以在其中添加函数和变量,它们将不能在外部访问。但是,如果我们想在外部访问它们,也就是说我们希望其中一些公开的,另一些是私有的?我们可以使用一种称为 模块模式 的闭包类型,它允许我们使用对象中公共和私有的作用域来对我们的函数进行调整。
模块模式
模块模式类似这样:
- var Module = (function() {
- function privateMethod() {
- // do something
- }
- return {
- publicMethod: function() {
- // can call privateMethod();
- }
- };
- })();
Module
中的 return
语句包含了我们公开的函数。私有函数只是那些没有返回的函数。没有返回的函数不可以在 Module
命名空间之外访问。但是公开函数可以访问私有函数,这使它们对于助手函数,AJAX调用和其他事情很方便。
- Module.publicMethod(); // 可以正常工作
- Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
私有函数一个惯例是用下划线开始,并返回一个包含我们公共函数的匿名对象。这使得它们很容易在长对象中管理。它看起来是这样子的:
- var Module = (function () {
- function _privateMethod() {
- // do something
- }
- function publicMethod() {
- // do something
- }
- return {
- publicMethod: publicMethod,
- }
- })();
立即执行函数表达式(IIFE)
另一种类型的闭包是立即执行函数表达式(IIFE)。这是一个在 window
上下文中调用的自动调用的匿名函数,这意味着 this
的值为window
。暴露一个单一的全局接口来进行交互。他是这样的:
- (function(window) {
- // do anything
- })(this);
使用 .call(), .apply() 和 .bind() 改变上下文
.call()
和 .apply()
函数用于在调用函数时改变上下文。这给了你令人难以置信的编程能力(和一些终极权限来驾驭代码)。
要使用call
或apply
函数,您只需要在函数上调用它,而不是使用一对括号调用函数,并将新的上下文作为第一个参数传递。
函数自己的参数可以在上下文之后传递。(注:call
或apply
用另一个对象来调用一个方法,将一个函数上下文从初始的上下文改变为指定的新对象。简单的说就是改变函数执行的上下文。)
- function hello() {
- // do something...
- }
- hello(); // 通常的调用方式
- hello.call(context); // 在这里你可以传递上下文(this 值)作为第一个参数
- hello.apply(context); // 在这里你可以传递上下文(this 值)作为第一个参数
.call()
和.apply()
之间的区别在于,在.call()
中,其余参数作为以逗号分隔的列表,而.apply()
则允许您在数组中传递参数。
- function introduce(name, interest) {
- console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
- console.log('The value of this is '+ this +'.')
- }
- introduce('Hammad', 'Coding'); // 通常的调用方式
- introduce.call(window, 'Batman', 'to save Gotham'); // 在上下文之后逐个传递参数
- introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // 在上下文之后传递数组中的参数
- // 输出:
- // Hi! I'm Hammad and I like Coding.
- // The value of this is [object Window].
- // Hi! I'm Batman and I like to save Gotham.
- // The value of this is [object Window].
- // Hi! I'm Bruce Wayne and I like businesses.
- // The value of this is Hi.
.call()
的性能要比.apply()
稍快。
以下示例将文档中的项目列表逐个记录到控制台。
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Things to learn</title>
- </head>
- <body>
- <h1>Things to Learn to Rule the World</h1>
- <ul>
- <li>Learn PHP</li>
- <li>Learn Laravel</li>
- <li>Learn JavaScript</li>
- <li>Learn VueJS</li>
- <li>Learn CLI</li>
- <li>Learn Git</li>
- <li>Learn Astral Projection</li>
- </ul>
- <script>
- // 在listItems中保存页面上所有列表项的NodeList
- var listItems = document.querySelectorAll('ul li');
- // 循环遍历listItems NodeList中的每个节点,并记录其内容
- for (var i = 0; i < listItems.length; i++) {
- (function () {
- console.log(this.innerHTML);
- }).call(listItems[i]);
- }
- // Output logs:
- // Learn PHP
- // Learn Laravel
- // Learn JavaScript
- // Learn VueJS
- // Learn CLI
- // Learn Git
- // Learn Astral Projection
- </script>
- </body>
- </html>
HTML仅包含无序的项目列表。然后 JavaScript 从DOM中选择所有这些项目。列表循环,直到列表中的项目结束。在循环中,我们将列表项的内容记录到控制台。
该日志语句包裹在一个函数中,该 call
函数包含在调用函数中的括号中。将相应的列表项传递给调用函数,以便控制台语句中的 this
关键字记录正确对象的 innerHTML 。
对象可以有方法,同样的函数对象也可以有方法。 事实上,JavaScript函数附带了四种内置方法:
- Function.prototype.apply()
- Function.prototype.bind() ( ECMAScript 5 (ES5) 中引进)
- Function.prototype.call()
- Function.prototype.toString()
Function.prototype.toString() 返回函数源代码的字符串表示形式。
到目前为止,我们讨论过 .call()
, .apply()
和 toString()
。与 .call()
和 .apply()
不同,.bind()
本身不调用该函数,它只能用于在调用函数之前绑定上下文和其他参数的值。在上面的一个例子中使用 .bind()
:
- (function introduce(name, interest) {
- console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
- console.log('The value of this is '+ this +'.')
- }).bind(window, 'Hammad', 'Cosmology')();
- // logs:
- // Hi! I'm Hammad and I like Cosmology.
- // The value of this is [object Window].
.bind()
就像.call()
函数一样,它允许你传递其余的参数,用逗号分隔,而不是像apply,在数组中传递参数。
结论
这些概念是 JavaScript 的根本,对于了解高级语法很重要。我希望你能更好地了解JavaScript作用域和他相关的事情。如果没用弄明白这些问题,欢迎在下面的评论中提问。
本系列知识相关阅读:
.
深入理解JavaScript中的作用域和上下文的更多相关文章
- 理解JavaScript中的作用域和上下文
JavaScript对于作用域(Scope)和上下文(Context)的实现是这门语言的一个非常独到的地方,部分归功于其独特的灵活性. 函数可以接收不同的的上下文和作用域.这些概念为JavaScrip ...
- 认识javascript中的作用域和上下文
javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的 ...
- 理解JavaScript中的作用域链
理解了作用域链,闭包就不难理解了,所以本文主要谈一谈我对作用域链的理解. 关于JavaScript中变量的作用域,全局变量在程序中始终都有定义.局部变量在声明它的函数体内以及其内部所嵌套的函数内始 ...
- 理解JavaScript中的作用域
什么是变量,什么是作用域? 变量:简单来说就是在特定时间内保存特定值的一个名字而已,由于不存在定义某个变量必须要保存某种数据类型值的规则,所以变量的值及其数据类型可以在脚本生命周期内任意改变,变量可 ...
- 深入理解JavaScript中的作用域、作用域链和闭包
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/qappleh/article/detai ...
- 理解 JavaScript 中的 this
前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...
- 深入理解javascript中执行环境(作用域)与作用域链
深入理解javascript中执行环境(作用域)与作用域链 相信很多初学者对与javascript中的执行环境与作用域链不能很好的理解,这里,我会按照自己的理解同大家一起分享. 一般情况下,我们把执行 ...
- 理解javascript中的回调函数(callback)【转】
在JavaScrip中,function是内置的类对象,也就是说它是一种类型的对象,可以和其它String.Array.Number.Object类的对象一样用于内置对象的管理.因为function实 ...
- JavaScript中的作用域
很多(JavaScript)开发者都在讨论"作用域",但它是什么?它们在JavaScript中的任何地方!我发现很多年轻的开发者不知道作用域是什么.他们中大多数人可以用jQuery ...
随机推荐
- go语言设计模式之adapter
adapter.go package adapter import ( "fmt" ) type LegacyPrinter interface { Print(s string) ...
- 浅谈JS重绘与回流
在说浏览器渲染页面之前,我们需要先了解两个点,一个叫 浏览器解析 URL,另一个就是本章节将涉及的 重绘与回流: 重绘(repaint):当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新, ...
- mock 模拟数据在框架中的简单使用
首先在框架中需要安装mock模块 cnpm i mockjs -S 其次在src文件夹下新建mock文件夹,在mock文件夹中新建一个index.js文件 代码如下: const Mock = req ...
- 移动端px自动转化为rem
注:不转换的px用大写PX代替 lib-flexible 作用:让网页根据设备dpr和宽度,利用viewport和html根元素的font-size配合rem来适配不同尺寸的移动端设备 安装:cnpm ...
- 小程序-API请求
Page({ onLoad:function(){ // 在onLoad中调用发送请求的函数 this.getProList(); } getProList:function(){ var self= ...
- prerender-spa-plugin预渲染踩坑
为什么要使用预渲染? 为了应付SEO(国内特别是百度)考虑在网站(vue技术栈系列)做一些优化.大概有几种方案可以考虑: 服务端做优化: 第一,ssr,vue官方文档给出的服务器渲染方案,这是一套完整 ...
- AtCoder Grand Contest 039
Preface 我发现我现在打AT真的是只会D-Before-- E,F都是抄曲明姐姐的,然后D还是几何画板猜结论做的(证明都是陈指导想的) 看来再这样下去就真的要退役了啊233 A - Connec ...
- 【译】3D打印:介绍
原文地址:(需要翻墙)https://ordina-jworks.github.io/iot/2018/09/28/3D-Printing-Intro.html 文章发表日期:2018-09-28 第 ...
- C语言程序设计100例之(9):生理周期
例9 生理周期 问题描述 人生来就有三个生理周期,分别为体力.感情和智力周期,它们的周期长度为 23 天.28 天和33 天.每一个周期中有一天是高峰.在高峰这天,人会在相应的方面表现出色.例如 ...
- js中的NaN,isNaN与Number.isNaN的区别,如何判断一个值严格等于NaN
在JavaScript的数字类型Number中,我们最常使用的大概是整数类型与浮点数类型,但除这两者外,还有个特殊的存在NaN,为什么NaN!==NaN?我们如何判断一个值是否等于NaN呢?这篇文章好 ...