你不知的 javascript(一)


原本的思维导图笔记 xmind 文件已丢失, 记录一个 markdown 版。

作用域和闭包

1. 作用域是什么

1.1 编译原理

1.1.4 js 引擎要复杂的多 不会有大量时间优化、发生在代码执行前几微秒

1.2 理解作用域

1.2.1 演员表

1.2.2 对话

var = 2 的分解

  1. var a 询问作用域该变量是否存在同一个作用于的集合中,是则忽略声明,否则声明新变量

  2. 生成运行时代码,处理 a=2 赋值操作, 询问作用域存在 a 变量则使用,否则继续查找

总结:变量赋值执行两个动作,首先编译器会在当前作用域声明变量(之前没声明过), 运行时引擎会在作用域中查找该变量,若找到就赋值。 1.2.3 编译器有话说

LHS 与 RHS “赋值操作”的左侧(目标)或右侧(源头)

  function foo(a){ 
    console.log(a)
  }
  foo(2) 
  // foo 进行 RHS 引用 () 则要执行 foo
  // 隐式 a=2 容易被忽略
  // 为了给 a 分配值,需要进行一次 LHS 查询对 a 进行 RHS 引用 将值传递给 console.log

1.3 作用域嵌套

1.3.1 当一个块或函数嵌套在另一个块或函数中时

1.4 异常

1.4.1 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎会抛出 Reference Error

1.4.2 RHS 查询到变量,但你尝试对变量值进行不合理的操作,引擎会抛出 Type Error

2 词法作用域

2.1 词法阶段

作用域查找始终从运行时所处的最内部作用域开始,逐级向外或说向上进行,直到遇见第一个匹配的标识符为止。
全局变量会自动成为全局对象

2.2 欺骗词法

2.2.1 eval

2.2.2 with

3. 函数作用域和块作用域

3.1 函数中的作用域

含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。

3.2 隐藏内部实现

3.2.1 可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来”隐藏”他们

3.2.2 最小授权原则、|最小暴露原则, 指在软件设计中,应该最小限度的暴露必要内容。

3.2.3 规避冲突 - “隐藏”作用域中的变量和函数可以避免同名标识符之间的冲突

3.2.4 1. 全局命名空间 - 第三方库:通常会在全局作用域中声明一个名字足够特的变量,通常是一个对象。这个对象被用作库的命名空间,所有要暴露给外界的功能会成为这个对象的属性。

3.2.5 2. 模块管理 - 使用工具,任何库都无需将便师傅加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显示地导入其他特定作用域

函数作用域

  1. 具名包装函数可以解决一些问题,但它并不理想 ,考虑用自执行包装函数。

  2. 如果 function 是声明中的第一个词,那么就是函数声明,否则就是函数表达式

  3. 匿名和具名

• 缺点

• 1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。

• 2. 引用自身时只能使用已经过期的 arguments.callee 引用

• 3. 一个描述性的名称可以让代码不言自明

立即执行函数表达式

> • (function() { .. })()
>
> • (function() { ..}() )
>
> • (function IIFE(global) { .. })(window)
>
> • var a = 2;(function IIFE(def) {def(window)})(function def(global)
> {var a = 3;console.log(global.a)})

块作用域

• let 关键字可以将变量绑定到所在的任意作用域中,换句话说 let 为其声明的变量隐士地劫持了所在的块作用域

• 垃圾收集

• let 循环

• 同样可以用来创建块作用域变量,但其值是固定的(常量)。

4 提升

包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

  1. var a = 2 会分成编译和留在原地等待执行两个阶段

  2. 先有声明, 后有赋值

  3. 函数声明会被提升但是函数表达式不会被提升

函数优先

  1. 函数首先提升,然后才是变量

  2. 避免重复声明

  3. 避免在块内部声明函数

5 作用域闭包

启示 - 闭包是基于词法作用域书写代码时所产生的自然结果

实质问题

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

> function foo() { var a = 2; function bar () { console.log(a); } return
> bar}var baz =
> foo()baz()foo() 执行后,因为 bar() 所声明的位置,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域一直存活。bar() 依然持有对该作用域的引用,而这个引用叫做闭包。

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

> function foo() { var a = 2; function baz() { console.log(a) }
> bar(baz);}function bar(fn) { fn() //闭包}foo();

现在我懂了

  1. 无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

  2. 尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包。

模块

CoolModule

> function CoolModule() { var something = \'cool\'; var another = \[1,
> 2, 3\]; function doSomething() { console.log(something) } function
> doAnother() { console.log(anohter.join(\'!\'); } return { doSomething:
> doSomething, doAnother: doAnother }}var foo =
> coolModule();foo.doSomething() // coolfoo.doAnother() //1!2!3

模块模式需要具备两个必要条件

> var foo = (function CoolModule(id) { function change() {
> publicAPI.identify = identify2; } function identify1() { console.log(
> id ); } function identify2() { console.log( id.toUpperCase() );  } var
> publicAPI = {  change: change, identify: identify1 }; return
> publicAPI; })( \"foo module\" );foo.identify(); // foo module
> foo.change(); foo.identify(); // FOO MODULE

现代的模块机制

> //核心概念: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 };})()

未来的模块机制 es6

动态作用域

this 的表亲

词法作用域最重要的特征是它的定义过程发生在代码的书写阶段

动态作用域不关心函数和作用域是如何声明以及在何处声明,只关心它们从何处调用