文档章节

Javascript 从作用域到闭包

那小么
 那小么
发布于 2017/02/17 15:42
字数 3779
阅读 70
收藏 1

此文帮助笔者梳理JS知识从作用域拥抱闭包, 若有幸被人查看欢迎交流;
0.编译原理:
    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();
 这里同样提示大家不要使用它们,这里附一个原生封装的一个轮子 可兼容至IE7的字符串转json解析函数 下载地址: https://github.com/liuyushao147/javascript_minCode
    with();也是如此 这里不作阐述了;
    都知道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
    }   

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

最后欢迎大神指正 !
                                                                                                                                                                                                                                     

© 著作权归作者所有

那小么
粉丝 3
博文 10
码字总数 9804
作品 0
朝阳
前端工程师
私信 提问
加载中

评论(4)

那小么
那小么 博主

引用来自“丿那一刻”的评论

感谢楼楼分享,期待下一篇!
感谢阅读 再接再厉
丿那一刻
丿那一刻
感谢楼楼分享,期待下一篇!
那小么
那小么 博主

引用来自“布莱恩奥复托杰森张”的评论

营养比较不全面啊 来点实用的给粉丝涨涨姿势!
ok 大神意见记下了,多多交流
布莱恩奥复托杰森张
布莱恩奥复托杰森张
营养比较不全面啊 来点实用的给粉丝涨涨姿势!
JavaScript 需要掌握的知识

不仅仅是面试,JavaScript 开发者都应该知道的十个概念 深入理解javascript原型和闭包(完结) javascript深入理解js闭包 js作用域 JS之作用域与闭包 JavaScript内存优化 可爱的小熊 js内存回...

IT追寻者
2016/06/25
80
0
javascript深入理解js闭包

一、变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域。 变量的作用域无非就是两种:全局变量和局部变量。 Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量...

Yamazaki
2012/06/15
16
0
JavaScript的闭包及函数重载

JavaScript的闭包及函数重载 闭包的概念 什么是闭包 说到JavaScript的闭包,需要先说一说JavaScript的作用域。 JavaScript在ECMA6之前,作用域是只有全局作用域跟函数作用域的。(这里先不涉...

csming1995
05/10
0
0
【译】理解JavaScript闭包——新手指南

闭包是JavaScript中一个基本的概念,每个JavaScript开发者都应该知道和理解的。然而,很多新手JavaScript开发者对这个概念还是很困惑的。 正确理解闭包可以帮助你写出更好、更高效、简洁的代...

LINJIAJUN
2018/11/28
0
0
深入理解JavaScript闭包

一、变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域。 变量的作用域无非就是两种:全局变量和局部变量。 Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量...

柯楠
2012/11/02
208
0

没有更多内容

加载失败,请刷新页面

加载更多

【TencentOS tiny】深度源码分析(4)——消息队列

消息队列 在前一篇文章中【TencentOS tiny学习】源码分析(3)——队列 我们描述了TencentOS tiny的队列实现,同时也点出了TencentOS tiny的队列是依赖于消息队列的,那么我们今天来看看消息...

杰杰1号
23分钟前
6
0
Hive

这就是那个 JAVA 类 package cn.itcast.bigdata;import java.util.HashMap;import org.apache.hadoop.hive.ql.exec.UDF;public class PhoneNbrToArea extends UDF{privat......

Garphy
23分钟前
6
0
Springboot开发,第二天

SpringBoot学习,第二天 目录:1、Springboot整合Listener 2、Springboot访问静态资源 3、异常处理 4、热部署 一、SpringBoot整合Listener 两种方式完成组件的注册 1、通过注解扫描完成组件的...

有一个小阿飞
27分钟前
7
0
BeginnersBook Perl 教程

来源:ApacheCN BeginnersBook 翻译项目 译者:飞龙 协议:CC BY-NC-SA 4.0 贡献指南 本项目需要校对,欢迎大家提交 Pull Request。 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并...

ApacheCN_飞龙
39分钟前
5
0
我的Java秋招面经大合集

阿里面经 阿里中间件研发面经 蚂蚁金服研发面经 岗位是研发工程师,直接找蚂蚁金服的大佬进行内推。 我参与了阿里巴巴中间件部门的提前批面试,一共经历了四次面试,拿到了口头offer。 然后我...

Java技术江湖
44分钟前
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部