javascript中关于做用域和闭包

  1. 列表项目javascript

前言

学习了javascript已经好久了,关于这个语言中的这两个特性也是早已耳熟能详,可是在实际的使用的过程当中或者是遇到相关的问题的时候,仍是不能很好的解决。
所以我以为颇有必要深刻的学习而且记录这个问题,以便在从此的学习和使用的过程当中回顾。html

正文

1. 全局做用域

  • 浏览器环境java

    全部浏览器都支持 window 对象,它表示浏览器窗口,JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。

    因此,全局变量是 window 对象的属性,全局函数是 window 对象的方法,甚至 HTML DOM 的 document 也是 window 对象的属性之一。

    全局变量是JavaScript里生命周期(一个变量多长时间内保持必定的值)最长的变量,其将跨越整个程序,能够被程序中的任何函数方法访问。

    在全局下声明的变量都会在window对象下,都在全局做用域中,咱们能够经过window对象访问,也能够直接访问。程序员

  • Node环境编程

    全局对象是global对象,与window相似,在全局下声明的全部的变量都在global对象之下,都在全局做用域中,能够经过glocal访问,也能够经过变量名访问。数组

    var name = 'clam';
    console.log(name); // clam
    // 浏览器环境
    console.log(window.name); // clam
    // Node环境
    console.log(global.name); // clam
  • 声明方式产生全局变量浏览器

    在js的任何位置,声明变量的时候没有使用var关键字,这个变量就是全局变量。闭包

    function add(a,b){
        return sum = a+b;
    }
    
    add(1,2); // 3
    console.log(sum); //3
    
    /* 至关于如下代码
*/
var sum;
function add(a,b){
    return sum = a+b;
}
add(1,2) ; // 3
console.log(sum);
```

全局变量存在于整个函数的生命周期中,然而其在全局范围内很容易被篡改,咱们在使用全局变量时必定要当心,尽可能不要使用全局变量。在函数内部声明变量没有使用var也会产生全局变量,会为咱们形成一些混乱,好比变量覆盖等。因此,咱们在声明变量的任什么时候候最好都要带上var。app

全局变量存在于程序的整个生命周期,但并非经过其引用咱们必定能够访问到全局变量。编程语言

2. 词法做用域

词法做用域:函数在定义它们的做用域里运行,而不是在执行它们的做用域里运行。也就是说词法做用域取决于源码,经过静态分析就能肯定,所以词法做用域也叫作静态做用域。with和eval除外,因此只能说JS的做用域机制很是接近词法做用域(Lexical scope)。词法做用域也能够理解为一个变量的可见性,及其文本表述的模拟值。

var name = "global";

function fun() {
    var name = "clam";
    return name;
}
console.log(fun()); // 输出:clam
console.log(name); // 输出:global

在一般状况下,变量的查询从最近接的绑定上下文开始,向外部逐渐扩展,直到查询到第一个绑定,一旦完成查找就结束搜索。就像上例,先查找离它最近的name="clam",查询完成后就结束了,将第一个获取的值做为变量的值。

3. 动态做用域

动态做用域与词法做用域相对而言的,不一样于词法做用域在定义时肯定,动态做用域在执行时肯定,其生存周期到代码片断执行为止。动态变量存在于动态做用域中,任何给定的绑定的值,在肯定调用其函数以前,都是不可知的。

在代码执行时,对应的做用域链经常是保持静态的。然而当遇到with语句、call方法、apply方法和try-catch中的catch时,会改变做用域链的。以with为例,在遇到with语句时,会将传入的对象属性做为局部变量来显示,使其便于访问,也就是说把一个新的对象添加到了做用域链的顶端,这样必然影响对局部标志符的解析。with语句执行完毕后,会把做用域链恢复到原始状态

// demo
var name = "global";

// 使用with以前
console.log(name); // 输出:global

with({name:"jeri"}){
    console.log(name); // 输出:jeri
}

// 使用with以后,做用域链恢复
console.log(name); // 输出:global

在做用域链中有动态做用域时,this引用也会变得更加复杂,再也不指向第一次建立时的上下文,而是由调用者肯定。好比在使用apply或call方法时,传入它们的第一个参数就是被引用的对象。

function globalThis() {
    console.log(this);
}

globalThis(); // 输出:Window {document: document,external: Object…}
globalThis.call({name:"clam"}); // 输出:Object {name: "clam"}
globalThis.apply({name:"clam"},[]); // 输出:Object {name: "clam"}

由于this引用是动态做用域,因此在编程过程当中必定要注意this引用的变化,及时跟踪this的变更。

4 .函数做用域

函数做用域,顾名思义就是在定义函数时候产生的做用域,这个做用域也能够称为局部做用域。和全局做用域相反,函数做用域通常只在函数的代码片断内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数做用域中,其生命周期随着函数的执行结束而结束。

var name = "global";

function fun() {
    var name = "clam";
    console.log(name); // 输出:jeri

    with ({name:"with"}) {
        console.log(name); // 输出:with
    }
    console.log(name); // 输出:clam
}

fun();

// 不能访问函数做用域
console.log(name); // 输出:global

5. 没有块级做用域

不一样于其余编程语言,在JavaScript里并无块级做用域,也就是说在for、if、while等语句内部的声明的变量与在外部声明是同样的,在这些语句外部也能够访问和修改这些变量的值.

function fun() {
    
    if(0 < 2) {
        var name = "clam";
    }    
    console.log(name); // 输出:clam
    name = "klay";
    console.log(name); // 输出:klay
}

fun();

6. 做用域链

JavaScript里一切皆为对象,包括函数。函数对象和其它对象同样,拥有能够经过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是做用域,包含了函数被建立的做用域中对象的集合,称为函数的做用域链,它用来保证对执行环境有权访问的变量和函数的有序访问

当一个函数建立后,它的做用域链会被建立此函数的做用域中可访问的数据对象填充。在全局做用域中建立的函数,其做用域链会自动成为全局做用域中的一员。而当函数执行时,其活动对象就会成为做用域链中的第一个对象(活动对象:对象包含了函数的全部局部变量、命名参数、参数集合以及this)。在程序执行时,Javascript引擎会经过搜索上下文的做用域链来解析诸如变量和函数名这样的标识符。其会从做用域链的最里面开始检索,按照由内到外的顺序,直到完成查找,一旦完成查找就结束搜索。若是没有查询到标识符声明,则报错。当函数执行结束,运行期上下文被销毁,活动对象也随之销毁。

var name = 'global';

function fun() {
    console.log(name); // output:global
    name = "change";
    // 函数内部能够修改全局变量
    console.log(name); // output:change
    // 先查询活动对象
    var age = "18";
    console.log(age); // output:18
}

fun();

// 函数执行完毕,执行环境销毁
console.log(age); // output:Uncaught ReferenceError: age is not defined

7. 闭包

闭包是JavaScript的一大谜团,关于这个问题有不少文章进行讲述,然而依然有至关数量的程序员对这个概念理解不透彻。闭包的官方定义为:一个拥有许多变量和绑定了这些变量的环境的表达式(一般是一个函数),于是这些变量也是该表达式的一部分。

一句话归纳就是:闭包就是一个函数,捕获做用域内的外部绑定。这些绑定是为以后使用而被绑定,即便做用域已经销毁。

  • 自由变量

自由变量与闭包的关系是,自由变量闭合于闭包的建立。闭包背后的逻辑是,若是一个函数内部有其余函数,那么这些内部函数能够访问在这个外部函数中声明的变量(这些变量就称之为自由变量)。然而,这些变量能够被内部函数捕获,从高阶函数(返回另外一个函数的函数称为高阶函数)中return语句实现“越狱”,以供之后使用。内部函数在没有任何局部声明以前(既不是被传入,也不是局部声明)使用的变量就是被捕获的变量。

function makeAdder(captured) {
    return function(free) {
        var ret = free + captured;
        console.log(ret);
    }
}

var add10 = makeAdder(10);

add10(2); // 输出:12

从上例可知,外部函数中的变量captured被执行加法的返回函数捕获,内部函数从未声明过captured变量,却能够引用它。

若是咱们再建立一个加法器将捕获到同名变量captured,但有不一样的值,由于这个加法器是在调用makeAdder以后被建立:

var add16 = makeAdder(16);
 
add16(18); // 输出:34
 
add10(10); // 输出:20
  • 变量遮蔽

在JavaScript中,当变量在必定做用域内声明,而后在另外一个同名变量在一个较低的做用域声明,会发生变量的遮蔽。

var name = "clam";
var name = "klay"

function glbShadow() {
    var name = "fun";

    console.log(name); // 输出:fun
}

glbShadow();

console.log(name); // 输出:tom

当在一个变量同一做用域内声明了屡次时,最后一次声明会生效,会遮蔽之前的声明。

变量声明的遮蔽很好理解,然而函数参数的遮蔽就略显复杂。

var shadowed = 0;

function argShadow(shadowed) {
    var str = ["Value is",shadowed].join(" ");
    console.log(str);
}

argShadow(108); // output:Value is 108

argShadow(); // output:Value is

函数argShadow的参数shadowed覆盖了全局做用域内的同名变量。即便没有传递任何参数,仍然绑定的是shadowed,并无访问到全局变量shadowed = 0

任何状况下,离得最近的变量绑定优先级最高。

var shadowed = 0;

function varShadow(shadowed) {
    var shadowed = 123;
    var str = ["Value is",shadowed].join(" ");
    console.log(str);
}

varShadow(108); // output:Value is 123

varShadow(); // output:Value is 123

varShadow(108)打印出来的并非108而是123,即便没有参数传入也是打印的123,先访问离得最近的变量绑定。

遮蔽变量一样发生在闭包内部

function captureShadow(shadowed) {

    console.log(shadowed); // output:108
    
    return function(shadowed) {

        console.log(shadowed); // output:2
        var ret = shadowed + 1;
        console.log(ret); // output:3
    }
}

var closureShadow = captureShadow(108);

closureShadow(2);

在编写JavaScript代码时,由于变量遮蔽会使不少变量绑定超出咱们的控制,咱们应尽可能避免变量遮蔽,必定要注意变量命名。

  • 典型的误区

首先看下面的代码:

var test = function() {
    var ret = [];

    for(var i = 0; i < 5; i++) {
        ret[i] = function() {
            return i;  
        }
    }

    return ret;
};
var test0 = test()[0]();
console.log(test0); // 输出:5

var test1 = test()[1]();
console.log(test1); //输出:5

从上面的例子可知,test这个函数执行以后返回一个函数数组,表面上看数组内的每一个函数都应该返回本身的索引值,然而并非如此。当外部函数执行完毕后,外部函数虽然其执行环境已经销毁,但闭包依然保留着对其中变量绑定的引用,仍然驻留在内存之中。当外部函数执行完毕以后,才会执行内部函数,而这时内部函数捕获的变量绑定已是外部函数执行以后的最终变量值了,因此这些函数都引用的是同一个变量i=5

// 更加优雅的描述方式
for(var i = 0; i < 5; i++) {

    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

// 每隔1秒输出一个5

按照咱们的推断,上例应该输出1,2,3,4,5。然而,事实上输出的是连续5个5。为何出现这种诡异的情况呢?其本质上仍是由闭包特性形成的,闭包能够捕获外部做用域的变量绑定。

上面这个函数片断在执行时,其内部函数和外部函数并非同步执行的,由于当调用setTimeout时会有一个延时事件排入队列,等全部同步代码执行完毕后,再依次执行队列中的延时事件,而这个时候 i 已经 是5了。

那怎么解决这个问题呢?咱们是否是能够在每一个循环执行时,给内部函数传进一个变量的拷贝,使其在每次建立闭包时,都捕获一个变量绑定。由于咱们每次传参不一样,那么每次捕获的变量绑定也是不一样的,也就避免了最后输出5个5的情况。实例以下:

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

    (function(j) {

        setTimeout(function() {
            console.log(j);  
        }, 1000);
    })(i);
}

// 输出:0,1,2,3,4

闭包具备很是强大的功能,函数内部能够引用外部的参数和变量,但其参数和变量不会被垃圾回收机制回,常驻内存,会增大内存使用量,使用不当很容易形成内存泄露。但,闭包也是javascript语言的一大特色,主要应用闭包场合为:设计私有的方法和变量

  • 模拟私有变量

从上文的叙述咱们知道,变量的捕获发生在建立闭包的时候,那么咱们能够把闭包捕获到的变量做为私有变量。

var closureDemo = (function() {
    var PRIVATE = 0;

    return {
        inc:function(n) {
            return PRIVATE += n;
        },
        dec:function(n) {
            return PRIVATE -= n;
        }
    };
})();

var testInc = closureDemo.inc(10);
//console.log(testInc);
// 输出:10

var testDec = closureDemo.dec(7);
//console.log(testDec);
// 输出:3

closureDemo.div = function(n) {
    return PRIVATE / n;
};

var testDiv = closureDemo.div(3);
console.log(testDiv);
//输出:Uncaught ReferenceError: PRIVATE is not defined

自执行函数closureDemo执行完毕以后,自执行函数做用域和PRIVATE随之销毁,但PRIVATE仍滞留在内存中,也就是加入到closureDemo.incclosureDemo.dec的做用域链中,闭包也就完成了变量的捕获。但以后新加入的closureDemo.div并不能在做用域中继续寻找到PRIVATE了。由于,函数只有被调用时才会执行函数里面的代码,变量的捕获也只发生在建立闭包时,因此以后新加入的div方法并不能捕获PRIVATE

  • 建立特权方法

经过闭包咱们能够建立私有做用域,那么也就能够建立私有变量和私有函数。建立私有函数的方式和声明私有变量方法一致,只要在函数内部声明函数就能够了。固然,既然能够模拟私有变量和私有函数,咱们也能够利用闭包这个特性,建立特权方法。

(function() {

    // 私有变量和私有函数
    var privateVar = 10;

    function privateFun() {
        return false;
    };

    // 构造函数
    MyObj = function() {

    };

    // 公有/特权方法
    MyObj.prototype.publicMethod = function() {
        privateVar ++;
        return privateFun();
    }
})();

上面这个实例建立了一个私有做用域,并封装了一个构造函数和对应的方法。须要注意的是在上面的实例中,在声明MyObj这个函数时,使用的是不带var的函数表达式,咱们但愿产生的是一个全局函数而不是局部的,否则咱们依然在外部没法访问。因此,MyObj就成为了一个全局变量,可以在外部进行访问,咱们在原型上定义的方法publicMethod也就可使用,经过这个方法咱们也就能够访问私有函数和私有变量了。

总的来讲,由于闭包奇特的特性,能够经过它实现一些强大的功能。但,咱们在平常编程中,也要正确的使用闭包,要时刻注意回收不用的变量,避免内存泄露。

总结

感谢原文做者,而且附上原文连接:JavaScript之做用域与闭包详解

花费了数小时来研读这篇分享,对javascript中的做用域和闭包的问题有了比较深入的认识和学习。在从此的编码过程当中,但愿能够将今天学到的东西及时应用,反复的巩固。

相关文章
相关标签/搜索