简介

JavaScript 有个特性称为作用域。尽管对于很多开发新手来说,作用域的概念不容易理解,我会尽可能地从最简单的角度向你解释它们。理解作用域能让你编写更优雅、错误更少的代码,并能帮助你实现强大的设计模式。

什么是作用域?

作用域是你的代码在运行时,各个变量、函数和对象的可访问性。换句话说,作用域决定了你的代码里的变量和其他资源各个区域中的可见性。

为什么需要作用域?最小访问原则

那么,限制变量的可见性,不允许你代码中所有的东西在任意地方都可用的好处是什么?其中一个优势,是作用域为你的代码提供了一个安全层级。计算机安全中,有个常规的原则是:用户只能访问他们当前需要的东西。

想想计算机管理员吧。他们在公司各个系统上拥有很多控制权,看起来甚至可以给予他们拥有全部权限的账号。假设你有一家公司,拥有三个管理员,他们都有系统的全部访问权限,并且一切运转正常。但是突然发生了一点意外,你的一个系统遭到恶意病毒攻击。现在你不知道这谁出的问题了吧?你这才意识到你应该只给他们基本用户的账号,并且只在需要时赋予他们完全的访问权。这能帮助你跟踪变化并记录每个人的操作。这叫做最小访问原则。眼熟吗?这个原则也应用于编程语言设计,在大多数编程语言(包括 JavaScript)中称为作用域,接下来我们就要学习它。

在你的编程旅途中,你会意识到作用域在你的代码中可以提升性能,跟踪 bug 并减少 bug。作用域还解决不同范围的同名变量命名问题。记住不要弄混作用域和上下文。它们是不同的特性。

JavaScript中的作用域

在 JavaScript 中有两种作用域

  • 全局作用域
  • 局部作用域

当变量定义在一个函数中时,变量就在局部作用域中,而定义在函数之外的变量则从属于全局作用域。每个函数在调用的时候会创建一个新的作用域。

全局作用域

当你在文档中(document)编写 JavaScript 时,你就已经在全局作用域中了。JavaScript 文档中(document)只有一个全局作用域。定义在函数之外的变量会被保存在全局作用域中。

  1. // the scope is by default global
  2. var name = 'Hammad';

  

全局作用域里的变量能够在其他作用域中被访问和修改。

  1. var name = 'Hammad';
  2.  
  3. console.log(name); // logs 'Hammad'
  4.  
  5. function logName() {
  6. console.log(name); // 'name' is accessible here and everywhere else
  7. }
  8.  
  9. logName(); // logs 'Hammad'

  

局部作用域

定义在函数中的变量就在局部作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

  1. // Global Scope
  2. function someFunction() {
  3. // Local Scope ##1
  4. function someOtherFunction() {
  5. // Local Scope ##2
  6. }
  7. }
  8.  
  9. // Global Scope
  10. function anotherFunction() {
  11. // Local Scope ##3
  12. }
  13. // Global Scope

块语句

块级声明包括if和switch,以及for和while循环,和函数不同,它们不会创建新的作用域。在块级声明中定义的变量从属于该块所在的作用域。

  1. if (true) {
  2. // this 'if' conditional block doesn't create a new scope
  3. var name = 'Hammad'; // name is still in the global scope
  4. }
  5.  
  6. console.log(name); // logs 'Hammad'

  

ECMAScript 6 引入了let和const关键字。这些关键字可以代替var。

  1. var name = 'Hammad';
  2.  
  3. let likes = 'Coding';
  4. const skills = 'Javascript and PHP';

  

和var关键字不同,let和const关键字支持在块级声明中创建使用局部作用域。

  1. if (true) {
  2. // this 'if' conditional block doesn't create a scope
  3.  
  4. // name is in the global scope because of the 'var' keyword
  5. var name = 'Hammad';
  6. // likes is in the local scope because of the 'let' keyword
  7. let likes = 'Coding';
  8. // skills is in the local scope because of the 'const' keyword
  9. const skills = 'JavaScript and PHP';
  10. }
  11.  
  12. console.log(name); // logs 'Hammad'
  13. console.log(likes); // Uncaught ReferenceError: likes is not defined
  14. console.log(skills); // Uncaught ReferenceError: skills is not defined

  

