[译] 你想晓得的关于 JavaScript 作用域的统统

原文链接: Everything you wanted to know about JavaScript scope
原文作者: Todd Motto
Github: 你想晓得的关于JavaScript作用域的统统(译)

JavaScript中有许多章节是关于scope的,然则关于初学者来讲(以至是一些有履历的JavaScript开发者),这些有关作用域的章节既不直接也不轻易明白.这篇文章的目标就是为了协助那些想更深一步进修相识JavaScript作用域的开发者,尤其是当他们听到一些关于作用域的单词的时刻,比方:作用域(scope),闭包(closure),this,定名空间(namespace),函数作用域(function scope),全局作用域(global scope),词法作用域(lexical),公有变量(public scope),私有变量(private scope).愿望经由过程这篇文章你可以晓得下面这些题目的答案:

  • 什么是作用域?

  • 什么是全局(部份)作用域?

  • 什么是定名空间,它和作用域有什么差别?

  • this症结字是什么,作用于又是怎样影响它的?

  • 什么是函数/词法作用域?

  • 什么是闭包?

  • 什么是共有/私有作用域?

  • 我怎样才可以明白/建立/实践上面一切的状况

什么是作用域?

在JavaScript中,作用域指的是你代码确当前上下文环境.作用域可以被全局或许部份地定义.明白JavaScript的作用域是让你写出妥当的代码而且成为一个更好的开发者的症结.你将会明白那些变量或许函数是可以接见的,而且有才能去转变你代码的作用域进而有才能去写出运转速率更快,更轻易保护,固然调试也异常轻易的代码.别把作用域想的太庞杂,那末我们现在是在A作用域照样B作用域?

什么是全局作用域

当你在最先誊写JavaScript代码的时刻,你所处的作用域就是我们所说的全局作用域.假如我们定义了一个变量,那末它就是被全局定义的:

// 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
  }
})();
    原文作者:dreamapplehappy
    原文地址: https://segmentfault.com/a/1190000005807487
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