作用域与作用域链

250 阅读11分钟

作用域

什么是作用域

• 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。

在javascript中有两种作用域
• 全局作用域
• 局部作用域两种。

如果一个变量在函数外面或者大括号{}外声明,那么就定义了一个全局作用域,在ES6之前局部作用域只包含了函数作用域,ES6为我们提供的 块级作用域,也属于局部作用域

全局作用域(Global Scope)

拥有全局作用域的对象可以在代码的任何地方访问到, 在js中一般有以下几种情形拥有全局作用域:

  1. 最外层的函数以及最外层变量:
var globleVariable= 'global';  // 最外层变量
function globalFunc(){         // 最外层函数
    var childVariable = 'global_child';  //函数内变量
    function childFunc(){        // 内层函数
        console.log(childVariable);
    }
    console.log(globleVariable)
}
console.log(globleVariable);  // global
globalFunc();                 // global
console.log(childVariable)   // childVariable is not defined
console.log(childFunc)       // childFunc is not defined

从上面代码中可以看到globleVariable和globalFunc在任何地方都可以访问到, 反之不具有全局作用域特性的变量只能在其作用域内使用。
2. 未定义直接赋值的变量(由于变量提升使之成为全局变量)

function func1(){
    special = 'special_variable';
    var normal = 'normal_variable';
}
func1();
console.log(special);    //special_variable
console.log(normal)     // normal is not defined

虽然我们可以在全局作用域中声明函数以及变量, 使之成为全局变量, 但是不建议这么做,因为这可能会和其他的变量名冲突,一方面如果我们再使用const或者let声明变量, 当命名发生冲突时会报错。

// 变量冲突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared

另一方面如果你使用var申明变量,第二个申明的同样的变量将覆盖前面的,这样会使你的代码很难调试。

var name = 'koala'
var name = 'xiaoxiao'
console.log(name);  // xiaoxiao

局部作用域(Local Scope)

和全局作用于相反,局部作用域一般只能在固定代码片段内可以访问到。最常见的就是函数作用域。

函数作用域

定义在函数中的变量就在函数作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

//全局作用域
function test(){
    var local = 9;
    // 内部可以访问
    console.log("test中:"+local);
}
//test外部不能访问
console.log("test外部:"+local); // error: local is not defined.

上面代码中,在函数体内定义了变量 local,在函数体内是可以访问了,在函数外访问就报错了。
注意点:

  • 如果在函数中定义变量时,如果不添加var关键字,造成变量提升,这个变量成为一个全局变量。
function doSomeThing(){
    // 在工作中一定避免这样写
    thing = 'writting';
    console.log('内部:'+thing);
}
console.log('外部:'+thing) // writting
  • 任何一对花括号{...}中的语句集都属于一个块, 在ES6之前,在块语句中定义的变量将保留在它已经存在的作用域中:
