浅尝辄止:《你不知道的JavaScript》上卷粗略记忆录

57 阅读6分钟

这是第二次刷《你不知道的JavaScript》上卷,相比第一次,有些地方有种豁然开朗的感觉,但是有些还是不能很好的理解,趁着对原文还有系统框架的记忆,写一下自己的粗略笔记

编译

JavaScript在执行之前会进行编译,这一过程涉及:词法分析,语法分析,和代码生成。对作用域有至关重要的作用

  • 词法分析:将代码分割成一个个单词,比如:var a = 2; 会分割成var(关键字)、a(标识符)、=(运算符)、2(数值)
  • 语法分析:将上述词法单元流转成AST抽象语法树,可以点击这里看一下var a = 2;的抽象语法树
  • 代码生成:这一过程比较复杂,会涉及语义分析,作用域建立,代码生成,内存分配,变量初始化等

作用域

作用域的定义

JavaScript采用的是词法作用域(Lexical Scoping),也称为静态作用域。这意味着变量的作用域是由编写的位置决定的,而不是由函数调用的位置决定的 例子:

function outerFunction() {
    var outerVariable = "I am from outerFunction";

    function innerFunction() { // 这里定义,可以访问outerFunction的作用域
        console.log(outerVariable);
    }

    return innerFunction;
}

var myInnerFunction = outerFunction(); // 这里返回的其实就是innerFunction函数

// 这里调用其实就是在调用innerFunction,他的作用域是在写的时候决定的,拥有outerFunction作用域的访问权限,虽然在全局调用,但是可以访问outerFunction的作用域
myInnerFunction();  // 输出 "I am from outerFunction"

两种方法可以通过运行时修改作用域

  • eval
function foo(str, a) { 
  eval(str); // 欺骗! 
  console.log(a, b); } 
  var b = 2; 
foo("var b = 3; ", 1); // 1, 3

不建议使用,会影响性能,而且严格模式下会报错

  • with
function foo(obj) {
  with (obj) {
    a = 2;
  } 
} 
var o1 = { a: 3 };
var o2 = { b: 3 }; 
foo(o1); 
console.log(o1.a); // 2
foo(o2); 
console.log(o2.a); // undefined console.log(a); // 2——不好,a被泄漏到全局作用域上了!

传入o1,执行a = 2时,执行左查询,找到了o1.a,赋值为2。传入o2时,在o2上没有找到a,a的前面也没有let , var, const修饰,因此创建了一个全局变量a

作用域作用

  • 隐藏实现细节和避免命名冲突:

类库通常会使用作用域来隐藏其内部实现,只暴露必要的公共接口,从而增强代码的安全性和稳定性

  • 内存管理:

变量在它们的作用域内被创建,当作用域不再需要时,这些变量可以被垃圾回收机制清理,释放内存

function process(data) { // 在这里做点有趣的事情 
} 
var someReallyBigData = { ..};
process(someReallyBigData); 
var btn = document.getElementById("my button"); 
btn.addEventListener("click", function click(evt) { 
  console.log("button clicked"); 
}, /*capturingPhase=*/false );

这里如果这样写,process执行之后someReallyBigData本来应该没有用了,应该被回收,但是click形成了对整个作用域的闭包引用,会造成someReallyBigData可能不会被回收。取决于click里面有没有使用 改:

function process(data) { // 在这里做点有趣的事情 
} // 在这个块中定义的内容完事可以销毁! 
{ 
  let someReallyBigData = { .. };
  process(someReallyBigData); 
} 
var btn = document.getElementById("my button"); 
btn.addEventListener("click", function click(evt){ 
console.log("button clicked"); 
}, /*capturingPhase=*/false );

这里通过块作用域把someReallyBigData包裹起来,click闭包不会形成对它的引用,process执行完之后一定会被销毁

  • 闭包和延迟执行:

作用域能够支持闭包的创建,允许函数访问和操作其外部作用域中的变量,即使外部函数已经执行完毕

变量提升

JavaScript并不完全是自上而下执行的,参考如下代码

a = 2;
var a;
console.log(a) // 2,这里不是undefined

当看到var a = 2;javascript其实看成了两部分,var a和a = 2,第一个声明在编译阶段执行,第二个赋值操作在执行阶段,JavaScript会把变量函数的声明从他们原来的位置移动到最上面(作用域内的最上面),就叫变量提升

对于函数,只有函数声明会被提升,函数表达式不会被提升,同时有函数和变量的时候,函数优先

foo(); // 1 
var foo; 
function foo() { 
  console.log(1); 
} 
foo = function() { 
  console.log(2); 
};

函数声明

  • 函数声明通常是以 function 关键字开始的,后面跟着函数名和函数体。
  • 函数声明通常是在代码的顶层作用域或函数作用域内部进行的。
  • 函数声明会被提升,可以在声明之前调用。

函数表达式

  • 函数表达式是将一个函数赋值给一个变量或者作为一个值传递给另一个函数。
  • 函数表达式可以是匿名的,也可以有一个函数名。
  • 函数表达式通常出现在表达式的位置,比如赋值语句的右侧。

闭包

当函数可以记住并访问定义时的词法作用域时,就形成了闭包,即使是在定义时的词法作用域之外被调用

function foo() {
  var a = 2; 
  function bar() { 
    console.log(a); // 2 
  } 
bar(); 
} 
foo();

这里是闭包吗?不是.虽然 bar 函数访问了外部函数 foo 中的变量 a,但它并没有在定义时捕获这个变量。(在本来的词法作用域内调用的)

function foo() { 
  var a = 2; 
  function bar() { 
   console.log(a); // 2 
  } 
  return bar; // 返回 bar 函数 
} 
var baz = foo(); // 这里 bar 函数被返回并赋值给 baz 
baz(); // 这里调用 bar 函数,输出 2

这里是闭包吗?是的。bar在本来的词法作用域外面被调用

原文:在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。比如下面这些都是闭包

function foo() {
  var a = 2; 
  function baz() {
    console.log(a); // 2 
  } 
  bar(baz); 
} 
function bar(fn) {
  fn(); // 妈妈快看呀,这就是闭包! 
}
var fn; 
  function foo() {
  var a = 2; 
  function baz() {
    console.log(a); 
  } 
  fn = baz; // 将baz分配给全局变量 
} 
function bar() {
  fn(); // 妈妈快看呀,这就是闭包!
} 
foo(); 
bar(); // 2
function wait(message) {
  setTimeout(function timer() {
    console.log(message); 
  }, 1000 ); 
} 
wait("Hello, closure! ");
function setupBot(name, selector) { 
  $(selector).click(function activator() {
    console.log("Activating: " + name); 
  } ); 
} 
setupBot("Closure Bot 1", "#bot 1"); 
setupBot("Closure Bot 2", "#bot 2");

模块

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

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

还剩下第二部分,this,对象, 原型,行为委托,后续更新