范围或确定变量所处位置的一组规则是任何编程语言中最基本的概念之一。 实际上,它是如此基础,以至于很容易忘记规则的微妙之处!

了解JavaScript引擎究竟是如何“认为”有关范围将让你从写常见的错误是提升可能会导致,为您准备环绕关闭你的头,并让你更接近于从不写错误以后再。

...嗯,无论如何,它可以帮助您了解起重和关闭。

在本文中,我们将看一下:

  • JavaScript范围的基础
  • 解释器如何确定哪些变量属于哪个范围
  • 吊车的真正工作原理
  • ES6关键字如何let const改变游戏规则

让我们潜入。

词汇范围

如果您以前甚至写过一行JavaScript,您就会知道 您在何处定义变量决定了可以在哪里使用它们。 变量的可见性取决于源代码的结构这一事实称为词法 范围。

使用JavaScript创建范围的三种方法:

  1. 创建一个功能 。 在函数内部声明的变量仅在该函数内部可见,包括在嵌套函数中。
  2. 在代码块 中用 letconst 声明变量 。 这样的声明仅在块内部可见。
  3. 创建一个 catch 。 信不信由你,这确实创造了一个新的范围!
"use strict";
var mr_global = "Mr Global";function foo () {var mrs_local = "Mrs Local";console.log("I can see " + mr_global + " and " + mrs_local + ".");function bar () {console.log("I can also see " + mr_global + " and " + mrs_local + ".");}
}foo(); // Works as expectedtry {console.log("But /I/ can't see " + mrs_local + ".");
} catch (err) {console.log("You just got a " + err + ".");
}{let foo = "foo";const bar = "bar";console.log("I can use " + foo + bar + " in its block...");
}try {console.log("But not outside of it.");
} catch (err) {console.log("You just got another " + err + ".");
}// Throws ReferenceError!
console.log("Note that " + err + " doesn't exist outside of 'catch'!")

上面的代码段演示了所有三种作用域机制。 您可以在Node或Firefox中运行它,但Chrome与let配合使用效果不佳。

我们将详细讨论其中的每一个。 让我们开始详细研究JavaScript如何找出哪些变量属于什么范围。

编译过程:鸟瞰图

当您运行一段JavaScript时,有两件事使它起作用。

  1. 首先,您的源代码被编译。
  2. 然后,执行编译的代码。

编译 步骤 ,JavaScript引擎:

  1. 记录所有变量名称
  2. 在适当的范围内注册它们
  3. 为其价值保留空间

只有在执行期间,JavaScript引擎才会实际设置变量引用的值等于它们的赋值。 在此之前,它们是undefined

步骤1:编译

// I can use first_name anywhere in this program
var first_name = "Peleke";function popup (first_name) {// I can only use last_name inside of this functionvar last_name = "Sengstacke";alert(first_name + ' ' + last_name);
}popup(first_name);

让我们逐步了解编译器的作用。

首先,它读取var first_name = "Peleke" 。 接下来,它确定将变量保存到什么范围 。 因为我们处于脚本的顶层,所以它意识到我们处于全局范围内 。 然后,它将变量first_name保存到全局范围,并将其值初始化为undefined

其次,编译器读取带有function popup (first_name) 。 因为function关键字是第一行,所以它为函数创建了一个新作用域,将函数的定义注册到全局作用域,并在内部窥视以查找变量声明。

果然,编译器找到了一个。 由于我们函数的第一行中有var last_name = "Sengstacke" ,因此编译器将变量last_name保存到 popup范围- 不在全局范围内,并将其值设置为undefined

由于函数内没有更多的变量声明,因此编译器将退回到全局范围。 并且因为那里没有更多的变量声明, 所以此阶段完成。

注意,我们实际上还没有运行任何东西。 此时,编译器的工作只是确保知道每个人的名字。 它并不关心他们做什么

至此,我们的程序知道:

  1. 全局范围中有一个名为first_name的变量。
  2. 全局范围中有一个名为popup的函数。
  3. popup的范围内有一个名为last_name的变量。
  4. first_namelast_name的值均未undefined

不在乎我们在代码的其他位置分配了这些变量值。 引擎在执行时会负责

步骤2:执行

在下一步中,引擎再次读取我们的代码,但是这次执行它。

首先,它读取行var first_name = "Peleke" 。 为此,引擎将查找名为first_name的变量。 由于编译器已经使用该名称注册了变量,因此引擎会找到它,并将其值设置为"Peleke"

接下来,它读取行, function popup (first_name) 。 由于我们不在这里执行函数,因此引擎对此不感兴趣,因此跳过了它。

最后,它读取行popup(first_name) 。 由于我们这里执行功能,因此引擎:

  1. 查找的数值popup
  2. 查找first_name的值
  3. popup作为函数执行,将first_name的值作为参数传递

