JavaScript 作用域和闭包理解

作用域:

分为函数作用域,和块级作用域;

函数作用域

函数作用域外面的无法访问函数作用域内部的变量和函数,这样就可以将一些变量和函数隐藏起来;

隐藏起来的好处是

  1. 形成命名空间,避免各个函数里面的变量冲突
  2. 实现模块管理

内部可以访问外部的;


function foo(a) { var b = 2;
    // 一些代码
    function bar() {
    // ...
    }
    // 更多的代码 var c = 3;
}

bar(); // 失败
console.log( a, b, c ); // 三个全都失败

此时,foo里面就是一个函数作用域,可以bar里面又是一个作用域;最外面当然就是全局作用域;

可以把函数看着一个可以单向向外访问的圈子;

函数表达式 vs 函数声明

这里需要重点区分一下:

  1. 函数声明: function 是声明中 的第一个词,那么就是一个函数声明;
  2. 函数表达式:除此之外就是函数表达式;
  3. 函数表达式可以是匿名的,函数声明不可以;

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处;

例子

var a = 2;
(function foo(){ // <-- 添加这一行 
    var a = 3;
    console.log( a ); // 3
})(); // <-- 以及这一行 
console.log( a ); // 2
  1. 这里的(function foo(){ .. })是一个函数表达式;而不是一 个标准的函数声明;
    所以,foo 被绑定在函数表达式自身的函数中而不是所在作用域中。

  2. 换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作 用域。

  3. 立即执行函数表达式:由于foo被包含在一对( )括号内部,因此成为了一个表达式;通过在末尾加上另外一个 ( ) 可以立即执行这个函数;即, (function foo(){ .. })()。第一个 ( ) 将函数变成表 达式,第二个 ( ) 执行了这个函数。

  4. 还可以穿参数

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3 
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2

这样就可以访问外面的a了,因为访问变量a的时候就近原则,就得到了3;

块级作用域

1. { }

var 定义的变量和函数,和在当前块级所处作用域定义没有什么区别;
let,const 在块级作用域外面就访问不到;

简单的说{ }这个就形成了一个块级作用域;

例子

{
    var a = 44;
    let b = 22;
    const c = 33
}
a // 44;
b // Uncaught ReferenceError: b is not defined;找不到引用,报错;
c // Uncaught ReferenceError: c is not defined;找不到引用,报错;

2. with

用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效。

3. try/catch

JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作
用域,其中声明的变量仅在 catch 内部有效。

闭包

无论论通过何种手段将内部函数传递到所在的作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

个人所理解的闭包就是一个作用域,及作用域内的变量和函数的缓存;不会被释放了,以供在今后访问;不得被垃圾回收掉

不知道大家是否还记得JavaScript的垃圾回收机制;

垃圾收集:就是执行完后,对没有引用的变量进行释放;常见手动释放就是设置成null;

例子1

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

分析:

  1. 函数 bar() 的作用域能够访问 foo() 的内部作用域。
    bar() 显然可以被正常执行。并且,它能在自己定义的作用域以外的地方 执行。
  2. 在 foo() 执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。
  3. 而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
  4. 拜 bar() 所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

例子2

function foo() { 
    var a = 2;
    function baz() {
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包!
}
foo(); // 2

等效如下:

var fn;
function foo() {
    var a = 2;
    function baz() { 
        console.log( a );
    }
    fn = baz; //将baz分配给全局变量 
}
function bar() {
    fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2

按正常的作用域思考方式,bar是没有办法访问foo的内部的变量的;

  1. 但是foo可以访问外部作用域下的bar;
  2. bar在foo内部;
  3. 将baz传递给bar的内部,baz无论在哪里都依然持有对foo内部变量的引用

baz 和 变量a,还有foo形成了一个闭包,这个作用域将被引擎缓存起来;baz随时都可以访问;

function foo(a) { 
    var b = 2;
    // 一些代码
    function bar() {
        // ...
    }
    // 更多的代码 
    var c = 3;
}
foo();
bar(); // 失败
console.log( a, b, c ); // 三个全都失败

这种就无法访问foo里面的变量和函数了,因为foo里面都是局部变量,外部无法直接访问,这种里面变量再会被其他地方引用,将会被引擎垃圾回收释放掉。

形成闭包的条件

  1. 一个函数foo包含一个函数baz和一个变量a;(名字随意)
  2. baz内部存在对a的引用;
  3. foo需要被执行;

正确例子示范

function wait(message) {
    setTimeout( function timer() {
        console.log( message );
    }, 1000 ); 
}

wait( "Hello, closure!" );

这就是个闭包;

  1. timer和message都在wait内部
  2. timer对wait的message有引用;
  3. wait被执行了

一般来说,只要使 用了回调函数,实际上就是在使用闭包!

错误例子示范

for (var i=1; i<=5; i++) { 
    (function() {
        setTimeout( function timer() { 
            console.log( i );
        }, i*1000 );
    })();
}
// 打印5次6;

这样不行!这只是一个都没有的空作用域。不能形成闭包

修改1:

for (var i=1; i<=5; i++) { 
    (function(j) {
        setTimeout( function timer() { 
            console.log( j );
        }, j*1000 );
    })(i);//从外部传进来
}

修改2:

for (let i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}
//块作用域和闭包联手便可天下无敌

应用——模块

模块有两个主要特征:

  1. 为创建内部作用域而调用了一个包装函数;
  2. 包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
MyModules.define( "bar", [], function() { 
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
     hello: hello
    }; 
} );

MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome(){
        console.log( bar.hello( hungry ).toUpperCase() )
    }
    return {
        awesome: awesome
    }; 
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO

"foo" 和 "bar" 模块通过一个返回公共 API 的函数来定义的。"foo" 甚至接受 "bar" 的 示例作为依赖参数,并能相应地使用它。

总结

最后记住:当函数可以记住并访问所在的作用域,即使函数是在当前作用域之外执行,这时 就产生了闭包。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容