Skip to content

Latest commit

 

History

History
253 lines (127 loc) · 23.1 KB

YDKJS.md

File metadata and controls

253 lines (127 loc) · 23.1 KB

作用域与闭包

第一章:什么是作用域?

作用域是一组规则,它决定了一个变量(标识符)在哪里和如何被查找。这种查询也许是为了向这个变量赋值,这时变量是一个 LHS(左手边)引用,或者是为取得它的值,这时变量是一个 RHS(右手边)引用。

LHS 引用得自赋值操作。作用域 相关的赋值可以通过 = 操作符发生,也可以通过向函数参数传递(赋予)参数值发生。

JavaScript 引擎 在执行代码之前首先会编译它,因此,它将 var a = 2; 这样的语句分割为两个分离的步骤:

  1. 首先,var a 在当前 作用域 中声明。这是在最开始,代码执行之前实施的。

  2. 稍后,a = 2 查找这个变量(LHS 引用),并且如果找到就向它赋值。

LHS 和 RHS 引用查询都从当前执行中的 作用域 开始,如果有需要(也就是,它们在这里没能找到它们要找的东西),它们会在嵌套的 作用域 中一路向上,一次一个作用域(层)地查找这个标识符,直到它们到达全局作用域(顶层)并停止,既可能找到也可能没找到。

未被满足的 RHS 引用会导致 ReferenceError 被抛出。未被满足的 LHS 引用会导致一个自动的,隐含地创建的同名全局变量(如果不是“Strict模式”[^note-strictmode]),或者一个 ReferenceError(如果是“Strict模式”[^note-strictmode])。

第二章:词法作用域

词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上可以知道所有的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。

在 JavaScript 中有两种机制可以“欺骗”词法作用域:eval(..)with。前者可以通过对一个拥有一个或多个声明的“代码”字符串进行求值,来(在运行时)修改现存的词法作用域。后者实质上是通过将一个对象引用看作一个“作用域”,并将这个对象的属性看作作用域中的标识符,(同样,也是在运行时)创建一个全新的词法作用域。

这些机制的缺点是,它压制了 引擎 在作用域查询上进行编译期优化的能力,因为 引擎 不得不悲观地假定这样的优化是无效的。这两种特性的结果就是代码 会运行的更慢。不要使用它们。

第三章:函数与块儿作用域

在 JavaScript 中函数是最常见的作用域单位。在另一个函数内部声明的变量和函数,实质上对任何外围“作用域”都是“隐藏的”,这是优秀软件的一个有意的设计原则。

但是函数绝不是唯一的作用域单位。块儿作用域指的是这样一种想法:变量和函数可以属于任意代码块儿(一般来说,就是任意的 { .. }),而不是仅属于外围的函数。

从 ES3 开始,try/catch 结构在 catch 子句上拥有块儿作用域。

在 ES6 中,引入了 let 关键字(var 关键字的表兄弟)允许在任意代码块中声明变量。if (..) { let a = 2; } 将会声明变量 a,而它实质上劫持了 if{ .. } 块儿的作用域,并将自己附着在这里。

虽然有些人对此深信不疑,但是块儿作用域不应当被认为是 var 函数作用域的一个彻头彻尾的替代品。两种机能是共存的,而且开发者们可以并且应当同时使用函数作用域和块儿作用域技术 —— 在它们各自可以产生更好,更易读/易维护代码的地方。

第四章:提升

我们可能被诱导而将 var a = 2 看作是一个语句,但是 JavaScript 引擎 可不这么看。它将 var aa = 2 看作两个分离的语句,第一个是编译期的任务,而第二个是执行时的任务。

这将导致在一个作用域内的所有声明,不论它们出现在何处,都会在代码本身被执行前 首先 被处理。你可以将它可视化为声明(变量与函数)被“移动”到它们各自的作用域顶部,这就是我们所说的“提升”。

声明本身会被提升,但不是赋值,即便是函数表达式的赋值,也 不会 被提升。

要小心重复声明,特别是将一般的变量声明和函数声明混在一起 —— 如果你这么做的话,危险就在眼前!

第五章:作用域闭包

