作用域

LHSRHS引用

LHSRHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是"= 赋值操作符的左侧或右侧"。赋值操作还有其他几种形式,因此在概念上最好将其理解为"赋值操作的目标是谁(LHS)"以及"谁是赋值操作的源头(RHS)"。

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

/*
  LHS查询(3处) --> c = ...、a = 2(传参隐式变量分配)、b = ...
  RHS查询(4处) --> foo(2..、 = a、函数内部return的a、b
*/

作用域嵌套与异常

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

作用域链
  • LHSRHS引用都会在当前作用域进行查找,如果没有找到,就会坐电梯似的前往上一层作用域,如果还是没有找到就继续向上
  • 一旦抵达全局作用域,可能找到所需变量,也可能没找到,但是不管结果如何,查找过程到此停止
  • RHS在作用域中找不到所需的变量(未声明),引擎就会抛出ReferenceError异常
  • LHS在作用域中找不到所需的变量,非严格模式会在全局作用域中创建一个该名称的变量返回给引擎,严格模式回抛出同RHS查询类似的ReferenceError异常
  • 如果RHS查询到了所需变量,但是尝试对变量的值进行不合理的操作,比如对非函数就行函数调用,或者引用null或者undefined类型值的属性,引擎会抛出TypeError异常
  • var a
    var number = 1
    var object = null
    
    function fn1(){    
      var b = 1
      console.log(b) // 1 在当前作用域找到了变量
    }
        
    function fn2(){
      console.log(a) // undefined 在当前作用域没找到变量,但是在全局作用域找到了对应变量
    }
        
    function fn3(){
      console.log(d) // ReferenceError
    }
    
    function fn4(){
      console.log(number()) // TypeError 能找到number变量,但是number不是函数类型
      console.log(a.x) // TypeError a的值为undefined
      console.log(object.x)  // TypeError object的值为null
    }
    
  • ReferenceError表示作用域判别失败相关,TypeError则表示作用域判别成了了,但是对结果的操作是非法的

词法作用域

遮蔽效应

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,还有一种叫动态作用域(Bash,Perl),动态作用域只关心函数在何处调用,也就是动态作用域链是基于调用栈的,而不是代码中的作用域嵌套。

作用域在查找到第一个匹配的标识符时停止。在嵌套的作用中,可以定义同名的标识符,这叫做"遮蔽效应",抛开"遮蔽效应",作用域查找都是从运行时所处的最内部作用域开始,逐级向上查找,直到找到匹配的第一个标识符为止

全局变量会自动成为全局对象的属性(浏览器中的window对象),全局对象可以访问被遮蔽的全局变量,非全局变量被遮蔽了,外部是访问不到的

var a = 1
function fn1(){
  var a = 2
  console.log(a) // 2 全局作用域的变量a被当前作用域的变量a遮蔽了
  function fn2(){
    var a = 3
    console.log(a) // 3 同上
  }
  fn2()
}

function fn3(){
  b = 4
}

fn1()
fn3()
console.log(b) 或者 console.log(window.b) // 4 虽然变量b是在fn3的内部,但是它是全局变量(window的属性),所以,可以在外部访问

欺骗词法与性能

evalwith可以实现作用域欺骗(运行时来修改作用域),严格模式下eval被部分禁用,with被完全禁用。不推荐使用

// 非严格模式
function foo(str, a) {
  eval(str) // 欺骗!
  console.log( a, b )
}
var b = 2
foo( 'var b = 3', 1 ) // 1, 3

evalwith的使用,使JavaScript引擎没办法做词法的静态分析,无法预先确定变量和函数的定义位置,所以,JavaScript引擎的编译优化,对于evalwith来说,就变得毫无意义,因此,大量使用evalwith,程序运行都会变慢,影响性能

函数作用域与块作用域

函数作用域

函数都具有自己的作用域,外部作用域对函数内部作用域的变量进行访问会报错

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  • 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用。
  • 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名

块作用域

除了函数作用域之外,还有一种块作用域,使创建的作用域仅作用在当前块中,避免使其泄漏到外部作用域中

withtry/catchletconst都可以将变量绑定到所在的块作用域中(通常是 { .. } 内部)

try{
  undefined() // 执行一个非法操作
}catch(error){
  console.log(error) // 正常执行
}
console.log(error) // ReferenceError

var foo = true
if(foo){
  // const同理,同时const创建的是固定的常量,任何修改都会引起错误
  let bar = !foo  
  console.log(bar) // false
}
console.log(bar) // ReferenceError

提升

引擎会 在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。因此,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

console.log(a) // undefined
var a = 2

上面代码等同于下面的流程

var a
console.log( a)
a = 2

你可能感兴趣的:(作用域)