理解作用域

1.理解引擎、编译器、作用域

传统编译:分词/词法分析、解析/语法分析、代码生成

比起编译步骤只有三部分的语言的编译器,javavscript引擎更加复杂得多,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等

引擎:负责编译器和作用域的调度,并执行编译好的代码。

编译器:负责语法分析及代码生成等脏活累活

作用域:的外表是一对大括号(块作用域)或一个函数(function(){}),负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

理解var a=2;的过程:

1.遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。

2.接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a=2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续向上级作用域查找该变量。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!

2.LHS和RHS

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

LHS(Left-hand Side): 可以理解为获取变量的空间

RHS(Right-hand Side): 可以理解为获取变量的值

JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a=2这样的声明会被分解成两个独立的步骤:

1.首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。

2.接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。

那为什么要区分这两种呢?

LHS和RHS查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。

(1)在代码执行前就已经声明新变量,出现变量提升现象

(2)不成功的LHS会自动隐式地创建一个全局变量,导致不声明也能赋值成功而不报错

3.词法阶段、词法作用域

大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。回忆一下,词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

欺骗词法作用域的方法,这些方法在词法分析器处理过后依然可以修改作用域

欺骗词法方法:eval函数、with关键字

(1) eval函数

JavaScript中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。

function foo(str, a){
    eval(str);
    console.log(a, b);
}
var b = 2
foo("var b = 3", 1)  //1,3

eval(..)调用中的"varb=3;"这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在foo(..)内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名变量。

(2)with关键字

with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

function foo(obj) {
    with (obj){
        a = 2;
    }
}
var o1 = {a: 3};
var o2 = {b: 3};
foo( o1 );
console.log( o1.a );  //2
foo( o2 );
console.log( o2.a );  // undefined
console.log( a ); //2    //不好,a被泄漏到全局作用域上了!

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

4.查找

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。

5.函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。

var a = 2
function foo(){
    var a = 3
    console.log(a)  //3
}
foo()
console.log(a)  //2

函数表达式:就是指将一个函数(一般指匿名函数)赋值给一个变量(注:不存在函数提升)

(1)匿名函数表达式

var a = function (){ };

(2)具名函数表达式

var a = function test (){ };

(3)立即执行函数表达式

(function(a,b){

console.log(a+b) //5

})(2,3)

var a = 2
(function foo(){
    var a = 3
    consoole.log(a) //3
})()
consoole.log(a)  //2

第一个片段中foo被绑定在所在作用域中,可以直接通过foo()来调用它。第二个片段中foo被绑定在函数表达式自身的函数中而不是所在作用域中。

6.块级作用域

(1)var不存在块级作用域

for(var i=0;i<=5;i++){
    console.log(i)
}
console.log(i)  //6

我们在for循环的头部直接定义了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽略了i会被绑定在外部作用域(函数或全局)中的事实。

if(true){
    var b = 1
    console.log(b);  //1
}
console.log(b);  //1

bar变量仅在if声明的上下文中使用,因此如果能将它声明在if块内部中会是一个很有意义的事情。但是,当使用var声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。

块级作用域例子:

with是块作用域的一个例子(块作用域的一种形式),用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。

JavaScript的ES3规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

(2)let关键字

let关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。

varfoo=true;
if(foo){
    let bar=foo*2;
    console.log(bar);  //2
}
console.log(bar);   //ReferenceError

for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

{
    let j;
    for(j=0;j<10;j++){
        let i=j;    //c!
        console.log(i);
    }
}
//每个迭代重新绑定有一定原因,后面闭包可了解到

(3)const关键字

ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。

var foo=true;
if(foo){
    var a=2;
    const b=3;//包含在if中的块作用域常量
    a=3;//正常!
    b=4;//错误!
}
console.log(a);  //3
console.log(b);  //ReferenceError!

7.提升

包括变量和函数在内的所有声明都会在任何代码被执行前(在编译阶段)首先被处理。

(1)变量提升

console.log(a);
var a = 2;
​
//提升后
var a;
console.log(a);  //undefined
a=2;

(2)函数声明,会提升

foo();
function foo(){
    console.log(a);//undefined
    var a=2;
}

(3)函数表达式,不会被提升

foo();  //TypeError
bar();  //ReferenceError
var foo= function bar(){
    //...
};
​
//提升后,可理解为
var foov
foo();  //TypeError
bar();  //ReferenceError
foo= function(){
    //...
};

(4)函数优先提升

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量。

foo(); //1
var foo;
function foo(){
    console.log(1)
}
foo = function(){
    console.log(2)
}
​
//提升后,可理解为
function foo(){
    console.log(1)
}
//var foo 重复声明,被忽略掉
foo(); //1
foo = function(){
    console.log(2)
}

尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值