对于那些还蒙在鼓里的人来说,闭包就像在 JavaScript 内部被隔离开的魔法世界,只有很少一些最勇敢的灵魂才能到达。但是它实际上只是一个标准的,而且几乎明显的事实 —— 我们如何在函数即是值,而且可以被随意传递的词法作用域环境中编写代码,

闭包就是当一个函数即使是在它的词法作用域之外被调用时,也可以记住并访问它的词法作用域。

如果我们不能小心地识别它们和它们的工作方式,闭包可能会绊住我们,例如在循环中。但它们也是一种极其强大的工具,以各种形式开启了像 模块 这样的模式。

模块要求两个关键性质:1)一个被调用的外部包装函数,来创建外围作用域。2)这个包装函数的返回值必须包含至少一个内部函数的引用,这个函数才拥有包装函数内部作用域的闭包。

现在我们看到了闭包在我们的代码中无处不在,而且我们有能力识别它们,并为了我们自己的利益利用它们!

this 与对象原型

第一章: this 是什么?

对于那些没有花时间学习 this 绑定机制如何工作的 JavaScript 开发者来说,this 绑定一直是困惑的根源。对于 this 这么重要的机制来说,猜测、试错、或者盲目地从 Stack Overflow 的回答中复制粘贴,都不是有效或正确利用它的方法。

为了学习 this,你必须首先学习 this不是 什么,不论是哪种把你误导至何处的臆测或误解。this 既不是函数自身的引用,也不是函数 词法 作用域的引用。

this 实际上是在函数被调用时建立的一个绑定,它指向 什么 是完全由函数被调用的调用点来决定的。

第二章: this 豁然开朗!

为执行中的函数判定 this 绑定需要找到这个函数的直接调用点。找到之后,四种规则将会以这种优先顺序施用于调用点:

  1. 通过 new 调用?使用新构建的对象。

  2. 通过 callapply(或 bind)调用?使用指定的对象。

  3. 通过持有调用的环境对象调用?使用那个环境对象。

  4. 默认:strict mode 下是 undefined,否则就是全局对象。

小心偶然或不经意的 默认绑定 规则调用。如果你想“安全”地忽略 this 绑定,一个像 ø = Object.create(null) 这样的“DMZ”对象是一个很好的占位值,以保护 global 对象不受意外的副作用影响。

与这四种绑定规则不同,ES6 的箭头方法使用词法作用域来决定 this 绑定,这意味着它们采用封闭他们的函数调用作为 this 绑定(无论它是什么)。它们实质上是 ES6 之前的 self = this 代码的语法替代品。

第三章:对象

JS 中的对象拥有字面形式(比如 var a = { .. })和构造形式(比如 var a = new Array(..))。字面形式几乎总是首选,但在某些情况下,构造形式提供更多的构建选项。

许多人声称“Javascript 中的一切都是对象”,这是不对的。对象是六种(或七中,看你从哪个方面说)基本类型之一。对象有子类型,包括 function,还可以被行为特化,比如 [object Array] 作为内部的标签表示子类型数组。

对象是键/值对的集合。通过 .propName["propName"] 语法,值可以作为属性访问。不管属性什么时候被访问,引擎实际上会调用内部默认的 [[Get]] 操作(在设置值时调用 [[Put]] 操作),它不仅直接在对象上查找属性,在没有找到时还会遍历 [[Prototype]] 链(见第五章)。

属性有一些可以通过属性描述符控制的特定性质,比如 writableconfigurable。另外,对象拥有它的不可变性(它们的属性也有),可以通过使用 Object.preventExtensions(..)Object.seal(..)、和 Object.freeze(..) 来控制几种不同等级的不可变性。

属性不必非要包含值 —— 它们也可以是带有 getter/setter 的“访问器属性”。它们也可以是可枚举或不可枚举的,这控制它们是否会在 for..in 这样的循环迭代中出现。

你也可以使用 ES6 的 for..of 语法,在数据结构(数组,对象等)中迭代 ,它寻找一个内建或自定义的 @@iterator 对象,这个对象由一个 next() 方法组成,通过这个 next() 方法每次迭代一个数据。

第四章: 混合(淆)“类”的对象

类是一种设计模式。许多语言提供语法来启用自然而然的面向类的软件设计。JS 也有相似的语法,但是它的行为和你在其他语言中熟悉的工作原理 有很大的不同

类意味着拷贝。

