js的作用域和作用域链

212 阅读10分钟

1.什么是作用域

作用域是你的代码在运行时,某些特定部分中的变量,函数,对象的可访问性,也就是作用域决定了变量和函数的可访问范围,即作用域控制者变量与函数的可见性和声明周期 作用域的主要功能是:

收集并维护所有声名的标识符

依照特定的规则对标识符进行查找

确定当前的代码对标识符的访问权限

function outFun2() {
    var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

上面的代码中 变量inVariable在全局作用域中没有被声明 所以在全局作用域下取值会报错 我们可以这样理解: 作用域就是一个独立的地盘 ,让变量不会外泄 暴漏出去也就是说作用域最大的作用就是隔离变量 ,不同作用域下同名变量不会有冲突

ES6之前js没有块级作用域 只有局部作用域和函数作用域 ES6的出现为我们提供了“块级作用域

可以通过新增的命令 let 和 const 来实现”

2.全局作用域和函数作用域

所谓全局作用域 就是在代码中的任何地方都能访问到的对象拥有全局作用域 一般来说以下情况具有全局作用域

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

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

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

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

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

2.局部作用域

局部作用域一般只在固定代码内可以访问到,最常见的就是函数作用域


2.1函数作用域

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

functon test(){
    var num = 9;
    //内部可以访问
    console.log("test中:" + num);
}
//test外部不能访问
console.log("test外部" + num);

注意:

  • 如果在函数中定义变量时,如果不添加var关键字 造成变量提升 这个变量成为一个全局变量
function doSomeThing(){
    // 在工作中一定避免这样写
    thing = 'writting';
    console.log('内部:'+thing);
}
console.log('外部:'+thing)

  • 任何一对花括号{。。。}中的 语句集都属于一个块 在es6之前 在块语句中定义的便来给你将保留在他已经存在的作用域中:
var name = '程序员成长指男';
for(var i=0; i<5; i++){
    console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4  {}外部:5

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


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

变量提升:

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

var tmp = new Date();
function f() {
    console.log(tmp);
    if(false) {
        var tmp='hello';
    }
}

上面的代码会输出undefined 原因是变量的提升 在这里申明提升了 定义的内容并不会提升 提升后对应的代码如下:

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 = '程序员成长指北';
    console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北


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

// var
var name = 'koloa';
console.log(name); // koala
if(true){
    name = '程序员成长指北';
    console.log(name); // 程序员成长指北
}
console.log(name); // 程序员成长指北


变量和函数同时出现的提升

如果有函数和变量同时声明了 会出现什么情况呢???

看下面的代码:

console.log(foo);
var foo ='i am jiangjiejie';
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';会被忽略
  • 函数申明的优先级优于变量申明,且函数声明会连带定义一起被提升(这里与变量不同)

2.3块级作用域

es6新增了let和const命令,可以用来创建块级作用域变量,使用let命令声名的变量只在let命令所在的代码块内有效

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 1.变量不会提升到代码块顶部 且不允许从外部访问块级作用域内部变量
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 = '程序员成长指北';
    console.log(name); // 程序员成长指北
}

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

3.作用域链

3.1 javascript是如何执行的???

3.1分析阶段

JavaScript编译器编译完成,生成代码后进行分析

  • 分析函数参数
  • 分析变量声名
  • 分析函数声名

分析阶段的核心就是再分析完成后(也就是接下来函数执行阶段的瞬间)会创建一个AO(active Object活动对象)

3.1.2执行阶段

分析阶段成功后,会把ao给执行阶段

  • 引擎询问作用域,作用域中是否有这个叫x的变量
  • 如果作用域有x变量 ,引擎会使用这个变量
  • 如果作用域中没有,引擎会自动寻找(向上层作用域)如果到了最后都没有找到这个变量 引擎会抛出错误。

执行阶段的核心就是找,具体怎么找,后面会讲解lhs查询与RHS查询

3.1.3 JavaScript执行举例说明

function a(age) {
    console.log(age);
    var age = 20
    console.log(age);
    function age() {
    }
    console.log(age);
}
a(18);

首先进入分析阶段 前面已经说到 ,函数运行的瞬间 创建一个AO(active object活动对象)

AO = {}


第一步:分析函数参数

形式参数:AO.age = undefined
实参:AO.age = 18


第二步:分析变量声明:

// 第3行代码有var age
// 但此前第一步中已有AO.age = 18, 有同名属性,不做任何事
即AO.age = 18


第三步:分析函数声明

// 第5行代码有函数age
// 则将function age(){}付给AO.age
AO.age = function age() {}

函数声明注意点:AO上如果有与函数名同名的属性,则会被此函数覆盖,但是下面这种情况声明的函数不会覆盖AO链中同名的属性

var age = function () {
            console.log('25');
        }

进入执行阶段

分析阶段分析成功后,会把给AO(Active Object 活动对象)给执行阶段,引擎会询问作用域,找的过程。所以上面那段代码AO链中最初应该是

AO.age = function age() {}
//之后
AO.age=20
//之后
AO.age=20

输出结果是:

function age(){
    
}
20
20

3.2:作用域链概念

看了前面一个完整的JavaScript函数执行过程,让我们来说下作用域链的概念吧JavaScript上每个函数执行时,会在自己创建的ao上找对应属性值,若找不到则往父函数的ao上找,再找不到则再上一层的ao,知道找到最后的全局作用域,而这一条形成的“ao链”就是JavaScript中的作用域链

3.3找 过程LHS和RLHS查询特殊说明

LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。 RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。

3.3.1LHS和RHS特性

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError 异常
  • 非严格模式下,LHs会比较特殊,会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出Typeerror异常

3.3.2LHS和RHS举例说明

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 时需要查找a和b)

作用域链总结