zoukankan      html  css  js  c++  java
  • 【追寻javascript高手之路02】变量、作用域知多少?

    前言

    本来想把这个与上篇博客写到一起的,但是考虑到是两个知识点还是分开算了,于是我们继续今天的学习吧。

    基本类型与引用类型

    ECMAScript的的变量有两种类型:

    基本类型(值类型):简单数据段

    引用类型:多个值构成的对象

    在变量赋值结束后,解析器必须知道这个变量时基本数据类型还是引用类型,需要注意的是string在js中是值类型。

    复制的差异

    值类型的复制会在内存中创建副本,所以彼此间不会影响,但是引用类型只是将变量的引用复制,其指向的仍然是一个对象,会相互影响:

    1 var a = {};
    2 a.a = 6;
    3 var b = a;
    4 b.a = 66;
    5 alert(a.a);//66

    这是一个典型的例子,值类型的就不举例了。

    执行环境与作用域

    PS:作用域是面试官的最爱,也是我们经常栽跟头的地方,这块我们有必要搞清楚

    执行环境(execution)是javascript中非常重要的一个概念。

    执行环境定义了变量或者函数有权限访问其他数据,决定了他们各自的行为。

    每一个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存到这个对象中,虽然代码无法访问,但是解析器会用到他。

    在web浏览器中window对象便是我们的全局执行环境(最外围的执行环境,宿主不同便有差异,比如node.js可能就不一样),因此所有全局变量和函数都是作为window对象的属性或者方法创建的。

    销毁

    某个执行环境中的所有代码执行完毕,该环境便销毁,变量对象中的属性和函数也就完蛋了(闭包会让情况有所不同)。

    函数作用域

    每个函数都有自己的“作用域”范围(执行环境),当执行流进入一个函数时,函数的环境就会进入一个环境栈中,在函数执行完毕后,栈会将其环境弹出,控制权又重新交回之前的环境。ECMAScript程序的执行流就是这个机制在控制。

    作用域链

    当代码在一个环境中执行时会创建变量对象(variable object),并且该对象拥有一个作用域链,作用域链的用途是保证对执行环境有权限访问的所有变量与函数的有序访问。

    作用域的最前端便是当前执行代码所在环境的变量对象,若这个变量时函数,就将其活动对象(activation object)作为变量对象,

    活动对象最开始只包含一个变量arguments,整个作用域链会一直向上延伸直到window,这里上一个经典的例子:

     1 var color = 'blue';
     2 function changeColor() {
     3     var anotherColor = 'red';
     4     function swapColor() {
     5         var tmpColor = anotherColor;
     6         anotherColor = color;
     7         color = tmpColor;
     8     }
     9     swapColor();
    10 }
    11 changeColor();

    以上代码涉及三个执行环境(execution context):

    ① 全局环境

    ② changeColor局部环境

    ③ swapColor局部环境

    全局变量中有一个color和一个函数changeColor

    changeColor中有一个anotherColor变量和swapColor函数

    swapColor函数中有tmpColor,但是他可以访问以上所有的变量

    我们看到三个框了,内部环境可以通过作用域链访问所有外部环境,但是外部环境不能访问内部的,js中只有函数能产生局部作用域。

    延长作用域链

    有些语句可以在作用域链顶端临时增加一个变量对象,该变量对象会在代码执行后被移除,以下情况会引起作用域链加长:

    ① try-catch的catch块

    ② with语句

    这两个语句都会在作用域链前端增加一个变量对象,对with语句来说,会将指定对象添加到作用域链中,对catch来说,会创建一个新的变量对象,其中包含被抛出的错误对象的声明。

    1 function buildUrl() {
    2     var q = '?p=true';
    3     with (location) {
    4         var url = href + q;
    5     }
    6     return url;
    7 }

    在此with语句接收的是location对象,因此其变量对象就包含了location对象的属性和方法,而且被放到作用域链的前端了,我们的href就是有意义的了,相当于location.href。

    闭包

    作用域链与闭包在一起后情况又会有所不同,在此我们再来看看闭包是什么:

    闭包是指有权限访问另一个函数作用域中变量的函数:
    
    在函数内部创建函数,使用到了外部的变量,并且将此函数返回,就形成了闭包。
    
    
    
    我认为闭包产生的原因是因为想私有化变量,但是提供一个对外的访问接口,所以提出了闭包的概念
     1 function Klass() {
     2     var name;
     3     var getName = function () {
     4     return name;
     5     };
     6     var setName = function (n) {
     7         name = n;
     8     };
     9     return { getName: getName, setName: setInterval };
    10 }

    比如这个应用,是比较常见的,name应该是私有的,但是我们要将其接口公布出来。

    这里,当Klass被调用了,即使他返回了,但是内部函数仍然可以访问name变量,内部函数包含了外部Klass函数的作用域,我们来理一理这个东西:

    当某个函数调用时会创建一个执行环境(execution context)以及相关的作用域链。
    然后,使用arguments和其它命名参数的值来初始化函数的活动对象(activation object)。
    在作用域链中,外部函数的活动对象始终处于第二位,可能还有第三位第四位。。。
    在函数执行过程中,为了读取和写入变量的值,就需要在作用域链中查找变量
     1 function compare(v1, v2) {
     2     if (v1 < v2) {
     3         return -1;
     4     } else if (v1 > v2) {
     5         return 1;
     6     } else {
     7         return 0;
     8     }
     9 }
    10 var r = compare(5, 10);

    以上定义了compare函数,然后在全局下调用了他,第一次调用时,会创建this、arguments、v1、v2的活动对象。

    全局执行环境的变量this、r、compare在compare执行环境作用域链的第二位:

    后台的每一个执行环境都有一个表示变量的对象——变量对象。

    全局环境的变量对象始终都在,像compare这种局部变量的变量对象只在函数执行时候存在

    在创建compare函数时,会创建一个预先包含全局变量的作用域链,这个作用域链被保存在内部的[[Scope]]属性中

    当调用compare函数时,会为函数创建一个执行环境,然后复制函数的[[Scope]]属性中的对象构建新的执行环境的作用域链

    为此又有一个活动对象(在此作为变量对象使用)被创建并推入执行环境作用域链的最前端。

    上例中,对于compare执行环境而言,其作用域链包含两个变量对象:

    1 本地活动对象

    2 全局变量对象

    一般来说,函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,但是闭包让情况有所转变

    在函数内部定义的函数将会包含函数(外部函数)的活动对象添加到它自己的作用域链中。
    所以在Klass内部定义的getName与setName的作用域链中事实上是包含了外部Klass的活动对象的。
    
    在函数Klass返回后,其中的getName作用域链被初始化为包含Klass函数的活动对象和全局变量对象
    这样getName就可以访问Klass函数中的所有变量,这样做的结果就是阻止了Klass执行结束后销毁其name变量。
    换句话说,Klass的执行环境与其作用域链会被回收,但是其activation object会被保留在内存中,知道getName被销毁后才回收,最后Klass的活动对象才被销毁

    PS:显然闭包占有更多的资源,若是不及时回收会对性能造成影响,这里各位要小心才行。

    闭包与变量

    作用域链的这种配置机制也产生了一个影响不到的问题:

    闭包只能取得包含函数(外部函数)中变量的最后一个值。
    因为闭包所保存的是函数的活动对象,是整个变量对象,而不是某一个值
    1 function createFunc() {
    2     var r = [];
    3     for (var i = 0; i < 10; i++) {
    4         r[i] = function () {
    5             return i;
    6         };
    7     }
    8     return r;
    9 }

    这是一有趣的代码,我们这里返回了一个数组,数组里面装了10函数,但是我们这是个函数保存的是同一个外部函数的活动对象,而i最后的值是10,所以所有函数最后打印出来的都是10。这里要怎么处理各位都知道了,我还是贴个代码吧:

     1 function createFunc() {
     2     var r = [];
     3     for (var i = 0; i < 10; i++) {
     4         r[i] = (function (num) {
     5            return function () {
     6                 return num;
     7             } 
     8         })(i);
     9     }
    10     return r;
    11 }

    这里把各个闭包函数保存的活动对象接不相同,所以产生了不同的结果。

    写到此处我感觉我所知道的东西都差不多了,若是您有什么补充,或者我这里有什么问题,请提出来。下面我们来演练一番

    做几道题吧

    好了,说了这么多虚的,我们来一点干货作为本文的结束吧,完了我就休息了,等晚上再写点东西就结束了。

    settimeout与setInterval

     1 var a = 6;
     2 setTimeout(function () {
     3     alert(a);
     4     a = 666;
     5 }, 1000);
     6 a = 66;
     7 
     8 
     9 var a = 6;
    10 setInterval(function () {
    11     alert(a);
    12     a = 666;
    13 }, 1000);
    14 a = 66;

    我这里稍微修改了一下代码,我们来看看:

    第一段代码是上次寒冬老师问我的问题,因为延时函数会后面点执行,所以打印的是66

    而第二段代码有所不同的是,先打印的是66,后面便一直是666了

    第二题

    1 var tt = 'aa'; 
    2 function test(){ 
    3 alert(tt); 
    4 var tt = 'dd'; 
    5 alert(tt); 
    6 } 
    7 test(); 

    这道题有点意思哦,他还考察了变量声明提前的问题。
    其中var tt的定义会提前,这道题相当于:

    1 var tt = 'aa'; 
    2 function test(){ 
    3 var tt;
    4 alert(tt); 
    5 tt = 'dd'; 
    6 alert(tt); 
    7 } 
    8 test(); 

    在互动变量中应该使用内部tt,所以第一次是undefined第二次是dd,这道题本身不难,但是容易引起混淆:

    1 var a = '11';
    2 var a;
    3 alert(a);//11

    我们这个重复定义不会覆盖其值的约定可能会给你带来迷糊,这里要将之看着两个国家的人。

    我们稍稍变形:

    1 var a = 10;
    2         function test() {
    3             a = 100;
    4             alert(a);
    5             alert(this.a);
    6             var a;
    7             alert(a);
    8         }
    9         test();

    这个代码相当于:

    1 var a = 10;
    2 function test() {
    3     var a;
    4     a = 100;
    5     alert(a);//100
    6     alert(this.a);//10
    7     alert(a);//100
    8 }
    9 test();

    我们只看this.a,这里this指向的是window,至于this的东西,我们后面点再说。

    第三题

    1 f = function () { return true; };
    2 g = function () { return false; };
    3 (function () {
    4     if (g() && [] == ![]) {
    5         f = function f() { return false; };
    6         function g() { return true; }
    7     }
    8 })();
    9 alert(f()); // true or false ? 

    这道题看上去有点乱。。。考察了很多东西,我们先说说其中的一个有问题的地方:

    不要在if里面定义function请使用函数表达式,在if中定义function会让结果难以估计

    javascript可能会修正其中的程序而忽略if判断,但是这不是我们这里要研究的,但是他直接影响我做题的思路啊,这里果断给把题改了:

    1 f = function () { return true; };
    2 g = function () { return false; };
    3 (function () {
    4     if (g() && [] == ![]) {
    5         f = function f() { return false; };
    6         g = function() { return true; }
    7     }
    8 })();
    9 alert(f()); // true or false ? 

    题目愿意是什么我们不管了,现在就这样做。其中第五行的function f()中的f会被忽略掉,他还有个坑爹的地方是:

    var s1 = g();//false
    var s2 = g() && []; //false
    var s3 = ![]; //false
    var s4 = g() && [] == ![]; //false
    var s5 = (g() && []) == ![]; //true

    这道题稍不注意就要完蛋,s4为什么是false我都猜不透。。。所以答案是true了,这道题我没有摸透,各位自己去看看吧。

    第四题

     1 function say667() {
     2     // Local variable that ends up within closure
     3     var num = 666;
     4     var sayAlert = function () { alert(num); }
     5     num++;
     6     return sayAlert;
     7 }
     8 
     9 var sayAlert = say667();
    10 sayAlert();

    这道题有点意思,与前面的循环闭包的例子类似,答案是667

    结语

    本文暂时到这里了,请各位也上两道与作用域有关的面试题呗

  • 相关阅读:
    浏览器缓存机制
    linux mail命令用法
    linux下Memcached安装以及PHP的调用
    JAVA和C# 3DES加密解密
    C++中函数调用时的三种参数传递方式详解

    const的用法,特别是用在函数前面与后面的区别!
    海底捞的“七宗罪”
    解决Qt5.7.0 cannot find -lGL
    怎么删除桌面右键"打开好桌道壁纸"
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3163026.html
Copyright © 2011-2022 走看看