当它执行popup ,会经历相同的过程,但是这次是在popup函数内部。 它:

  1. 查找名为last_name的变量
  2. last_name的值设置为等于"Sengstacke"
  3. 查找alert ,以"Peleke Sengstacke"作为参数将其作为函数执行

事实证明,引擎盖下发生的事情比我们想象的要多得多!

既然您已经了解了JavaScript如何读取和运行您编写的代码,那么我们就可以着手解决离家更近的事情了:提升的工作原理。

显微镜下吊装

让我们从一些代码开始。

bar();function bar () {if (!foo) {alert(foo + "? This is strange...");}var foo = "bar";
}broken(); // TypeError!
var broken = function () {alert("This alert won't show up!");
}

如果运行此代码,您会注意到三件事:

  1. 可以在分配给foo之前先对其进行引用,但其值是undefined
  2. 可以在定义它之前调用broken ,但是会得到TypeError
  3. 可以在定义它之前调用bar ,它会按需工作。

起重指JavaScript,使我们所有的在他们随处可见声明的变量名的范围,包括之前我们分配给他们的事实。

摘要中的三​​种情况是您在自己的代码中需要了解的三种情况,因此我们将一步一步地进行介绍。

吊装变量声明

请记住,当JavaScript编译器读取类似var foo = "bar"的行时,它会:

  1. 将名称foo注册到最近的作用域
  2. foo的值设置为undefined

我们可以在赋值之前使用foo的原因是,当引擎查找具有该名称的变量时,它确实存在。 这就是为什么它不会引发ReferenceError

相反,它获取值undefined ,并尝试使用该值执行您要求的任何操作。 通常,这是一个错误。

记住这一点,我们可以想象JavaScript在功能bar看到的内容更像是这样:

function bar () {var foo; // undefinedif (!foo) {// !undefined is true, so alertalert(foo + "? This is strange...");}foo = "bar";
}

如果您愿意,这是起重第一规则 :变量在它们的整个作用域内都是可用 ,但是在您的代码分配给它们之前,其值是undefined

一个常见JavaScript习惯用法是将所有var声明写在其作用域的顶部 ,而不是您第一次使用它们的位置。 用道格·克罗克福德(Doug Crockford)来解释,这有助于您的代码像运行时一样阅读更多。

当您考虑它时,这是有道理的。 很明显,为什么bar以JavaScript读取方式编写代码时会表现出这种行为,不是吗? 那么,为什么不只是写像所有的时间?

起重功能表达式

我们得到了一个事实TypeError ,当我们试图执行broken之前,我们定义它仅仅是提升的第一条规则的一个特例。

我们定义了一个称为broken的变量,编译器在全局范围内注册该变量并将其设置为undefined 。 当我们尝试运行它时,发动机查找的价值broken ,认定它是undefined ,并试图执行undefined的功能。

显然, undefined 不是一个函数-这就是为什么我们得到TypeError的原因!

起重功能声明

最后,回想一下,我们可以在定义bar之前调用它。 这是由于第二个提升规则 :当JavaScript编译器找到函数声明时,它的名称定义都在其作用域的顶部可用。 再次重写我们的代码:

function bar () {if (!foo) {alert(foo + "? This is strange...");}var foo = "bar";
}var broken; // undefinedbar(); // bar is already defined, executes finebroken(); // Can't execute undefined!broken = function () {alert("This alert won't show up!");
}

同样,当您以JavaScript 读取的方式进行 编写时,这更有意义,您认为呢?

回顾:

  1. 变量声明和函数表达式的名称在整个作用域内都可用,但是在赋值之前它们的undefined
  2. 函数声明的名称定义在整个作用域内都可用, 甚至在 定义 之前也可以使用

现在,让我们看一下两个工作原理稍有不同的新工具: letconst

let const const 和时间死区

var声明不同,使用letconst声明的变量不会被编译器挂起。

至少不是完全正确。

还记得我们如何能够调用broken ,但是由于尝试执行undefined却遇到TypeError ? 如果我们想定义的brokenlet ,我们就已经获得了ReferenceError ,而不是:

"use strict";
// You have to "use strict" to try this in Node
broken(); // ReferenceError!
let broken = function () {alert("This alert won't show up!");
}

当JavaScript编译器在第一遍将变量注册到其作用域时,与var相比,对letconst有所不同。

当找到var声明时,我们将变量名注册到其作用域,然后立即将其值初始化为undefined

但是,使用let ,编译器会将变量注册到其作用域,但不会   将其值初始化为undefined 。 相反,它将保留未初始化的变量, 直到引擎执行您的赋值语句。 访问未初始化变量的值将引发ReferenceError ,这说明了为什么在运行该代码段时会抛出该错误。

