zoukankan      html  css  js  c++  java
  • 关于js作用域问题详解

    执行上下文

    函数表达式和函数声明

    1.
    console.log(a); // ReferenceError: a is not defined
    // ReferenceError(引用错误)对象表明一个不存在的变量被引用。
    2.
    console.log(a); // undefined
    var a;
    3.
    console.log(a); // undefined
    var a = 10;
    4.
    var a = 10;
    console.log(a); // 10


    在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)。

    context

    接下来的这段代码需要注意代码注释中的两个名词——“函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。

    “准备工作”

    1.
    console.log(a); // ReferenceError: a is not defined
    // ReferenceError(引用错误)对象表明一个不存在的变量被引用。
    2.
    console.log(a); // undefined
    var a;
    3.
    console.log(a); // undefined
    var a = 10;
    4.
    var a = 10;
    console.log(a); // 10


    在“准备工作”中,对待
    函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。看以上代码。“函数声明”时我们看到了第二种情况,而“函数表达式”时我们看到了第一种情况。

    而对待函数声明时,却把函数整个赋值了。

    总结一下,在“准备工作”中完成了哪些工作:

    1. 变量、函数表达式——变量声明,默认赋值为undefined;
    2. this——赋值;
    3. 函数声明——把函数整个赋值;

    这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

    function fn(x) {
    console.log(arguments); //Arguments { 0: 10, 等 2 项… }
    console.log(x); //10
    }
    fn(10);


    以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

    另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。用一个例子说明一下:

    var a = 10;
     
    function fn() {
    console.log(a); //a是自由变量
    //函数创建时,就确定了a要取值的作用域
    }
     
    function bar() {
    var a = 20;
    fn(); //打印10不是20
    }
    bar(fn); //10


    执行上下文栈
    给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

    执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

    其实这是一个压栈出栈的过程——执行上下文栈。

    js-context

    1 var a = 10, //1.进入全局上下文环境
    2 fn,
    3 bar = function(x) {
    4 var b = 5;
    5 fn(x + b); //3.进入fn函数上下文环境
    6 };
    7
    8 fn = function(y) {
    9 var c = 5;
    10 console.log(y + c);
    11 }
    12
    13 bar(10); //2.进入bar函数上下文环境


    在执行代码之前,首先将创建全局上下文环境。 

    全局上下文环境
    a undefined
    fn undefined
    bar undefined
    this window

    然后是代码执行。代码执行到第12行之前,上下文环境中的变量都在执行过程中被赋值。

    全局上下文环境
    a 10
    fn function
    bar function
    this window

    执行到第13行,调用bar函数。

    跳转到bar函数内部,执行函数体语句之前,会创建一个新的执行上下文环境。
    全局 | 上下文环境
    —|—
    b | undefined
    x | 10
    arguments | [10]
    this | window

    并将这个执行上下文环境压栈,设置为活动状态。

    js-context
    执行到第5行,又调用了fn函数。进入fn函数,在执行函数体语句之前,会创建fn函数的执行上下文环境,并压栈,设置为活动状态。

    js-context

    待第5行执行完毕,即fn函数执行完毕后,此次调用fn所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。

    js-context

    同理,待第13行执行完毕,即bar函数执行完毕后,调用bar函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。

    js-context

    好了,给大家介绍了一段简短代码的执行上下文环境的变化过程,一个完整的闭环。其中上下文环境的变量赋值过程我省略了许多,因为那些并不难,一看就知道。

    作用域

    基础认识

    “javascript没有块级作用域”。所谓“块”,就是大括号“{}”中间的语句。

    比如一个if语句

    var i = 10;
    if (i > 1) {
    var name = "yzh";
    }
    console.log(name); //yzh
    for (var i = 0; i < 10; i++) {
     
    }
    console.log(i); //10


     for语句

    我们在编写代码的时候,不要在“块”里面声明变量,要在代码的一开始就声明好了。以避免发生歧义

    var i;
    for (i = 0; i < 10; i++) {
     
    }
    console.log(i);


    我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式
    你光知道“javascript没有块级作用域”是完全不够的,你需要知道的是——javascript除了全局作用域之外,只有函数可以创建的作用域。

    概念

    js-context

    如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

    作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

    例如以上代码中,三个作用域下都声明了“a”这个变量,但是他们不会有冲突。各自的作用域下,用各自的“a”。

    作用域和上下文环境

    js-context

    如上图,我们在上文中已经介绍了,除了全局作用域之外

    每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。

    下面我们将按照程序执行的顺序,一步一步把各个上下文环境加上

    第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。

    js-context

    第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。

    js-context

    第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。

    js-context

    第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。

    js-context

    第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变为活动状态。

    js-context

    第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。

    js-context

    最后我们可以把以上这几个图片连接起来看看。

    js-context

    作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。

    同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

    如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

    自由变量

    在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
    例:

    var x = 50;
     
    function fn() {
    var b = 20;
    console.log(x + b);
    }
    fn(); //70
    var x = 50;
     
    function fn() {
    console.log(x);
    }
     
    function show(f) {
    var x = 20;
    (function() {
    f(); //50 不是20
    })();
    }
    show(fn); //50 不是20



    在调用fn()函数时,函数体中第6行。取b的值就直接可以在fn作用域中取,因为b就是在这里定义的。而取x的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
    有人说过要到父作用域中取,其实有时候这种解释会产生歧义
    例如:

    不要在用以上说法了。相比而言,用这句话描述会更加贴切——要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

    对于本文第一段代码,在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取——无论fn函数将在哪里调用

    上面描述的只是跨一步作用域去寻找。

    如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

    这个一步一步“跨”的路线,我们称之为——作用域链。

    全局环境changeColor()的局部环境swapColors()的局部环境
    变量color 变量anotherColor 变量tempColor
    函数changeColor() 函数swapColors()

    以上代码共涉及3个执行环境:全局环境、changeColor()的局部环境和swapColors()的局部环境。

    内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

    每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境

    最后这个例子是从书上找到的,比较经典和简单。

    var color = "blue";
     
    function changeColor() {
    var anotherColor = "red";
     
    function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
     
    //这里可以访问color,anotherColor和tempColor
    }
     
    //这里可以访问color和anotherColor,但不能访问tempColor 
    swapColors();
    }
     
    changeColor(); //注释后alert显示为blue
     
    //这里只能访问color
    alert("Color is now " + color); //red
  • 相关阅读:
    Spring有用功能--Profile、WebService、缓存、消息、ORM
    opencv标定程序(改动)
    Install Docker Mac OS X
    Android eclipse 提示java代码 快捷键
    Mac使用Docker-machine訪问docker publish port
    决策树之C4.5算法学习
    为ImageView设置背景图片(代码中)
    BZOJ 3675 APIO2014 序列切割 斜率优化DP
    思科模拟器之路由器-RIP-DNS解析server
    POJ 3177 Redundant Paths
  • 原文地址:https://www.cnblogs.com/liutianzeng/p/10167807.html
Copyright © 2011-2022 走看看