当一个传统的类被实例化时,就发生了类的行为向实例中拷贝。当类被继承时,也发生父类的行为向子类的拷贝。

多态(在继承链的不同层级上拥有同名的不同函数)也许看起来意味着一个从子类回到父类的相对引用链接,但是它仍然只是拷贝行为的结果。

JavaScript 不会自动地 (像类那样)在对象间创建拷贝。

mixin 模式常用于在 某种程度上 模拟类的拷贝行为,但是这通常导致像显式假想多态那样(OtherObj.methodName.call(this, ...))难看而且脆弱的语法,这样的语法又常导致更难懂和更难维护的代码。

明确的 mixin 和类 拷贝 又不完全相同,因为对象(和函数!)仅仅是共享的引用被复制,不是对象/函数自身被复制。不注意这样的微小之处通常是各种陷阱的根源。

一般来讲,在 JS 中模拟类通常会比解决当前 真正 的问题埋下更多的坑。

第五章: 原型(Prototype)[重点]

当试图在一个对象上进行属性访问,而对象没有该属性时,对象内部的[[Prototype]]链接定义了[[Get]]操作(见第三章)下一步应当到哪里寻找它。这种对象到对象的串行链接定义了对象的“原形链”(和嵌套的作用域链有些相似),在解析属性时发挥作用。

所有普通的对象用内建的Object.prototype作为原形链的顶端(就像作用域查询的顶端是全局作用域),如果属性没能在链条的前面任何地方找到,属性解析就会在这里停止。toString()valueOf(),和其他几种共同工具都存在于这个Object.prototype对象上,这解释了语言中所有的对象是如何能够访问他们的。

使两个对象相互链接在一起的最常见的方法是将new关键字与函数调用一起使用,在它的四个步骤中(见第二章),就会建立一个新对象链接到另一个对象。

那个用new调用的函数有一个被随便地命名为.prototype的属性,这个属性所引用的对象恰好就是这个新对象链接到的“另一个对象”。带有new的函数调用通常被称为“构造器”,尽管实际上它们并没有像传统的面相类语言那样初始化一个类。

虽然这些JavaScript机制看起来和传统面向类语言的“初始化类”和“类继承”类似,而在JavaScript中的关键区别是,没有拷贝发生。取而代之的是对象最终通过[[Prototype]]链链接在一起。

由于各种原因,不光是前面提到的术语,“继承”(和“原型继承”)与所有其他的OO用语,在考虑JavaScript实际如何工作时都没有道理。

相反,“委托”是一个更确切的术语,因为这些关系不是 拷贝 而是委托 链接

第六章: 行为委托 [重点]

在你的软件体系结构中,类和继承是你可以 选用不选用 的设计模式。多数开发者理所当然地认为类是组织代码的唯一(正确的)方法,但我们在这里看到了另一种不太常被提到的,但实际上十分强大的设计模式:行为委托

行为委托意味着对象彼此是对等的,在它们自己当中相互委托,而不是父类与子类的关系。JavaScript的[[Prototype]]机制的设计本质,就是行为委托机制。这意味着我们可以选择挣扎着在JS上实现类机制,也可以欣然接受[[Prototype]]作为委托机制的本性。

当你仅用对象设计代码时,它不仅能简化你使用的语法,而且它还能实际上引领更简单的代码结构设计。

OLOO(链接到其他对象的对像)是一种没有类的抽象,而直接创建和关联对象的代码风格。OLOO十分自然地实现了基于[[Prototype]]的行为委托。

附录A: ES6 class

class在假装修复JS中的类/继承设计模式的问题上做的很好。但他实际上做的却正相反:它隐藏了许多问题,而且引入了其他微妙而且危险的东西

class为折磨了JavaScript语言将近20年的“类”的困扰做出了新的贡献。在某些方面,它问的问题比它解决的多,而且在[[Prototype]]机制的优雅和简单之上,它整体上感觉像是一个非常不自然的匹配。

底线:如果ES6class使稳健地利用[[Prototype]]变得困难,而且隐藏了JS对象机制最重要的性质 —— 对象间的实时委托链接 —— 我们不应该认为class产生的麻烦比它解决的更多,并且将它贬低为一种反模式吗?

类型与文法

第一章:类型

