JavaScript--作用域和闭包

--摘自《You Don‘t Know JS- Scope, Closures》

对于所有的编程语言,作用域是一个基础的概念。深入了解JavaScript中的作用域,对正确的使用这个语言有重要的作用。

什么是作用域

作用域是一组变量如何存储和读取的规则,存在两类模型:

  1. 静态作用域(也称作字面作用域、词法作用域)。

  2. 动态作用域。

作用域的操作

对作用域有两类操作:读操作,写操作。

在编译原理中被读取的操作数叫右操作数(RHS),被修改的操作数叫做操作数(LHS)。

这种命名来自赋值表达式: a = b;

a在左为LHS,b在右为RHS。

静态作用域

JavaScript中变量的作用域为静态作用域。

静态作用域在代码书写时决定,并且是可嵌套的。

如下所示,代码片段中存在三个作用域,作用域间存在严格的嵌套关系:

作用域3是作用域2的子集,作用域2是作用域1的子集。


function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }

    bar(b * 3);
}

foo( 2 ); // 2 4 12


变量的查找

JavaScript执行引擎从当前作用域开始查询操作数(LHS,RHS),若操作数不在当前作用域,则向上一级作用查找,直至全局作用域。

若全局作用域仍未找到操作数,则查询失败。

因此上例代码的输出为 2 4 12。

操作数查询失败的行为

RHS查询失败,抛出ReferenceError异常。

LHS查询失败,则在全局作用域中创建同名变量。(strict模式中,抛出ReferenceError异常)。

HACK作用域

Hack作用域是不推荐的行为,但这有助于深入了解JavaScript的作用域。通过evalwith关键字,可以修改执行代码的作用域。

eval修改已存在的作用域

eval中的代码运行在当前作用域中,并修改或访问当前作用域中可查询到的操作数。

如下所示:

function foo(str, a) {
    eval( str ); // cheating!
    console.log( a, b );
} 

var b = 2; 

foo( "var b = 3;", 1 ); // 1, 3

"var b = 3;"运行在 function foo() 的作用域中,并在此作用域中声明了变量b。

function foo()作用域中的变量b覆盖了全局作用域中的b,因此程序输出为1,3。

strict模式中,eval中的代码运行在新的作用域中,此作用域为当前作用域的子集。

如下所示:

function foo(str) {
    "use strict";
    eval( str );
    console.log( a ); // ReferenceError: a is not defined
} 

foo( "var a = 2" );

"var a = 2;"运行在独立的作用域中,此作用域为function foo()作用域的子集,因此console.log(a)在查询右操作数a时,抛出ReferenceError异常。

with创建新的作用域

with关键字为对象创建一个独立的作用域,此作用域为当前作用域的子集。

function foo(obj) {
    with (obj) {
        a = 2;
    }
} 

var o1 = {
    a: 3
}; 

var o2 = {
    b: 3
}; 

foo( o1 );
console.log( o1.a ); // 2 

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- Oops, leaked global!

foo(o1)在o1作用域中查找做操作数a并修改其值,因此console.log(o1.a)输出为2。

foo(o2)在o2作用域中查找做操作数a, 查询失败。继续向上级作用域function foo()中查找,同样失败。全局作用域同样没有做操左数a的定义。

跟据左操作数查询规则,JavaScript执行引擎在全局作用域中创建新的变量a,并赋值为2,出现了作用域泄漏的现象。

函数作用域

ES3之前,函数是创建作用域的唯一途径。每个函数都会创建一个作用域,并嵌套在当前作用域中。

函数作用域中定义的操作数可在此函数任何位置使用,也可在其子作用域中使用,但上级作用域中不可使用。

因此函数作用域可作为命名空间使用,防止多模块的命名冲突。

如下所示:

function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    } 

    var b = a + doSomethingElse( a * 2 ); 

    console.log( (b * 3) );
} 

doSomething( 2 ); // 15

function doSomethingElse()仅在function doSomething()作用域可访问,函数作用域起到了封装的作用。

ES3之前函数是作用域的最小单位。函数中任何位置创建的操作数,其作用域都为此函数。

如下所示:

