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

原文链接javascript

在学习js的过程对闭包什么的,理解很差,偶然搜到这篇文章。豁然开朗,随翻译。java


Javacript 中有一系列做用域的概念。对于新的JS的开发人员没法理解这些概念,甚至一些经验丰富的开发者也未必能。这篇文章主要目的帮助理解JavaScript中的一些概念如:scope,closure, this, namespace, function scope, global scope, lexical scope and public/private scope. 但愿从这篇文章中能回答以下的问题:编程

  • 什么是做用域(scope)?
  • 什么是全局(Global)和局部(Local)做用域?
  • 什么是命名空间和做用域的区别?
  • 什么是this关键字且做用域对其的影响?
  • 什么是函数做用域、词汇做用域?
  • 什么是闭包?
  • 什么是公有和私有做用域?
  • 如何理解和建立上述内容?windows

    什么是做用域( Scope)?

在JavaScript中,做用域一般是指代码的上下文(context)。可以定义全局或者局部做用域。理解JavaScript的做用域是编写强健的代码和成为一个好的开发者的前提。你须要掌握在那里获取变量和函数,在那里可以可以改变你的代码上下文的做用域以及如何可以编写快速和可读性强以及便于调试的代码。设计模式

想象做用域很是简单,咱们在做用域A仍是做用域B?数组

什么是全局做用域( Global Scope)?

在写第一行JavaScript代码以前,咱们处在全局做用域中。此时咱们定义一个变量,一般都是全局变量。缓存

// global scopevar name = 'Todd';

全局做用域便是你的好友又是你的噩梦。学习控制做用域很简单,学会后使用全局变量就不会遇到问题(一般为命名空间冲突)。常常会听到大伙说 “全局做用域很差”,可是从没有认真想过为何。不是全局做用域很差,而是使用问题。在建立跨做用域Modules/APIs的时候,咱们必须在不引发问题的状况下使用它们。安全

jQuery('.myClass');

...咱们正在全局做用域中获取jQuery,咱们能够把这种引用称为命名空间。命名空间一般是指做用域中能够交换word,可是其一般引用更高级别的做用域。在上面的例子中,jQuery 在全局做用域中,也称为命名空间。jQuery 做为命名空间定义在全局做用域中,其做为jQuery库的命令空间,库中的全部内容成为命名空间的子项(descendent )。闭包

什么是局部做用域( Local Scope)?

局部做用域一般位于全局做用域后。通常来讲,存在一个全局做用域,每一个函数定义了本身的局部做用域。任何定义于其余函数内部的函数都有一个局部做用域,该局部做用域连接到外部函数。
若是定义了一个函数并在里面建立变量,那么这些变量就是局部变量。例如:app

// 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为局部变量,没有暴露给父做用域,所以出现not defined。

函数做用域

JavaScript 中函数域为最小域范围。for与while循环或者if和switch都不能构建做用域。规则就是,新函数新域。一个建立域的简单示例以下:

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

很是方便的建立新的域和本地变量、函数和对象。

词汇做用域( Lexical Scope)

当遇到一个函数嵌套到另外一函数中,内部函数可以访问外部函数的做用域,那么这种方式叫作词汇做用域(Lexical Socpe)或者闭包,也称为成为静态做用域。最能说明该问题的示例以下:

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

这里只是简单的定义了myOtherFunction,并无调用。这种调用顺序也会影响变量的输出。这里我在另外一控制台中再定义和调用一个函数。

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的引用,可是永远也没法返回变量自己。

做用域链

函数的做用域由做用域链构成。咱们知道,每一个函数能够定义嵌套的做用域,任何内嵌函数都有一个局部做用域链接外部函数。这种嵌套关系咱们能够称为链。域通常由代码中的位置决定。当解释(resolving)一个变量,一般从做用域链的最里层开始,向外搜索,直到发现要寻找的变量、对象或者函数。

闭包(Closures)

