JavaScript 有个特性称为做用域。尽管对于不少开发新手来讲,做用域的概念不容易理解,我会尽量地从最简单的角度向你解释它们。理解做用域能让你编写更优雅、错误更少的代码,并能帮助你实现强大的设计模式。javascript
做用域是你的代码在运行时,各个变量、函数和对象的可访问性。换句话说,做用域决定了你的代码里的变量和其余资源在各个区域中的可见性。html
那么,限制变量的可见性,不容许你代码中全部的东西在任意地方均可用的好处是什么?其中一个优点,是做用域为你的代码提供了一个安全层级。计算机安全中,有个常规的原则是:用户只能访问他们当前须要的东西。java
想一想计算机管理员吧。他们在公司各个系统上拥有不少控制权,看起来甚至能够给予他们拥有所有权限的帐号。假设你有一家公司,拥有三个管理员,他们都有系统的所有访问权限,而且一切运转正常。可是忽然发生了一点意外,你的一个系统遭到恶意病毒攻击。如今你不知道这谁出的问题了吧?你这才意识到你应该只给他们基本用户的帐号,而且只在须要时赋予他们彻底的访问权。这能帮助你跟踪变化并记录每一个人的操做。这叫作最小访问原则。眼熟吗?这个原则也应用于编程语言设计,在大多数编程语言(包括 JavaScript)中称为做用域,接下来咱们就要学习它。编程
在你的编程旅途中,你会意识到做用域在你的代码中能够提高性能,跟踪 bug 并减小 bug。做用域还解决不一样范围的同名变量命名问题。记住不要弄混做用域和上下文。它们是不一样的特性。设计模式
在 JavaScript 中有两种做用域数组
当变量定义在一个函数中时,变量就在局部做用域中,而定义在函数以外的变量则从属于全局做用域。每一个函数在调用的时候会建立一个新的做用域。浏览器
当你在文档中(document)编写 JavaScript 时,你就已经在全局做用域中了。JavaScript 文档中(document)只有一个全局做用域。定义在函数以外的变量会被保存在全局做用域中。安全
// the scope is by default global var name = 'Hammad';
全局做用域里的变量可以在其余做用域中被访问和修改。闭包
var name = 'Hammad'; console.log(name); // logs 'Hammad' function logName() { console.log(name); // 'name' is accessible here and everywhere else } logName(); // logs 'Hammad'
定义在函数中的变量就在局部做用域中。而且函数在每次调用时都有一个不一样的做用域。这意味着同名变量能够用在不一样的函数中。由于这些变量绑定在不一样的函数中,拥有不一样做用域,彼此之间不能访问。app
// 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) { // this 'if' conditional block doesn't create a new scope var name = 'Hammad'; // name is still in the global scope } 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) { // this 'if' conditional block doesn't create a scope // name is in the global scope because of the 'var' keyword var name = 'Hammad'; // likes is in the local scope because of the 'let' keyword let likes = 'Coding'; // skills is in the local scope because of the 'const' keyword 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
一个应用中全局做用域的生存周期与该应用相同。局部做用域只在该函数调用执行期间存在。
不少开发者常常弄混做用域和上下文,彷佛二者是一个概念。但并不是如此。做用域是咱们上面讲到的那些,而上下文一般涉及到你代码某些特殊部分中的this值。做用域指的是变量的可见性,而上下文指的是在相同的做用域中的this的值。咱们固然也可使用函数方法改变上下文,这个以后咱们再讨论。在全局做用域中,上下文老是 Window 对象。
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} console.log(this); function logFunction() { console.log(this); } // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} // because logFunction() is not a property of an object 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。
为了解决掉咱们从上面学习中会出现的各类困惑,“执行环境(context)”这个词中的“环境(context)”指的是做用域而并不是上下文。这是一个怪异的命名约定,但因为 JavaScript 的文档如此,咱们只好也这样约定。
JavaScript 是一种单线程语言,因此它同一时间只能执行单个任务。其余任务排列在执行环境中。当 JavaScript 解析器开始执行你的代码,环境(做用域)默认设为全局。全局环境添加到你的执行环境中,事实上这是执行环境里的第一个环境。
以后,每一个函数调用都会添加它的环境到执行环境中。不管是函数内部仍是其余地方调用函数,都会是相同的过程。
每一个函数都会建立它本身的执行环境。
当浏览器执行完环境中的代码,这个环境会从执行环境中弹出,执行环境中当前环境的状态会转移到父级环境。浏览器老是先执行在执行栈顶的执行环境(事实上就是你代码最里层的做用域)。
全局环境只能有一个,函数环境能够有任意多个。
执行环境有两个阶段:建立和执行。
第一阶段是建立阶段,是函数刚被调用但代码并未执行的时候。建立阶段主要发生了 3 件事。
变量对象(Variable Object)也称为活动对象(activation object),包含全部变量、函数和其余在执行环境中定义的声明。当函数调用时,解析器扫描全部资源,包括函数参数、变量和其余声明。当全部东西装填进一个对象,这个对象就是变量对象。
'variableObject': { // contains function arguments, inner variable and function declarations }
在执行环境建立阶段,做用域链在变量对象以后建立。做用域链包含变量对象。做用域链用于解析变量。当解析一个变量时,JavaScript 开始从最内层沿着父级寻找所需的变量或其余资源。做用域链包含本身执行环境以及全部父级环境中包含的变量对象。
'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts }
执行环境能够用下面抽象对象表示:
executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis }
执行环境的第二个阶段就是代码执行阶段,进行其余赋值操做而且代码最终被执行。
词法做用域的意思是在函数嵌套中,内层函数能够访问父级做用域的变量等资源。这意味着子函数词法绑定到了父级执行环境。词法做用域有时和静态做用域有关。
function grandfather() { var name = 'Hammad'; // likes is not accessible here function parent() { // name is accessible here // likes is not accessible here function child() { // Innermost level of the scope chain // name is also accessible here var likes = 'Coding'; } } }
你可能注意到了词法做用域是向前的,意思是子执行环境能够访问name。但不是由父级向后的,意味着父级不能访问likes。这也告诉了咱们,在不一样执行环境中同名变量优先级在执行栈由上到下增长。一个变量和另外一个变量同名,内层函数(执行栈顶的环境)有更高的优先级。
闭包的概念和咱们刚学习的词法做用域紧密相关。当内部函数试着访问外部函数的做用域链(词法做用域以外的变量)时产生闭包。闭包包括它们本身的做用域链、父级做用域链和全局做用域。
闭包不只能访问外部函数的变量,也能访问外部函数的参数。
即便函数已经return,闭包仍然能访问外部函数的变量。这意味着return的函数容许持续访问外部函数的全部资源。
当你的外部函数return一个内部函数,调用外部函数时return的函数并不会被调用。你必须先用一个单独的变量保存外部函数的调用,而后将这个变量当作函数来调用。看下面这个例子:
function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet(); // nothing happens, no errors // the returned function from greet() gets saved in greetLetter greetLetter = greet(); // calling greetLetter calls the returned function from the greet() function greetLetter(); // logs 'Hi Hammad'
值得注意的是,即便在greet函数return后,greetLetter函数仍能够访问greet函数的name变量。若是不使用变量赋值来调用greet函数return的函数,一种方法是使用()两次()(),以下所示:
function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet()(); // logs 'Hi Hammad'
在许多其余编程语言中,你能够经过 public、private 和 protected 做用域来设置类中变量和方法的可见性。看下面这个 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 })();
函数结尾的括号告诉解析器当即执行此函数。咱们能够在其中加入变量和函数,外部没法访问。但若是咱们想在外部访问它们,也就是说咱们但愿它们一部分是公开的,一部分是私有的。咱们可使用闭包的一种形式,称为模块模式(Module Pattern),它容许咱们用一个对象中的公有做用域和私有做用域来划分函数。
模块模式以下所示:
var Module = (function() { function privateMethod() { // do something } return { publicMethod: function() { // can call privateMethod(); } }; })();
Module 的return语句包含了咱们的公共函数。私有函数并无被return。函数没有被return确保了它们在 Module 命名空间没法访问。但咱们的共有函数能够访问咱们的私有函数,方便它们使用有用的函数、AJAX 调用或其余东西。
Module.publicMethod(); // works Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
一种习惯是如下划线做为开始命名私有函数,并返回包含共有函数的匿名对象。这使它们在很长的对象中很容易被管理。向下面这样:
var Module = (function () { function _privateMethod() { // do something } function publicMethod() { // do something } return { publicMethod: publicMethod, } })();
另外一种形式的闭包是当即执行函数表达式(Immediately-Invoked Function Expression,IIFE)。这是一种在 window 上下文中自调用的匿名函数,也就是说this的值是window。它暴露了一个单一全局接口用来交互。以下所示:
(function(window) { // do anything })(this);
Call 和 Apply 函数来改变函数调用时的上下文。这带给你神奇的编程能力(和终极统治世界的能力)。你只须要使用 call 和 apply 函数并把上下文当作第一个参数传入,而不是使用括号来调用函数。函数本身的参数能够在上下文后面传入。
function hello() { // do something... } hello(); // the way you usually call it hello.call(context); // here you can pass the context(value of this) as the first argument hello.apply(context); // here you can pass the context(value of this) as the first argument
.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'); // the way you usually call it introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context // Output: // 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> // Saves a NodeList of all list items on the page in listItems var listItems = document.querySelectorAll('ul li'); // Loops through each of the Node in the listItems NodeList and logs its content 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函数。相应的列表项传入 call 函数,确保控制台输出正确对象的 innerHTML。
对象能够有方法,一样函数对象也能够有方法。事实上,JavaScript 函数有 4 个内置方法:
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 做用域及相关概念有了更好地理解。若是有东西不清楚,能够在评论区提问。
做用域常伴你的代码左右,享受编码!