你真的理解JavaScript的作用域吗

javascript有一些对于初学者甚至是有经验的开发者都难以理解的概念. 这个部分是针对那些听到 : 作用域, 闭包, this, 命名空间, 函数作用域, 函数作用域, 全局作用域, 变量作用域(后续翻译这个词我也没太懂), 公共/私有 作用域 后想要深入了解的人的. 当你看完这篇文章后你将了解有关以下问题的答案:

  • 什么是作用域
  • 什么是全局/局部 作用域
  • 什么是命名空间,他和作用域的不同
  • this是什么,它是怎样被作用域影响的
  • 什么是函数/lexical 作用域
  • 什么是闭包
  • 什么是公共/私有 作用域
  • 我们如何了解/创建/使用 以上的概念

什么是作用域 ?

在javasript中, 作用域指你的代码当前的上下文. 作用域有全局和局部的区别. 了解javascript作用域是你书写出更健壮的代码和成为更好的开发者的钥匙. 你将了解变量/函数 在那里是可访问的, 改变你的代码的作用域, 并且更快的写出更易于维护和调试的代码.思考作用域是很容易的, 我们是在作用A 或 作用域B的内部

什么是全局作用域 ?

当你写下一行代码之前, 你就在我们叫做全局作用域里 ; 如果此时我们声明(declare)一个变量(variable), 那他默认就是全局的( globally ).

  //全局作用域 global scope
  var name = "Todd"

 

 

全局作用域是你最好朋友, 同时也是你的噩梦,学习控制作用域是容易的, 这样做你将避免在全局作用域中运行时的问题(通常是命名空间冲突);你经常听说'全局作用域是不好的', 但是没有人证明为什么. 全局作用域不是坏的, 你需要通过它创建可访问的 Modules/APIs, 你必须使用并发挥它的优势, 确保不会出现问题.

任何人使用jQuery的时候要这样做:

  jQuery('.myClass')

 

 

我们在全局作用域访问jQuery, 我们可以参考此次访问的命名空间. 命名空间偶尔作为一个作用域使用, 但是通常是指那些最顶级的作用域. 在这个例子里, jQuery在全局作用域中, 但是也是我们的命名空间. 这个jQuery的命名空间默认的存在在全局作用域下, 它充当了jQuery库的命名空间, 在它内部的所有东西充当了这个命名空间的后代.

什么是局部作用域 ?

局部作用域是指在全局作用域内部定义(defined:定义, default:默认)的作用域. 通常在一个全局作用域内定义的任何一个函数都有自身的局部作用域. 在函数内部定义的函数也拥有一个看起来像外部函数的局部作用域. 如果我们在一个函数内部定义了变量,那么这些函数的作用域就在函数内部. 像这个例子 :

  //Scope A : global scope out here 
  var myFuntion = function () {
    //ScopeB : local scope in here
  }

 

 

除非暴漏出来,否则局部作用域成员在全局作用域内是不可见的. 意思是如果我们定义一些函数和变量在一个新的作用域内, 在这个作用域外它是不可访问的. 一个简单的例子如下 :

  var myFunction = function () {
    var name = "ittce";
    console.log( name );
  }
  console.log( name );// Uncaught ReferenceError: name is not defined

 

 

这个变量name在局部作用域内, 它没有暴漏在父级作用域内, 因此显示未定义.

函数作用域

javascript中的所有作用域只能靠函数创建, 不能靠for/while 循环或者表达式声明(if, switch)来创建. 新函数 = 新作用域, 这是规矩.一个例子来演示作用域的创建:

 
  //Scope A
  var myFunction = function () {
    //Scope B
    var muOtherFunction = function () {
      //Scope C
    }
  }
 

 

 

创建一个局部作用域和局部 变量/函数/对象 是很容易的.

词法作用域( Lexical scope )

当你看到一个函数在另外一个函数内部, 内部函数可以访问到外部函数的作用域, 它被叫做词法作用域 或 闭包, 也被称为静态作用域.下面是一个最容易的演示例子:

 
  //Scope A
  var myFunction = function(){
    //Scope B
    var name = "ittce"; // defined in Scope B
    var myOtherFunction = function () {
      //Scope C : name is accessible here ! name 可以在这里访问到
    }
  } 
 

 

 