JavaScript有7种内建 类型nullundefinedbooleannumberstringobjectsymbol。它们可以被typeof操作符识别。

变量没有类型,但是值有类型。这些类型定义了值的固有行为。

许多开发者会认为“undefined”和“undeclared”大体上是同一个东西,但是在JavaScript中,它们是十分不同的。undefined是一个可以由被声明的变量持有的值。“未声明”意味着一个变量从来没有被声明过。

JavaScript很不幸地将这两个词在某种程度上混为了一谈,不仅体现在它的错误消息上(“ReferenceError: a is not defined”),也体现在typeof的返回值上:对于两者它都返回"undefined"

然而,当对一个未声明的变量使用typeof时,typeof上的安全防卫机制(防止一个错误)可以在特定的情况下非常有用。

第二章:值

在JavaScript中,array仅仅是数字索引的集合,可以容纳任何类型的值。string是某种“类array”,但它们有着不同的行为,如果你想要将它们作为array对待的话,必须要小心。JavaScript中的数字既包括“整数”也包括浮点数。

几种特殊值被定义在基本类型内部。

null类型只有一个值nullundefined类型同样地只有undefined值。对于任何没有值存在的变量或属性,undefined基本上是默认值。void操作符允许你从任意另一个值中创建undefined值。

number包含几种特殊值,比如NaN(意为“不是一个数字”,但称为“非法数字”更合适);+Infinity-Infinity; 还有-0

简单基本标量(stringnumber等)通过值拷贝进行赋值/传递,而复合值(object等)通过引用拷贝进行赋值/传递。引用与其他语言中的引用/指针不同 —— 它们从不指向其他的变量/引用,而仅指向底层的值。

第三章:原生类型

