你不知道的JS之-做用域和闭包

做用域是什么

理解做用域

  • 引擎
    • 从头至尾负责整个JavaScript程序的编译和执行过程
  • 编译器
    • 负责语法分析及代码生成
  • 做用域
    • 负责收集并维护由全部声明的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符有访问权限。

做用域嵌套

当一个块或者函数嵌套在另外一个函数或函数中时,就发生了做用域嵌套。浏览器

遍历嵌套做用域规则:引擎从当前的执行做用域开始查找变量,若是找不到,就向上一级继续查找。直到抵达最外层的全局做用域, 不管找到仍是没找到,查找过程都会中止。bash

小结

做用域是一套规则,用于肯定在何处以及如何查找变量(标志符)。 若是查找目的是对变量进行赋值,就是执行LHS查询 若是查找目的是获取变量的值,就是执行RHS查询闭包

词法做用域

做用域主要两种工做模式:词法做用域和动态做用域app

词法阶段

  • 大部分标准语言编译器的第一个工做阶段叫作词法化(也叫单词化)。
  • 简单的说, 词法做用域就是定义在词法阶段的做用域。换句话说,词法做用域是由你在写代码的时候将变量和块做用域写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变。
  • 做用域查找会在找到第一个匹配的标识符时中止。在多层的嵌套做用域中能够定义同名的标识符,叫作“遮蔽效应”
  • 做用域查找始终是从运行时所处的最内部做用域开始,逐级向外或者向上查找, 知道碰见第一个匹配的标识符为止。
  • 全局变量会自动成为全局对象(例如浏览器中的window对象)的属性,所以能够不直接经过全局对象的词法名称, 而是间接的经过对全局对象属性的引用来对其进行访问。 例如window.a。经过这种技术能够访问那些被同名变量锁遮蔽的全局变量。但非全局变量若是被遮蔽了,不管如何都没法被访问到。
  • 不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定。

小结

词法做用域意味着做用域是由代码书写时候函数声明的位置来决定的。函数

函数做用域和块做用域

函数中的做用域

函数做用域是指,属于这个函数的所有变量均可以在整个函数的范围内使用以及复用(事实上在嵌套的做用域中也可使用)。ui

隐藏内部实现

不该该这样:spa

function doSomething(a) {
 b = a + doSomethingElse(a * 2);

 console.log(b * 3);
}

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

var b;

doSomething(2);
复制代码

而是应该这样, 隐藏变量:调试

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

 b = a + doSomethingElse(a * 2);

 console.log(b * 3);
}

doSomething(2);
复制代码

规避冲突

“隐藏”做用域中的变量和函数所带来的另外一个好处,是能够避免同名标识符之间的冲突,两个标识符可能具备相同的名字可是用途却不同,无心间可能形成命名冲突。 冲突会致使变量的值被意外覆盖。code

函数做用域

匿名和具名

例如以下函数:cdn

setTimeout(function() {
 console.log('I waited 1 second');
 
}, 1000);
复制代码

这叫作匿名函数表达式。 匿名函数表达式书写起来简单快捷,可是有几个缺点:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难
  2. 若是没有函数名,当函数须要引用自身时只能使用已通过期的arguments.callee引用。
  3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。

行内函数表达式很是强大且有用----匿名和具名之间的区别并不会对这一点有任何影响。给函数表达式指定一个函数名能够有效解决以上问题。因此,最好始终给函数表达式命名。

setTimeout(function timeoutHandler() { // 有名字了
  console.log('I waited 1 second');
  
}, 1000);
复制代码

当即执行函数表达式

(function(){})()(function(){}())

提高

  • 函数会首先别提高,而后才是变量。
  • 出如今后面的函数声明仍是能够覆盖前面的。
  • 一个普通块内部的函数声明一般会被提高到所在做用域的顶部。

总结

  • 全部的声明(变量和函数)都会被“移动”到各自做用域的最顶端, 这个过程被称为 提高。
  • 声明自己会被提高,而包含函数表达式的赋值在内的赋值操做并不会被提高。
  • 要注意避免重复声明,特别是当普通的var声明和函数声明混合在一块儿的时候, 不然会引发不少危险的问题。

做用域闭包

定义

当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在所在词法做用域之外被执行,这个引用,就叫作闭包。

  • 不管经过何种手段将内部函数传递到所在词法做用域之外,它都会持有对原始定义做用域的引用,不管在何处执行这个函数都会使用闭包
  • 本质上讲,不管什么时候何地,若是将函数看成第一级的值类型并处处传递,你就会看到闭包在这些函数中的应用。
  • 例如在一些定时器、事件监听器、Ajax请求等,只要使用了回调函数,实际上就是在使用闭包

循环和闭包

  • let声明能够用来劫持块做用域,而且在这个做用域中声明一个变量。
  • for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程当中不止被声明一次,每次迭代都会声明。随后每一个迭代都会使用上一个迭代结束时的值来初始化这个变量。

模块

模块模式须要具有两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少别调用一次(每次调用都会建立一个新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有做用域中造成闭包,而且能够访问或者修改私有得状态。

一个具备函数属性的对系那个自己并非真正的模块。从方便观察的角度看,一个从函数调用锁返回的,只有数据属性而没有闭包函数得对象并非真正的模块。

现代的模块机制

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的API。

var MyModules = (function Manager() {
  var modules = {};

  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define: define,
    get: get
  };
})();

MyModules.define('bar', [], function() {
  function hello(who) {
    return 'let me introduce: ' + who;
  }

  return {
    hello: hello
  };
});

MyModules.define('foo', ['bar'], function(bar) {
  var hungry = 'xiaofan';

  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('xiaofan'));

foo.awesome();
复制代码

foobar模块都是经过一个返回公共API的函数来定义的。foo甚至接受bar的实例做为依赖参数,并能响相应的使用它。

总结

当函数能够记住并访问所在的词法做用域,即便函数是在当前词法做用域之外执行,这时就产生了闭包。

模块有两个主要特征:

  1. 为建立内部做用域而调用了一个包装函数
  2. 包装函数的返回值必须包含至少一个对内部函数的引用,这样就会建立涵盖整个包装函数内部做用域的闭包
相关文章
相关标签/搜索