JS作用域与作用域链总结

本来以为自己对JS的作用域已经理解了,但是最近碰到面试和面试题,突然才发现自己对JS的作用域及作用域链理解的还不是那么透彻。重新学习了一下,再做总结。

一、函数作用域

先看一小段代码:

var scope="global";
function t(){
    console.log(scope);
    var scope="local";
    console.log(scope);
}
t();

PS:console是浏览器调试工具提供的对象,可以打印输出一些信息。

第一句输出的是: "undefined",而不是 "global";第二讲输出的是:"local"

你可能会认为第一句会输出:"global",因为代码还没执行var scope="local",所以肯定会输出“global"。

有这种想法是理所当然的,毕竟 JS 的执行也是逐条执行。但是,JS 中 var 有变量提升效果!什么意思呢,就是说:在某个函数体里定义变量时,这个变量会在函数体内的代码执行前预先定义!

也就是说,在执行第一条 console.log() 的时候,t 函数内的变量 scope 已经定义了!

所以上述代码可以重写如下:

var scope="global";
function t(){
    var scope;
    console.log(scope);
    scope="local";
    console.log(scope);
}
t();

由于 JS  中 var 变量提升效果,局部变量在整个函数体始终是由定义的,我们可以将变量声明“提前”到函数体顶部,同时变量初始化还在原来位置。

为什么说 JS 没有块级作用域呢,有以下代码为证:

var name="global";
if(true){
    var name="local";
    console.log(name);
}
console.log(name);

结果输出都是是“local”。

如果有块级作用域,明显 if 语句将创建局部变量 name,并不会修改全局变量 name,可是没有这样,因为 JS 中没有块级作用域。(ES6 中新增了 let 变量声明,可以是变量的作用域限制在 {} 中,也就相当于实现了块级作用域)

所以下面的代码也就很好理解了。

function t(flag){
    if(flag){
        var s="ifscope";
        for(var i=0; i < 2; i++);
    }
    console.log(i);
    console.log(s);
}
t(true);

输出结果:2、"ifscope"

二、变量作用域

还是首先看一段代码:

function t(flag){
    if(flag){
        s="ifscope";
        for(var i = 0; i<2; i++);
    }
    console.log(i);
}
t(true);
console.log(s);

就是上面的翻版,知识将声明s中的var去掉。

程序会报错还是输出 "ifscope" 呢?

结果是会输出:"ifscope"

这主要是 JS 中没有用 var 声明的变量都会变成全局变量,也就是顶层对象的属性。

所以你用 console.log(window.s) 也是会输出 "ifconfig"。

当使用 var 声明一个变量时,创建的这个属性是不可配置的,也就是说无法通过delete运算符删除。

var name = 1; // 不可删除
sex = "girl"; // 可删除
this.age = 22; // 可删除

三:作用域链

先来看一段代码:

name="lwy";
function t(){
    var name="tlwy";
    function s(){
        var name="slwy";
        console.log(name);
    }
    function ss(){
        console.log(name);
    }
    s();
    ss();
}
t();

当执行函数 s 时,将创建函数 s 的执行环境(调用对象),并将该对象置于链表开头,然后将函数 t 的调用对象链接在之后,最后是全局对象。

然后从链表开头寻找变量name,很明显 name 是"slwy"。

但执行ss()时,作用域链是: ss()->t()->window,所以name是”tlwy"

下面看一个很容易犯错的例子:

<html>
<head>
<script type="text/javascript">
    function buttonInit(){
        for(var i = 1; i < 4; i++){
            var b=document.getElementById("button" + i);
            b.addEventListener("click", function(){
                alert("Button" + i);
            },false);
        }
    }
    window.onload=buttonInit;
</script>
</head>
<body>
    <button id="button1">Button1</button>
    <button id="button2">Button2</button>
    <button id="button3">Button3</button>
</body>
</html>

当文档加载完毕,给几个按钮注册点击事件,当我们点击按钮时,会弹出什么提示框呢?

很容易犯错,对是的,三个按钮都是弹出:"Button4",你答对了吗?

当注册事件结束后,i 的值为4,当点击按钮时,事件函数即 function(){ alert("Button"+i);} 这个匿名函数中没有 i,根据作用域链,所以到 buttonInit 函数中找,此时 i 的值为4,所以弹出 "button4"。

四、with语句(严格模式下不可用)

说到作用域链,不得不说 with 语句。with 语句主要用来临时扩展作用域链,将语句中的对象添加到作用域的头部。

看下面代码

person={
    name:"yhb",
    age:22,
    height:175,
    wife:{
        name:"lwy",
        age:21
    }
};

with(person.wife){
    console.log(name);
}

通过 with 将person.wife 添加到作用域链头部,再执行 console.log() 的时候,通过作用域链会优先找到 wife 对象的属性 name,也就是结果输出为 "lwy"。