你发现 myOtherFunction 在这里不应该被这样叫, 它只是简单的定义在这里. 调用命令也会引起变量和作用域的反应, 这里我定义我的函数并调用它在另外一个console的下面 :

 
   var myFunction = function () {
     var name ="ittce";
     var myOtherFunction = function () {
       console.log("My name is" + name );
     }
     console.log( name );
     myOtherFunction();
   }
   // 将要输出:
   // ittce
   // My name is ittce


 

 

词法作用域是易于使用的, 任何定义于它作用域链顶端的变量/函数/对象,在它的作用域内都是可用的.像这个例子:

  var name = 'ittce';
  var scope1 = function () {
    // name is available here  name 在这里是可用的
    var scope2 = function () {
      // name is  available here too
      var scope3 = function () {
        // name is also available here
      }
    }
  }
 

 

 

这里最重要的是记得词法作用域是不可逆的. 这里我们可以看到为什么词法作用域不可访问 :

 
  // name = undefined
  var scope1 = function () {
    // name = undefined
    var scope2 = function () {
      // name = undefined
      var scope3 = function () {
        var name = 'ittce';
      }
    }
  }
 

 

 

我可以总是返回name的一个引用, 但是并不是它自身.

作用域链

作用域链建立在一个特定的函数作用域之上. 定义的每个函数都有它自身的作用域嵌套, 因此我们知道, 定义在另一个函数内部的函数拥有一个和外部函数联系的局部作用域, 这种联系我们叫做链. 它总是有一个位置在那个确定的作用域内. 当开始解析一个变量, javascript会从最内部的作用域逐层向外查找直到找到这个变量/对象/函数, 或没找到返回undefined错误.

闭包

闭包和词法作用域的联系非常密切, 一个更好的例子去演示闭包如何工作, 一个实际例子如下,当函数内部返回另一个函数的引用时.在父作用域内, 我们可以访问到内部作用域返回的东西 :

 var sayHello = function ( name ) {
   var text = "hello," + name ();
   return function () {
     console.log( text );
   }
 }

 

 

闭包概念可以使我们内部作用域的东西私有化,无法被外部作用域所访问. 当我们单独调用这个函数时, 他将不做任何事, 因为它返回的是一个函数

  sayHello( 'ittce' );// 没有事情发生, 也不报错, 只有沉默

 

 

该函数返回一个函数, 这意味着它需要分配, 然后再次调用 :

  var helloIttce = sayHello( 'ittce' );
  helloIttce(); // will call the closure and log 'hello, ittce';

 

 

好的, 我说谎了, 你可以不用分配就调用它, 你可以能已经看到过这种调用闭包的方式 :

  sayHello( 'ittce' )();

AngularJS中的 $.compile 使用了以上的技术在闭包中传递当前的作用域引用 :

  $.compile( template )( scope );

 

 

意思是我们可以猜到它内部的代码简化版本像这样:

 
  var $compile = function ( template ) {
    // some magic stuff here
    // scope is out of scope, though...
    return function ( scope ){
      // access to `template` and `scope` to do magic with too
      // 这里仍然可以访问到 'template' 和 'scope' 做我们想做的事情
    }
  }
 

 

 

一个函数不需要返回一个调用闭包的命令也可以调用闭包. 简单的直接访问外部变量的词法作用域来创建一个闭包.

作用域 和 this

依据函数如何被调用给this绑定一个不同的作用域. 我们都使用过this, 但并不都了解它被调用时的不同. 在客户端默认的this指向最外部的全局对象, window. 我们很容易发现在不同的调用方式下, this具有不同的值 :

 
  var myFunction = function () {
    console.log( this ) // this 这是是全局对象 window
  }
  myFunction();
  var myObject = {};
  myObject.myMethod = function () {
    console.log( this ) // this 这里指向 对象 myObject
  }
  var nav = document.querySelector('.nav');
  var toggleNav = function () {
    console.log( this );
  }
  nav.addEventListener('click', toggleNav, false );  // 这里调用函数时, this 指向 .nav 这个DOM element
 

 

 

