关于JavaScript作用域的理解

    作用域是JavaScript中一个基本的知识点,但是如果要比较全面的了解它,不免需要涉及到很多知识点。

    首先,我们需要明确作用域的定义或者描述。作用域,也就是规定如何存储变量,在需要的时候如何去访问这些变量的规则。作用域分为两类:动态作用域,静态作用域(词法作用域)。一动一静,从字面上理解也能看出他们的区别,动态作用域是在运行时确定的(JavaScript中的this符合这一点,先挖个坑),而词法作用域是在写代码的时候确定的,或者说变量定义的位置。很明显,JavaScript是词法作用域(假设你写过JavaScript代码)。

    先看一段代码:

var arr = [1,2,3,4,5];
var average = countAverage(arr);
for(var i = 0;i<arr.length;i++){
    if(arr[i] < average){
        arr.splice(i,1);
        i--;
    }
}
function countAverage(arr) {
    var sum = 0;
    var len = arr.length;
    if(len){
        for(var j = 0;j<len;j++){
            sum += arr[j];
        }
    }
    return sum/len;
}

    首先,我们找一下通过var声明的变量一共有哪些:arr,average,i,countAverage,sum,len,j。上述代码段执行完毕之后,我们去访问下这些变量,看一下会输出什么:

arr            //[3,4,5]
average        //3
i              //3
countAverage   //f countAverage(){...}
sum            //sum is not defined
len            //len is not defined
j              //j is not defined

    第一个关注点在变量‘i’,‘i’的定义是在for语句块中,但是在后续访问的时候任然可以访问。我们创建变量‘i’的初衷是希望它只存在于循环体中,但是现在它污染到了全局。也就是说,js中似乎没有块作用域。是否能通过一些手段达到块作用域的效果呢,先挖个坑。

    第二个关注点在变量‘sum’,‘len’,‘j’,这三个变量的定义是在函数体内,后续访问时报错未定义,同时我们可以在函数内部访问到函数外部变量‘arr’。也就是说,函数会创建一个作用域。综合上述两点,可以得出一个结论:Js是基于函数的作用域,其他结构绝大多数不会创建作用域。为方便理解,叙述,给出下面集合:


    直观的:椭圆的房子里面有三个人‘arr’,‘average’和‘i’,以及一个圆形的房间‘countAverage’,圆形房间里面有三个居民‘sum’,‘len’和‘j’。现在站在椭圆的房子里叫‘arr’,会有人回应,但是叫‘sum’,没有人答应,因为圆形房间此时是关闭的。如果走进圆形房间,再去做同样的事情,就都能得到回应。也就是说,我们只能由内向外去访问居民。这里提到的“走进”的实施者,其实就是JavaScript的“执行流”。

    由此,我们拓展开来,在圆形集合里面可以有一个方形集合,方形集合里面也可以有个三角形集合,甚至可以无限的往下做嵌套。我们可以在最后一个x形集合中向外访问居民,反之则不行。

    在JavaScript中,这些椭圆区域,圆形区域...,每个区域都是一个执行环境,每个环境都有一个与之对应的变量对象,环境中定义的变量和函数都保存在这个对象中。‘最外围’的环境就是我们常说的全局执行环境,在web浏览器中,全局环境对应的变量对象是window,例如我们也可以通过window.arr去访问变量‘arr’。

    暂时先不去考虑块作用域的事情。现在知道,每个函数都有自己的执行环境,那js是怎么知道这些环境的嵌套关系的呢?在程序运行时,代码执行到某个函数,对应函数的执行环境就会被推入环境栈中,很明显,栈底保存的就是全局执行环境,而栈顶元素到栈底所形成的就是当前函数对象可访问的作用域,也就是当前函数对象的作用域链。作用域链的最前端是栈顶元素对应的变量对象,最末端就是栈底元素全局环境的变量对象。当函数执行完毕,环境栈就会将当前函数执行环境弹出。例如,给出以下示例:

function groupA(){
    var str = 'groupA';
    ......//代码块
    function groupB(){
        var str = 'groupB';
        ......//代码块
        function groupC(){
            var str = 'groupC';
            ......//代码块
        }
    }
}

    在环境中,变量查找是沿着作用域链进行线性的,有序的搜索的。例如在上述代码中,当执行到函数groupC时,代码块中访问变量“str”,groupC中定义了“str”,因此返回“groupC”;如果访问的变量在groupC中未定义,就会沿着作用域链往上搜索,依次查找groupB,groupA,全局环境,如果找到就终止搜索,如果在全局环境中任然没有找到,就会报错未定义(严格模式下,非严格模式下就会创建一个全局变量)。

    现在来说说块作用域。在es5中实现块作用域的方法,以下几种:

  1. with
  2. try/catch
  3. 立即执行函数IIFE

    其中with不推荐使用,而且在严格模式下是被禁止的。catch分句会创建一个块作用域,了解下就好。然后说以下立即执行函数。一般常见的函数写法是这样的:

//函数声明
function foo(){
    ......//代码块
}
//函数表达式
var another = function(){
    ......//代码块
}
//匿名函数
function(){
    ......//代码块
}

    如果你看过jQuery源码,你会熟悉这样的结构:

(function(global,factory){
    ......//代码块
})(..........)

    将函数包含在()中使其变为一个函数表达式,然后再用一个()去执行它。看一个简单的对比

(function IIFE(){
    var str = 'IIFE';
    console.log(str);
})();
function fun(){
    var str = 'fun';
    console.log(str);
}

将代码放到控制台执行,然后分别访问IIFE和fun,结果显示为IIFE未定义,fun会返回函数。可以这样理解,变量fun保存的是指向函数的指针,并且fun作为全局执行环境变量对象window的属性被保存。而IIFE整体全都被封印在‘()’中。IIFE--立即执行函数很强大,可以作为一个专题来写了,这里就不入了。

    在es6中,加入了let,const,解决了以往的一个var声明一切的尴尬。对let,const,var的对比网上有很多文章写的都很好,之后我也会做一个总结,这里不多说。

    说到作用域,闭包这个话题是逃不开的。刚接触js时就听说闭包很难,感觉很神秘,似乎大多人不太愿意谈论这个话题。事实上,你已经写过很多‘闭包’了,只是自己没有意识到,例如各种回调。常见的讲解闭包的代码类似下面的:

function fun(name){
    var str = 'hello,'+name;
    return function(){
        console.log(str);
    };
}
var test = fun('world!');
test();     //hello,world!

    首先函数fun内部定义了一个变量str,函数返回的是另一个匿名函数,匿名函数做的事情就是打印出fun定义的内部变量str。然后我们在函数fun外部调用fun,并把结果赋值给变量test。这个test指向就是匿名函数。按照之前说的词法作用域,在执行test的时候应该报错:str未定义才对。但实际上,test能够正常执行,并输出'hello,world!'。

    我们在函数定义的词法作用域外部对函数进行调用,它仍然可以访问其定义所在的词法作用域,这个时候就产生了闭包。再想想代码中写过的各种回调,我们实际上就是将内部函数传递到了其定义所在的词法作用域之外,自然都用到了闭包。

    这篇文章跨度时间有点久,一来自己不满意,感觉想写的东西多,不知道怎么合理的串起来但不至于文章看起来杂乱多,二是工作内容多时间不充裕。算是匆匆结个尾。回头看了一遍,作用域和闭包的基本知识应该是交待清楚了。文中提到的立即执行函数,块作用域,es6的let,const等等这些,都需要都值得去深入学习。之后找时间再去针对这些点写总结。

    期待您的指正交流。   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值