这一次弄懂作用域和闭包

1 作用域是什么

1.1 定义
      是一套规则,用于确定在何处以及如何查找变量(标识符)。
1.2 查找的两种类型
       如果查找的目的是对变量进行赋值,那么就会使用LHS查询,=操作符或调用函数时传入参数的操作都会导致赋值操作;如果查找的目的是获取变量的值,就会使用RHS查询。
一个栗子:

var a = 2; // 对a进行LHS查询
console.log(a); // 对a进行RHS查询

function foo (b) {
  console.log(b);
}
foo(3); // 首先需要查找foo,找到foo之后,对入参b进行隐式地分配2,然后查找console,最后其查找b。于是总的来说进行了3次LHS查询,1此RHS查询

1.3 区分LHS和RHS干啥?
      因为在变量还没声明的情况下,这两种查询的行为是不一样的。
       如果RHS查询在所有嵌套作用域中都找不到所需的变量是,引擎就会抛出ReferenceError;
       相比之下,当引擎执行LHS查询,若在顶层(全局作用域)中也无法找到目标变量,全局作用域就会创建一个该名称的变量,前提是程序运行在非“严格模式”下。
1.4 js是一门编译语言
     对于js来说,任何代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒。
一个栗子:

var a = 2;
// 1.编译阶段,遇到var a,编译器询问作用域是否已存在该名称变量。如果存在,则忽略该声明;否则会要求作用域在当前作用域中声明一个新变量a。
// 2.编译器为引擎生成运行时所需代码。
// 3.运行a = 2这个赋值操作,对a进行LHS查询并赋值。

2 词法作用域

2.1 定义
      意味着作用域是由书写代码时变量和块作用域的位置来决定的。
2.2 特殊情况
     eval(...)和with可以“欺骗”词法作用域。

3 函数作用域和块作用域

3.1 js有基于函数的作用域
3.2 隐藏的内部实现
     对函数的传统认知是先声明一个函数,然后向里面添加代码。但反过来想,从所写代码中挑出任意一个片段,用函数包裹起来,实际上就是把这些代码“隐藏”起来了。
      意义: 基于软件设计的原则,应最小限度地暴露必要内容,而将其他内容隐藏起来(称为最小授权原则)。
      应用: 规避冲突。一种情况是全局的命名空间,比如导入三方库时,如果没有妥善地将内部私有函数或变量,就很容易引起冲突,通常的做法是暴露一个名字独特的对象变量,用作库的命名空间;另一种是模块管理,无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显示地导入到特定的作用域中,以规避冲突。
3.3 块作用域
      作用: 是一个用来对最小授权原则进行扩展的工具,从函数中的隐藏扩展为在块中隐藏。
      方式: 1) try/catch,catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效;2) let关键字,可将变量绑定在{...}内部;3) const关键字,同let,只是其值是固定不变的。

try {
  throw new Error();
} catch (a) {
  a = 3;
  console.log(a); // 3
}
console.log(a); // ReferenceError

4 提升

     一个代码片段会被进行两次处理:一是编译,二是执行。编译阶段的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。注意以下几点:
      函数声明会被提升,函数表达式却不会。另:区分这两者的方法是,是否以"function"关键字开头。以"function"开头的是函数声明,否则是函数表达式。
      函数声明优于变量声明。重复的声明,会被忽略,所以与函数声明同名的变量声明会被忽略。

5 作用域闭包

5.1 定义
     当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
一个栗子:

function wait (message) {
   // 具名的行内函数表达式,始终给函数命名是一个最佳实践。匿名的缺点:
   // 1在栈追踪中不会显示出有意义的函数名,使得调试很困难
   // 2如果没有函数名,当函数需要调用自身时只能使用已经过期的arguments.callee引用,比如递归;另一是添加事件监听器后需要解绑自身
   // 3省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以比代码不言自明
   setTimeout(function timer() {
     console.log(message); 
   }, 100);
}
wait('Hello, closure!');

另一个循环与闭包的栗子:

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i*1000);
}
// 我们预期的效果是,分别输出1~5,每秒一次。但实际上,会以每秒一次的频率输出五次6。
// 问题在哪里呢?我们以为每个迭代都会给自己“捕获”一个i的副本。实际上,根据作用域的原理,它们都被封闭在全局作用域中,共享了一个i

for (var i = 1; i <= 5; i++) {
  (function() {
    setTimeout(function timer() {
      console.log(i);
    }, i*1000);
  })();
}
// 用立即执行函数包裹起来呢?结果也是同上边一样

for (var i = 1; i <= 5; i++) {
  (function() {
    var j = i;
    setTimeout(function timer(j) {
      console.log(j);
    }, j*1000);
  })();
}
// 这样就可以了,每次迭代都保存了一个i的副本

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

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i*1000);
}
// 最简单的方式是使用let,特殊点在于,变量在循环过程中不止被声明一次,每次迭代都会声明,也就是一个副本

5.2 应用
     还有其他的代码模式利用闭包的强大威力,比如其中最强大的一个:模块。最常实现模块的方式被称为模块暴露。
比如这个栗子:

function myModule() {
  let name = 'minya';
  
  function setName(customName) {
    name = customName;
  }

  function getName() {
    return name;
  }
  return {
    setName: setName,
    getName: getName
  }
}
let moduleApi = myModule();
moduleApi.getName(); // 'minya'
moduleApi.setName('haha');
moduleApi.getName(); // 'haha'
// getName和setName在定义时的作用域外边被调用,但仍然访问到了之前作用域里边相关的变量,所以是一个闭包

      现代的模块机制:大多数模块加载器/管理器本质上都是将这种模块定义封装进一个友好的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); // 核心!将各模块的回调函数执行一遍,该模块暴露的方法被收集到modules对象中
  }
  function get(name) {
    return modules[name];
  }
  return {
    define: define,
    get: get
  };
})();

MyModules.define('foo', [], funtion () {
  function hello() {
    return 'This is foo';
  }
  return {
    hello: hello
  }
});

var foo = MyModules.get('foo');
console.log(foo.hello()); // 'This is foo'

MyModules.define('bar', ['foo'], function (foo) {
  function hello() {
    return 'This is bar ' + foo.hello();
  }
  return {
    hello: hello
  }
});
var bar = MyModules.get('bar');
console.log(bar.hello()); // 'This is bar This is foo'

      未来的模块机制:es6的模块机制,将文件当作独立的模块来处理,使用import关键字导入其他模块,export关键字导出特定api。与基于函数的模块的区别是,es6模块api是静态的,在编译阶段就可以判断依赖关系,而基于函数的模块api语义只有在运行时才会被考虑。

参考《你不知道的JavaScript》上卷 / Simpson, K.
     

你可能感兴趣的:(这一次弄懂作用域和闭包)