1.全局作用域

所有浏览器都支持 window 对象,它表示浏览器窗口,JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。所以,全局变量是 window 对象的属性,全局函数是 window 对象的方法,甚至 HTML DOM 的 document 也是 window 对象的属性之一。

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

在全局下声明的变量都会在window对象下,都在全局作用域中,我们可以通过window对象访问,也可以直接访问。

1 var name = "jeri";
2 console.log(window.name); // 输出:jeri
3 console.log(name); // 输出:jeri

在JS中任何位置,没有使用var关键字声明的变量也都是全局变量。

1 function fun() {
2 name = "jeri";
3 alert(name);
4 }
5
6 console.log(name); // 输出:jeri

全局变量存在于整个函数的生命周期中,然而其在全局范围内很容易被篡改,我们在使用全局变量时一定要小心,尽量不要使用全局变量。在函数内部声明变量没有使用var也会产生全局变量,会为我们造成一些混乱,比如变量覆盖等。所以,我们在声明变量的任何时候最好都要带上var。

全局变量存在于程序的整个生命周期,但并不是通过其引用我们一定可以访问到全局变量。

2.词法作用域

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

1 var name = "global";
2
3 function fun() {
4 var name = "jeri";
5 return name;
6 }
7
8 console.log(fun()); // 输出:jeri
9 console.log(name); // 输出:global

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

3.动态作用域

在编程实践中,最容易低估和过度滥用的概念就是动态作用域,因为很少有语言支持这种方式为绑定解析方案。

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

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

 1 var name = "global";
2
3 // 使用with之前
4 console.log(name); // 输出:global
5
6 with({name:"jeri"}){
7 console.log(name); // 输出:jeri
8 }
9
10 // 使用with之后,作用域链恢复
11 console.log(name); // 输出:global

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

1 function globalThis() {
2 console.log(this);
3 }
4
5 globalThis(); // 输出:Window {document: document,external: Object…}
6 globalThis.call({name:"jeri"}); // 输出:Object {name: "jeri"}
7 globalThis.apply({name:"jeri"},[]); // 输出:Object {name: "jeri"}

因为this引用是动态作用域,所以在编程过程中一定要注意this引用的变化,及时跟踪this的变动。

4.函数作用域

函数作用域,顾名思义就是在定义函数时候产生的作用域,这个作用域也可以称为局部作用域。和全局作用域相反,函数作用域一般只在函数的代码片段内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数作用域中,其生命周期随着函数的执行结束而结束。实例如下:

 1 var name = "global";
2
3 function fun() {
4 var name = "jeri";
5 console.log(name); // 输出:jeri
6
7 with ({name:"with"}) {
8 console.log(name); // 输出:with
9 }
10 console.log(name); // 输出:jeri
11 }
12
13 fun();
14
15 // 不能访问函数作用域
16 console.log(name); // 输出:global

5.没有块级作用域

不同于其他编程语言,在JavaScript里并没有块级作用域,也就是说在for、if、while等语句内部的声明的变量与在外部声明是一样的,在这些语句外部也可以访问和修改这些变量的值。实例如下:

 1 function fun() {
2
3 if(0 < 2) {
4 var name = "jeri";
5 }
6 console.log(name); // 输出:jeri
7 name = "change";
8 console.log(name); // 输出:change
9 }
10
11 fun();

6.作用域链

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

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

按 Ctrl+C 复制代码
按 Ctrl+C 复制代码

7.闭包

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

一句话概括就是:闭包就是一个函数,捕获作用域内的外部绑定。这些绑定是为之后使用而被绑定,即使作用域已经销毁。

自由变量

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

 1 function makeAdder(captured) {
2 return function(free) {
3 var ret = free + captured;
4 console.log(ret);
5 }
6 }
7
8 var add10 = makeAdder(10);
9
10 add10(2); // 输出:12

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

如果我们再创建一个加法器将捕获到同名变量captured,但有不同的值,因为这个加法器是在调用makeAdder之后被创建:

1 var add16 = makeAdder(16);
2
3 add16(18); // 输出:34
4
5 add10(10); // 输出:20

如上述代码所示,每一个新的加法器函数都保留了自己创建时捕获的captured实例。

变量遮蔽