function foo() {
    function bar(a) {
        i = 3; // changing the `i` in the enclosing scope‘s for-loop
        console.log( a + i );
    } 

    for (var i=0; i<10; i++) {
        bar( i * 2 ); // oops, infinite loop ahead!
    }
}

这段代码将陷入死循环。虽然"var i = 0"是在for循环中声明,因为函数是作用域的最小单位,i的作用域为function foo()。

function bar()将i修改为3,导致了死循环的发生。

函数表达式

函数创建的作用域对其内部起到了封装作用,防止了命名冲突。但因此函数名便成了命名冲突的主要来源。

函数表达式解决了函数名冲突的问题。

如何区分函数定义和函数表达式

整个语句以function关键字开始,则此语句为函数定义。

反之,function标识的为函数表达式。

如下所示:

function foo1() {....} //函数定义
(function foo2(){....}) //函数表达式

函数表达式中函数名的作用域

函数表达式创建了一个新的作用域,此作用域为当前作用域的子集。

以(function foo(){ … })为例, 此函数表达式中的函数名foo,仅在"…"所示位置中可被访问。

块作用域

ES3中规定,try/catch中catch语句定义的变量,作用域为此catch语句。

如下所示:

try {
    undefined(); // illegal operation to force an exception!
}
catch (err) {
    console.log( err ); // works!
} 

console.log( err ); // ReferenceError: `err` not founde

err的作用域仅为catch语句,因此console.log(err)抛出ReferenceError异常。

let关键字

ES6中引入了let关键字。let与var都用于定义变量。

区别是:

  1. var定义的变量作用域为函数,let定义的变量作用域为代码块。

  2. var定义出现的位置不影响作用域中对其引用,let定义的变量仅在定义后才可引用。

如下所示:

var foo = true;
if (foo) {
    { // <-- explicit block
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
}
console.log( bar ); // ReferenceError
{
   console.log( bar ); // ReferenceError!
   let bar = 2;
}

let循环

let在for循环中定义变量时,此变量作用域为for循环。

如下所示:

for (let i=0; i<10; i++) {
    console.log( i );
}
console.log( i ); // ReferenceError

let将i的作用域绑定到for循环,并且每次循环绑定一次,可以用如下代码说明:

{
    let j;
    for (j=0; j<10; j++) {
        let i = j; // re-bound for each iteration!
        console.log( i );
    }
}

const

ES6中引入的const关键字所定义的变量,作用域为代码块。

与let关键字不同的是,const定义的是常量,不可修改。

如下所示:

var foo = true;
if (foo) {
    var a = 2;
    const b = 3; // block-scoped to the containing `if`
    a = 3; // just fine!
    b = 4; // error!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!

闭包

什么是闭包

闭包是函数在其静态作用域外被执行时,仍能访问其上级作用域的能力。

如下所示:

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); // 2 -- Whoa, closure was just observed, man.

foo()函数返回了其内部函数bar()。

函数bar()在其静态作用域之外被调用(baz()),仍能访问其上级作用域中的变量a。

这种行为能力称为闭包。

可以说:bar()的闭包包含了foo()的作用域。原文为:function bar() has a closure over the scope of foo()).

闭包和循环

当闭包和循环纠结到一起时,情况就变的有意思了。

现在实现一段代码输出1 2 3 4 5,每次输出的时间间隔为1秒。

请看如下代码:

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

函数timer()访问了其上级作用域中的变量i,timer()由计时器调用,调用处在其静态作用域外,因此构成了闭包。

代码的输出为五个6。

原因是计时器最早在1秒后运行,此时循环已经执行结束,i的值变为6。由于i的作用域为for循环所在的函数,因此timer()五次执行都获取到了i的最新值。

立即调用函数表达式(IIFE)能解决这个问题吗

函数表达式会创建一个新的作用域,但能解决这个问题吗?

请看如下代码:

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

执行后结果仍为五个6。

原因是此函数表达式虽然创建了一个新的作用域,但作用域为空,最终timer()函数还是引用了更上一级作用域中的i。

那如果在函数表达式作用域内部存储i的当前值呢?

请看如下代码:

for (var i=1; i<=5; i++) {
    (function(){
        var j = i;
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })();
}

