JS进击之路:做用域

引言

几乎全部的编程语言都有做用域的概念,那做用域到底指的是什么呢?做用域就是编程语言在定义变量时,变量如何存储、变量如何访问的一套规则,不一样的编程语言的规则大同小异,接下来就来看看这套规则是怎么设定的编程

编译原理

在传统编译语言中,在代码执行以前都会有一个编译过程:编程语言

  • 分词/词法分析:将代码语句分解成有意义的代码块,又叫词法单元。
  • 解析/语法分析:将词法单元转换一个逐级嵌套的具备语法规则的树状结构,又叫抽象语法树(AST)
  • 代码生成:解析AST并转化成机器指令

和传统编译语言不太同样,js的编译和执行并非分开执行,大多数状况都是编译过程结束就会马上执行,为了在短期的编译过程内达到较优性能,js引擎较通常编译器更复杂,如今就让来看js的编译过程,简单的以编译var a = 2为例:函数

  • 遇到var a,编译器会询问做用域是否已经有一个该名称的变量存在于同一个做用域的集合中。若是是,编译器会忽略该声明,继续进行编译;不然它会要求做用域在当前做用域的集合中声明一个新的变量a
  • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a=2这个赋值操做。引擎运行时会首先询问做用域,在当前的做用域集合中是否存在一个叫作a的变量。若是否,引擎就会使用这个变量;若是引擎最终找到了a,就会将2赋值给它。不然引擎就会抛出一个异常

词法做用域

做用域通常有两种工做模型,第一种是被大多数编程语言所采用的词法做用域,另一种叫做动态做用域,如Bash脚本采用的就是动态做用域。词法做用域就是定义在词法阶段的做用域,词法做用域是由你在写代码时将变量和块做用域写在哪里来决定的,由变量定义位置决定,而动态做用域则是由变量使用的位置来决定的。下面来看个例子:性能

function foo(a) {
  var b = a * 2;
  function bar(c) {
    console.log(a, b, c)
  }
  bar(b * 3)
}
foo(2)

首先来分析一下这里一共存在几个做用域?设计

  1. 全局做用域,里面存在foo变量
  2. foo函数建立的做用域,里面有a,b,bar变量
  3. bar函数建立的做用域,里面有c变量

接下来再来分析一下变量的查找过程,引擎执行console.log()须要查找a、b、c三个变量的引用,首先从最里面的bar()做用域开始找,引擎没法找到a,所以会再往上到foo()做用域中找,在这里找到了a,中止查找,对于b、c来讲查找过程同样。做用域查找始终从运行时最内层开始查找,逐级向外查找,直到碰见第一个匹配的变量为止。code

函数做用域

函数做用域指的是属于这个函数的所有变量均可以在整个函数的范围内使用及复用,这是你们都知道的定义,可是函数做用域的存在到底有什么用呢?接下来就一块儿看看函数做用域的秒用。对象

隐藏内部实现

隐藏内部实现就是将变量和函数包裹在一个函数的做用域中,达到隐藏的目的,为何要这么作呢?软件设计中有一个很是有名的原则叫最小暴露原则,指最小限度暴露必要内容,而将其余内容都隐藏起来,好比模块或对象的API设计。用函数做用域来包裹变量和函数来达到最小暴露原则,阻止外部直接访问,来看下面的例子:ip

function doSomething(a) {
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
}
  
function doSomethingElse(a) {
  return a - 1;
}
var b;
doSomething( 2 ); // 15”

在这段代码中doSomethingElse和b应该是doSomething内部私有的,可是却被暴露出来,这样会致使以预期以外的形式被使用,产生意料以外的结果,更合理的设计应该是将这些私有的内容隐藏在doSomething内部,例如:作用域

function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }
  var b;
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
}
doSomething( 2 );

这样b和doSomethingElse都没法从doSomething外部访问,可是这样也会存在一些问题,首先在全局做用域中声明了doSomething函数,污染了全局做用,其次,必须经过显示调用才能执行,那么有没有什么办法既不会污染做用域也不须要调用就能够自执行呢?答案就是函数表达式,看下面的例子:编译器

(function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }
  var b;
  b = a + doSomethingElse( a * 2 );
  console.log( b * 3 );
})(2)

首先来看(function doSomething(){})这是一个函数表达式,和函数声明不一样的是用括号包起来的,而后再(function doSomething(a){})()调用传值,这样既能自执行也不会污染做用域,社区给这种用法定义了一个术语:IIFE,表明当即执行函数表达式

块做用域

除JavaScript外不少编程语言都支持块做用域,尽管你可能写过很伪块做用域形式的代码,最多见的就是for循环:

for(var i=0; i<10; i++) {
  console.log(i)
}

写这段代码一般是但愿变量i在循环内部使用,可是实际上i会被绑定到外部做用域中,要确保没有在做用域的其余地方意外使用i,就只能依靠自觉,这时候块级做用域就显得尤其有用,ES6改变了现状,引入了新的let、const关键字,let关键字能够将变量绑定到所在的任意做用域中,也就是let为其声明的变量隐式地建立了做用域:

for(let i=0; i<10; i++) {
  console.log(i)        
}
console.log(i) // ReferenceError

这时候i就只会在for循环的内部有效

总结

这篇文章主要介绍了JS做用域相关的内容。若是有错误或不严谨的地方,欢迎批评指正,若是喜欢,欢迎点赞。

相关文章
相关标签/搜索