你想知道的关于JavaScript做用域的一切

原文: https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/javascript

JavaScript中有许多章节是关于scope的,可是对于初学者来讲(甚至是一些有经验的JavaScript开发者),这些有关做用域的章节既不直接也不容易理解. 这篇文章的目的就是为了帮助那些想更深一步学习了解JavaScript做用域的开发者,尤为是当他们听到一些关于做用域的单词的时候, 比如:做用域(scope),闭包(closure),this,命名空间(namespace),函数做用域(function scope),全局做用域(global scope),词法做用域(lexical),公有变量(public scope),私有变量(private scope). 但愿经过这篇文章你能够知道下面这些问题的答案:java

  • 什么是做用域?
  • 什么是全局(局部)做用域?
  • 什么是命名空间,它和做用域有什么不一样?
  • this关键字是什么,做用于又是怎么影响它的?
  • 什么是函数/词法做用域?
  • 什么是闭包?
  • 什么是共有/私有做用域?
  • 我怎么样才可以理解/建立/实践上面全部的状况

什么是做用域?

在JavaScript中,做用域指的是你代码的当前上下文环境.做用域能够被全局或者局部地定义.理解JavaScript的做用域是让你写出稳健的代码而且成为一个更好的开发者的关键. 你将会理解那些变量或者函数是能够访问的,而且有能力去改变你代码的做用域进而有能力去写出运行速度更快,更容易维护,固然调试也很是容易的代码. 别把做用域想的太复杂,那么咱们如今是在A做用域仍是B做用域?git

什么是全局做用域

当你在开始书写JavaScript代码的时候,你所处的做用域就是咱们所说的全局做用域.若是咱们定义了一个变量,那么它就是被全局定义的:github

// global scope
var name = 'Todd';

全局做用域是你最好的朋友也是你最坏的噩梦;学会去掌控你的做用域是容易的,若是你那样作了,你将不会遇到一些关于全局做用域的问题(一般是关于命名空间的冲突). 你也许会常常听到有人在说全局做用域是很差的,可是你历来没有考虑过他们那样说的真正缘由.全局做用域固然没有他们说的那样,相反全局做用域是很好的, 你须要使用它去建立可以在别的做用域访问的模块还有接口(APIs),你要在使用它的优势的同时确保不产生新的问题.编程

不少人之前都使用过jQuery,当你写下下面的代码的时候...设计模式

jQuery('.myClass');

咱们这时就是经过全局做用域来使用jQuery的,咱们能够把这种使用叫作命名空间.有时命名空间就是一个能够用不一样单词来替代的做用域,可是一般指的是最高一级的做用域. 在这个例子中,jQuery是在全局做用域中,因此也是咱们的命名空间.这个jQuery的命名空间是定义在全局做用域上的,它做为这个jQuery库的命名空间, 全部在jQuery库内的东西都是这个命名空间的派生物.数组

什么是局部做用域

局部做用域指的是那些从全局做用域中定义的许多做用域.JavaScript只有一个全局做用域,每个定义的函数都有本身的局部(嵌套)做用域.那些定义在别的函数中的函数有一个局部的做用域, 而且这个做用域是指向外部的函数.缓存

若是我定义了一个函数,而且在里面建立了一些变量,这些变量的做用域就是局部的.安全

把下面的当作一个例子:闭包

