javascript从作用域到闭包-笔记

读《你不知道的javascript》一书做个笔记;
编译原理:
    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
    } 

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

最后欢迎大神指正 !

时间: 2024-07-29 05:31:11

javascript从作用域到闭包-笔记的相关文章

JavaScript从作用域到闭包

作用域(scope) 全局作用域和局部作用域 通常来讲这块是全局变量与局部变量的区分. 参考引文:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链 全局作用域:最外层函数和在最外层函数外面定义的变量拥有全局作用域. 1)最外层函数和在最外层函数外面定义的变量拥有全局作用域 2)所有末定义直接赋值的变量自动声明为拥有全局作用域,即没有用var声明的变量都是全局变量,而且是顶层对象的属性. 3)所有window对象的属性拥有全局作用域 局部作用域:和全局作用域相反,局部作

你不知道的JavaScript(作用域和闭包)

作用域和闭包 ?作用域 引擎:从头到尾负责整个JavaScript的编译及执行过程. 编译器:负责语法分析及代码生成等. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限. 作用域是一套规则,用于确定在何处以及如何查找变量(标识符). 如果查找的目的是对变量进行赋值,那么就会使用LHS查询: 如果目的是获取变量的值,就会使用RHS查询. ?词法作用域 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由

前端知识体系:JavaScript基础-作用域和闭包-闭包的实现原理和作用以及堆栈溢出和内存泄漏原理和相应解决办法

闭包的实现原理和作用 闭包: 有权访问另一个函数作用域中的变量的函数. 创建闭包的常见方式就是,在一个函数中创建另一个函数. 闭包的作用: 访问函数内部变量.保持函数在环境中一直存在,不会被垃圾回收机制处理 因为函数内部声明 的变量是局部的,只能在函数内部访问到,但是函数外部的变量是对函数内部可见的,这就是作用域链的特点了. 子级可以向父级查找变量,逐级查找,找到为止 因此我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后我们就可以访问到他的变量了. <scr

JavaScript之作用域与闭包详解

前言: JavaScript是一种应用非常广泛的语言,其也有一些自身特点和优势,本文重在讲述其作用域机制以及闭包,会从一些实例来探讨其机理. 作用域在JavaScript程序员日常使用中有不同的含义,如下所示: this绑定的值: this绑定的值定义的执行上下文: 一个变量的“生命周期”: 变量的值解析方案,或词法绑定. 下面将讲诉JavaScript作用域概念,由此引出变量值解析方案的一般想法,最后再探讨JavaScript里闭包这一重要知识点. 1.全局作用域 所有浏览器都支持 windo

javascript的作用域以及闭包现象

1.  词法作用域 词法作用域就是定义在词法阶段的作用域,换句话说,也就是js的作用域时在定义阶段决定的,和调用无关. 1.1 作用域沿着作用链向上查找 <!DOCTYPE html> <html> <head> <title></title> </head> <body> </body> <script type="text/javascript"> //引擎执行console

前端知识体系:JavaScript基础-作用域和闭包-如何处理循环的异步操作

如何处理循环的异步操作 先看一段代码 function getMoney(){ var money=[100,200,300] for( let i=0; i<money.length; i++){ compute.exec().then(()=>{ console.log(money[i]) //alert(i) }) } } //compute.exec()这是个异步方法,在里面处理一些实际业务 //这时候打印出来的很可能就是300,300,300(因为异步for循环还没有等异步操作返回P

javasrcipt的作用域和闭包(二)

这篇博客主要对词法作用域与欺骗词法作用域.函数作用域与块级作用域.函数内部的变量提成原理进行详细的分析,在这篇博客之前,关于作用域.编译原理.浏览器引擎的原理及关系在javaScript的作用域和闭包(一)有详细的阐述,而今天这篇博客是在其基础上对作用域的工作原理进行深入的分析,所有如果有对编译和引擎原理的是很清楚的客官可以查阅一下前面的博客. 一.词法作用域 在大部分标准语言编译器的第一部分工作就是词法化(单词化),词法化的过程会对源代码中的字符串进行检查,如果是有状态的解析过程,还会赋予单词

JavaScript【5】高级特性(作用域、闭包、对象)

笔记来自<Node.js开发指南>BYVoid编著 1.作用域 if (true) { var somevar = 'value'; } console.log(somevar); JavaScript的作用域完全是由函数决定的,if.for语句中的花括号不是独立的作用域. 1.1.函数作用域 作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,我们称为函数作用域.在函数中引用一个变量时,JavaScript会先搜索当前函数作用域,或者称为"局部作用域",

举例详细说明javascript作用域、闭包原理以及性能问题(转)

这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存,关系到闭包,关系到javascript运行机制.关系到功能,关系到性能. 文章内容主要参考自<High Performance JavaScript>,这本书对javascript性能方面确实讲的比较深入,大家有空都可以尝试着阅读一下,我这里有中英电子版,需要的话QQ317665171或者QQ邮箱联系. 复习,笔记,更深入的理解. 欢迎拍砖指正. 作用域: 下面我们先搞明白这样几个概念: 函数对象的[[scope]]属性.S