理解作用域

  • 引擎

    • 从头到尾负责整个JavaScript程序的编译和执行过程
  • 编译器

    • 负责语法分析及代码生成
  • 作用域

    • 负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符有访问权限。

作用域嵌套

当一个块或者函数嵌套在另一个函数或函数中时,就发生了作用域嵌套。

遍历嵌套作用域规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。直到抵达最外层的全局作用域, 无论找到还是没找到,查找过程都会停止。

小结

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

词法作用域

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

词法阶段

  • 大部分标准语言编译器的第一个工作阶段叫做词法化(也叫单词化)。
  • 简单的说, 词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码的时候将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
  • 作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,叫做“遮蔽效应”
  • 作用域查找始终是从运行时所处的最内部作用域开始,逐级向外或者向上查找, 知道遇见第一个匹配的标识符为止。
  • 全局变量会自动成为全局对象(例如浏览器中的window对象)的属性,因此可以不直接通过全局对象的词法名称, 而是间接的通过对全局对象属性的引用来对其进行访问。

例如window.a。通过这种技术可以访问那些被同名变量锁遮蔽的全局变量。但非全局变量如果被遮蔽了,无论如何都无法被访问到。

  • 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

小结

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

函数作用域和块作用域

函数中的作用域

函数作用域是指,属于这个函数的全部变量都可以在整个函数的范围内使用以及复用(事实上在嵌套的作用域中也可以使用)。

隐藏内部实现

不应该这样:

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);

#### 规避冲突
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但是用途却不一样,无意间可能造成命名冲突。 冲突会导致变量的值被意外覆盖。

### 函数作用域

#### 匿名和具名
例如如下函数:

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. 包装函数的返回值必须包含至少一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包

devzhang
30 声望0 粉丝

Something Good Will Be Happen