// Scope A: Global scope out here
var myFunction = function () { // Scope B: Local scope in here };

任何局部的东西在全局是不可见的,除非这些东西被导出;这句话的意思是这样的,若是我在一个新的做用域里定义了一些函数或者变量的话,这些变量或者函数在当前的做用域以外是不能够访问的. 下面的代码是关于上面所说的那些的一个小例子:

var myFunction = function () { var name = 'Todd'; console.log(name); // Todd }; // Uncaught ReferenceError: name is not defined console.log(name);

变量name是局部的变量,它并无暴露在父做用域上,所以它是没有被定义的.

函数做用域

JavaScript中全部的做用域在建立的时候都只伴随着函数做用域,循环语句像for或者while,条件语句像if或者switch都不可以产生新的做用域. 新的函数 = 新的做用域这就是规则.下面一个简单的例子用来解释做用域的建立:

// Scope A
var myFunction = function () { // Scope B var myOtherFunction = function () { // Scope C }; };

因此说很容易建立新的做用域和局部的变量/函数/对象.

词法做用域

每当你看到一个函数里面存在着另外一个函数,那么内部的函数可以访问外部函数的做用域,这就叫作词法做用域或者闭包;也被认为是静态做用域,下面的代码是最简单的方法再一次去解释咱们所说的内容:

// Scope A
var myFunction = function () { // Scope B var name = 'Todd'; // defined in Scope B var myOtherFunction = function () { // Scope C: `name` is accessible here! }; };

你也许注意到myOtherFunction没有在这里被调用,它只是简单地被定义.固然它的调用顺序也会影响到做用域里面变量的表现, 在这里我定义了myOtherFunction而且在console语句以后调用了它:

var myFunction = function () { var name = 'Todd'; var myOtherFunction = function () { console.log('My name is ' + name); }; console.log(name); myOtherFunction(); // call function }; // Will then log out: // `Todd` // `My name is Todd`

很容易理解和使用词法做用域,任何被定义在它的父做用域上的变量/对象/函数,在做用域链上都是能够访问到的.例如:

var name = 'Todd'; var scope1 = function () { // name is available here var scope2 = function () { // name is available here too var scope3 = function () { // name is also available here! }; }; };

须要记住的一个重要地方是,词法做用域是不可逆的,咱们能够从下面的例子中看到结果:

// name = undefined
var scope1 = function () { // name = undefined var scope2 = function () { // name = undefined var scope3 = function () { var name = 'Todd'; // locally scoped }; }; };

固然咱们能够返回一个指向name的引用,可是永远不会是name变量自己.

做用域链

做用域链为一个给定的函数创建了做用域.就像咱们知道的那样,每个被定义的函数都有它本身嵌套的做用域,而且任何定义在别的函数中的函数都有一个 链接外部函数的局部做用域,这个链接就是咱们所说的做用域链中的链.它经常是在代码中那些可以定义做用域的位置,当咱们访问一个变量的时候, JavaScript从最里面的做用域沿着做用域链向外部开始查找,直到找到咱们想要的那个变量/对象/函数.

闭包

闭包和词法做用域是紧密联系在一块儿的,关于闭包是如何工做的一个好例子就是当咱们返回一个函数的引用的时候,这是一个更实际的用法. 在咱们的做用域里,咱们能够返回一些东西以便这些东西可以在父做用域里被访问和使用:

var sayHello = function (name) { var text = 'Hello, ' + name; return function () { console.log(text); }; };

咱们这里使用的闭包概念使咱们在sayHello的做用域不可以被外部(公共的)做用域访问.单独运行这个函数不会有什么结果由于它只是返回了一个函数:

sayHello('Todd'); // nothing happens, no errors, just silence...

这个函数返回了一个函数,那就意味着咱们须要对它进行赋值,而后对它进行调用:

var helloTodd = sayHello('Todd'); helloTodd(); // will call the closure and log 'Hello, Todd'

好吧,我撒谎了,你也能够直接调用它,你也许以前已经见到过像这样的函数,这种方式也是能够运行你的闭包:

sayHello('Bob')(); // calls the returned function without assignment

AngularJS的$compile方法使用了上面的技术,你能够将当前做用的引用域传递给这个闭包:

$compile(template)(scope);

咱们能够猜想他们关于这个方法的(简化)代码大概是下面这个样子:

var $compile = function (template) { // some magic stuff here // scope is out of scope, though... return function (scope) { // access to `template` and `scope` to do magic with too }; };

固然一个函数没必要有返回值也可以被称为一个闭包.只要可以访问外部变量的一个即时的词法做用域就建立了一个闭包.

做用域和this

每个做用域都绑定了一个不一样值的this,这取决于这个函数是如何调用的.咱们都使用过this关键词,可是并非全部的人都理解它,还有当它被调用的时候是如何的不一样. 默认状况下,this指向的是最外层的全局对象window.咱们能够很容易的展现关于不一样的调用方式咱们绑定的this的值也是不一样的:

var myFunction = function () { console.log(this); // this = global, [object Window] }; myFunction(); var myObject = {}; myObject.myMethod = function () { console.log(this); // this = Object { myObject } }; var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { console.log(this); // this = <nav> element }; nav.addEventListener('click', toggleNav, false);

当咱们处理this的值的时候咱们又遇到了一些问题,举个例子若是我添加一些代码在上面的例子中.就算是在同一个函数内部,做用域和this都是会发生改变的:

var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { console.log(this); // <nav> element setTimeout(function () { console.log(this); // [object Window] }, 1000); }; nav.addEventListener('click', toggleNav, false);

因此这里发生了什么?咱们建立了一个新的做用域,这个做用域没有被咱们的事件处理程序调用,因此默认状况下,这里的this指向的是window对象. 固然咱们能够作一些事情不让这个新的做用域影响咱们,以便咱们可以访问到这个正确的this值.你也许已经见到过咱们这样作的方法了,咱们可使用that变量缓存当前的this值, 而后在新的做用域中使用它.

var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { var that = this; console.log(that); // <nav> element setTimeout(function () { console.log(that); // <nav> element }, 1000); }; nav.addEventListener('click', toggleNav, false);

这是一个小技巧,让咱们可以使用到正确的this值,而且在新的做用域解决一些问题.

使用.call(),.apply()或者.bind()改变做用域

有时,你须要根据你所处理的状况来处理JavaScript的做用域.一个简单的例子展现如何在循环的时候改变做用域:

var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { console.log(this); // [object Window] }

这里的this没有指向咱们须要的元素,咱们不可以在这里使用this调用咱们须要的元素,或者改变循环里面的做用域. 让咱们来思考一下如何可以改变咱们的做用域(好吧,看起来好像是咱们改变了做用域,可是实际上咱们真正作的事情是去改变咱们那个函数的运行上下文).

  • .call()和.apply() .call().apply()函数是很是实用的,它们容许你传递一个做用域到一个函数里面,这个做用与绑定了正确的this值. 让咱们来处理上面的那些代码吧,让循环里面的this指向正确的元素值:

    var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { (function () { console.log(this); }).call(links[i]); }

    你能够看到我是如何作的,首先咱们建立了一个当即执行的函数(新的函数就代表建立了新的做用域), 而后咱们调用了.call()方法,将数组里面的循环元素link[i]当作参数传递给了.call()方法, 而后咱们就改变了哪一个当即执行的函数的做用域.咱们可使用.call()或者.apply()方法,可是它们的不一样之处是参数的传递形式, .call()方法的参数的传递形式是这样的.call(scope, arg1, arg2, arg3),.apply()的参数的传递形式是这样的.apply(scope, [arg1, arg2]).

    因此当你须要改变你的函数的做用域的时候,不要使用下面的方法:

    myFunction(); // invoke myFunction

    而应该是这样,使用.call()去调用咱们的方法

    myFunction.call(scope); // invoke myFunction using .call()
  • .bind() 不像上面的方法,使用.bind()方法不会调用一个函数,它仅仅在函数调用以前,绑定咱们须要的值.就像咱们知道的那样, 咱们不可以给函数的引用传递参数.就像下面这样:

    // works
    nav.addEventListener('click', toggleNav, false); // will invoke the function immediately nav.addEventListener('click', toggleNav(arg1, arg2), false);

    咱们能够解决这个问题,经过在它里面建立一个新的函数:

    nav.addEventListener('click', function () { toggleNav(arg1, arg2); }, false);

    可是这样就改变了做用域,咱们又一次建立了一个不须要的函数,这样作须要花费不少,当咱们在一个循环中绑定事件监听的时候. 这时候就须要.bind()闪亮登场了,由于咱们可使用他来进行绑定做用域,传递参数,而且函数还不会当即执行:

    nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);

    上面的函数没有被当即调用,而且做用域在须要的状况下也会改变,并且函数的参数也是能够经过这个方法传入的.

私有/共有的做用域

在许多编程语言中,你应该听到过私有做用域或者共有做用域,在JavaScript中,是没有这些概念的.固然咱们也能够经过一些手段好比闭包来模拟公共做用域或者是私有做用域.

经过使用JavaScript的设计模式,好比模块模式,咱们能够创造公共做用域和私有做用域.一个简单的方法建立私有做用域就是使用一个函数去包裹咱们本身定义的函数. 就像上面所说的那样,函数建立了一个与全局做用域隔离的一个做用域:

(function () {
  // private scope inside here })();

咱们可能须要为咱们的应用添加一些函数:

(function () {
  var myFunction = function () { // do some stuff here }; })();

可是当咱们去调用位于函数内部的函数的时候,这些函数在外部的做用域是不可获得的:

(function () {
  var myFunction = function () { // do some stuff here }; })(); myFunction(); // Uncaught ReferenceError: myFunction is not defined

成功了,咱们建立了私有的做用域.可是问题又来了,我如何在公共做用域内使用咱们以前定义好的函数?不要担忧,咱们的模块设计模式或者说是提示模块模式, 容许咱们将咱们的函数在公共做用域内发挥做用,它们使用了公共做用域和私有做用域以及对象.在下面我定义了个人全局命名空间,叫作Module, 这个命名空间里包含了与那个模块相关的全部代码:

// define module
var Module = (function () { return { myMethod: function () { console.log('myMethod has been called.'); } }; })(); // call module + methods Module.myMethod();

上面的return声明代表了咱们返回了咱们的public方法,这些方法是能够在全局做用域里使用的,不过须要经过命名空间来调用. 这就代表了咱们的那个模块只是存在于哪一个命名空间中,它能够包含咱们想要的任意多的方法或者变量.咱们也能够按照咱们的意愿来扩展这个模块:

// define module
var Module = (function () { return { myMethod: function () { }, someOtherMethod: function () { } }; })(); // call module + methods Module.myMethod(); Module.someOtherMethod();

那么咱们的私有方法该如何使用以及定义呢?老是有许多的开发者随意的堆砌他们的方法在那个模块里面,这样的作法污染了全局的命名空间. 那些帮助咱们的代码运行而且是没必要要出如今全局做用域的方法,就不要导出在全局做用域中,咱们只导出那些须要在全局做用域内被调用的函数. 咱们能够定义私有的方法,只要不返回它们就行:

var Module = (function () { var privateMethod = function () { }; return { publicMethod: function () { } }; })();

上面的代码意味着,publicMethod是能够在全局的命名空间里调用的,可是privateMethod是不能够的,由于它是在私有的做用域中被定义的. 这些私有的函数方法通常都是一些帮助性的函数,好比addClass,removeClass,Ajax/XHR calls,Arrays,Objects等等.

这里有一些概念须要咱们知道,就是同一个做用域中的函数变量能够访问在同一个做用域中的函数或者变量,甚至是这些函数已经被做为结果返回. 这意味着,咱们的公共函数能够访问咱们的私有函数,因此这些私有的函数是仍然能够运行的,只不过他们不能够在公共的做用域里被访问而已.

var Module = (function () { var privateMethod = function () { }; return { publicMethod: function () { // has access to `privateMethod`, we can call it: // privateMethod(); } }; })();

这容许一个很是强大级别的交互,以及代码的安全;JavaScript很是重要的一个部分就是确保安全.这就是为何咱们不可以把全部的函数都放在公共的做用域内, 由于一旦那样作了就会暴漏咱们系统的漏洞,让一些心怀恶意的人可以对这些漏洞进行攻击.

下面的例子就是返回了一个对象,而后在这个对象上面调用一些公有的方法的例子:

var Module = (function () { var myModule = {}; var privateMethod = function () { }; myModule.publicMethod = function () { }; myModule.anotherPublicMethod = function () { }; return myModule; // returns the Object with public methods })(); // usage Module.publicMethod();

一个比较规范的命名私有方法的约定是,在私有方法的名字前面加上一个下划线,这能够快速的帮助你区分公有方法或者私有方法:

var Module = (function () { var _privateMethod = function () { }; var publicMethod = function () { }; })();

这个约定帮助咱们能够简单地给咱们的函数索引赋值,当咱们返回一个匿名对象的时候:

var Module = (function () { var _privateMethod = function () { }; var publicMethod = function () { }; return { publicMethod: publicMethod, anotherPublicMethod: anotherPublicMethod } })();