当我们使用this时, 也会经常出现问题. 例如像下面这样, 甚至在相似的函数内部那些作用域和this的值都可以被改变的:

  var nav = document.querySelector('.nav');
  var toggleNav = function () {
    console.log( this ); // nav element
    setTimeout( function (){
      console.log( this ); // window
    }, 500 );
  };
  nav.addEventListener('click', toggleNav, false );

 

 

这里发生了什么呢 ? 我们创建了一个不调用我们事件处理程序的新的作用域, 因此他像预期的那样指向了全局作用域: window. 有几种方法可以让this不受新作用域的影响, 像我们预期的那样工作. 你可以已经了解过这个, 我们可以使用一个 that 变量缓存this的引用 :

 
  var nav = document.querySelector( '.nav' );
  var toggleNav = function () {
    var that = this;
    setTimeout( function (){
      console.log( that );
    }, 500);
  };
  nav.addEventListener('click', toggleNav, false );

 

 

这样一个小花招可以让this在新创建的作用域内按我们预期的那样指向正确的值.

使用 apply(), call(), bind() 修改作用域

有时你需要操纵javascript作用域像你想的那样工作. 一个简单的例子来演示在循环中, 作用域是怎么改变的:

  var links = document.querySelectorAll('a');
  for ( var i = 0; i < links.length; i++ ){
    console.log( this ); // this = window
  }

 

 

这里this的值不指向我们的DOM节点, 我们没有调用任何东西或者改变作用域. 让我们看下我们如何修改作用域( 它看起来像是我们改变了作用域, 其实是改变了函数被调用时的上下文执行环境 ).

call() && apply()

call() && apply() 方法真的很棒, 它可以允许你传递一个绑定正确this的作用域给函数. 让我们操作以上的函数, 让他们的this指向各自的在数组中DOM节点 :

  var links = document.querySelectorAll( 'a' );
  for ( var i = 0; i < links.length; i++ ) {
    ( function () {
      console.log( this );
    }).call( links[i] );
  } 

 

 

你可以看到我们传递传递在数组中迭代的当前的节点: links[i], 这改变了函数的作用域, 使this指向了迭代的DOM节点. 如果我们需要的话, 我们可以使用这个this. 我们可以使用call() && apply() 来改变函数的作用域, 但是更进一步的参数传递两者有区别 : call( scope, arg1, arg2 )需要单个的用参数, 使用逗号分隔 ; apply( scope, [ agr1, agr2] )接受一个参数数组.重要的是要记住: 使用 apply() 或 call()时实际上调用了函数,因此不要这样:

  myFunction();

 

你需要用.call()处理它,并链接到这个方法

  myFunction.call();

 

 

bind()

不向上面一样, 使用 .bind() 不会调用函数, 它仅仅是在函数被调用之前绑定值.这个方法在ECMAScipt5中才被引入真是一个耻辱,因为它实在太棒了. 你知道在函数引用时我们不能够传递参数, 像这样 :

  //工作
  nav.addEventListener('click', toggleNav, false );
  //立刻执行
  nav.addEventListener('click', toggleNav( arg1, agr2 ), false ); 

 

 

我们可以解决这个问题, 但是需要在它里面创建一个新的函数 :

  nav.addEventListener('click', function () {
    toggleNav( arg1, agr2 );
  }, false );

 

 

但是这改变了作用域并且我们创建了一个不必要的函数, 如果我们需要在循环中添加事件监听,这将是巨大的性能浪费. 这就是 .bind()闪耀的原因, 我们可以不调用函数的情况下, 传递参数给它 :

  nav.addEventListener( 'click', toggleNav.bind( scope, arg1, arg2 ), false );

 

 

函数没有被调用, 但是作用域已经根据需求改变, 参数坐等函数调用时传递.

私有和公共作用域

