龙空技术网

《你不知道的javascript》---从作用域到闭包

清墨语 148

前言:

今天我们对“js唯一标识符”都比较重视,咱们都想要剖析一些“js唯一标识符”的相关文章。那么小编也在网摘上汇集了一些有关“js唯一标识符””的相关资讯,希望你们能喜欢,各位老铁们一起来学习一下吧!

编译原理:
js是一门编译型的语言,与传统编译语言类似,传统编译的过程分为三个阶段 ;
1. 分词/词法分析; 2.解析/语法分析; 3.代码生成 ;

js引擎在编译时会比较复杂 具体多么复杂我也不造,大概就是对1,3 进行了优化使其快速编译完成并立即执行,这里就要注意了,,js是在执行前编译的 也许几微秒就OK了

1.作用域 : // 收集并维护所有声明的变量组成一个查询机制,用一套严格的规则以确保当前执行的代码对这些变量的访问权限;

作用域有两种工作模型 一种是普遍使用的静态作用域也叫词法作用域 另一种是动态作用域

以 var a = 1; 为例

编译器(上面的 1 2 3)开始工作了,看到 var a 时,编译器 会去询问作用域 在当前作用域的集合中有没有一个叫a的变量, 如果有 则忽略 继续编译,要是没有,那就在当前作用域的集合中声明一个叫a的变量;编译完成后就要生成代码以便引擎运行它,当引擎运行 a = 1;时,引擎也得询问作用域 在当前作用域的集合中有没有一个叫a的变量,如果有 引擎就使用并它为其赋值为1,要是没用那就向当前作用域的外层作用域询问查找 一直到全局作用域为止(要是全局作用域也没用 作用域会说“你的小祖宗没找到但帮你创建了一个”非严格模式下);

引擎在向作用域询问变量时 有两种方式 LHS 与 RHS; 上面那个就是LHS;简单过一下 LHS即 赋值操作的对象 RHS即赋值操作的源头; 简单讲 var a = 1; 这里a 就是LHS; console.log( a ) 这个a就是RHS;换言之一个是赋值 一个是取值(恩 可以这么简单理解);

非严格模式下 RHS 查找失败时( 查找到全局作用域也没找到 ) 引擎会抛出ReferenceError 异常;

LHS 查找失败时( 查找到全局作用域也没找到 ) 全局作用域会非常热心的给你创建一个;

严格模式下 LHS 查找失败时( 查找到全局作用域也没找到 ) 引擎会抛出类似 ReferenceError 的异常;
RHS 查找失败时 同非严格模式一样;

RHS如果查找到了该变量,但做了不合理的操作时( 比如一个数值型变量当方法使用 )或是引用了null / undefined类型值得属性时,引擎会抛出TypeError;

ReferenceError 是作用域查找失败时抛出的;

TypeError 是作用域查找成功了,但做了不合理的操作抛出的;

1.0 作用域链 // 当一个函数嵌套另一个函数时 就形成了作用域嵌套 产生了作用域链


    var b = 1;    function fn(a){        return a + b;    }    fn(2);// 3

当b进行RHS查询时在fn的作用域集合中找不到 会沿着作用域链向上查找 这里是全局作用域


1.1.词法作用域 // 就是编译原理的第一阶段 定义词法的作用域 就是写代码时的作用域 也叫静态作用域

    function fn(a){        var b = a.num*2;        function fn1(c){            console.log(c);        }        fn1(b+1);    }    fn({"name":2});//5

这个例子中有三层作用域即 1.全局作用域 一个标识符 fn;2.fn作用域 三个标识符 a ,b ,fn1;3.fn1作用域 一个标识符 c;

作用域查找某个标识符时会在遇到的第一个匹配的标识符时停止,多层嵌套作用域中定义同名标识符这种做法称为"遮蔽效应'(内部标识符遮蔽了外部标识符);

全局变量会成为全局对象window的属性; 利用这一特性可以访问被遮蔽的全局标识符,但除了全局标识符以外的标识符如果被遮蔽 是没有办法访问的;

无论函数怎么调用以及如何调用 他的词法作用域都只由函数声明时所在的位置决定;

词法作用域只查找一级标识符 如 a, fn, arr; 对于查找像json.name.value 这样的对象访问时,词法作用域只会查找json 找到该标识符后 由对象属性访问规则分别接管访问name和value;