一个应用中全局作用域的生存周期与该应用相同。局部作用域只在该函数调用执行期间存在。

上下文

很多开发者经常弄混作用域和上下文,似乎两者是一个概念。但并非如此。作用域是我们上面讲到的那些,而上下文通常涉及到你代码某些特殊部分中的this值。作用域指的是变量的可见性,而上下文指的是在相同的作用域中的this的值。我们当然也可以使用函数方法改变上下文,这个之后我们再讨论。在全局作用域中,上下文总是 Window 对象。

  1. // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
  2. console.log(this);
  3.  
  4. function logFunction() {
  5. console.log(this);
  6. }
  7. // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
  8. // because logFunction() is not a property of an object
  9. logFunction();

如果作用域定义在一个对象的方法中,上下文就是这个方法所在的那个对象

  1. class User {
  2. logName() {
  3. console.log(this);
  4. }
  5. }
  6.  
  7. (new User).logName(); // logs User {}

(new User).logName()是创建对象关联到变量并调用logName方法的一种简便形式。通过这种方式你并不需要创建一个新的变量。

你可能注意到一点,就是如果你使用new关键字调用函数时上下文的值会有差异。上下文会设置为被调用的函数的实例。考虑一下上面的这个例子,用new关键字调用的函数。

  1. function logFunction() {
  2. console.log(this);
  3. }
  4.  
  5. new logFunction(); // logs logFunction {}

  

当在严格模式(strict mode)中调用函数时,上下文默认是 undefined。

执行环境

为了解决掉我们从上面学习中会出现的各种困惑,“执行环境(context)”这个词中的“环境(context)”指的是作用域而并非上下文。这是一个怪异的命名约定,但由于 JavaScript 的文档如此,我们只好也这样约定。

JavaScript 是一种单线程语言,所以它同一时间只能执行单个任务。其他任务排列在执行环境中。当 JavaScript 解析器开始执行你的代码,环境(作用域)默认设为全局。全局环境添加到你的执行环境中,事实上这是执行环境里的第一个环境。

之后,每个函数调用都会添加它的环境到执行环境中。无论是函数内部还是其他地方调用函数,都会是相同的过程。

每个函数都会创建它自己的执行环境。

当浏览器执行完环境中的代码,这个环境会从执行环境中弹出,执行环境中当前环境的状态会转移到父级环境。浏览器总是先执行在执行栈顶的执行环境(事实上就是你代码最里层的作用域)。

全局环境只能有一个,函数环境可以有任意多个。
执行环境有两个阶段:创建和执行。

创建阶段

第一阶段是创建阶段,是函数刚被调用但代码并未执行的时候。创建阶段主要发生了 3 件事。

  • 创建变量对象
  • 创建作用域链
  • 设置上下文(this)的值

变量对象

变量对象(Variable Object)也称为活动对象(activation object),包含所有变量、函数和其他在执行环境中定义的声明。当函数调用时,解析器扫描所有资源,包括函数参数、变量和其他声明。当所有东西装填进一个对象,这个对象就是变量对象。

  1. variableObject': {
  2. // contains function arguments, inner variable and function declarations
  3. }

  

作用域链

在执行环境创建阶段,作用域链在变量对象之后创建。作用域链包含变量对象。作用域链用于解析变量。当解析一个变量时,JavaScript 开始从最内层沿着父级寻找所需的变量或其他资源。作用域链包含自己执行环境以及所有父级环境中包含的变量对象。

  1. 'scopeChain': {
  2. // contains its own variable object and other variable objects of the parent execution contexts
  3. }

执行环境对象

执行环境可以用下面抽象对象表示:

  1. executionContextObject = {
  2. 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
  3. 'variableObject': {}, // contains function arguments, inner variable and function declarations
  4. 'this': valueOfThis
  5. }

代码执行阶段

执行环境的第二个阶段就是代码执行阶段,进行其他赋值操作并且代码最终被执行。

词法作用域

词法作用域的意思是在函数嵌套中,内层函数可以访问父级作用域的变量等资源。这意味着子函数词法绑定到了父级执行环境。词法作用域有时和静态作用域有关。

  1. function grandfather() {
  2. var name = 'Hammad';
  3. // likes is not accessible here
  4. function parent() {
  5. // name is accessible here
  6. // likes is not accessible here
  7. function child() {
  8. // Innermost level of the scope chain
  9. // name is also accessible here
  10. var likes = 'Coding';
  11. }
  12. }
  13. }