let声明的范围的开头的顶部与赋值语句之间的空间称为时间死区 。 该名称来自以下事实:即使引擎知道 bar范围顶部的名为foo的变量,该变量也是“死的”,因为它没有值。

...也因为如果您尝试过早使用它,它将杀死您的程序。

const关键字的工作方式与let相同,但有两个主要区别:

  1. 使用const声明时, 必须分配一个值。
  2. 不能将值重新分配给用const声明的变量。

这保证了const始终   具有您最初为其分配的值。

// This is legal
const React = require('react');// This is totally not legal
const crypto;
crypto = require('crypto');

区块范围

letconst在另一方面与var不同:它们的范围大小。

当您使用var声明变量时,它是可见的 尽可能在作用域链上位于最高位置-通常在最接近的函数声明的顶部,或者在全局作用域中(如果在顶层声明)。

但是,当使用letconst声明变量时,该变量在本地尽可能地可见- 在最近的块中可见。

是由花括号引起的一段代码,如if / else块, for循环以及显式“被阻止”的代码块(如本段所示)所示。

"use strict";{let foo = "foo";if (foo) {const bar = "bar";var foobar = foo + bar;console.log("I can see " + bar + " in this bloc.");}try {console.log("I can see " + foo + " in this block, but not " + bar + ".");} catch (err) {console.log("You got a " + err + ".");}
}try {console.log( foo + bar ); // Throws because of 'foo', but both are undefined
} catch (err) {console.log( "You just got a " + err + ".");
}console.log( foobar ); // Works fine

如果您声明一个变量与constlet一个块中,它是唯一可见的您分配它的块中,经过而已

但是,用var声明的变量在尽可能远的位置可见-在这种情况下,在全局范围内。

如果您对letconst的细节感兴趣,请查看Rauschmayer博士在“ 探索ES6:变量和作用域”中 对它们的评价 ,并查看它们的MDN文档 。

词法this和Arrow函数

从表面上看, this似乎与范围无关。 而且,实际上,JavaScript并不能根据我们在这里讨论的范围规则来解决this问题的含义。

至少,不是通常。 JavaScript的,出了名的, 不能解决的意思是this基于关键字,你用它在:

var foo = {name: 'Foo',languages: ['Spanish', 'French', 'Italian'],speak : function speak () {this.languages.forEach(function(language) {console.log(this.name + " speaks " + language + ".");})}
};foo.speak();

我们大多数人都希望thisforEach循环内表示foo ,因为这就是它在循环外的含义。 换句话说,我们所期待JavaScript来解决的意思this 词汇

但事实并非如此。

相反,它创建一个新的 this里面每一个你定义的功能,并决定它意味着根据您如何调用该函数,不是定义它是什么。

第一点类似于在子作用域中重新定义任何变量的情况:

