《你不知的javascript》-作用域和闭包
原本的思维导图笔记xmind文件已丢失, 记录一个markdown版。
作用域和闭包
1作用域是什么
编译原理
-
传统编译语言的流程中,程序中的一段源码执行之前会经历三个步骤,统称编译
- 分解词法单元 var、a、=、2、;
-
解析/语法分析
-
将词法单元流(数组)转换成“抽象语法树”(AST)
- 代码生成
-
js引擎要复杂的多
不会有大量时间优化、发生在代码执行前几微秒
-
理解作用域
-
演员表
-
引擎 · 负责整个过程的编译及执行过程
-
编译器 · 负责语法分析及代码生成
-
作用域 · 负责收集并维护由所有声明的标识符查询,确定当前代码对这些标识符的访问权限
-
对话
-
var = 2的分解
-
- var a 询问作用域该变量是否存在同一个作用于的集合中,是则忽略声明,否则声明新变量
-
- 生成运行时代码, 处理a=2赋值操作, 询问作用域存在a变量则使用,否则继续查找
-
-
总结:变量赋值执行两个动作,首先编译器会在当前作用域声明变量(之前没声明过), 运行时引擎会在作用域中查找该变量,若找到就赋值.
-
编译器有话说
-
LHS与RHS “赋值操作”的左侧(目标)或右侧(源头)
-
function foo(a){ console.log(a) } foo(2) //foo进行RHS引用 () 则要执行foo //隐式a=2容易被忽略 为了给a分配值 需要进行一次LHS查询对a进行RHS引用 将值传递给console.log
-
-
-
作用域嵌套
- 当一个块或函数嵌套在另一个块或函数中时
异常
- RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎会抛出Reference Error
- RHS查询到变量,但你尝试对变量值进行不合理的操作,引擎会抛出Type Error
2词法作用域
词法阶段
- 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或说向上进行,知道遇见第一个匹配的标识符为止。全局变量会自动成为全局对象
欺骗词法
- eval
- with
3函数作用域和块作用域
函数中的作用域
- 含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
隐藏内部实现
- 可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”他们
- 最小授权原则|最小暴露原则, 指在软件设计中,应该最小限度的暴露必要内容。
- 规避冲突 - “隐藏”作用域中的变量和函数可以避免同名标识符之间的冲突
- 1.全局命名空间 - 第三方库: 通常会在全局作用域中声明一个名字足够特的变量,通常是一个对象。这个对象被用作库的命名空间,所有要暴露给外界的功能会成为这个对象的属性。
- 2.模块管理 - 使用工具,任何库都无需将便师傅加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显示地导入其他特定作用域
函数作用域
-
具名包装函数可以解决一些问题,但它并不理想 ,考虑用自执行包装函数。
-
如果function是声明中的第一个词,那么就是函数声明,否则就是函数表达式
-
匿名和具名
-
匿名函数表达式 function().. 没有名称标识符
- 缺点
- 1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
- 2.引用自身时只能使用已经过期的arguments.callee引用
- 3.一个描述性的名称可以让代码不言自明
-
立即执行函数表达式
-
函数被包含在一对( )括号内部,因此成为了一个表达式,通过在末尾加上另一个( ) 可以立即执行这个函数,这种模式被规定为 IIFE - 立即执行函数表达式
(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为其声明的变量隐士地劫持了所在的块作用域
- 垃圾收集
- let循环
-
const
- 同样可以用来创建块作用域变量,但其值是固定的(常量)。
-
-
-
4提升
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理
var a = 2会分成编译和留在原地等待执行两个阶段- 先有声明, 后有赋值
- 函数声明会被提升但是函数表达式不会被提升
函数优先
- 函数首先提升,然后才是变量
- 避免重复声明
- 避免在块内部声明函数
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();
-
-
-
现在我懂了
- 无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
- 尽管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-
模块模式需要具备两个必要条件
-
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用创建新模块实例)
-
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态
-
模块是普通函数可以接受参数
-
命名将要作为公共API返回的对象
-
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
-
-
-
-
-
-