在JavaScript中,当变量在一定作用域内声明,然后在另一个同名变量在一个较低的作用域声明,会发生变量的遮蔽。实例如下:

 1 var name = "jeri";
2 var name = "tom";
3
4 function glbShadow() {
5 var name = "fun";
6
7 console.log(name); // 输出:fun
8 }
9
10 glbShadow();
11
12 console.log(name); // 输出:tom

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

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

 1 var shadowed = 0;
2
3 function argShadow(shadowed) {
4 var str = ["Value is",shadowed].join(" ");
5 console.log(str);
6 }
7
8 argShadow(108); // output:Value is 108
9
10 argShadow(); // output:Value is

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

任何情况下,离得最近的变量绑定优先级最高。实例如下:

 1 var shadowed = 0;
2
3 function varShadow(shadowed) {
4 var shadowed = 123;
5 var str = ["Value is",shadowed].join(" ");
6 console.log(str);
7 }
8
9 varShadow(108); // output:Value is 123
10
11 varShadow(); // output:Value is 123

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

遮蔽变量同样发生在闭包内部,实例如下:

 1 function captureShadow(shadowed) {
2
3 console.log(shadowed); // output:108
4
5 return function(shadowed) {
6
7 console.log(shadowed); // output:2
8 var ret = shadowed + 1;
9 console.log(ret); // output:3
10 }
11 }
12
13 var closureShadow = captureShadow(108);
14
15 closureShadow(2);

在编写JavaScript代码时,因为变量遮蔽会使很多变量绑定超出我们的控制,我们应尽量避免变量遮蔽,一定要注意变量命名。

典型误区

下面是一个非常典型的问题,曾经困扰了很多人,下面也来探讨下。

 1 var test = function() {
2 var ret = [];
3
4 for(var i = 0; i < 5; i++) {
5 ret[i] = function() {
6 return i;
7 }
8 }
9
10 return ret;
11 };
12 var test0 = test()[0]();
13 console.log(test0); // 输出:5
14
15 var test1 = test()[1]();
16 console.log(test1); //输出:5

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

下面有个更优雅的例子来表述这个问题:

1 for(var i = 0; i < 5; i++) {
2
3 setTimeout(function() {
4 console.log(i);
5 }, 1000);
6 }
7
8 // 每隔1秒输出一个5

按照我们的推断,上例应该输出1,2,3,4,5。然而,事实上输出的是连续5个5。为什么出现这种诡异的状况呢?其本质上还是由闭包特性造成的,闭包可以捕获外部作用域的变量绑定。

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

那怎么解决这个问题呢?我们是不是可以在每个循环执行时,给内部函数传进一个变量的拷贝,使其在每次创建闭包时,都捕获一个变量绑定。因为我们每次传参不同,那么每次捕获的变量绑定也是不同的,也就避免了最后输出5个5的状况。实例如下:

 1 for(var i = 0; i < 5; i++) {
2
3 (function(j) {
4
5 setTimeout(function() {
6 console.log(j);
7 }, 1000);
8 })(i);
9 }
10
11 // 输出:0,1,2,3,4

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

模拟私有变量

从上文的叙述我们知道,变量的捕获发生在创建闭包的时候,那么我们可以把闭包捕获到的变量作为私有变量。实例如下:

 1 var closureDemo = (function() {
2 var PRIVATE = 0;
3
4 return {
5 inc:function(n) {
6 return PRIVATE += n;
7 },
8 dec:function(n) {
9 return PRIVATE -= n;
10 }
11 };
12 })();
13
14 var testInc = closureDemo.inc(10);
15 //console.log(testInc);
16 // 输出:10
17
18 var testDec = closureDemo.dec(7);
19 //console.log(testDec);
20 // 输出:3
21
22 closureDemo.div = function(n) {
23 return PRIVATE / n;
24 };
25
26 var testDiv = closureDemo.div(3);
27 console.log(testDiv);
28 //输出:Uncaught ReferenceError: PRIVATE is not defined

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

创建特权方法

