深入理解javascript作用域和闭包

作用域

作用域是一个变量和函数的作用范围,javascript中函数内声明的所有变量在函数体内始终是可见的,在javascript中有全局作用域和局部作用域,但是没有块级作用域,局部变量的优先级高于全局变量,通过几个示例来了解下javascript中作用域的那些“潜规则”(这些也是在前端面试中经常问到的问题)。

1. 变量声明提前
示例1:

var scope="global";
function scopeTest(){
  console.log(scope);
  var scope="local"
}
scopeTest(); //undefined

此处的输出是undefined,并没有报错,这是因为在前面我们提到的函数内的声明在函数体内始终可见,上面的函数等效于:

var scope="global";
function scopeTest(){
  var scope;
  console.log(scope);
  scope="local"
}
scopeTest(); //local

注意,如果忘记var,那么变量就被声明为全局变量了。

2. 没有块级作用域

和其他我们常用的语言不同,在Javascript中没有块级作用域:

function scopeTest() {
  var scope = {};
  if (scope instanceof Object) {
    var j = 1;
    for (var i = 0; i < 10; i++) {
      //console.log(i);
    }
    console.log(i); //输出10
  }
  console.log(j);//输出1

}

在javascript中变量的作用范围是函数级的,即在函数中所有的变量在整个函数中都有定义,这也带来了一些我们稍不注意就会碰到的“潜规则”:

var scope = "hello";
function scopeTest() {
  console.log(scope);//①
  var scope = "no";
  console.log(scope);//②
}

在①处输出的值竟然是undefined,简直丧心病狂啊,我们已经定义了全局变量的值啊,这地方不应该为hello吗?其实,上面的代码等效于:

var scope = "hello";
function scopeTest() {
  var scope;
  console.log(scope);//①
  scope = "no";
  console.log(scope);//②
}

声明提前、全局变量优先级低于局部变量,根据这两条规则就不难理解为什么输出undefined了。

作用域链

在javascript中,每个函数都有自己的执行上下文环境,当代码在这个环境中执行时,会创建变量对象的作用域链,作用域链是一个对象列表或对象链,它保证了变量对象的有序访问。
作用域链的前端是当前代码执行环境的变量对象,常被称之为“活跃对象”,变量的查找会从第一个链的对象开始,如果对象中包含变量属性,那么就停止查找,如果没有就会继续向上级作用域链查找,直到找到全局对象中:

作用域链的逐级查找,也会影响到程序的性能,变量作用域链越长对性能影响越大,这也是我们尽量避免使用全局变量的一个主要原因。

闭包

基础概念
作用域是理解闭包的一个前提,闭包是指在当前作用域内总是能访问外部作用域中的变量。

function createClosure(){
  var name = "jack";
  return {
    setStr:function(){
      name = "rose";
    },
    getStr:function(){
      return name + ":hello";
    }
  }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面的示例在函数中返回了两个闭包,这两个闭包都维持着对外部作用域的引用,因此不管在哪调用总是能够访问外部函数中的变量。在一个函数内部定义的函数,会将外部函数的活跃对象添加到自己的作用域链中,因此上面实例中通过内部函数能够访问外部函数的属性,这也是javascript模拟私有变量的一种方式。

注意:由于闭包会额外的附带函数的作用域(内部匿名函数携带外部函数的作用域),因此,闭包会比其它函数多占用些内存空间,过度的使用可能会导致内存占用的增加。

闭包中的变量

在使用闭包时,由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。

  //该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题
  function timeManage() {
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      },1000)
    };
  }

上面的程序并没有按照我们预期的输入1-5的数字,而是5次全部输出了5。再来看一个示例:

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

调用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。通过以上两个例子可以看出闭包在带有循环的内部函数使用时存在的问题:因为每个函数的作用域链中都保存着对外部函数(timeManage、createClosure)的活跃对象,因此,他们都引用着同一变量i,当外部函数返回时,此时的i值为5,所以内部的每个函数i的值也为5。
那么如何解决这个问题呢?我们可以通过匿名包裹器(匿名自执行函数表达式)来强制返回预期的结果:

function timeManage() {
  for (var i = 0; i < 5; i++) {
    (function(num) {
      setTimeout(function() {
        console.log(num);
      }, 1000);
    })(i);
  }
}

或者在闭包匿名函数中再返回一个匿名函数赋值:

function timeManage() {
  for (var i = 0; i < 10; i++) {
    setTimeout((function(e) {
      return function() {
        console.log(e);
      }
    })(i), 1000)
  }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        console.log(num);
      }
    }(i);
  }
  return result;
}
//createClosure()[1]()输出1;createClosure()[2]()输出2

无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

闭包中的this

在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    return function(){
      return this.scope;
    }
  }
}

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。

幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    var that = this;
    return function(){
      return that.scope;
    }
  }
}
object.getScope()()返回值为local。

内存与性能

由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:

 function bindEvent(){
  var target = document.getElementById("elem");
  target.onclick = function(){
    console.log(target.name);
  }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:

 function bindEvent(){
  var target = document.getElementById("elem");
  var name = target.name;
  target.onclick = function(){
    console.log(name);
  }
  target = null;
 }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。

(0)

相关推荐

  • 浅谈JavaScript中的作用域和闭包问题

    JavaScript的作用域以函数为界,不同的函数拥有相对独立的作用域.函数内部可以声明和访问全局变量,也可以声明局部变量(使用var关键字,函数的参数也是局部变量),但函数外部无法访问内部的局部变量: function test() { var a = 0; // 局部变量 b = 1; // 全局变量 } a = ?, b = ? // a为undefined,b为1 同名的局部变量会覆盖全局变量,但本质上它们是两个独立的变量,一方发生变化不会影响另一方: a = 5; // 函数外a的值为

  • 浅谈JavaScript作用域和闭包

    作用域和闭包在JavaScript里非常重要.但是在我最初学习JavaScript的时候,却很难理解.这篇文章会用一些例子帮你理解它们. 我们先从作用域开始. 作用域 JavaScript的作用域限定了你可以访问哪些变量.有两种作用域:全局作用域,局部作用域. 全局作用域 在所有函数声明或者大括号之外定义的变量,都在全局作用域里. 不过这个规则只在浏览器中运行的JavaScript里有效.如果你在Node.js里,那么全局作用域里的变量就不一样了,不过这篇文章不讨论Node.js. `const

  • JavaScript 变量作用域及闭包第1/2页

    实例一: 复制代码 代码如下: <script type="text/javascript"> var i = 1; // 弹出内容为 1 true 的提示框 alert(window.i + ' ' + (window.i == i)); </script> 分析: 在全局定义的变量其实就是 window 对象的属性. 上面的例子可以看到,我们定义全局变量的同时,window 对象会产生一个相应的属性,如何让我们的代码避免产生这个属性呢,看下面的例子. 实例二

  • javascript中的作用域和闭包详解

    一.JavaScript作用域 JavaScript变量实际上只有两种作用域,全局变量和函数的内部变量.在函数内部任何一个地方定义的变量(var scope)其作用域都是整个函数体. 全局变量:指的是window对象下的对象属性. 作用域划分:基于上下文,以函数进行划分的,而不是由块划分的. 强调两点: 1. 在同一作用域中,JavaScript是允许变量的重复定义,并且后一个定义将覆盖前一个定义. 2. 函数内部如果不加关键字var而定义的变量,默认为全局变量. var scope="glob

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • JavaScript中的作用域链和闭包

    作用域 全局作用域 局部作用域 作用域链 执行上下文 活动对象 闭包 闭包优化 JavaScript中出现了一个以前没学过的概念--闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情形拥有全局作

  • javascript作用域和闭包使用详解

    作用域的嵌套将形成作用域链,函数的嵌套将形成闭包.闭包与作用域链是 JavaScript 区别于其它语言的重要特性之一. 作用域JavaScript 中有两种作用域:函数作用域和全局作用域. 在一个函数中声明的变量以及该函数的参数享有同一个作用域,即函数作用域.一个简单的函数作用域的例子: 复制代码 代码如下: function foo() {    var bar = 1;    {        var bar = 2;    }    return bar; // 2} 不同于C等其它有块

  • 深入理解javascript作用域和闭包

    作用域 作用域是一个变量和函数的作用范围,javascript中函数内声明的所有变量在函数体内始终是可见的,在javascript中有全局作用域和局部作用域,但是没有块级作用域,局部变量的优先级高于全局变量,通过几个示例来了解下javascript中作用域的那些"潜规则"(这些也是在前端面试中经常问到的问题). 1. 变量声明提前 示例1: var scope="global"; function scopeTest(){ console.log(scope); v

  • 深入理解JavaScript作用域和作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global Sc

  • 深入理解javascript作用域第二篇之词法作用域和动态作用域

    前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作模型导致的,作用域分为词法作用域和动态作用域,分清这两种作用域模型就能够对变量查找过程有清晰的认识.本文是深入理解javascript作用域系列第二篇--词法作用域和动态作用域 词法作用域 第一篇介绍过,编译器的第一个工作阶段叫作分词,就是把由字符组成的字符串分解成词法单元.这个概念是理解词法作用域

  • JavaScript作用域、闭包、对象与原型链概念及用法实例总结

    本文实例讲述了JavaScript作用域.闭包.对象与原型链概念及用法.分享给大家供大家参考,具体如下: 1 JavaScript变量作用域 1.1 函数作用域 没有块作用域:即作用域不是以{}包围的,其作用域完成由函数来决定,因而if /for等语句中的花括号不是独立的作用域. 如前述,JS的在函数中定义的局部变量只对这个函数内部可见,称之谓函数作用域. 嵌套作用域变量搜索规则:当在函数中引用一个变量时,JS会搜索当前函数作用域,如果没有找到则搜索其上层作用域,一直到全局作用域. var va

  • Javascript作用域与闭包详情

    目录 1.作用域 2.作用域链 3.词法作用域 5.闭包的应用 6.闭包的缺陷 7.高频闭包面试题 1.作用域 简单来说,作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限 在ES5中,一般只有两种作用域类型: 全局作用域:全局作用域作为程序的最外层作用域,一直存在 函数作用域:函数作用域只有在函数被定义时才会被创建,包含在父级函数作用域或全局作用域中 说完概念,我们来看下面这段代码: var a = 100 function test(){ var b = a * 2 var

  • 深入理解Javascript作用域与变量提升

    下面的程序是什么结果? 复制代码 代码如下: var foo = 1;function bar() { if (!foo) {  var foo = 10; } alert(foo);}bar(); 结果是10: 那么下面这个呢? 复制代码 代码如下: var a = 1;function b() { a = 10; return; function a() {}}b();alert(a); 结果是1. 吓你一跳吧?发生了什么事情?这可能是陌生的,危险的,迷惑的,同样事实上也是非常有用和印象深刻

  • 全面理解JavaScript中的闭包

    引子 闭包是有权访问另一个函数作用域中的变量的函数. 闭包是javascript中很难理解的部分,很多高级的应用都依靠闭包来实现的,我们先来看下面的一个例子: function outer() { var i = 100; function inner() { console.log(i); } } 上面代码,根据变量的作用域,函数outer中所有的局部变量,对函数inner都是可见的:函数inner中的局部变量,在函数inner外是不可见的,所以在函数inner外是无法读取函数inner的局部

  • 理解javascript中的闭包

    阅读目录 什么是闭包? 闭包的特性 闭包的作用: 闭包的代码示例 注意事项 总结 闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 什么是闭包? 官方说法: 闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量------<javascript高级程序设计第三版> 下面就是一个简单的闭包: function A(){ var text="

随机推荐