第一章:作用域是什么

作用域:一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量,这套规则被称为作用域。

词法单元: 比如说 var a= 2 ,其中词法单元 :var a = 2

  • var a = 2 的过程(变量赋值操作)
    • 编译器会在当前作用域中声明一个变量(如果之前没有声明过)
    • 在运行时,引擎会在作用域中查找该变量,如果能够找到就会对它赋值

编译器

LHS 查询 赋值操作的目标是谁 试图找到变量的容器本身,从而对其赋值
RHS 查询 谁是赋值操作的源头 取到它的源值,意味着“得到某某某的值”

  1. console.log(a)
  2. 其中对a的引用是一个RHS 引用,因为这里a并没有赋予任何值
  3. a = 2
  4. LHS引用,想要为 =2 这个赋值操作找到一个目标
  5. function foo(a){
  6. console.log(a) //2
  7. }
  8. foo(2)
  9. foo(2) 调用需要对foo进行RHS 引用
  10. foo(a) 参数传递时,2会被分配给参数a,为了给参数a(隐式)分配值,需要进行一次 LHS 查询
  11. console.log(a ) RHS引用
  12. console.log() 本身也需要一个引用才能执行,因此会对console对象进行RHS查询

参数 是一个隐式的赋值操作

作用域嵌套

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

异常

区分LHS 和 RHS 很重要?
因为在变量还没有声明(任何作用域中无法找到该变量)的情况下,这两种查询的行为不一样

  1. function foo(a){
  2. console.log(a+b)
  3. b=a
  4. }
  5. foo(2)

第一次对b进行RHS查询时是无法找到变量的,也就是未声明
RHS查询 会抛出 ReferenceError 异常
LHS查询 全局作用域会创建一个具有该名称的变量,并将其返回给引擎,前提是程序运行在非“严格模式”

ES5中引入“严格模式”,其中一个不同的行为就是严格模式禁止自动或隐式的创建全局变量。因此在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出错误

  1. a = 2 执行的是LHS查询,不会报错
  2. "use strict";
  3. a = 2 报错

第二章 词法作用域

词法阶段

词法作用域 就是定义在词法阶段的作用域,换种说法,就是由你在写代码时将变量和块作用域写在哪里决定的。

查找

作用域查找会在找到第一个匹配的标识符时停止。

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

欺骗词法

  • eval()

    • 可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的
  • with

    • with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身
  • eval()函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域
  • with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域

第三章 : 函数作用域和块作用域

函数中的作用域

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

隐藏内部实现

最小授权或最小暴露原则:指在软件设计中,应该最小限度的暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的api设计

  1. 变量b和函数doSomethingElese() 应该是doSomething() 内部具体实现的“私有”内容
  2. function doSomething(a){
  3. b = a + doSomethingElese(a*2)
  4. console.log(b*3)
  5. }
  6. function doSomethingElese(a){
  7. return a - 1
  8. }
  9. var b
  10. doSomething(2) //15
  1. 现在 bdoSomethingElese()都无法从外部被访问,设计上将具体内容私有化了
  2. function doSomething(a) {
  3. function doSomethingElese(a) {
  4. return a - 1
  5. }
  6. var b
  7. b = a + doSomethingElese(a * 2)
  8. console.log(b * 3)
  9. }
  10. doSomething(2) //15

规避冲突

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

  1. function foo(){
  2. function bar(a){
  3. i = 3; //修改for循环所属作用域中的i
  4. console.log(a+i)
  5. }
  6. for(var i=0;i<10;i++){
  7. bar(i*2) //无限循环
  8. }
  9. }
  10. foo()
  11. 如何解决: 修改为 var i = 3 就可以

1.全局命名空间
在全局作用域中,当程序加载了很多第三方库时,如果他们没有妥善的将内部私有的函数或变量隐藏起来,就会容易引发冲突。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。

  1. var MyReallyCoolLibrary = {
  2. awesome:"stuff",
  3. doSomething:function(){
  4. },
  5. doAnotherThing:function(){
  6. }
  7. }

2.模块管理
通过依赖管理器的机制将库的标识符显式的导入到另一个特定的作用域中。

函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。