闭包和词法域( Lexical Scope)很像。返回函数引用,这种实际应用,是一个能够用来解释闭包工做原理的好例子。在咱们的域内部,咱们能够返回对象,可以被父域使用。

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'

好吧,欺骗你们感情了。在实际状况中可能会遇到以下调用闭包的函数,这样也是行的通的。

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

Angular js 在$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默认是执行最外层的全局对象,windows对象。咱们可以很容易的列举出不一样触发函数绑定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);

发生了什么?咱们建立了一个新的做用域且没有在event handler中触发,因此其获得预期的windows对象。若是想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()改变做用域

有时候,须要根据实际的需求来变化代码的做用域。一个简单的例子,如在循环中如何改变做用域:

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

这里的this并无指向咱们的元素,由于咱们没有触发或者改变做用域。咱们来看看如何改变做用域(看起来咱们是改变做用域,其实咱们是改变调用函数执行的上下文)。

.call() and .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]);}

可以看到刚将数组循环的当前元素经过links[i]传递进去,这改变了函数的做用域,所以this的值变为当前循环的元素。这个时候,若是须要咱们可使用this。咱们既可使用.call()又可使用.apply()来改变域。可是这二者使用仍是有区别的,其中.call(scope, arg1, arg2, arg3)输入单个参数,而.apply(scope, [arg1, arg2])输入数组做为参数。

很是重要,须要注意的事情是.call() or .apply()实际已经已经取代了以下调用函数的方式调用了函数。

myFunction(); // invoke myFunction

可使用.call()来链式调用:

myFunction.call(scope); // invoke myFunction using .call()

.bind()

和上面不同的是,.bind()并不触发函数,它仅仅是在函数触发前绑定值。很是遗憾的是其只在 ECMASCript 5中才引入。咱们都知道,不能像下面同样传递参数给函数引用:

// 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);

函数并没被触发,scope能够被改变,且参数在等着传递。

私有和公开做用域

在许多的编程语言中,存在public和private的做用域,可是在javascript中并不存在。可是在JavaScript中经过闭包来模拟public和private的做用域。

使用JavaScript的设计模式,如Module模式为例。一个建立private的简单方式将函数内嵌到另外一个函数中。如咱们上面掌握的,函数决定scope,经过scope排除全局的scope:

(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

成功的建立了一个私有做用域。那么怎么让函公有呢?有一个很是好的模式(模块模式)容许经过私有和公共做用域以及一个object对象来正确的设定函数做用域。暂且将全局命名空间称为Module,里面包含了全部与模块相关的代码:

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

这儿的return 语句返回了公共的方法,只有经过命名空间才可以被访问到。这就意味着,咱们使用Module 做为咱们的命名空间,其可以包含咱们须要的全部方法。咱们能够根据实际的需求来扩展咱们的模块。

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

那私有方法怎么办呢?许多的开发者采起错误的方式,其将全部的函数都至于全局做用域中,这致使了对全局命名空间污染。 经过函数咱们能避免在全局域中编写代码,经过API调用,保证能够全局获取。下面的示例中,经过建立不返回函数的形式建立私有域。

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

这就意味着publicMethod 可以被调用,而privateMethod 因为私有做用域不能被调用。这些私有做用域函数相似于: helpers, addClass, removeClass, Ajax/XHR calls, Arrays, Objects等。

下面是一个有趣事,相同做用域中的对象只能访问相同的做用域,即便有函数被返回以后。这就意味咱们的public方法可以访问咱们的private方法,这些私有方法依然能够起做用,可是不可以在全局左右域中访问。

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

这提供了很是强大交互性和安全性机制。Javascript 的一个很是重要的部分是安全性,这也是为何咱们不能将全部的函数放在全局变量中,这样作易于被攻击。这里有个经过public和private返回Object对象的例子:

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 () {};})();

当返回匿名对象的时候,经过简单的函数引用赋值,Module能够按照对象的方式来用。

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

Happy scoping!