你可能注意到了词法作用域是向前的,意思是子执行环境可以访问name。但不是由父级向后的,意味着父级不能访问likes。这也告诉了我们,在不同执行环境中同名变量优先级在执行栈由上到下增加。一个变量和另一个变量同名,内层函数(执行栈顶的环境)有更高的优先级。

闭包

闭包的概念和我们刚学习的词法作用域紧密相关。当内部函数试着访问外部函数的作用域链(词法作用域之外的变量)时产生闭包。闭包包括它们自己的作用域链、父级作用域链和全局作用域。

闭包不仅能访问外部函数的变量,也能访问外部函数的参数。

即使函数已经 return,闭包仍然能访问外部函数的变量。这意味着 return 的函数允许持续访问外部函数的所有资源。

当你的外部函数 return 一个内部函数,调用外部函数时 return 的函数并不会被调用。你必须先用一个单独的变量保存外部函数的调用,然后将这个变量当做函数来调用。看下面这个例子:

  1. function greet() {
  2. name = 'Hammad';
  3. return function () {
  4. console.log('Hi ' + name);
  5. }
  6. }
  7.  
  8. greet(); // nothing happens, no errors
  9.  
  10. // the returned function from greet() gets saved in greetLetter
  11. greetLetter = greet();
  12.  
  13. // calling greetLetter calls the returned function from the greet() function
  14. greetLetter(); // logs 'Hi Hammad'

  

值得注意的是,即使在greet函数return后,greetLetter函数仍可以访问greet函数的name变量。如果不使用变量赋值来调用greet函数return的函数,一种方法是使用()两次()(),如下所示:

  1. function greet() {
  2. name = 'Hammad';
  3. return function () {
  4. console.log('Hi ' + name);
  5. }
  6. }
  7.  
  8. greet()(); // logs 'Hi Hammad'

共有作用域和私有作用域

在许多其他编程语言中,你可以通过 public、private 和 protected 作用域来设置类中变量和方法的可见性。看下面这个 PHP 的例子

  1. // Public Scope
  2. public $property;
  3. public function method() {
  4. // ...
  5. }
  6.  
  7. // Private Sccpe
  8. private $property;
  9. private function method() {
  10. // ...
  11. }
  12.  
  13. // Protected Scope
  14. protected $property;
  15. protected function method() {
  16. // ...
  17. }

将函数从公有(全局)作用域中封装,使它们免受攻击。但在 JavaScript 中,没有 共有作用域和私有作用域。然而我们可以用闭包实现这一特性。为了使每个函数从全局中分离出去,我们要将它们封装进如下所示的函数中:

  1. (function () {
  2. // private scope
  3. })();

函数结尾的括号告诉解析器立即执行此函数。我们可以在其中加入变量和函数,外部无法访问。但如果我们想在外部访问它们,也就是说我们希望它们一部分是公开的,一部分是私有的。我们可以使用闭包的一种形式,称为模块模式(Module Pattern),它允许我们用一个对象中的公有作用域和私有作用域来划分函数。

模块模式

模块模式如下所示:

  1. var Module = (function() {
  2. function privateMethod() {
  3. // do something
  4. }
  5.  
  6. return {
  7. publicMethod: function() {
  8. // can call privateMethod();
  9. }
  10. };
  11. })();

Module 的return语句包含了我们的公共函数。私有函数并没有被 return。函数没有被 return 确保了它们在 Module 命名空间无法访问。但我们的共有函数可以访问我们的私有函数,方便它们使用有用的函数、AJAX 调用或其他东西。

  1. Module.publicMethod(); // works
  2. Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined

一种习惯是以下划线作为开始命名私有函数,并返回包含共有函数的匿名对象。这使它们在很长的对象中很容易被管理。向下面这样:

  1. var Module = (function () {
  2. function _privateMethod() {
  3. // do something
  4. }
  5. function publicMethod() {
  6. // do something
  7. }
  8. return {
  9. publicMethod: publicMethod,
  10. }
  11. })();

立即执行函数表达式(IIFE)