在许多程序设计语言中, 你可能听说过 公共 和 私有 作用域, 但是在 javascript 中不存在这些. 可是我们可以通过闭包来模拟公共和私有作用域. 通过javascript设计模式 , 例如单例模式, 我们可以创建公共和私有作用域. 一个简单的方法去创建私有作用域, 是在我们的函数们外包一层函数. 像我们学习的那样, 函数创建的作用域内 , 可以保存东西不被外部作用域访问到 :

( function () {
  // 私有作用域
} )();

 

 

我们可以在添加一些功能, 在我们的app中使用 :

  ( function () {
    var myFunction = function () {
      //可以在这里做一些事
    }
  } )()

 

 

当我们调用我们的功能, 在外部作用域我们无法访问到它 :

  ( function () {
    var myFunction = function () {
      //可以在这里做一些事
    }
  } )();
  myFunction();// Uncaught ReferenceError: myFunction is not defined

 

 

成功了! 我们创建了私有作用域. 但是我想把函数公开呢 ? 这里有一个很棒的模式( 叫 模块模式 或 揭示模块模式 )允许我们的函数在作用域内正常的工作, 使用私有和公共的作用域 和一个对象. 这里我们将包含我们所有的代码的全局命名空间叫做模块.

 
// 定义 module
var Module = (function () {
  return {
    myMethod: function () {
      console.log('myMethod has been called.');
    }
  };
})();

// 调用 module + methods
Module.myMethod();
 

 

 

这里的 return 语句 返回我们所有可以通过命名空间访问的公共方法. 这意味到我们的模块关联到了我们的命名空间, 并且包含了我们想要的许多方法. 我们可以按照我们的希望扩展我们的模块 :

 
  // 定义模块
  var module = (function () {
    return {
      myMethod : function () {},
      someOtherMethod : function () {}
    }
  })();
  // call module + methods
  Module.myMethod();
  Module.someOtherMethod();
 

 

 

那么为什么使用私有方法 ? 因为很多的开发者将他们的函数放在全局作用域下污染命名空间. 函数帮助我们的代码工作, 但是并不需要将他们放在全局命名空间内, 只需要使用 API 调用就可以了. 下面是我们通过不返回函数创建的私有作用域 :

 
  var Module = ( function () {
    var privateMethod = function () {

    }
    return {
      publicMethod : function () {

      }
    }
  })()
 

 

 

这意味着 publicMethod 可以被调用, 但是 privateMethod 不可以, 因为它在私有作用域内. 这些私有作用域内的函数可以是任何你想到功能 : addClass, removeClass, Ajax/XHR calls, Arrays, Objects, helper.这是一个有趣的转折, 在同一个作用域内的东西可以访问同一作用域内的所有东西, 即使它被返回出去. 这意味着 我们公共的方法可以访问到我们私有的那些, 因此他们虽然在全局作用内不可访问, 但仍受到影响.

 
  var Module = (function () {
    var privateMethod = function () {

    };
    return {
      publicMethod: function () {
        // 可以访问私有方法
      }
    };
  })();
 

 

 

这使得我们将代码的安全提升到一个更高的水平. 确保代码安全是javascript一个很重要的方面, 因此我们不能将所有的功能都放在全局作用域公开, 这样面对脆弱的攻击也是很容易攻破的. 下面是一个利用公共和私有方法, 返回一个对象的例子 :

 
  var Module = (function () {
    var myModule = {};
    var privateMethod = function () {

    };
    myModule.publicMethod = function () {

    };
    myModule.anotherPublicMethod = function () {

    };
    return myModule; // returns the Object with public methods
  })();

  // usage
  Module.publicMethod();
 

 

 

一个整洁的命名规法是用 '_' 开头命名私有方法, 这可以直观的帮你区分私有和公共方法 :

 
var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
})();

 

 

我们可以返回一个匿名的对象帮助我们通过对象的方式简单的访问一个功能引用.

 
var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
  return {
    publicMethod: publicMethod,
    anotherPublicMethod: anotherPublicMethod
  }
})();
 

 

posted @ 2018-11-22 13:27  rimo  阅读(244)  评论(0编辑  收藏  举报