JavaScript为基本类型提供了对象包装器,被称为原生类型(StringNumberBoolean,等等)。这些对象包装器使这些值可以访问每种对象子类型的恰当行为(String#trim()Array#concat(..))。

如果你有一个像"abc"这样的简答基本类型标量,而且你想要访问它的length属性或某些String.prototype方法,JS会自动地“封箱”这个值(用它所对应种类的对象包装器把它包起来),以满足这样的属性/方法访问。

第四章:强制转换

在这一章中,我们将注意力转向了JavaScript类型转换如何发生,也叫 强制转换,按性质来说它要么是 明确的 要么是 隐含的

强制转换的名声很坏,但它实际上在许多情况下很有帮助。对于负责任的JS开发者来说,一个重要的任务就是花时间去学习强制转换的里里外外,来决定哪一部分将帮助他们改进代码,哪一部分他们真的应该回避。

明确的 强制转换时这样一种代码,它很明显地有意将一个值从一种类型转换到另一种类型。它的益处是通过减少困惑来增强了代码的可读性和可维护性。

隐含的 强制转换是作为一些其他操作的“隐藏的”副作用而存在的,将要发生的类型转换并不明显。虽然看起来 隐含的 强制转换是 明确的 反面,而且因此是不好的(确实,很多人这么认为!),但是实际上 隐含的 强制转换也是为了增强代码的可读性。

特别是对于 隐含的,强制转换必须被负责地,有意识地使用。懂得为什么你在写你正在写的代码,和它是如何工作的。同时也要努力编写其他人容易学习和理解的代码。

第五章:文法

JavaScript文法有相当多的微妙之处,我们作为开发者应当比平常多花一点儿时间来关注它。一点儿努力可以帮助你巩固对这个语言更深层次的知识。

语句和表达式在英语中有类似的概念 —— 语句就像句子,而表达式就像短语。表达式可以是纯粹的/自包含的,或者他们可以有副作用。

JavaScript文法层面的语义用法规则(也就是上下文),是在纯粹的语法之上的。例如,用于你程序中不同地方的{ }可以意味着块儿,object字面量,(ES6)解构语句,或者(ES6)被命名的函数参数。

JavaScript操作符都有严格定义的优先级(哪一个操作符首先结合)和结合性(多个操作符表达式如何隐含地分组)规则。一旦你学会了这些规则,你就可以自己决定优先级/结合性是否是为了它们自己有利而 过于明确,或者它们是否会对编写更短,更干净的代码有所助益。

ASI(自动分号插入)是一种内建在JS引擎找中的解析器纠错机制,它允许JS引擎在特定的环境下,在需要;但是被省略了的地方,并且插入可以纠正解析错误时,插入一个;。有一场争论是关于这种行为是否暗示着大多数;都是可选的(而且为了更干净的代码可以/应当省略),或者是否它意味着省略它们是在制造JS引擎帮你扫清的错误。

JavaScript有几种类型的错误,但很少有人知道它有两种类别的错误:“早期”(编译器抛出的不可捕获的)和“运行时”(可以try..catch的)。所有在程序运行之前就使它停止的语法错误都明显是早期错误,但也有一些别的错误。

函数参数值与它们正式声明的命名参数之间有一种有趣的联系。明确地说,如果你不小心,arguments数组会有一些泄漏抽象行为的坑。尽可能避开arguments,但如果你必须使用它,那就设法避免同时使用arguments中带有位置的值槽,和相同参数的命名参数。

附着在try(或try..catch)上的finall在执行处理顺序上提供了一些非常有趣的能力。这些能力中的一些可以很有帮助,但是它也可能制造许多困惑,特别是在与打了标签的块儿组合使用时。像往常一样,为了更好更干净的代码而使用finally,不是为了显得更聪明或更糊涂。

switchif..else if..语句提供了一个不错的缩写形式,但是要小心许多常见的关于它的简化假设。如果你不小心,会有几个奇怪的地方绊倒你,但是switch手上也有一些隐藏的高招!

异步与性能

第一章: 异步: 现在与稍后

一个JavaScript程序总是被打断为两个或更多的代码块儿,第一个代码块儿 现在 运行,下一个代码块儿 稍后 运行,来响应一个事件。虽然程序是一块儿一块儿地被执行的,但它们都共享相同的程序作用域和状态,所以对状态的每次修改都是在前一个状态之上的。

不论何时有事件要运行,事件轮询 将运行至队列为空。事件轮询的每次迭代称为一个“tick”。用户交互,IO,和定时器会将事件在事件队列中排队。

在任意给定的时刻,一次只有一个队列中的事件可以被处理。当事件执行时,他可以直接或间接地导致一个或更多的后续事件。

并发是当两个或多个事件链条随着事件相互穿插,因此从高层的角度来看,它们在 同时 运行(即便在给定的某一时刻只有一个事件在被处理)。

在这些并发“进程”之间进行某种形式的互动协调通常是有必要的,比如保证顺序或防止“竞合状态”。这些“进程”还可以 协作:通过将它们自己打断为小的代码块儿来允许其他“进程”穿插。

第二章: 回调

回调是JS中异步的基础单位。但是随着JS的成熟,它们对于异步编程的演化趋势来讲显得不够。

首先,我们的大脑用顺序的,阻塞的,单线程的语义方式规划事情,但是回调使用非线性,非顺序的方式表达异步流程,这使我们正确推理这样的代码变得非常困难。不好推理的代码是导致不好的Bug的不好的代码。

我们需要一个种方法,以更同步化,顺序化,阻塞的方式来表达异步,正如我们的大脑那样。

第二,而且是更重要的,回调遭受着 控制反转 的蹂躏,它们隐含地将控制权交给第三方(通常第三方工具不受你控制!)来调用你程序的 延续。这种控制权的转移使我们得到一张信任问题的令人不安的列表,比如回调是否会比我们期望的被调用更多次。

制造特殊的逻辑来解决这些信任问题是可能的,但是它比它应有的难度高多了,还会产生更笨重和更难维护的代码,而且在bug实际咬到你的时候代码会显得在这些危险上被保护的不够。

我们需要一个 所有这些信任问题 的一般化解决方案。一个可以被所有我们制造的回调复用,而且没有多余的模板代码负担的方案。

我们需要比回调更好的东西。目前为止它们做的不错,但JavaScript的 未来 要求更精巧和强大的异步模式。

第三章: Promises

Promise很牛。用它们。它们解决了肆虐在回调代码中的 控制倒转 问题。

它们没有摆脱回调,而是重新定向了这些回调的组织安排方式,是它成为一种坐落于我们和其他工具之间的可靠的中间机制。

Promise链还开始以顺序的风格定义了一种更好的(当然,还不完美)表达异步流程的方式,它帮我们的大脑更好的规划和维护异步JS代码。