另一种形式的闭包是立即执行函数表达式(Immediately-Invoked Function Expression,IIFE)。这是一种在 window 上下文中自调用的匿名函数,也就是说this的值是window。它暴露了一个单一全局接口用来交互。如下所示:

  1. (function(window) {
  2. // do anything
  3. })(this);

使用 .call(), .apply() 和 .bind() 改变上下文

Call 和 Apply 函数来改变函数调用时的上下文。这带给你神奇的编程能力(和终极统治世界的能力)。你只需要使用 call 和 apply 函数并把上下文当做第一个参数传入,而不是使用括号来调用函数。函数自己的参数可以在上下文后面传入。

  1. function hello() {
  2. // do something...
  3. }
  4.  
  5. hello(); // the way you usually call it
  6. hello.call(context); // here you can pass the context(value of this) as the first argument
  7. hello.apply(context); // here you can pass the context(value of this) as the first argument

.call()和.apply()的区别是 Call 中其他参数用逗号分隔传入,而 Apply 允许你传入一个参数数组。

  1. function introduce(name, interest) {
  2. console.log('Hi! I'm '+ name +' and I like '+ interest +'.');
  3. console.log('The value of this is '+ this +'.')
  4. }
  5.  
  6. introduce('Hammad', 'Coding'); // the way you usually call it
  7. introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt
  8. introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context
  9.  
  10. // Output:
  11. // Hi! I'm Hammad and I like Coding.
  12. // The value of this is [object Window].
  13. // Hi! I'm Batman and I like to save Gotham.
  14. // The value of this is [object Window].
  15. // Hi! I'm Bruce Wayne and I like businesses.
  16. // The value of this is Hi.

Call 比 Apply 的效率高一点。

下面这个例子列举文档中所有项目,然后依次在控制台打印出来。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Things to learn</title>
  6. </head>
  7. <body>
  8. <h1>Things to Learn to Rule the World</h1>
  9. <ul>
  10. <li>Learn PHP</li>
  11. <li>Learn Laravel</li>
  12. <li>Learn JavaScript</li>
  13. <li>Learn VueJS</li>
  14. <li>Learn CLI</li>
  15. <li>Learn Git</li>
  16. <li>Learn Astral Projection</li>
  17. </ul>
  18. <script>
  19. // Saves a NodeList of all list items on the page in listItems
  20. var listItems = document.querySelectorAll('ul li');
  21. // Loops through each of the Node in the listItems NodeList and logs its content
  22. for (var i = 0; i < listItems.length; i++) {
  23. (function () {
  24. console.log(this.innerHTML);
  25. }).call(listItems[i]);
  26. }
  27.  
  28. // Output logs:
  29. // Learn PHP
  30. // Learn Laravel
  31. // Learn JavaScript
  32. // Learn VueJS
  33. // Learn CLI
  34. // Learn Git
  35. // Learn Astral Projection
  36. </script>
  37. </body>
  38. </html>

HTML文档中仅包含一个无序列表。JavaScript 从 DOM 中选取它们。列表项会被从头到尾循环一遍。在循环时,我们把列表项的内容输出到控制台。

输出语句包含在由括号包裹的函数中,然后调用call函数。相应的列表项传入 call 函数,确保控制台输出正确对象的 innerHTML。

对象可以有方法,同样函数对象也可以有方法。事实上,JavaScript 函数有 4 个内置方法:

  1. Function.prototype.apply()
  2. Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
  3. Function.prototype.call()
  4. Function.prototype.toString()

Function.prototype.toString()返回函数代码的字符串表示。

到现在为止,我们讨论了.call()、.apply()和toString()。与 Call 和 Apply 不同,Bind 并不是自己调用函数,它只是在函数调用之前绑定上下文和其他参数。在上面提到的例子中使用 Bind:

  1. (function introduce(name, interest) {
  2. console.log('Hi! I'm '+ name +' and I like '+ interest +'.');
  3. console.log('The value of this is '+ this +'.')
  4. }).bind(window, 'Hammad', 'Cosmology')();
  5.  
  6. // logs:
  7. // Hi! I'm Hammad and I like Cosmology.
  8. // The value of this is [object Window].

Bind 像call函数一样用逗号分隔其他传入参数,不像apply那样用数组传入参数。

结论

这些概念是 JavaScript 的基础,如果你想钻研更深的话,理解这些很重要。我希望你对 JavaScript 作用域及相关概念有了更好地理解。如果有东西不清楚,可以在评论区提问。

作用域常伴你的代码左右,享受编码!

前端技术书箱列表: https://github.com/jobbole/awesome-web-dev-books

原文地址:http://web.jobbole.com/91134/

理解 JavaScript 作用域(转)的更多相关文章

  1. 深入理解javascript作用域系列第一篇——内部原理

    × 目录 [1]编译 [2]执行 [3]查询[4]嵌套[5]异常[6]原理 前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域 ...

  2. 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

    × 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...

  3. 深入理解javascript作用域系列第四篇——块作用域

    × 目录 [1]let [2]const [3]try 前面的话 尽管函数作用域是最常见的作用域单元,也是现行大多数javascript最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用 ...

  4. 深入理解javascript作用域系列第三篇——声明提升(hoisting)

    × 目录 [1]变量 [2]函数 [3]优先 前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javasc ...

  5. JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望 ...

  6. JavaScript 开发进阶:理解 JavaScript 作用域和作用域链(转载 学习中。。。)

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望 ...

  7. 深入理解JavaScript作用域和作用域链

    前言 JavaScript 中有一个被称为作用域(Scope)的特性.虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,本文我会尽我所能用最简单的方式来解释作用域和作用域链,希望大家有所收获! ...

  8. 深入理解javascript作用域系列第三篇

    前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javascript作用域系列第三篇——声明提升(hois ...

  9. 深入理解javascript作用域系列第二篇

    前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作 ...

  10. 深入理解javascript作用域系列第一篇

    前面的话 javascript拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域.作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域的原 ...

随机推荐

  1. Kudu的Using Apache Kudu with Apache Impala(官网推荐的步骤)

    不多说,直接上干货! http://kudu.apache.org/docs/kudu_impala_integration.html http://blog.csdn.net/lovebyz/art ...

  2. http三次握手四次挥手

    最近一直忙于看前端vue相关内容,后端相关内容没有跟进,文章停了3周,,,哎,还是懒吧!子曰生命在于运动,该学习还是要学的,文章嘛也还是要整理滴,不扯了--- 参考: https://blog.csd ...

  3. 虚拟机复制操作CentOS6导致eth0转为eth0以至于网络服务启动失败的解决方案

    CentOS6复制到虚拟机中eth0转为了eth1 原因: CentOS6硬盘上的信息保存着操作系统的信息, 将该硬盘上的信息拷贝到另一开新的硬盘上时, 硬件的环境就会一定会发生变化, 就那网卡来首, ...

  4. GET 和 POST详解

    什么是 HTTP? 超文本传输协议(HTTP)的设计目的是保证客户机与服务器之间的通信. HTTP 的工作方式是客户机与服务器之间的请求-应答协议. web 浏览器可能是客户端,而计算机上的网络应用程 ...

  5. 利用jquery给指定的table动态添加一行、删除一行,复制,值不重复等操作

    $("#mytable tr").find("td:nth-child(1)") 1表示获取每行的第一列$("#mytable tr").f ...

  6. ORACLE 查看表空间

    select tablespace_name, file_id, file_name,round(bytes/(1024*1024),0) total_spacefrom dba_data_files ...

  7. JDBC之Java连接mysql实现增删改查

    使用软件:mysql.eclipse 链接步骤: 1.注册驱动 2.创建一个连接对象 3.写sql语句 4.执行sql语句并返回一个结果或者结果集 5.关闭链接(一般就是connection.stat ...

  8. maven课程 项目管理利器-maven 3-1 maven常用的构建命令

    mvn -v 查看mvn版本 mvn compile  编译 mvn test 测试 mvn package 编译,打包(这个命令会在你的项目路径下一个target目录,并且拥有compile命令的功 ...

  9. 计算Sn

    求Sn=a+aa+aaa+…+aa…aaa(有n个a)之值,其中a是一个数字. 例如:2+22+222+2222+22222(n=5), 输入 输入两个数.第一个为a ,第二个为n(表示有多少个数相加 ...

  10. js获取url的参数和值的N种有效方法

    js获取url的参数和值的N种有效方法 function getParameterByName(name) { name = name.replace(/[\[]/, "\\\[" ...