导致的问题

  • 首先,必须声明一个具名函数foo() 意味着foo这个名称本身“污染”了所在作用域
  • 其次,必须显示的通过函数名(foo())调用这个函数才能运行其中的代码 ```javascript var a = 2 function foo(){ //新增加 var a = 3; console.log(a) //3 } //新增加 foo() //新增加 console.log(a) //2
  1. ```javascript
  2. var a = 2
  3. (function foo(){ //新增加
  4. var a = 3;
  5. console.log(a) //3
  6. })() //新增加
  7. console.log(a) //2
  8. foo 被绑定在 函数表达式自身的函数中(是函数) 而不是所在作用域中 (重点)

区分函数声明和表达式:

看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置),如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
(function 开始,( 注意前面的 ( ),这就是函数表达式。

区别 : 最重要的区别是它们的名称标识符将会绑定在何处
( function foo(){…} )作为函数表达式意味着foo只能在 .. 所代表的的位置中被访问,外部作用域则不行,foo变量名被隐藏在自身中意味着不会非必要的污染外部作用域

补充:这样写会避免污染所在作用域

匿名和具名

对于函数表达式的场景 —- 回调参数(上一节,因为 function 前面有 ( )

  1. setTimeout(function(){
  2. console.log("1")
  3. },1000)

这叫做匿名函数表达式,因为function()..没有名称标识符
缺点:

  • 1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难
  • 2.如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用,比如在递归中,另一个函数需要引用自身的例子,就是事件触发后事件监听器需要解绑自身
  • 3.匿名函数省略了对于代码可读性/可理解性很重要的函数名。

行内函数表达式 给函数表达式指定一个函数名可以有效解决问题。推荐做法始终给函数表达式命名

  1. setTimeout(function timeoutHandler(){
  2. console.log("1")
  3. },1000)

立即执行函数表达式

()() 第一个()把函数变成表达式,第二个()执行了这个函数。

IIFE:立即执行函数表达式。

  1. var a = 2;
  2. (function foo(){
  3. var a =3;
  4. console.log(a)
  5. })()
  6. console.log(a)

两种形式
A:作用域和闭包 - 图1
B:作用域和闭包 - 图2

进阶用法:把他们当做函数调用并传递参数进去

  1. var a = 2; //注意:这个后面的分号一定需要写
  2. (function IIFE(global){
  3. var a =3;
  4. console.log(a) //3
  5. console.log(global.a) //2
  6. })(window)
  7. console.log(a) //2

拓展:变化的用途:倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去。

块作用域

javascript 没有块作用域的相关功能,(ES6加了)

这段代码只是为了风格更易读而伪装出的形式上的块作用域

  1. for(var i=0;i<10;i++){
  2. console.log(i)
  3. }

with
是一个块作用域的一个例子,用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。

try/catch
ES3中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效
**

  1. try{
  2. undefined() //执行一个非法操作来强制制造一个异常
  3. }
  4. catch(err){
  5. console.log(err) //正常执行
  6. }
  7. console.log(err) //err not found

注意:这样可以在ES6之前的环境中作为块作用域的替代方案

let

let 关键字可以将变量绑定在所在的任意作用域中,通常是{…} 内部

1.垃圾收集
另一个块作用域非常有用的原因和闭包以及回收垃圾内存的回收机制有关。

click 函数的点击回调并不需要someReallyBigData变量,所以就当process(..)执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click函数形成了一个覆盖整个作用域的闭包,js引擎极有可能依然保存着这个结构(取决于具体实现)

  1. function process(data){
  2. }
  3. var someReallyBigData = {}
  4. process(someReallyBigData)
  5. var btn = document.getElementById("my_button")
  6. btn.addEventListener('click',function click(evt){
  7. console.log("button clicked")
  8. },false)

为变量显示声明块作用域,并对变量进行本地绑定是非常有用的工具
image.png

2.let 循环

for循环头部的let不仅把i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值、

  1. for(let i=0;i<10;i++){
  2. console.log(i)
  3. }

(?下面这种图有疑问 不知道要表达的是什么意思)
image.png

const

其值是固定的常量,修改值的操作都会报错。

第四章 提升

函数作用域和块作用域的行为是一样的: 任何声明在某个作用域内的变量都将附属于这个作用域

先有鸡(赋值)还是先有蛋(声明)

  1. //第一段
  2. a = 2
  3. var a
  4. console.log(a) //2
  5. //第二段
  6. console.log(a)
  7. var a = 2 //undefined

编译器

提升: 变量和函数声明从它们在代码中出现的位置被“移动”到了最上面的过程

解析两段代码的执行过程

  1. var a = 2 会看成两个声明
  2. var a 编译阶段进行
  3. a=2 留在原地等待执行阶段
  4. //第一段
  5. var a
  6. a = 2
  7. console.log(a)
  8. //第二段
  9. var a
  10. console.log(a)
  11. a=2

=>先有声明后有赋值

注意:每个作用域都会进行提升操作

  1. foo()
  2. function foo(){
  3. console.log(a) //undefined
  4. var a = 2
  5. }

可以理解为 =>

  1. function foo(){
  2. var a
  3. console.log(a)
  4. a = 2
  5. }
  6. foo()

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

TypeError 类型错误 值存在,但是操作方法不对或者不存在这种方法
ReferenceError 当你尝试使用一个不存在的变量

下面的代码,变量标识符foo()被提升并分配给所在作用域(这里是全局作用域),因此foo() 不会导致ReferenceError,但是 foo并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值),foo()由于对undefined值进行函数调用而导致非法操作,因此抛出TypeError异常。

  1. foo() //TypeError 不是ReferenceError
  2. var foo = function bar(){
  3. }

即使是具名的函数表达式,名称标识符在赋值之前也无法在所在的作用域中使用

image.png

函数优先

重要: 函数声明和变量声明都会被提升,但是需要注意(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量

  1. foo() //1
  2. var foo
  3. function foo(){
  4. console.log(1)
  5. }
  6. foo = function(){
  7. console.log(2)
  8. }

理解代码 =>

  1. function foo(){
  2. console.log(1)
  3. }
  4. // var foo 重复的声明忽略
  5. foo()
  6. foo = function(){
  7. console.log(2)
  8. }

注意:var foo 尽管出现在function foo() … 的声明之前,但它(var)是重复的声明(因此被忽略),因为函数声明会被提升到普通变量之前。
=》尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的

var重复的声明会被忽略,函数声明会覆盖之前

  1. foo() //3
  2. function foo(){
  3. console.log(1)
  4. }
  5. var foo = function(){
  6. console.log(2)
  7. }
  8. function foo(){
  9. console.log(3)
  10. }

第五章 作用域闭包

实质问题

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

  1. function foo(){
  2. var a =2
  3. function bar(){
  4. console.log(a)
  5. }
  6. return bar
  7. }
  8. var baz = foo()
  9. baz()

函数 bar() 的词法作用域能够访问 foo() 的内部作用域,然后我们将 bar() 函数本身当作一个值类型进行传递。

理解:正常情况下,在foo()执行后,通常会期待foo()的整个内部作用域被销毁,因为引擎有垃圾回收器来释放不在使用的内存空间,但是闭包可以阻止这件事的发生,事实上内部作用域依然存在,是bar()本身在使用

bar() 本身是bar所引用的函数对象,就是bar 这个函数,因为它声明的位置,使得它拥有foo()内部作用域的闭包,让该作用域可以一直存活,以供bar()在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,这个引用就叫做闭包。

image.png

image.png

现在我懂了

3.06晚上 11.00 新增解析

setTimeout 定义

setTimeout() 方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码

  1. for (var i = 0; i < 10; i++) {
  2. setTimeout(function () {
  3. console.log(i) //以每秒一次的频率输出10次10
  4. }, i * 1000)
  5. }

setTimeout()里的回调函数共享了全局作用域,for循环执行完 i 的结果是10,最后回调函数都用这个10
setTimeot() 的一次延时 1000 2000则指的是响应的延迟时间之后才将作业添加到作用队列中。
setTimeout(…,0),所有的回调函数依然在循环结束后才会执行


?不是很理解

批注:7.9 理解了,将一个内部函数timer传递给setTimeout(),timer涵盖wait()作用域的闭包

  1. function wait(message){
  2. setTimeout(function timer(){
  3. console.log(message)
  4. },1000)
  5. }
  6. wait("hello")

image.png

循环和闭包

经典问题

  1. for(var i =1;i<=5;i++){
  2. setTimeout(function timer(){
  3. console.log(i) //以每秒一次的频率输出10次10
  4. },i*1000)
  5. }

预期:分别输出数字1-5,每秒一次,每次一个
实际:这段代码在运行时会以每秒一次的频率输出五次6

解析:
这个循环终止的条件是i 不再 <= 5 ,条件首次成立时i的值是6,因此输出显示的是循环结束时i的最终值。
延迟函数的回调会在循环结束时执行、
事实上,当定时器运行时即使每个迭代中执行的是setTimeout(..,0)所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6来

image.png

IIFE会通过声明并立即执行一个函数来创建作用域,如果作用域是空的,那么仅仅将它们进行封闭是不够的
也就是我们的IIFE 只是一个什么都没有的空作用域。

  1. for (var i = 1; i <= 5; i++) {
  2. (function () {
  3. setTimeout(function timer() {
  4. console.log(i)
  5. }, i * 1000)
  6. })()
  7. }

=>

他需要有自己的变量,用来在每个迭代中存储i的值:

  1. for (var i = 1; i <= 5; i++) {
  2. (function () {
  3. var j = i
  4. setTimeout(function timer() {
  5. console.log(j)
  6. }, j * 1000)
  7. })()
  8. }

=> 进一步优化
在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问

  1. for (var i = 1; i <= 5; i++) {
  2. (function (j) {
  3. setTimeout(function timer() {
  4. console.log(j)
  5. }, j * 1000)
  6. })(i)
  7. }

重返块作用域

/?不是很理解

7.9补充:let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

  1. for (var i = 1; i <= 5; i++) {
  2. let j = i //闭包的块作用域
  3. setTimeout(function timer() {
  4. console.log(j)
  5. }, j * 1000)
  6. }

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

  1. for(let i=1;i<=5;i++){
  2. setTimeout(function timer(){
  3. console.log(i)
  4. },i*1000)
  5. }

模块

doSomething() 和 doAnother() 两个内部函数,他们的词法作用域(而这就是闭包)也就是foo() 的内部作用域

  1. function foo(){
  2. var something = "cool"
  3. var another = [1,2,3]
  4. function doSomething(){
  5. console.log(something)
  6. }
  7. function doAnother(){
  8. console.log(another.join("!"))
  9. }
  10. }

下面这种模式被称作为模块,最常见的实现模块模式的方法通常被称为模块暴露。这里是其变体。

  1. function CoolModule(){
  2. var something = "cool"
  3. var another = [1,2,3]
  4. function doSomething(){
  5. console.log(something)
  6. }
  7. function doAnother(){
  8. console.log(another.join("!"))
  9. }
  10. return {
  11. doSomething:doSomething,
  12. doAnother:doAnother
  13. }
  14. }
  15. var foo = CoolModule()
  16. foo.doSomething() //cool
  17. foo.doAnother() //1!2!3

image.png

模块模式需要具备两个必要条件
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

=>
对这个模式进行简单的改进来实现单例模式:

  1. var foo = (function CoolModule(){
  2. var something = "cool"
  3. var another = [1,2,3]
  4. function doSomething(){
  5. console.log(something)
  6. }
  7. function doAnother(){
  8. console.log(another.join("!"))
  9. }
  10. return {
  11. doSomething:doSomething,
  12. doAnother:doAnother
  13. }
  14. })()
  15. foo.doSomething()
  16. foo.doAnother()

模块也是普通的函数,因此可以接受参数

  1. function CoolModule(id){
  2. function identify(){
  3. console.log(id)
  4. }
  5. return {
  6. identify:identify
  7. }
  8. }
  9. var foo1 = CoolModule("foo1")
  10. var foo2 = CoolModule("foo2")
  11. foo1.identify() //"foo1"
  12. foo2.identify() //"foo2"

模块模式另一个变化用法,命名将要作为公共API返回的对象

7.9批注:看懂了,但是不知道实际运用场景

  1. var foo = (function CoolModule(id){
  2. function change(){
  3. //修改公共api
  4. publicAPI.identify = identify2
  5. }
  6. function identify1(){
  7. console.log(id)
  8. }
  9. function identify2(){
  10. console.log(id.toUpperCase())
  11. }
  12. var publicAPI = {
  13. change:change,
  14. identify:identify1
  15. }
  16. return publicAPI
  17. })("foo module")
  18. foo.identify() //foo module
  19. foo.change()
  20. foo.identify() //FOO MODULE

通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法的属性,以及修改他们的值。

现代的模块机制

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

  1. var MyModules = (function Manager(){
  2. var modules = {}
  3. function define(name,deps,impl){
  4. for(var i=0;i<deps.length,i++){
  5. deps[i] = modules[deps[i]]
  6. }
  7. modules[name] = impl.apply(impl,deps)
  8. }
  9. function get(name){
  10. return modules[name]
  11. }
  12. return {
  13. define:define,
  14. get:get
  15. }
  16. })()

image.png

未来的模块机制

ES6中为模块增加了一级语法支持,但通过模块系统进行加载时,ES6会将文件当做独立的模块来处理,每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。