function foo () {var bar = "bar";function baz () {// Reusing variable names like this is called "shadowing" var bar = "BAR";console.log(bar); // BAR}baz();
}foo(); // BAR

更换barthis ,整个事情应该立即清理!

传统上,要使this按我们期望的普通旧词法作用域变量正常工作,需要以下两种解决方法之一:

var foo = {name: 'Foo',languages: ['Spanish', 'French', 'Italian'],speak_self : function speak_s () {var self = this;self.languages.forEach(function(language) {console.log(self.name + " speaks " + language + ".");})},speak_bound : function speak_b () {this.languages.forEach(function(language) {console.log(this.name + " speaks " + language + ".");}.bind(foo)); // More commonly:.bind(this);}
};

speak_self ,我们保存的意义this给变量self ,并使用变量来得到我们想要的参考。 在speak_bound ,我们使用bind 永久指向this给定的对象。

ES2015为我们带来了新的选择:箭头功能。

与“普通”函数不同,箭头函数不会通过设置其自身来遮蔽其父范围的this值。 相反,他们用词法解决了它的含义

换句话说,如果在箭头函数中使用this ,JavaScript会像查找其他任何变量一样查找其值。

首先,它在本地范围内检查this值。 由于箭头功能没有设置一个,因此找不到。 接下来,它将检查作用域的this值。 如果找到一个,它将使用它。

这样我们就可以像这样重写上面的代码:

var foo = {name: 'Foo',languages: ['Spanish', 'French', 'Italian'],speak : function speak () {this.languages.forEach((language) => {console.log(this.name + " speaks " + language + ".");})}
};

如果您想了解有关箭头功能的更多详细信息,请查看Envato Tuts +讲师Dan Wellman的 JavaScript ES6基础基础教程,以及有关箭头功能的MDN文档 。

结论

到目前为止,我们已经涵盖了很多领域! 在本文中,您了解到:

您还看到了两个起吊规则:

下一步很不错,那就是利用您对JavaScript范围的重新认识,将您的头脑包裹在闭包上。 为此,请查看Kyle Simpson的范围和闭包 。

最后,关于this我还有很多要说的。 如果关键字仍然看起来像是个黑魔法,请查看此&Object Prototypes以了解它。

同时,利用所学知识,减少编写错误!

学习JavaScript:完整指南

我们已经建立了完整的指南,可以帮助您学习JavaScript ,无论您是刚开始作为Web开发人员还是想探索更高级的主题。

翻译自: https://code.tutsplus.com/tutorials/grokking-scope-in-javascript--cms-26259

JavaScript中的Grokking范围相关推荐

  1. 浅析 JavaScript 中的 函数 uncurrying 反柯里化

    柯里化 柯里化又称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果. 因此柯里化的过程是 ...

  2. JavaScript中,this的绑定规则

    对于 JavaScript 新手来说,this 是非常基础同时也难以理解的知识点. 比如下面的代码,this 指向就有三种方式. 在<你不知道的 JavaScript>一书中,我总算比较清 ...

  3. Javascript中undefined,NaN等特殊比较

    以下内容转自: http://blog.csdn.net/hongweigg/article/details/38090093 1.问题:在Javascript中,typeof(undefined) ...

  4. Javascript中二进制数据处理方法

    Javascript中二进制数据处理方法 转载于:https://www.cnblogs.com/motadou/archive/2012/02/19/2358514.html

  5. JavaScript 中的有限状态机

    http://www.ibm.com/developerworks/cn/web/wa-finitemach/ JavaScript 中的有限状态机 Page navigation 系列文章 有限状态 ...

  6. 在Javascript中使用面向对象的编程

    by Mike Koss March 26th, 2003 这是一篇,我个人认为最好的,Javascript面向对象编程的文章.翻译不好的地方,还望大家指正,谢谢. 如果您需要,可以访问下面的地址取得 ...

  7. 取出url中的字符_如何在JavaScript中解析URL:例如主机名,路径名,查询,哈希?...

    统一资源定位符(缩写URL)是对Web资源(网页,图像,文件)的引用.URL指定资源位置和检索资源的机制(http,ftp,mailto). 例如,这是此博客文章的URL: 通常,您需要访问URL的特 ...

  8. 在javascript中判断类型

    String 一个字符串始终是一个字符串,所以这一块是很容易.除非使用new(new String)调用,否则typeof将返回"object".所以也要包含那些可以使用的字符串i ...

  9. JavaScript中几个重要的知识点(1) ---- 面向对象

    JavaScript中几个最重要的大知识点 面向对象 DOM事件 异步交互ajax 面向对象 在JS中可以把任意的引用和变量都看成是一个对象.面向对象的主要三个表现形式: 封装 继承 多态 1. 封装 ...

最新文章

  1. 情感分析:基于循环神经网络
  2. 青海省西宁市职称计算机考试试题,【青海西宁2017年第一批职称计算机考试时间4月8日起】- 环球网校...
  3. 计算机除法和取余在实际运用中的意义
  4. EntLib.com Forum / YAF 开源论坛--源码的目录结构(对分析代码很有帮助)
  5. java 线程缓存_Java 实现缓存,一个线程存,一个线程取
  6. jQuery.ajax()方法
  7. python命令解析_python学习(命令行的解析)
  8. NES专题——一块带给无数人年少欢乐的CPU(6502)
  9. Labelme直接生成灰度图
  10. c语言写定时闹钟程序,定时闹钟C语言程序.doc
  11. aop:aspectj-autoproxy
  12. idea run with coverage异常
  13. 手机屏幕画面实时直播
  14. 【uniapp】canvas画海报保存图片兼容H5和APP
  15. 程序员在上海税前12000的工资,真实发到手能拿到多少?
  16. 网页回拨-Web CallBack
  17. 人工智能的创业“风口”
  18. sa387gr11cl2相当于什么材料,sa387gr11cl2对应国内材质
  19. java 保存gif图片_java gif图片保存处理逻辑
  20. 基于Rocky Linux搭建Windows域控制器

热门文章

  1. linux内核延时:短延迟
  2. 以太网眼图测试整改案例分析
  3. conda创建使用虚拟环境
  4. SQL 常用语句书写格式以及示例
  5. 关于AutoCAD反复弹窗Nonvalid Software Detected的解决办法
  6. 利众讲故事:攻下隔壁女生路由器后,我都做了些什么
  7. Window命令行(转载)
  8. 面向过程和面向对象的设计思想、java类、Java类的定义、java对象、对象的创建和使用、类和对象、变量分类、方法分类、构造方法、方法的重载
  9. Upload-labs Pass-19 /.绕过
  10. 配电室综合监控系统的设计与应用