var name = 'nihao';
for(var i=0; i<5; i++){
    console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4  {}外部:5

我们可以看到变量name和变量i是同级作用域。

在ES6块级作用域未讲解之前注意点

变量提升 变量申明是在任意代码执行前处理的,在代码区中任意地方申明变量和在最开始(最上面)的地方申明是一样的,也就是说,看起来一个变量可以在申明之前被使用!这种行为就是所谓的“hoisting”,也就是变量提升,看起来就像变量的申明被自动移动到了函数或全局代码的最顶上。

var tmp = new Date();
function f() {
    console.log(tmp); //undefined
    if(false) {
        var tmp='hello';
    }
}
// 变量提升后
var tmp = new Date();
function f() {
    var tmp;
    console.log(tmp);
    if(false) {
        tmp='hello';
    }
}
f();

console在输出的时候,tmp变量仅仅申明了但未定义。所以输出undefined。虽然能够输出,但是并不推荐这种写法推荐的做法是在申明变量的时候,将所用的变量都写在作用域(全局作用域或函数作用域)的最顶上,这样代码看起来就会更清晰,更容易看出来哪个变量是来自函数作用域的,哪个又是来自作用域链 重复声明

// var
var name = 'koloa';
console.log(name); // koala
if(true){
    var name = 'nihao';
    console.log(name); // nihao
}
console.log(name); // nihao

虽然看起来里面name申明了两次,但上面说了,js的var变量只有全局作用域和函数作用域两种,且申明会被提升,因此实际上name只会在最顶上开始的地方申明一次,var name='程序员成长指北'的申明会被忽略,仅用于赋值。也就是说上面的代码实际上跟下面是一致的。

// var
var name = 'koloa';
    console.log(name); // koala
if(true){
    name = 'nihao';
    console.log(name); // nihao
}
console.log(name); // nihao

变量和函数同时出现的提升 如果有函数和变量同时声明了,会出现什么情况呢?看下面但代码

console.log(foo);
var foo ='i am koala';
function foo(){}

输出结果是function foo(){},也就是函数内容 如果是另外一种形式呢?

console.log(foo);
var foo ='i am koala';
var foo=function (){}

输出结果是undefined
对两种结果进行分析说明:
第一种:函数申明。就是上面第一种,function foo(){}这种形式
另一种:函数表达式。就是上面第二种,var foo=function(){}这种形式
第二种形式其实就是var变量的声明定义,因此上面的第二种输出结果为undefined应该就能理解了。
而第一种函数申明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此第一种形式跟下面的这种方式是等价的!
而第一种函数申明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此第一种形式跟下面的这种方式是等价的!

var foo=function (){}
console.log(foo);
var foo ='i am koala';

原因是:

  • 函数声明被提升到最顶上;
  • 申明只进行一次,因此后面var foo='i am koala'的申明会被忽略。
  • 函数申明的优先级优于变量申明,且函数声明会连带定义一起被提升(这里与变量不同) 接下来讲,在ES6中引入的块级作用域之后的事! 块级作用域ES6 ES6新增了let和const命令,可以用来创建块级作用域变量,使用let命令声明的变量只在let命令所在代码块内有效。 块级作用域特点 • 变量不会提升到代码块顶部且不允许从外部访问块级作用域内部变量
console.log(bar);//抛出`ReferenceErro`异常: 某变量 `is not defined`
let bar=2;
for (let i =0; i<10;i++){
    console.log(i)
}
console.log(i);//抛出`ReferenceErro`异常: 某变量 `is not defined`

其实这个特点带来了许多好处,开发者需要检查代码时候,可以避免在作用域外意外但使用某些变量,而且保证了变量不会被混乱但复用,提升代码的可维护性。就像代码中的例子,一个只在for循环内部使用的变量i不会再去污染整个作用域。
• 不允许反复声明
ES6的let和const不允许反复声明,与var不同

// var
function test(){
    var name = 'koloa';
    var name = 'nihao';
    console.log(name); // 程序员成长指北
}
// let || const
function test2(){
    var name ='koloa';
    let name= 'nihao'; 
    // Uncaught SyntaxError: Identifier 'count' has already been declared
}

看到这里是不是感觉到了块级作用域的出现还是很有必要的。

词法作用域

“词法作用域是作用域的一种工作模型”,作用域有两种工作模型,在JavaScript中的词法作用域是比较主流的一种,另一种动态作用域(比较少的语言在用)。 词法作用域(静态作用域),意思就是函数的作用域在函数定义的时候就确定了,也就是说函数的作用域取决于函数在哪里定义,和函数在哪里调用并无关系。

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar();

假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

作用域链

在讲解作用域链之前先说一下,先了解一下 JavaScript是如何执行的?

执行过程

分析阶段

javascript编译器编译完成,生成代码后进行分析 • 分析函数参数 • 分析变量声明 • 分析函数声明 分析阶段的核心,在分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(Active Object 活动对象) 执行阶段 分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段

  • 引擎询问作用域,作用域中是否有这个叫X的变量
  • 如果作用域有X变量,引擎会使用这个变量
  • 如果作用域中没有,引擎会继续寻找(向上层作用域),如果到了最后都没有找到这个变量,引擎会抛出错误。
    执行阶段的核心就是找,具体怎么找,后面会讲解LHS查询与RHS查询 代码:
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  • 全局上下文的变量对象初始化是全局对象
  • 函数上下文的变量对象初始化只包括 Arguments 对象
  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  • 在代码执行阶段,会再次修改变量对象的属性值

作用域概念

JavaScript上每一个函数执行时,会先在自己创建的AO上找对应属性值。若找不到则往父函数的AO上找,再找不到则再上一层的AO,直到找到大boss:window(全局作用域)。 而这一条形成的“AO链” 就是JavaScript中的作用域链。

找过程LHS和RHS查询特殊说明

LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。在《你不知道的Javascript(上)》也有很清楚的描述。在这里,我想引用freecodecamp上面的回答来解释:
LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。 Learning Javascript, LHS RHS
LHS和RHS特性

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError异常。
  • 非严格模式下,LHR稍微比较特殊: 会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出TypeError异常
    LHS和RHS举例说明 例子来自于《你不知道的Javascript(上)》
function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

直接看引擎在作用域找这个过程: LSH(写入内存)

c=, a=2(隐式变量分配), b=  

RHS(读取内存)

读foo(2), = a, a ,b  
(return a + b 时需要查找ab)  

作用域链总结

最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图解释

作用域链