通过闭包我们可以创建私有作用域,那么也就可以创建私有变量和私有函数。创建私有函数的方式和声明私有变量方法一致,只要在函数内部声明函数就可以了。当然,既然可以模拟私有变量和私有函数,我们也可以利用闭包这个特性,创建特权方法。实例如下:

 1 (function() {
2
3 // 私有变量和私有函数
4 var privateVar = 10;
5
6 function privateFun() {
7 return false;
8 };
9
10 // 构造函数
11 MyObj = function() {
12
13 };
14
15 // 公有/特权方法
16 MyObj.prototype.publicMethod = function() {
17 privateVar ++;
18 return privateFun();
19 }
20 })();

上面这个实例创建了一个私有作用域,并封装了一个构造函数和对应的方法。需要注意的是在上面的实例中,在声明MyObj这个函数时,使用的是不带var的函数表达式,我们希望产生的是一个全局函数而不是局部的,不然我们依然在外部无法访问。所以,MyObj就成为了一个全局变量,能够在外部进行访问,我们在原型上定义的方法publicMethod也就可以使用,通过这个方法我们也就可以访问私有函数和私有变量了。

总的来说,因为闭包奇特的特性,可以通过它实现一些强大的功能。但,我们在日常编程中,也要正确的使用闭包,要时刻注意回收不用的变量,避免内存泄露。

下面一些是简单的总结:

闭包

  • 理解闭包首先要理解,js垃圾回收机制,也就是当一个函数被执行完后,其作用域会被收回,如果形成了闭包,执行完后其作用域就不会被收回。

  • 如果某个函数被他的父函数之外的一个变量引用,就会形成闭包

  • 闭包的作用,就是保存自己私有的变量,通过提供的接口(方法)给外部使用,但外部不能直接访问该变量

  • 例子(使用闭包):

     
    var test=(function(){
    var a=0;
    return function(){
    ++a
    console.info(a);
    }
    })()
    test();// 1
    test();// 2
    test();//

    例子(不使用闭包):

    var test=function(){
    var a=0;
    ++a
    console.info(a); }
    test();// 1
    test();// 1
    test();//

    上面例子形成了闭包,函数第一次执行完后,作用域没有被回收机制销毁,所以变量a也被保存下来了,每次调用test,执行后a都会自加1。如果没有形成闭包,每次调用test,执行后a都会销毁染后重新创建,执行结果都为1.

作用域

简单的说,作用域是针对变量的,比如我们创建一个函数a1,函数里面又包了一个子函数a2。此时就存在三个作用域:

全局作用域-a1作用域-a2作用域;即全局作用域包含了a1的作用域,a2的作用域包含了a1的作用域。

当a1在查找变量的时候会先从自身的作用域区查找,找不到再到上一级a2的作用域查找,如果还没找到就到全局作用域区查找,这样就形成了一个作用域链。

原型

javascript中一切皆对象,每个对象都有一个__proto_ 属性(由浏览器自动创建),该属性指向它原型。当一个对象在查找一个属性的时,自身没有就会根据__proto__ 向它的原型进行查找,如果都没有,直到查到Object.prototype._proto_为nul,这样也就形成了原型链。

那prototype又是什么?!我们知道可以通过构造函数来创造一个类,prototype的作用就是,让类的属性可以被继承。所以只有构造函数才会有prototype这个属性。

下面直接用代码来验证下:

function Foo(){
this.name='xiaoming'
}
var foo=new Foo();
foo.age="22"

console.info(foo.name)//xiaoming ; foo集成了Foo的name属性
console.info(foo.__proto__)//object() ; 指向了她的原型Foo.prototype
console.info(foo.__proto__ === Foo.prototype)//true
console.info(foo.prototype)//undefine ; 只有构造函数才有prototype属性
console.info(foo.constructor)//function Foo(){……} ; 其实foo并没有constructor属性,他只是继承了原型中的consturctor属性
console.info(foo.constructor === Foo.prototype.constructor)//true