1.2 欺骗词法作用域// 词法作用域时由写代码时决定的,在代码运行时来修改(欺骗)词法作用域的手段/方式 就达成了欺骗词法作用域的目的

以eval();为例:

js中的eval();接受一个字符串作为参数;可以将代码用程序生成 就像程序本就写在那一样;根据这个原理 eval()可以达到欺骗词法作用域的目的;

    var a = 2;    function fn(str){        eval(str);// 欺骗        console.log(a);    }    fn("var a = 1;"); // 1

这里的str是写死的 如果需要完全可以用程序自己生成;

严格模式下 eval(); 有自己的词法作用域 不会影响所在的作用域;

类似eval();的还有 setInterval(),setTimeout()的第一个参数可以是字符串,字符串会被解释成一段动态的函数代码;还有new Function();

这里同样提示大家不要使用它们,也是如此 这里不作阐述了;

都知道eval() with() Function() 是魔鬼,如果仅仅是因为它们欺骗词法作用域而定义为魔鬼的话那就太极端了; js引擎在编译阶段会进行各种优化,其中一项就是根据代码的词法进行静态分析,预先确定它们的位置 ,以便运行时快速找到它们;但发现eval() with() 等的时候引擎无法确定它们会接受什么样的代码,对作用域做什么样的修改,因此引擎会忽略它们不作任何优化,代码中过多使用eval()等函数程序会运行的相当慢,尽管引擎很聪明;有时使用不当它们会不只不觉的改变全局变量 这个就更神奇了..
1.3函数作用域 // 是指这个函数的全部变量/标识符 都可在其内部范围内被使用及复用(嵌套的作用域也能使用);

   function fn(){        var a = 1;        function fn1(){        // 代码....        }    };

上面这段代码 同样的三层作用域 全局 fn 及fn1 , 在fn函数内部 都可以访问使用变量a( fn1中也可以使用 ), 但在全局作用域下 是访问不到这个a的,它是fn私有的; 接着我们写一段类似这样的代码:

    var b;    function getadd(a){        b = a+add(a+2);        console.log(b)    }    function add(a){        return a*2    }    getadd(2);//10

有点眼熟额,这可能是入门或初级学者普遍写法,这样写讲真,前期问题不大,但后期维护成本就高了,在版本迭代的时候 保不准可能标识符覆盖 ,为什么这么说呢,变量b与函数add应该是getadd函数的私有属性应在其内部实现,要是在外部能访问使用他们不仅没必要也可能会产生超出getadd的使用条件;是很危险的;so 应将其私有化

   function getadd(a){        var b;        function add(a){            return a*2        }        b = a+add(a+2);        console.log(b);    }    getadd(2);//10

这样就舒服多了,b与add都无法从外部被访问而只被getadd所控制了;功能上也没有影响,并且也体现了私有化的设计,更符合了最小授权或最小暴露原则;在看一段代码:

    function fn(){        function fn1(a){            i = 2;            console.log(a*i);        }        for(var i=0;i<5;i++){            fn1(--i);        }    }    fn();// 完美的让浏览器崩掉了...i=2 意外的覆盖for中的i了 循环条件永远满足.

我知道实际中一定不会有这么写的, 提这段代码主要是为了理清一个概念 隐藏作用域 即隐藏作用域中的变量及函数; 好处多多可以避免标识符冲突也可预防类似上面的问题(遮蔽效应可完美解决这个尴尬);另外提一下关于全局命名冲突的解决方案有个专业的叫法 全局命名空间;其就是将自己私有的变量函数都给隐藏起来 对外只提供了一个变量(一般是json对象); 在任意代码段外部添加一个包装函数就可以实现隐藏作用域的目的了,外部函数即便是上天了也访问不到被包装函数内部的任何内容(不要提闭包) 上代码:

    var a = 2;    function fn(){ // 添加一个包装函数        var a = 3;    }    fn();    console.log(a);// 2

同样的实际中相信即使不加这个包装函数也不会有人这么写的;一方面用来阐述包装函数的意义;另一方面得找个坑跳下去; 问题就是 在添加包装函数的时候 这个函数本身 (fn)就已经污染了所在的作用域啊 想一下 在一个函数中有N多包装函数 场面一定混乱不堪;好了 函数表达式上场了; 函数声明与表达式的区别即 function 如果是声明的第一个词 那就是函数声明 否则就是表达式

表达式可分为 匿名表达式 立即执行表达式 (IIFE),对于这块日后会专门记录一篇的 ; 函数表达式可以解决这个尴尬.

呼呼,接下来了解下块作用域的概念 js是没有块级作用域的(es3),with 关键字是个异类它类似块作用域的形式 可以自行了解这货,除了with外 try catch语句中的catch也是一个块作用域,es6 有了块作用域的概念及用法 后期会一一交流;

块作用域有什么卵用?这么说吧 它是最小授权原则的一个扩展 在简单点 如果有块作用域就不需要包装函数了..看代码:

for(var i=0;i<5;i+=1){...};

如果js有块作用域那么 for中的i只在for中使用 ,不出所料 i在for外边也能访问使用了,

js有标识符提升的概念; 笔者也不在赘述 但给段代码自行感受下

    a = 1;    var a;    console.log(a);// 1    console.log(b);// undefined    var b = 2;

3.闭包 //当函数记住并可以访问所在词法作用域时 就产生了闭包 即便函数在当前作用域之外调用;

   function fn(){        var a = 1;        function fn1(){            console.log(a)        }        fn1();    }    fn();//1

这段代码与前面作用域嵌套类似 按照词法作用域查找规则 fn1作用域可以访问fn作用域下的标识符a;这个是闭包么?貌似像是个闭包昂,但严格的根据上面所述 他还不是 虽然他能访问当前词法作用域;继续

   function fn(){        var a = 1;        function fn1(){            console.log(a)        }        return fn1;    }    var f = fn();    f();// 1  没错这里才是一个标准(便于理解闭包)的闭包

分析下 fn1()的词法作用域能够访问fn的内部作用域,然后我们将fn1()当一个返回值(fn1当作一个值的类型进行传递,这个值类型就是函数类型,换言之就是当作函数类型的值);然后定义f用于接受fn的返回值(就是fn1()函数),再调用自身f();简单讲就是通过不同变量引用fn内部函数fn1而已 ;

在执行完fn的时候 通常引擎的垃圾回收机制会对不再使用的标识符回收掉,从而释放内存,表面看 貌似fn 可以被回收了;事实上闭包的优点就体现出来了,,fn的内部作用域并没被回收;怎么会这样呢,哦,其内部作用域下的fn1还在使用啊,fn1声明在fn的内部作用域中,使其拥有涵盖fn内部作用域的权限,从而fn作用域一直存在,以便fn1在任何时间引用;没错 这个引用就是闭包;

关于闭包 有多种多样的写法 无论以何种方式对函数类型的值进行传递,函数调用时都会产生一个闭包:

   function fn(){        var a  =1;        function fn1(){            console.log(a);        }        fn2(fn1);    }    function fn2(f){        f();    }    fn();// 1

没错f()处就是一个闭包了.接下来说说IIFE( 立即执行函数 );

   function fn(){        var a = 1;        (function f(){            conosle.log(a);        }())    }    fn();

这个f函数并不是在其本身词法作用域之外执行的,根据这个观点来看IIFE貌似不是闭包昂;IIFE 也就是上面的函数f 并不是在词法作用域外执行的,而是在定义时的词法作用域执行的 a 是通过普通的词法作用域查找规则找到的而不是闭包发现的;理论上讲闭包应该发生在定义时的,IIFE 确实创建了闭包, 还是用于创建闭包最常有的工具,尽管本身并不会真的使用闭包;

   function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){             b[i] = function (){                return i;            }        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//10个10     }

这就尴尬了,用IIFE 改良下

   function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){        (function (){                 b[i] = function (){                    return i;                    }        })();        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//10个10     }

这就更加尴尬了,,依然不行,怎么回事 IIFE不是可以创建闭包么。仔细看下原来是我们创建的IIFE作用域是空的啊,,什么都没有 简单讲 我们需要为iife 包含点东西 在这就是i了;

    function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){        (function (){            var j = i;                    b[j] = function (){                    return j;                    }        })();        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//0-9    } 

这下可以了,其实任何使用回调的地都在使用闭包,闭包实质上是一个标准,是关于如何在函数按值传递的词法作用域中写的代码.无疑闭包是强大的,可以用他实现各种模块等 同时闭包也是无处不在的;

标签: #js唯一标识符