若有问题,欢迎指教。更多内容请关注 GitHub
做用域是追踪全部变量的方式,是代码的当前上下文以及对变量的访问权限。了解做用域,能够知道变量/函数在何处可访问。javascript
JavaScript
使用词法做用域,这种方法容许做用域嵌套,所以外部做用域包含内部做用域。css
若是一个变量在全部函数或花括号({})以外声明,则它是在全局做用域内定义的前端
全局变量能够在代码的任何地方使用。java
const name = 'sueRimn'; function person () { console.log(name); } console.log(name); // 'sueRimn' person() // 'sueRimn'
虽然能够在全局范围内声明变量,但不建议这样作,由于存在命名冲突的可能性。git
若是使用const
或let
声明变量,那么每当发生名称冲突时,都会抛错。这是不可取的。github
let name = 'sueRimn'; let name = '八至'; // 报错
若是使用var
声明变量,第二个变量会在声明后覆盖第一个变量。这也不可取,由于代码将很难调试。web
var name = 'sueRimn'; var name = '八至'; console.log(name); // '八至'
因此,你应该声明局部变量,而不是全局变量。浏览器
这只适用于web浏览器中的JavaScript。
只在代码的特定部分中可用的变量被认为是在局部做用域中。这些变量也称为局部变量。闭包
在JavaScript
中,有两种局部做用域:函数做用域和块做用域。函数
当在一个大括号({})内声明一个const
或let
变量时,只能在那个大括号内访问这个变量。
{ let name = 'sueRimn'; console.log(name); // 'sueRimn' } console.log(name); // error, name is not defined
块做用域是函数做用域的一个子集,由于函数须要用花括号声明(除非使用带隐式返回的箭头函数)。
在函数中声明变量时,只能在函数中访问该变量,对变量的访问仅限于函数的局部做用域。
function person () { let name = 'sueRimn'; console.log(name); } person(); // 'sueRimn' console.log(name); // 报错 name is not defined
当使用函数声明声明函数时,老是将其提高到当前范围的顶部,如下两种结果是同样的:
person(); // 'sueRimn is beautiful' function person () { console.log('sueRimn is beautiful'); } person(); // 'sueRimn is beautiful'
当使用函数表达式代表时,函数不会提高到当前范围的顶部。
person(); // 报错 person is not defined const person = () =>{ console.log('sueRimn is beautiful'); } person(); // 'sueRimn is beautiful'
因此,尽可能在使用函数以前声明它。
若是分别独立声明函数,即便函数之间能够彼此调用,可是没法访问彼此的变量,由于每一个函数的做用域是独立的。
function name () { const name = 'sueRimn'; } function age () { const age = '22' name() console.log(name); // error name id not defined. }
当在一个函数中定义另外一个函数时,内部函数能够访问外部函数的做用域。函数嵌套也会致使做用域嵌套,做用域嵌套也称为词法做用域或闭包,也成为静态做用域。
可是,外部函数没法访问内部函数的做用域。就像单向玻璃,你在里面能够看见外面,外面的看不见里面。
function person () { let name = 'sueRimn'; function my () { console.log('my name is' + name); } console.log(name); my(); } // 打印结果是: 'sueRimn' 'my name is sueRimn'
JavaScript属于解释型语言,JavaScript的执行分为解释和执行两个阶段:
解释阶段:
执行阶段:
静态做用域是指函数定义决定了函数的做用域。JavaScript采用的是静态做用域。JavaScript解释阶段便会肯定做用域规则,所以做用域在函数定义时就已经肯定了,而不是在函数调用时肯定。
执行上下文是函数执行以前建立的,即在函数执行准备阶段建立好的。
执行上下文最明显的就是this的指向是执行时肯定的,即函数调用决定执行上下文的指向。
由于 JavaScript
采用的是词法做用域(静态做用域),函数定义时肯定本身的做用域做为该函数的属性,做用域没法改变,一直保存至函数销毁。
因此说函数定义时是基于静态做用域的,由于即便函数不调用,其[[scope]]属性也会一直存在,而且保持不变。
每一个上下文都有本身的变量对象,对于全局上下文,它是全局对象自身;对于函数,它是活动对象。
当查找变量对象时,计算机会从当前上下文的变量对象中找,若是找不到,就会从父级上下文也就是层层往上查找,直到全局上下文,到那时还找不到,就会抛出ReferenceError
。
做用域链正是内部上下文全部变量对象的链表,用于变量查询。
函数上下文的做用域链在函数调用时建立的,包含活动对象和这个函数内部的[[scope]]
属性。
由于当函数调用时,会生成执行上下文,此执行上下文的[[scope]]
和定义函数时的[[scope]]
是不一样的,执行上下文的[[scope]]
是在函数定义时的[[scope]]
属性基础上又新增一个当前AO对象构成的。
所以,函数定义时候的[[scope]]
做为函数的属性,函数执行时候的[[scope]]
做为函数执行上下文的属性。
通常状况下,一个做用域链包括父级变量对象(variable object)(做用域链的顶部)、函数自身变量VO和活动对象(activation object)。
当查找标识符的时候,会从做用域链的活动对象部分开始查找,而后(若是标识符没有在活动对象中找到)查找做用域链的顶部,循环往复,就像做用域链那样。
标识符解析过程与函数声明周期相关。
函数周期分为函数建立和函数调用
函数建立
在进入上下文时函数声明放到变量/活动(VO/AO)对象中。
函数调用
进入上下文建立AO/VO以后,上下文的Scope属性(变量查找的一个做用域链)做以下定义:
Scope = AO|VO + [[Scope]]
一个函数对象被调用的时候,会建立一个活动对象(也就是一个对象),对于每个函数的形参,都命名为该活动对象的命名属性,而后将这个活动对象做为此时的做用域链最前端,并将这个函数对象的[[scope]]加入到做用域链中。
闭包与词法做用域直接相关,函数建立时存储做用域,直到到函数销毁都不会改变。
实际上,闭包是由函数以及建立该函数的词法环境组合而成。这个环境包含了这个闭包建立时所能访问的全部局部变量。
闭包容许你从内部函数访问外部函数的做用域。在JavaScript中,每次在函数调用时都会建立闭包。
要使用闭包,就要在一个函数中定义另外一个函数并暴露该内部函数。若要公开一个内部函数,就要将其返回或传递给另外一个函数。即便被外部函数返回以后,内部函数也能够访问外部函数做用域中的变量。
在JavaScript中,闭包是用来保护数据隐私的主要机制。闭包是外部范围和程序其他部分之间的通道。它能够选择公开什么数据,而不公开什么数据。
function person() { let age = 22; return { getAge: function() { return age; }, setAge: function(v) { age = v; } }; } obj = person(); console.log(obj.getAge()); // 22 obj.setAge(22); console.log(obj.getAge()); // 22 obj.setAge("sueRimn"); console.log(obj.getAge()); // sueRimn
这里函数返回了一个有两个函数的对象。由于它们是绑定到局部做用域的对象的属性,因此它们是闭包。经过getAge
和setAge
,能够操做age
属性,但不能直接访问它。
对象不是产生数据隐私的惟一方法。闭包也能够用来建立有状态函数,这些函数的返回值可能会受到其内部状态的影响,好比:
const name = name => () => name;
因为保存了来自外部做用域的数据,因此使用闭包建立迭代器至关容易。
function buildContor(i) { var contor = i; var displayContor = function() { console.log(contor++); contor++; }; return displayContor; } var myContor = buildContor(1); myContor(); // 1 myContor(); // 2 myContor(); // 3 // new closure - new outer scope - new contor variable var myOtherContor = buildContor(10); myOtherContor(); // 10 myOtherContor(); // 11 // myContor was not affected myContor(); // 4
上面的buildContor()
函数其实是一个迭代器,每次调用都建立一个新的迭代器,并使用固定的起始索引,而后在每次连续调用迭代器时,返回下一个值。
每次调用其中一个计数器时,经过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另一个闭包中的变量。
jQuery(或任何JavaScript)中的事件都是闭包。事件处理程序能够访问外部做用域。
$(function() { var contor = 0; $("#Button").click(function() { // 闭包从外部做用域更新变量 contor++; } }
单例对象是在程序执行过程当中只有一个实例的对象。
咱们知道,每次函数调用都会建立一个新的闭包。但若是咱们想阻止外部函数的另外一次调用呢?
很简单:使用匿名函数。
var person = function () { var age = 22; return { get: function () { return "age: " + age; }, increment: function() { age++; } }; }(); // 注意 单例是该函数回调的结果 console.log(person.get()); // age:22 console.log(person.get()); // age:22 person.increment(); console.log(person.get()); // age:23 person.increment(); console.log(person.get()); // age: 24
这个例子与前面惟一的区别是外部函数是匿名的,它没有名字。
咱们声明它并当即调用它,person
对象(即闭包)是访问其做用域的惟一来源。对于确保建立的age
不会有多个做用域是很是有用的。
若是不是某些特定任务须要使用闭包,在其它函数中建立函数是不明智的,由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,因此不能滥用闭包,不然会形成网页的性能问题,在IE中可能致使内存泄露。解决方法是,在退出函数以前,将不使用的局部变量所有删除。
做用域和闭包若是从单向玻璃理解就很容易。
做用域是在函数定义时产生的,在一个函数内定义任何内部函数,其内部函数称为闭包,闭包保留对外部函数中建立的变量的访问权。
参考:Master the JavaScript Interview: What is a Closure?