执行后结果为1 2 3 4 5

因为函数表达式创建了一个新的作用域,五次循环产生了这个作用域的5个副本,每个副本里j存储了i的不同值。

timer()函数在作用域的不同副本中执行,并打印出不同值。

上述代码更简洁的写法为:

for (var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })( i );
}

let关键字能解决这个问题吗

let的作用域是块,可以解决这个问题吗?

请看如下代码:

for (var i=1; i<=5; i++) {
    let j = i; // yay, block-scope for closure!
    setTimeout( function timer(){
        console.log( j );
    }, j*1000 );
}

执行结果为1 2 3 4 5。

因为做域是块,所以五次循环的五个副本中j的值不同,因此输出值不同。

当用let定义循环变量时,此变量的作用域为for循环,并且每次循环都会绑定一次作用域(产生一个副本),因此上述代码可进一步精简为:

for (let i=1; i<=5; i++) {
    setTimeout( function timer(){
        console.log( i );
    }, i*1000 );
}
时间: 2024-10-31 14:19:59

JavaScript--作用域和闭包的相关文章

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

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

javascript作用域与闭包

Javasript作用域概要 在javascript中,作用域是执行代码的上下文,作用域有三种类型: 1)  全局作用域 2)  局部作用域(函数作用域) 3)  eval作用域 var foo = 0; //全局作用域 console.log(foo);//输出0 var myFunction = function() { var foo = 1; //局部作用域 console.log(foo); //输出1 var myNestedFunction = function() { var f

javaScript——作用域和闭包概念

js是函数级别作用域,在内部的变量,内部能都访问到,外部不能访问内部的,内部的可以访问外部的 闭包就是,拿到本不是应该属于他的东西. 当在函数内部定义了其他函数时,就创建了闭包,闭包有权访问包含函数内部的所有变量,原理如下: 1:在后台执行环境中,闭包的作用域链包含着他自己的作用域,包含函数的作用域和全局作用域. 2:通常,函数的作用域以及所有变量都会在函数执行结束后销毁. 3:但是,当函数返回了一个闭包的时候,这个函数的作用域就会一直再内存中保存,知道闭包不存在为止.

javascript作用域和闭包

:当定义一个独立函数(级不绑定于任何对象)时,this关键字绑定于全局名称空间.作为一个最直接的结果,当在一个方法内创建一个内部函数时,内部函数的this关键字将绑定于全局名称空间,而不是绑定于该方法.为了解决这一问题,可以将包裹方法的this关键字简单地赋值给一个名为that的中间变量. obj = {}; obj.method = function(){ var that = this; this.counter = 0; var count = function(){ that.count

20170917 前端开发周报:JavaScript函数式编程、作用域和闭包

1.用函数式编程对JavaScript进行断舍离 当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!! https://juejin.im/entry/59b86... 2.JavaScript作用域和闭包 作用域和闭包在JavaScript里非常重要.但是在我最初学习JavaScript的时候,却很难理解.这篇文章会用一些例子帮你理解它们.我们先从作用域开始.作用域 JavaScript的作用域限定了你可以访问哪些变量.有两种作

JavaScript函数,作用域以及闭包

JavaScript函数,作用域以及闭包 1. 函数 (1). 函数定义:函数使用function关键字定义,它可以用在函数定义表达式或者函数声明定义. a. 函数的两种定义方式: * function functionName() {} * var functionName = function(){} b. 两种函数定义不同之处 1). 声明提前问题 函数声明语句   :声明与函数体一起提前 函数定义表达式 :声明提前,但是函数体不会提前 请看下面图示:绿色线上面实在js初始加载的时候,查看

JavaScript从作用域到闭包

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

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

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

javascript中的闭包、模仿块级作用域和私有变量

闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见方式为:在一个函数内部创建另一个函数. "当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链.然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object).但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象出于第三位.....直至作用域链终点的全局执行环境." function creawteCompariso

javascript 作用域 闭包 对象 原理和示例分析(上)

                                                                                             阅读.理解.思考.实践,再实践.再思考....  深圳小地瓜献上 javascript高级特性包含:作用域.闭包.对象 -----------------------------------------------作用域-----------------------------------------------