you dont know js 读后感
(前言)我们迎面挑战这种趋势:JS开发者总是得过且过地学习“将就够用”的东西,而从来不强迫他们自己去学习这门语言究竟是为何与如何工作的。另外,我们摒弃那些当路途艰难时常见的 逃跑 意见。
来自 https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/preface.md
(序)
你学的最后一个新东西是什么?
也许是一门外语,比如意大利语或德语。或者可能是一种图像编辑器,比如 Photoshop。或者是一种烹饪技术,木工活,日常锻炼。我想让你回忆一下你最终学会它时的感觉:醍醐灌顶的时刻。当事情从模糊不清变得豁然开朗,正如你掌握了如何使用台锯,或者理解了法语中雄性名词和雌性名词的区别。那种感觉怎么样?非常美妙,对吧?
现在我想让你再多向前回忆一些,找到你学会新技能之前的那一刻。它 感觉如何?可能有点儿吓人,也可能有点儿沮丧,是吧?在某一个时刻,我们都还不知道我们现在知道的事情,而这完全没问题;我们是从某处开始的。学习新的东西是一次激动人心的冒险,特被是当你想高效地学习它时。
来自https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/up%20%26%20going/foreword.md
一年前,我也是这样,得过且过的学习一些将就够用的东西,一遇到不会的,第一时间想的就是逃跑,完全不知道怎么样去克服它,幸好我遇到一个同事,他告诉我,整天学习一些自己已经知道的东西是没有进步的,当你觉得一个东西很难理解,这个感觉就是你在进步
作用域和闭包
-编译器
- 词法分析:词法分析处理是检查一串源代码字符,并给 token 赋予语法含义作为某种有状态解析的输出。(这个和浏览器渲染引擎解析html是一样的,把源代码转化成一些带有状态的token形成树形结构[编译器和解析器的规则都是一样的])
- 词法作用域:一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量,我们将“作用域”定义为一组规则,它主宰着 引擎 如何通过标识符名称在当前的 作用域,或者在包含它的任意 嵌套作用域 中来查询一个变量
不管函数是从 哪里 被调用的,也不论它是 如何 被调用的,它的词法作用域是由这个函数被声明的位置 唯一 定义的。也就是说 当函数被声明的那一刻起,作用域就已经定下了
为什么eval(..) 和 with 都可以欺骗编写时定义的词法作用域 并且导致性能低下的?
JavaScript 引擎 在编译阶段期行许多性能优化工作。其中的一些优化原理都归结为实质上在进行词法分析时可以静态地分析代码,并提前决定所有的变量和函数声明都在什么位置,这样在执行期间就可以少花些力气来解析标识符。
但如果 引擎 在代码中发现一个 eval(..) 或 with,它实质上就不得不 假定 自己知道的所有的标识符的位置可能是无效的,因为它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法作用域,或者你可能会向with传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。
换句话说,悲观地看,如果 eval(..) 或 with 出现,那么它 将 做的几乎所有的优化都会变得没有意义,所以它就会简单地根本不做任何优化。
总结:词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上可以知道所有的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。
变量提升:
提升是 以作用域为单位的,函数声明和变量声明都会被提升。但一个微妙的细节(可以 在拥有多个“重复的”声明的代码中出现)是,函数会首先被提升,然后才是变量。
我们可能被诱导而将 var a = 2 看作是一个语句,但是 JavaScript 引擎 可不这么看。它将 var a 和 a = 2 看作两个分离的语句,第一个是编译期的任务,而第二个是执行时的任务。
这将导致在一个作用域内的所有声明,不论它们出现在何处,都会在代码本身被执行前 首先 被处理。你可以将它可视化为声明(变量与函数)被“移动”到它们各自的作用域顶部,这就是我们所说的“提升”。
声明本身会被提升,但不是赋值,即便是函数表达式的赋值,也 不会 被提升
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
当 b 的 RHS 查询第一次发生时,它是找不到的。它被说成是一个“未声明”的变量,因为它在作用域中找不到。
如果 RHS 查询在嵌套的 作用域 的任何地方都找不到一个值,这会导致 引擎 抛出一个 ReferenceError。必须要注意的是这个错误的类型是 ReferenceError。
相比之下,如果 引擎 在进行一个 LHS 查询,但到达了顶层(全局 作用域)都没有找到它,而且如果程序没有运行在“Strict模式”[^note-strictmode]下,那么这个全局 作用域 将会在 全局作用域中 创建一个同名的新变量,并把它交还给 引擎。
“不,之前没有这样的东西,但是我可以帮忙给你创建一个。”
在 ES5 中被加入的“Strict模式”[^note-strictmode],有许多与一般/宽松/懒惰模式不同的行为。其中之一就是不允许自动/隐含的全局变量创建。在这种情况下,将不会有全局 作用域 的变量交回给 LHS 查询,并且类似于 RHS 的情况, 引擎 将抛出一个 ReferenceError。
现在,如果一个 RHS 查询的变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError。
ReferenceError 是关于 作用域 解析失败的,而 TypeError 暗示着 作用域 解析成功了,但是试图对这个结果进行了一个非法/不可能的动作。
闭包
闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了,伙计。
// 在执行 foo() 之后,我们将它返回的值(我们的内部 bar() 函数)赋予一个称为 baz 的变量,然后我们实际地调用 baz(),这将理所当然地调用我们内部的函数 bar(),只不过是通过一个不同的标识符引用。
// bar() 被执行了,必然的。但是在这个例子中,它是在它被声明的词法作用域 外部 被执行的。
// foo() 被执行之后,一般说来我们会期望 foo() 的整个内部作用域都将消失,因为我们知道 引擎 启用了 垃圾回收器 在内存不再被使用时来回收它们。因为很显然 foo() 的内容不再被使用了,所以看起来它们很自然地应该被认为是 消失了。
实质上 无论何时何地 只要你将函数作为头等的值看待并将它们传来传去的话,你就可能看到这些函数行使闭包。计时器、事件处理器、Ajax请求、跨窗口消息、web worker、或者任何其他的异步(或同步!)任务,当你传入一个 回调函数,你就在它周围悬挂了一些闭包!意思就是:当你的函数在你定义的词法作用域以外执行并记住词法作用域的时候就是闭包了