JavaScript作用域与闭包总结的更多相关文章

  1. javascript作用域和闭包之我见

    javascript作用域和闭包之我见 看了<你不知道的JavaScript(上卷)>的第一部分--作用域和闭包,感受颇深,遂写一篇读书笔记加深印象.路过的大牛欢迎指点,对这方面不懂的同学 ...

  2. JavaScript 作用域和闭包——另一个角度:扩展你对作用域和闭包的认识【翻译+整理】

    原文地址 --这篇文章有点意思,可以扩展你对作用域和闭包的认识. 本文内容 背景 作用域 闭包 臭名昭著的循环问题 自调用函数(匿名函数) 其他 我认为,尝试向别人解释 JavaScript 作用域和 ...

  3. JavaScript作用域和闭包

    在JavaScript中,作用域是执行代码的上下文.作用域有3种类型: 1.全局作用域 2.局部作用域---(又叫函数作用域) 3.eval作用域 var foo =0;//全局作用域console. ...

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

    转自:http://www.cnblogs.com/mrsunny/archive/2011/11/03/2233978.html 这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存 ...

  5. JavaScript 作用域和闭包

    作用域的嵌套将形成作用域链,函数的嵌套将形成闭包.闭包与作用域链是 JavaScript 区别于其它语言的重要特性之一. 作用域 JavaScript 中有两种作用域:函数作用域和全局作用域. 在一个 ...

  6. javascript作用域、闭包、对象与原型链

    原文作者总结得特别好,自己收藏一下.^-^ 1.作用域1.1函数作用域JS的在函数中定义的局部变量只对这个函数内部可见,称之谓函数作用域.它没有块级作用域(因此if.for等语句中的花括号不是独立作用 ...

  7. javascript作用域与闭包

    Javasript作用域概要 在javascript中,作用域是执行代码的上下文,作用域有三种类型: 1)  全局作用域 2)  局部作用域(函数作用域) 3)  eval作用域 var foo = ...

  8. javascript——作用域与闭包

    http://www.cnblogs.com/lucio-yr/p/4047972.html 一.作用域: 在函数内部:用 var 声明的表示局部变量,未用var的是全局变量. 作用域取决于变量定义时 ...

  9. 《JavaScript 闯关记》之作用域和闭包

    作用域和闭包是 JavaScript 最重要的概念之一,想要进一步学习 JavaScript,就必须理解 JavaScript 作用域和闭包的工作原理. 作用域 任何程序设计语言都有作用域的概念,简单 ...

随机推荐

  1. Ajax的学习

    AJAX的学习 AJAX的简介 AJAX即“Asynchronous Javascript And XML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术. AJAX ...

  2. Mybatis设计模式

    mybatis中使用到的设计模式 Mybatis 使用的 9 种设计模式 建造者模式(Configuration) 构造者模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不 ...

  3. navicat远程连接mysql错误

    确保端口与服务正常. 如果都正常,那么服务有可能没有添加远程访问. 本人链接报错10060,那么问题出在阿里云的主机默认不开放3306端口,那么在安全组策略中加入对应的端口   登录后输入 use m ...

  4. Ubuntu 系统信息相关命令

    系统信息相关命令 本节内容主要是为了方便通过远程终端维护服务器时,查看服务器上当前 系统日期和时间 / 磁盘空间占用情况 / 程序执行情况 本小结学习的终端命令基本都是查询命令,通过这些命令对系统资源 ...

  5. FTP下载文件和操作系统的关系

    标题不知道该怎么写了. 最近调试AGPS,嵌入式设备需要从FTP服务器上下载星历数据,星历数据是二进制数据.嵌入式设备下载完数据后和原始数据对比,发现数据量变大了(但是通过pc端的FTP软件下载下来的 ...

  6. DNS原理极限剖析

    how does DNS server work DNS Root Servers: The most critical infrastructure on the internet What is ...

  7. RT-Thread--内核基础

    内核介绍 内核处于硬件层之上,内核部分包括内核库.实时内核实现. 实时内核的实现包括:对象管理.线程管理及调度器.线程间通信管理.时钟管理及内存管理等等,内核最小的资源占用情况是 3KB ROM,1. ...

  8. mysql workbench使用技巧,使用workbench导出部分表

    最近在刚开始用workbench导出数据的时候,需要导出部分表数据,找来半天找不到,原来是选中库之后,不要要点右边的字母,然后表才显示出来 点左边的对勾的话,右边的表是不会显示出来的!

  9. 【转】Golang关于channel传递引用引发的坑

    原文: https://studygolang.com/articles/12310/comment/17923 ------------------------------------------- ...

  10. Lua 学习之基础篇四<Lua table(表)>

    table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组.字典等. Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil. Lua ta ...