《你不知道的JavaScript》--读书笔记

📑 上卷: 作用域和闭包, this和对象原型

📑 中卷: 类型和语法, 异步和性能

📑 下卷:起步上路,es6及更新版本

目录

Part 1 作用域和闭包

chart 1 作用域是什么

1.1 编译原理

1.2 理解作用域

1.3 作用域嵌套

1.4 异常

1.5 📝小结

chart 2 词法作用域

2.1 词法阶段

2.2 欺骗词法

2.3 📝小结

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

3.1 函数中的作用域

3.2 隐藏内部实现

3.3 函数作用域

3.4 块作用域

3.5 📝小结

chart 4 提升

4.1 先有鸡还是先有蛋(声明在前,还是赋值在前)

4.2 什么叫做提升

4.3 提升顺序

4.4 📝小结

chart 5 作用域闭包

5.1 什么是闭包?

5.2 模块

5.3 📝小结



Part 1 作用域和闭包

chart 1 作用域是什么

1.1 编译原理

  1. javascript被归类为“动态”或者“解释执行”语言,但事实上它也是一门编译语言

  2. 程序中的源代码在执行之前会经历三个步骤,统称为“编译”

    • 分词/词法分析(Tokenizing/Lexing):将字符串分解为有意义的代码块。这些代码块就被称为词法单元(token)

    • 解析/语法分析(Parsing):将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这棵数被称为“抽象语法树”(Abstract Syntax Tree, AST)

    • 代码生成:将AST转为可执行的代码过程。这个过程与语言、目标平台等息息相关。

1.2 理解作用域

三个概念:编译器、引擎、作用域

  • 引擎:从头到尾负责JavaScript程序的编译及执行过程

  • 编译器:负责词法分析及代码生成等脏活累活

  • 作用域:负责收集和维护所有声明的标识符(变量)组成的一系列查询

首先编译器会在当前作用域中声明一个变量(如果之前没有被声明),然后运行时引擎会在作用域中查找这个变量,如果能够找到就会对它复制。

 

编译器术语:

  1. LHS 当变量出现在赋值操作的左侧时(查找变量的容器本身,以便对其赋值

  2. RHS 当变量出现在赋值操作的右侧时(查找某个变量) retrieve his source value(取到它的值)

赋值操作的目标是谁(LHS)以及谁是赋值操作的源头(RHS)

函数声明不能理解为LHS查询和赋值的形式。

function foo(a) {
  console.log(a);
}
foo(2);

 

✏️ 习题:

function foo(a) {
  var b = a;
  return a + b;
}
​
var c = foo(2);
  1. 找到其中所有的LHS查询(3处)

  2. 找到其中所有的RHS查询(4处)

1.3 作用域嵌套

什么是作用域: 根据名称查找变量的一套规则。

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

遍历嵌套作用域规则:引擎从当前作用域开始查找变量,如果找不到,就像上一级继续查询。当抵达全局作用域还没找到,查找过程都会停止。

1.4 异常

ReferenceError: 同作用域判别失败相关

TypeError:代表作用域判别成功,但是对结果的操作是非法或者不合理的

为什么区分LHS和RHS? 因为在变量还没声明的情况下,这两种查询的行为是不一样的。

非严格模式:

  1. RHS查询在所有嵌套的作用域中查询不到变量时,引擎会抛出”ReferenceError“

  2. LHS查询在所有嵌套的作用域中查询不到变量时,全局作用域会创建并返回一个该名称的全局变量

严格模式:

1. LHS查询在所有嵌套作用域查询不到变量时,引擎会抛出”TypeError“

严格模式是ES5引入的。

严格模式禁止自动或隐式创建全局变量。

1.5 📝小结

  1. 编译原理是什么?有哪些步骤?

  2. 理解作用域、引擎、编译器之间的关系。

  3. 查询方式分为RHS和LHS,他们有什么区别?

  4. 遍历嵌套作用域规则是什么?

chart 2 词法作用域

作用域的两种主要工作模式:词法作用域(大多数编程语言采用)和动态作用域

2.1 词法阶段

词法作用域:定义在词法阶段的作用域。

换句话说:词法作用域是由你在写代码时将变量和作用域块写在哪里决定。

函数的词法作用域只由它被声明的所处位置决定。

2.2 欺骗词法

欺骗词法:在运行”修改“词法作用域。

欺骗词法作用域会导致性能下降:引擎无法在编译时对作用域查找进行优化。

有两种机制可以实现欺骗词法(eval with):

  1. eval:运行时,对词法作用域进行修改,但是在严格模式下,不能修改词法作用域,会抛出ReferenceError

    1. 同类型的还有new Function(),最后一个参数接受代码字符串,并将其转为动态生成的函数

  2. with:通常被被当作重复引用一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

    var obj = {
      a: 1,
      b: 2,
    }
    // 乏味的重复obj
    obj.a = 2;
    obj.b = 3;
    ​
    //使用with
    with(obj) {
     a = 3;
     b = 4;
    }

    eval:函数如果接受了一个或者多个声明的代码,就会修改其所在的词法作用域

    with:声明实际是根据你传递给它的对象凭空创建了一个全新的词法作用域

2.3 📝小结

  1. 什么是词法作用域?

  2. 什么是欺骗词法作用域?欺骗词法作用域有哪些方式?欺骗词法作用域的坏处是什么?

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

3.1 函数中的作用域

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

3.2 隐藏内部实现

最小限度地暴露必要内容,而将其它内容“隐藏”起来,比如某个模块或者对象的API设计。

“隐藏”作用域的另外一个好处就是:可以避免同名标识符之间的冲突

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

  2. 模块管理

3.3 函数作用域

可将任意代码片段外部添加包装函数,将这部分代码隐藏起来。具名函数也会污染全局作用域,且需要显式调用函数才能够运行这部分代码。

解决方案:使用立即执行函数表达式。

(function foo() {
  //do something
})();

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

函数表达式与函数声明的最大区别:他们的名称标识符将会绑定在何处

3.3.1 匿名与具名

关于函数表达最熟悉的场景,写回调函数的时候,比如:

setTimeout(function() {
 console.log('I waited 1 second!')
}, 1000)

这就叫匿名函数表达式,因为function() ... 没有名称表示符。

函数表达式是可以匿名的,但是函数声明不可以省略函数名

匿名函数表达式的几个缺点:

  1. 调试困难:在栈追踪时不会显示出有意义的函数名

  2. 代码可读性低

  3. 调用自身时只能使用arguments.callee (arguments在ES6时就不推荐使用了)

因此,建议使用函数表达式时,都指定一个函数名。

3.3.2 立即执行函数表达式(Immediatey Invoked Function Expression)

立即执行函数有两种写法

(function(){...})()
(function(){...} ())

以上两种写法在功能上都是一样的。

🔺 IIFE的进阶用法:把它们当作函数调用并传递参数进去

一个场景:代码风格上对全局对象的引用比引用一个没有”全局“字样的变量更加清晰。

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

另一个应用场景:解决undefined表示符的默认值被错误覆盖导致的异常

undefined = true;//其他代码挖了一个大坑
(function IIFE(undefined) {
  var a;
  if(a == undefined) {
   console.log("Undefined is safe here!")
  }
})()

另一种用途:倒置代码的运行顺序。将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。这种模式在UMD(universal model definetion)项目中广泛使用。

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

 

3.4 块作用域

变量的声明应该离使用的地方越近越好,并最大限度地本地化。

  1. with

  2. try/catch catch分句会创建一个块作用域,其中声明的变量只在 catch内部有效

  3. 4.3 let

    let 用let将变量附加到一个已经存在的块作用域上的行为是隐式的。在声明中的任意一个位置都可以使用{...} 来为let创建一个用于绑定的块。let 没有声明提升。

    1. 垃圾收集

    function process(data) {
      //do something
    }
    var someReallyBigData = {..};
    process(someReallyBigData);
    var btn = document.getElementById('my_button');
    btn.addEventListener("click", function click(evt) {
      console.log('button clicked')
    });

    这里的click函数的点击回调并不需要someReallyBigData 变量。理论上当process(..)执行之后,在内存中站用大量空间的数据结构届可以被回收了。但是由于click函数形成了一个覆盖整个作用域的闭包,Javascript引擎极有可能依然保存着这个结构(取决于具体实现)。块级作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存着这个结构。

    {
      let someReallyBigData = {..};
      process(someReallyBigData);
    }
    1. let 循环

      let可以发挥优势的经典栗子就是for循环中

      for(let i=0; i<10;i++) {
        console.log(i);
      }
      console.log(i); // ReferenceError

      let声明附属于一个新的作用域而不是当前的函数作用域(也不属于全局作用域)

    3.4.4 const

    用于创建块级作用域变量,其值是固定的(常量)。试图修改值的操作会引起错误。

3.5 📝小结

  1. 函数不是唯一的作用域单元,块级作用域指的是变量和函数不仅可以处于所属的作用域,也可以属于某个代码块。(通常指{..}内部)

  2. 可以产生块级作用域的情形 (1)try/catch 中的catch分句 (2)with (3) let (4)const

  3. 有些时候选择显示声明块级作用域更佳。

chart 4 提升

讨论函数作用域同其中变量声明出现的位置的某种微妙联系。

4.1 先有鸡还是先有蛋(声明在前,还是赋值在前)

a = 2;
var a;
console.log(a); //这里会输出什么 ?

4.2 什么叫做提升

回顾关于编译器的内容:引擎会在解释Javascript代码之前进行编译,将找到所有的声明并用合适的作用域将它们关联起来。

因此, 包括变量和函数声明都会在任何代码被执行之前首先被处理。

这个过程就好像变量和函数声明从它们的代码中出现的位置被“移动”到了最上面。这个过程叫做提升

每个作用域都会进行提升操作。

函数会被提升,函数表达式不会被提升。

4.3 提升顺序

函数会首先被提升,然后才是变量。

尽量避免在块内部声明函数。因为这个行为不可靠。

4.4 📝小结

  1. 提升的概念

  2. 函数和变量提升的顺序

  3. 重复的var声明会被忽略,但出现在后面的函数声明还是可以覆盖前面的。

chart 5 作用域闭包

5.1 什么是闭包?

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

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

举个例子 🌰

function foo() {
  var a = 2;
  function bar() {
   console.log(a)
  }
  return bar;
}
​
var baz = foo();
baz(); // 2 --这就是闭包的效果

引擎的垃圾回收机器用来回收不再使用的内存空间,但是由于这里使用了闭包,所以不会回收foo的内容。

bar依然保持对这个作用域的引用,这个引用就叫做闭包。

在定时器,事件监听器,ajax请求、跨窗口通信、web workers或者其他的异步任务中,只要使用了回调函数,实际上就是在使用闭包。

循环和闭包:

for(var i=1; i<=5; i++) {
 setTime(function timer(){
    console.log(i);
 }, i*1000)
}

note: 当循环函数包含函数定义的时候,代码格式检查器会经常发出警告。

以上代码: 我们的期望是:分别输出1-5,每秒一次,每次一个

实际:每秒输出一个6

因为所有回调函数在循环结束之后才会被执行,因此每次都输出一个6.

那怎么能写这段代码能够输出我们期望的值?

  1. 使用IIFE的方式,每次迭代中创建的作用域封闭起来

for(var i=1; i<=5;i++) {
  (function(j){
   setTimeout(function timer() {
    console.log(j)
   }, j*1000)
  })(i)
}
  1. 块级作用域与闭包联合使用

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

     

5.2模块

模式模块需要具备的两个条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次。(每次调用都会创建一个新的模块实例)

  2. 封闭函数必须返回至少一个内部函数,这样函数才能在私有作用与中形成闭包,并且可以访问或者改变私有的状态。

当只需要一个实例的时候,可以对这个模式进行简单的改进来实现单例模式

var foo = (function CoolModule(){
  var something = "cool";
  var anthor = [1,2,3];
  function doSomething() {
   console.log(something)
  }
  function doAnther() {
   console.log(another.join("!"))
  }
  
  return {
   doSomthing,
   doAnother,
  }
})();
foo.doSomthing();
foo.doAnother();

模块模式的另一个简单但强大的用法是 命名将要作为公共API返回的对象。

5.3 📝小结

  1. 什么是闭包?

  2. 闭包有什么作用?

  3. 模块的两个主要特征


未完待续.......

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值