理解JavaScript的核心知识点原型

JavaScript 中的原型机制一直以来都被众多开发者(包括本人)低估甚至忽视了,这是因为绝大多数人没有想要深刻理解这个机制的内涵,以及越来越多的开发者缺乏计算机编程相关的基础知识。对于这样的开发者来说 JavaScript 的原型机制是一个尚待发掘的大宝藏,深入了解下去会让大家在编程这条路上走得更长远,当然你不能妄想任何一种机制、模式或范式是完美无缺的。 首先,需要来理清一些基础的计算机编程概念: 编程哲学与设计模式:Programming Philosophy and Design Pattern计算机编程理念源自于对现实抽象的哲学思考,面向对象编程(OOP)是其一种思维方式,与它并驾齐驱的是另外两种思路:过程式和函数式编程。这三种方式对应于解决计算机架构问题的三种不同思路。它们也分别代表了不同的编程哲学。 具体实现编程架构的代码方案可以称为设计模式。设计模式是解决具体问题的一种最佳实践,可以用在设计语言本身,也可以用在具体业务场景中。 三种思路在语言本身的设计和应用业务中是可能混用的,灵活的语言正如 JavaScript ,内部虽然是基于面向对象编程而实现,但在开发过程中也可以运用过程式编程或函数式编程的思路进行具体业务的设计。正因为这容易造成开发者的混乱,所以特别指出,下面一段讨论的是针对语言内部的实现方式而不是应用业务。 面向对象编程语言的核心是对象,针对如何设计出一套语言的对象模型编程大师们又提出了三种不同的模式:类、原型、元类(元类是基于类模型产生的新模型)。三种模型造就了许多不同的编程语言,JavaScript 恰好是原型模式的典型代表,正如 JAVA 是基于类模式的典范,请谨记这一语言本身在设计模式上的区别。 很多语言由于自身的实现而限制了在其中可能应用到业务中的设计模式。但对于 JavaScript 这样的语言来说,选择是开放性的,因为我们经常在应用业务上听到大家讨论类继承或原型继承这样的实现方案,这便是它非常灵活的一个表现。但对于类模式和原型模式,有一些本质上的概念区别和使用混淆是很多人没有注意到的,下面对这两种设计模式做一个详细的讨论。 作为一种设计模式的类:"Class" Design Pattern基于类的应用或业务架构实现可以称为类设计模式,我们在业务开发中不可避免地会使用到继承的概念便是出自于类的范畴。类不专属于 JavaScript 语言范畴,JavaScript 中实质上也没有实现真正的基于类设计模式的接口。JavaScript 中一切关于“类”的说法实际上都是一种有名无实的冒充和混淆。 我们通常以为在 JavaScript 中“类”是必选的,使用它来实现业务架构不仅天经地义而且是唯一的——这是对 JavaScript 的最大误解。JavaScript 虽然是面向对象的编程语言,但以类作为对象模型来实现业务需求的方式只能说是一种设计模式:面向对象绝不等同于类。 类是一份产品制造说明书,指导生产机器生产符合其定义参数、具有相应功能的产品。它的用途在于规定而不在于实际使用,使用的是通过类制造出来的产品,在 JavaScript 中即对象。我们基于复用、继承等工业化生产需求而使用类这套设计模式:规定 -> 制造 -> 使用。但我们千万不能忘记,在工业化时代出现之前,通过手工的方式一样可以制造产品,如果你需要批量生产模样一样的东西才需要这份产品制造说明说。就手段来说要澄清的一个误区是,类并不是实现功能复用、广义上的继承等业务目标的唯一模式。 类:What's Class类,是面向对象编程中一种通用对象模型,它是基于一种对现实中事物进行分类的抽象,天生带有类别层级的观念,如生物是一级类、动物是一个具有所有生物特性而派生出自己独有特性的二级类,依照这样的逻辑还可以继续推及到其下更多细别的子类,这是一种将所有对象进行树状类别组织关联的思维方式: 通过这张图可以得出一个显而易见却容易被忽视的事实:永远没有一只具体的哺乳动物(比如说一只狮子)等同于哺乳动物这个类别,就像你不等于人类一样。类是一个并不具有实体的概念,是人为的发明,为了将具有类似特性的事物分门别类以适应人脑简化处理信息的方式,尽管自然并不是出于这样的目的而生成各种事物的。 JavaScript 中类的概念也是人为的设计,为的是更靠近本身以类模式设计而成的语言,尽管它本身是以原型模式设计而成的。因此我们有了 new 一个对象这种操作,为的是更符合采用类这一设计模式来实践面向对象编程。所以在此处埋下了第一个令人迷惑的种子:JavaScript 原生基于原型关联起来的对象与基于类创建的与类关联起来的对象两种概念的混淆。对于发现了这一对使人迷惑的概念的开发者来说,便有了第一个疑问: 为什么基于原型模式设计而成的 JavaScript 不继续在业务场景中使用原型设计模式,而是转而求向类设计模式? 之前有过说明,实践面向对象编程的方式有三种的,并且没有任何一种是完美无缺的。所以请把类模式是最好的这种想法抛到九霄云外吧。暂且将这个问题移到潜意识中去,继续了解一下类范畴的的其他相关概念。 实例:What's Instance实例的概念基于类之上。正如自然界中单一的个体即是它所属类别中的一个实例,面向对象语言中的一个对象就是它所属类中的一个实例。语言通过类的规定,生成了具有内存实体的对象。在这样的语言中,实例和对象的指代物是一致的,我们通常在类设计模式中采用实例来描述一个内存实体,而在编程实践中使用对象来描述一个内存实体,其实是在不同层面上的语言转换。理解这种词语的转换,对于我们在阅读各种技术书籍时了解作者所选择的表述视角是有帮助的。 创建实例操作的结果是将类的属性和方法分别复制到不同的实例对象中,它们持有各自独立的版本,这也意味着每一个由同一个类创建出的实例都是各自独立互不影响的个体。 而在 JavaScript 中,事情就变得没那么简单了。不管在它的设计者设计出模拟类模式的原生 API 之前还是之后(当然官方一直有关于类的语法糖的支持),JavaScript 的世界实际上都是由且只由对象组成。当你创建了一个构造器函数或使用 ES6 的类定义语法时,其实质根本没有真的定义了类,它是由对象伪装而成的。 在这一事实的基础上,就能发现既然“类”也是对象,那么我们本以为应用类模式建立的类与实例之间的纯粹关系就被基于对象的模拟打破了。使用上面那个大自然的归类例子再来解释下这是什么意思:当哺乳动物这一类别是一只狮子时,它既是具体又是抽象的,作为一个类这只狮子囊括了所有的哺乳动物,它是凌驾于其他具体生物之上的;作为一个具体生物它又是被包含进它本身的...这似乎变成了一个逻辑问题。 ...

July 9, 2019 · 2 min · jiezi

怎样阅读-ECMAScript-规范

翻译自:How to Read the ECMAScript SpecificationEcmascript 语言规范 The ECMAScript Language specification(又名:Javascript 规范 the JavaScript specification 或 ECMA-262)是学习 JavaScript 底层工作原理的非常好的资源。 然而,这是一个庞大的专业文本资料,咋一眼看过去,大家可能会感到迷茫、恐惧,满怀激情却无从下手。 前言不管你是打算每天阅读一点 ECMAScript 规范,还是把它当成一个年度或者季度的目标,这篇文章旨在让你更轻松的开始阅读最权威的 JavaScript 语言参考资料。 为什么要阅读 ECMAScript 规范Ecmascript 规范是所有 JavaScript 运行行为的权威来源,无论是在你的浏览器环境,还是在服务器环境( Node.js ),还是在宇航服上[ NodeJS-NASA ] ,或在你的物联网设备上[ JOHNNY-FIVE ]。 所有 JavaScript 引擎的开发者都依赖于这个规范来确保他们各种天花乱坠的新特性能够其他 JavaScript 引擎一样,按预期工作。 Ecmascript 规范 绝不仅仅对 JavaScript 引擎开发者有用,它对普通的 JavaScript 编码人员也非常有用,而你只是没有意识到或者没有用到。 假设有一天你在工作中发现了下面这个奇怪的问题: > Array.prototype.push(42)1> Array.prototype[ 42 ]> Array.isArray(Array.prototype)true> Set.prototype.add(42)TypeError: Method Set.prototype.add called on incompatible receiver #<Set> at Set.add (<anonymous>)> Set.prototypeSet {}并且非常困惑为什么一个方法在它的原型上工作,但是另一个方法在它的原型上却不工作。 不幸的是,这种问题你 Google 不到, Stack Overflow 可能也解决不了你的疑惑。 ...

May 21, 2019 · 9 min · jiezi

理解JavaScript的核心知识点:This

Understanding JavaScript Core: Thisthis 是 JavaScript 中非常重要且使用最广的一个关键字,它的值指向了一个对象的引用。这个引用的结果非常容易引起开发者的误判,所以必须对这个关键字刨根问底。执行上下文:Execution Context在深入了解 this 对象之前先介绍另一个概念:执行上下文。 没错,执行上下文与 this 在本质上是两个概念,或者说它们指代的范畴有差异,想要准确认识 this,就得先把它们区分开。可以把执行上下文想象为一个容器,其中包含了一句句待执行的代码。代码在这个容器中有上下行两条路线,是由某一些特殊代码所触发(如函数),上行路线跳入了一个新的容器,开始在新容器中执行另一些代码,本容器中的后续代码被暂时中断;如果新容器中还有代码会触发上行路线,就继续往上增加新容器,并交出控制权,层层叠加,形成了一个从底往上形式的叠罗汉,这就是 JavaScript 运行时的执行上下文栈。执行上下文这一抽象概念本身包含了更多有关 JavaScript 这门语言的内部机制,对于语言使用者来说是不透明的,其中与运行前的编译规则有很大关联,并被包含到整个程序运行前的初始化过程中,与词法作用域的变量解析规则相配合,将这些静态解析后的变量带入运行时的环境,所以它是程序运行时的关键内部组件或者说容器,而 JavaScript 将对执行上下文的引用提供给程序开发者的唯一入口就是 this,它得以访问被编译后带入到某个执行上下文运行环境中的变量。this 指代的其实只是内部抽象的执行上下文向用户所开放的那一部分,其实体是一个对象,绑定了许多编译后的变量。以下是一段关于执行上下文精辟的总结:An execution context is purely a specification mechanism and need not correspond to any particular artefact of an ECMAScript implementation. It is impossible for ECMAScript code to directly access or observe an execution context.翻译:执行上下文纯粹是一种规范机制,它不需要与基于 ECMAScript 规范的任何特定扩展实现对应。ECMAScript 代码无法直接访问或观察执行上下文。关于This对象:What’s This我将官方文档和一些别的文章里的说明稍加梳理,可以从以下段落中较为清晰地看出 this 的本质:First, know that all functions in JavaScript have properties, just as objects have properties. And when a function executes, it gets the this property—a variable with the value of the object that invokes the function where this is used.The this keyword evaluates to the value of the ThisBinding of the current execution context.The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword thisthis is not assigned a value until an object invokes the function where this is defined.翻译:首先,要知道 JavaScript 中所有的函数与对象一样都拥有属性。当一个函数执行时,它得到 this 属性——一个指向调用函数的对象的变量。this 关键字计算为当前执行上下文的 ThisBinding 属性的值。GetThisEnvironment 抽象运算查找当前提供 this 关键字的绑定的环境记录。在对象调用了定义了 this 的函数之前,this 不会被赋值。由此可得出关于 this 的完全定义:this 是在程序运行时,通过语言内部抽象操作在执行上下文中动态计算得到的,指向调用使用了其的函数的对象的变量。执行上下文 vs. This关键字:Execution Context vs. This Keyword执行上下文和 this 关键字的关系与潜意识相对于意识的关系类似,执行上下文是冰山下深邃庞大而不可窥探的秘地,而 this 只将其一个小部分显露出来。由于 JavaScript 是面向对象的编程语言,所以执行上下文其实质相当于一个对象,this 指向了它向开发者开放了的一系列属性集合的对象,因而我把 this 叫做执行上下文的引用对象。This因何而来:Why ThisJavaScript 在编写初始借鉴了JAVA 和 C 语言的特性,即便本质上不同,但还是把这个如同惯例般存在的 this 拿了过来。使用 this 的原因其实很简单:首先,我们时常无法得知调用了函数的对象的名称,并且有时候根本就没有名称可以用来引用调用对象。这是一个迫切的原因,因为我们在开发时必定会遇到需要引用调用函数的对象的场景。其次,避免重复指代,就像我们经常使用第三人称来指代前文的主体一样,作为程序员大家当然很乐意使用一个快捷方式来避免机械重复一些不必要的代码,这也是“语言”这一重要产品的特性。最后,它提供给我们实现高级功能的可能性,我们可以通过 this 动态对于执行上下文的指代而实现程序的复用性和扩展。This的判断规则:Rules of This对 this 的根源进行深入探究的目的就是为了在开发中对自己所使用的 this 关键字指代的对象进行准确的判定,它就是一个变量,所以当我们使用它的时候,必须清晰地知道它的值到底是什么。一般来说,我们可以通过确定是哪个对象拥有所调用的函数来确定其 this 的指向。这是由于 this 的绑定值是在函数调用的时候才赋予的,要看函数在哪个上下文对象中调用,但有时候这不是仅用肉眼就能观察出来的。此外还要严肃声明一下,虽然在之前下定义的时候将 this 的概念明确地划分到了运行阶段,但由于它作为一个变量的特性,是可以改变引用值的,它的值的计算与词法规则还是息息相关,得将编译和运行时两个阶段结合起来,总结出关于判断 this 绑定值的基本原则。this 关键字绑定的操作是在语言内核机制的运行时里执行的,由于无法去探索其内部,只能通过官方文档中给出的一系列描述程序来得知其如何判断,可以梳理出函数调用的内部过程中对 this 的绑定计算的依据:前置知识 1: 内部机制创建执行上下文、初始化函数所属领域和创建相关环境记录在函数被真正执行之前,内部机制会执行创建拥有函数的领域、创建执行上下文、移交当前执行上下文控制权、创建环境记录、环境记录对象参数的绑定等一系列操作,为程序运行做编译准备。在将函数推入执行栈顶层的时候,对其上下文的归属有以下的判断过程,此处与一个新的概念领域有关:如果领域中的属性 this 返回了一个对象,就将内部属性 thisValue 设置为以此对象为基础按照规格创建的 js 对象,否则 thisValue 绑定值为 undefined,表明领域的全局对象(本地全局对象)将设置为全局对象(程序全局对象)。这里在新规范里出现的一个概念领域取代了之前版本中简单的作用域的概念,由于实现了模块化等其他新特性,所以作用域的概念可以相当于扩展成了现在的领域,它下属了其他几个环境记录,其中变量的绑定分别在不同环境记录中,这里就不做深入解释了。领域中比较重要的属性是领域中的全局对象,这与程序运行时的全局对象的概念要加以区别,所以可以把领域中的全局对象看作是本地全局变量,其实也就是函数所属的上下文对象,它的值就是在刚才的以上的判断中确定的,如果没有这个前置对象,就会把全局对象设置为本地全局对象的值。前置知识 2: 内部机制创建函数内部机制在词法分析阶段会通过函数的定义方式向创建函数操作传入几种不同类型的函数类型:Normal、Arrow、Method,相对应的是普通函数、箭头函数、作为对象方法的函数。同时在这一步还传入指定代码严格模式的参数 strict。然后进行函数的初始化的。方式 1: 内部机制初始化普通函数内部机制在这一步会设置函数的一个重要属性 ThisMode 的值,它是决定 this 绑定值的依据,它的值是根据上一步传入的参数来判断的,依次执行一下三条判断分支:函数类型为 Arrow:将 ThisMode 赋值为 lexical ,这个值在计算 this 绑定时将按照词法作用域的规则来赋值,也就是说 this 的值与定义函数的词法作用域中的 this 相一致。代码模式为 strict :将 ThisMode 赋值 strict,按照这个值计算 this 绑定时只会将显式传入的上下文对象绑定给 this。非以上两种条件:将 ThisMode 赋值 global,被设置为 global 之后,函数在运行阶段被调用时,this 的值就会指向全局对象。方式 2: 内部机制创建对象方法函数作为对象属性的方法是另外来计算 this 的,只有在作为对象方法被调用的函数,在内部创建函数时才会传入 Method 值。毫无疑问它将 this 指向了这个前置的对象。构造函数也是同理。总结一下对一般使用到的函数的判断规则如下:箭头函数:无论调用位置,取它词法定义处的外层上下文中绑定的 this,没有中间本地对象存在时总是能够取到全局对象。严格模式:无论调用位置,只取显式给定的上下文绑定的 this,通过 call()、apply()、bind() 方法传入的第一参数,否则是 undefined。new 关键字调用的构造器函数:无论调用位置,this 必为在内部创建的新的实例对象显式绑定上下文对象的普通函数:无论调用位置,this 必为传入的上下文对象方法函数:属于隐式绑定,无论词法定义位置,实际情况视调用处而定:直接调用时:this 为前置上下文对象作为被引用值时:this 为调用时的上下文对象,在其他对象中引用 this 就是这个调用它的对象;被全局变量引用,this 就是全局对象。普通函数:无论词法定义位置,视调用处而定,其实质在内存都都是被作为引用值调用的,所以 this 都指向全局对象,严格模式规则优先。另外关于事件造成的一些 this 误解可以参考The this keyword这篇文章。其实并不属于特殊规则,是由于各种事件监听定义方式本身造成的。在实际开发中可以参考《You Don’t Know JS》里关于 this 的绑定规则和优先级的章节Nothing But Rules。在这套基础通用规则之外,箭头函数利用了另一套方式来判断 this 的绑定值,这篇文章里也有详尽的叙述。参考文献:ReferenceThe ECMAScript 9.0 StandardExecutable Code and Execution ContextsThe ECMAScript 5.1 StandardThe this KeywordMDN web docs: thisYou Don’t Know JS: this & object prototypesChapter 1: this Or That?Chapter 2: this All Makes Sense Now!Understanding the “this” keyword in JavaScriptThe this keywordStackOverflow: How does the “this” keyword work?Scope In JavaScriptUnderstand JavaScript’s “this” With Clarity, and Master ItJavaScript 的 this 原理 ...

April 18, 2019 · 2 min · jiezi

理解JavaScript的核心知识点:作用域

关于作用域:About Scope作用域是程序设计里的基础特性,是作用域使得程序运行时可以使用变量存储值、记录和改变程序的“状态”。JavaScript 也毫不例外,但在 JavaScript 中作用域的特性与其他高级语言稍有不同,这是很多学习者久久难以理清的一个核心知识点。定义:Definition首先引用两处我认为比较精辟的对作用域定义的总结:Scope is the accessibility of variables, functions, and objects in some particular part of your code during runtime. In other words, scope determines the visibility of variables and other resources in areas of your code.翻译:作用域是在运行时对代码某些特定部分中的变量、函数和对象的可访问性。换句话说,作用域决定代码区域中变量和其他资源的可见性。Scope is the set of rules that determines where and how a variable (identifier) can be looked-up.翻译:作用域是一套规则,决定变量定义在何处以及如何查找变量。综上所述,我们可以把作用域理解成是在一套在程序运行时控制变量访问的管理机制。它规定了变量可见的区域、变量查找规则、嵌套时的检索方法。目的:Purpose利用作用域是为了遵循程序设计中的最小访问原则,也称最小特权原则,这是一种以安全性为考量的程序设计原则,可以便于快速定位错误,将发生错误时的损失控制在最低程度。这篇文章的这一部分举了一个电脑管理员的例子来说明最小访问原则在计算机领域的重要性。在编程语言中,作用域还有另外两个好处——规避变量名称冲突和隐藏内部实现。我们知道每个作用域具有自己的权利控制范围,在不同的作用域中定义相同名称的变量是完全可行的。实现这一可能性的底层机制叫做“遮蔽效益”。这一机制体在嵌套作用域下得到了更好的体现,因为变量查找的规则是逐级向上,遇到匹配则停止,当内外层都有同名变量的时候,如已在内层找到匹配的变量,就不会再继续向外层作用域查找了,就像是内层的变量把外层的同名变量遮蔽住了一样。是不是感觉非常熟悉?没错,这也是 JavaScript 中原型链查找的内部机制!隐藏内部实现其实是一种编程的最佳实践,因为只要编程者愿意,大可暴露出全部代码的内部实现细节。但众所周知,这是不安全的。如果第三者在不可控的情况下修改了正常代码,影响程序的运行,这将带来灾难性的后果,这不仅是库开发者们首先会考虑的安全性问题,也是业务逻辑开发者们需要谨慎对待的可能冲突,这就是模块化之所以重要的原因。其他编程语言在语法特性层面就支持共有和私有作用域的概念,而 JavaScript 官方暂时还没有正式支持。目前用以隐藏内部实现的模块模式主要依赖闭包,所以闭包这一在JS领域具有独特神秘性的机制被广大开发者们又恨又爱。即便 ES6 的新模块机制支持以文件形式划分模块,仍然离不开闭包。生成:Generate作用域的生成主要依靠词法定义,许多语言中有函数作用域和块级作用域。JavaScript 主要使用的是函数作用域。怎么理解词法定义作用域?词法就是书写规则,编译器会按照所书写的代码确定出作用域范围。大多数编程语言里都用 {} 来包裹一些代码语句,编译器就会将它理解为一个块级,它内部的范围就是这个块级的作用域,函数也是如此,写了多少个函数就有相应数量的作用域。虽然 JavaScript 是少数没有实现块级作用域的编程语言,但其实在早期的 JavaScript 中就有几个特性可以变相实现块级作用域,如 with、catch 语句:with 语句会根据传入的对象创建出一个特殊作用域,只在 with 中有效;而 catch 语句中捕捉到的错误变量在外部无法访问的原因,正是因为它创建出了一个自己的块级作用域,据 You Don’t Know JS 的作者说市面上支持块级作用域书写风格的转译插件或 CoffeeScript 之类的转译语言内部都是依靠 catch 来实现的,that’s so tricky!相关概念:Relevant Concepts在这里只讨论 JavaScript 中以下概念的内容和实现方式。词法作用域:Lexical Scope通过上面所说的相关知识可以总结出词法作用域就是按照书写时的函数位置来决定的作用域。看看下面这段代码,这段代码展示了除全局作用域之外的 3 个函数作用域,分别是函数 a 、函数 b 、函数 c 所各自拥有的地盘:function a () { var aa = ‘aa’; function b () { var bb = ‘bb’ console.log(aa, bb) c(); } b();}function c () { var cc = ‘cc’ console.log(aa, bb, cc)}a();各个变量所属的作用域范围是显而易见的,但这段代码的执行结果是什么呢?一但面临嵌套作用域的情景,或许很多人又要犹疑了,接下来才是词法作用域的重点。上面代码的执行结果如下所示:// b():aa bb// c():Uncaught ReferenceError: aa is not defined函数 c 的运行报错了!错误说没有找到变量 aa。按照函数调用时的代码来看,函数 c 写在函数 b 里,按道理来讲,函数 c 不是应该可以访问它嵌套的两层父级函数作用域么?从执行结果得知,词法作用域不关心函数在哪里调用,只关心函数定义在哪里,所以函数 c 其实直接存在全局作用域下,与函数 a 同级,它俩根本就是没有任何交点的世界,无法互相访问,这就是词法作用域的法则!请谨记 JavaScript 就是一个应用词法作用域法则的世界。而按照函数调用时决定的作用域叫做动态作用域,在 JavaScript 里我们不关心它,所以把它扔出字典。函数作用域:Function Scope很长时间以来,JavaScript 里只存在函数作用域(让我们暂时忽略那些里世界的块级作用域 tricky),所有的作用域都是以函数级别存在。对此做出最明显反证的就是条件、循环语句。函数作用域的例子在上述词法作用域中已经得到了很好的体现,就不再赘述了,这里主要探讨一下函数作用域链的机制。以下面一段代码为例:function c () { var cc = ‘cc’ console.log(cc)}function a () { var aa = ‘aa’ console.log(aa) b();}function b () { var bb = ‘bb’ console.log(aa, bb)}a();c();一个程序里可以有很多函数作用域,引擎怎么确定先从哪个作用域开始,按照词法规则先写先执行?当然不,这时就看谁先调用。函数在作用域中的声明会被提升,函数声明的书写位置不会影响函数调用,参照上例,即便是函数 a 定义在函数 c 后面,由于它会被先调用,所以在全局作用域之后还是会先进入函数 a 的作用域,那函数 b 和函数 c 的顺序又如何,为了解释清楚词法作用域是如何与函数调用机制结合起来,接下来要分两部分研究程序运行的细节。都说 JavaScript 是个动态编程语言,然而它的作用域查找规则又是按照词法作用域(也是俗称的静态作用域)规则来决定的,实在让人费解。理解它动(执行时编译)静(运行前编译)结合的关键在于引擎在执行程序时的两个阶段:编译和运行。为了避免歧义,区分了两个词:执行:引擎对程序的整体执行过程,包括编译、运行阶段。运行:具体代码的执行或函数调用的过程。JavaScript 的动指的是在程序被执行时才进行编译,仅在代码运行前。而一般语言是先经过编译过程,随后才会被执行的,编译器与引擎执行是继时性的。静指函数作用域是根据编译时按照词法规则来确定的,不由调用时所处作用域决定。简单来说,函数的运行和其中变量的查找是两套规则:函数作用域中的变量查找基于作用域链,而函数的调用顺序依赖函数调用的背后机制——调用栈来决定。在编译阶段,编译器收集了函数作用域的嵌套层级,形成了变量查找规则依赖的作用域链。函数调用栈使函数像栈的数据结构一样排成队列按照先进后出的规则先后运行,再根据JavaScript 的同步执行机制,得出正确的执行顺序是:函数 a =>函数 b =>函数 c。最后再结合词法作用域法则推断出上面示例的执行结果仅仅是一句报错提示:Uncaught ReferenceError: aa is not defined。把函数 b 引用的变量 aa 去掉,就可以得到完整的执行顺序的展示。块级作用域:Block Scopelet、const 声明的出现终于打破了 JavaScript 里没有块级作用域的规则,我们可以显示使用块级语法 {} 或隐式地与 let、const 相结合实现块级作用域。隐式(let、const 声明会自动劫持所在作用域形成绑定关系,所以下例中并不是在 if 的块级定义,而是在它的代码块内部创建了一个块级作用域,注意在 if 的条件语句中 a 尚未定义):if (a === ‘a’) { let a = ‘a’ console.log(a)} else { console.log(‘a is not defined!’)}显式(显式写法揭露了块级变量定义的真实所在):// 普通写法,稍显啰嗦if (true) { { let a = ‘a’ … }}// You Don’t Know JS的作者提倡的写法,保持let声明在最前,与代码块语句区分开if (true) { { let a = ‘a’ … }}// 希望未来官方能支持的写法if (true) { let (a = ‘a’) { … }}关于块级作用域最后要关注的一个问题是暂时性死区,这个问题可以描述为:当提前使用了以 var 声明的变量得到的是 undefined,没有报错,而提前使用以 let 声明的变量则会抛出 ReferenceError。暂时性死区就是用来解释这个问题的原因。很简单,规范不允许在还没有运行到声明语句时就引用变量。来看一下根据官方非正式规范得出的解释:When a JavaScript engine looks through an upcoming block and finds a variable declaration, it either hoists the declaration to the top of the function or global scope (for var) or places the declaration in the TDZ (for let and const). Any attempt to access a variable in the TDZ results in a runtime error. That variable is only removed from the TDZ, and therefore safe to use, once execution flows to the variable declaration.翻译:当 JavaScript 引擎浏览即将出现的代码块并查找变量声明时,它既把声明提升到了函数的顶部或全局作用域(对于 var ),也将声明放入暂时性死区(对于 let 和const)。任何想要访问暂时性死区中变量的尝试都会导致运行时错误。只有当执行流到达变量声明的语句时,该变量才会从暂时性死区中移除,可以安全访问。另外,把 let 跟 var 声明作两点比较能更好排除其他疑惑。以下述代码为例:console.log(a);var a;console.log(b);let b;变量提升:let 与 var 定义的变量一样都存在提升。默认赋值:let 与 var 声明却未赋值的变量都相当于默认赋值 undefined。let 与 var 声明提前引用导致的结果的区别仅仅是因为在编译器在词法分析阶段,将块级作用域变量做了特殊处理,用暂时性死区把它们包裹住,保持块级作用域的特性。全局作用域:Global Scope全局作用域仿佛是透明存在的,容易受到忽视,就像人们经常忘记身处氧气包裹中一样,变量无法超越全局作用域存在,人们也无法脱离地球给我们提供的氧气圈。简而言之,全局作用域就是运行时的顶级作用域,一切的一切都归属于顶级作用域,它的地位如同宇宙。我们在所有函数之外定义的变量都归属于全局作用域,这个“全局”视 JavaScript 代码运行的环境而定,在浏览器中是 window 对象,在 Node.js 里就是 global 对象,或许以后还会有更多其他的全局对象。全局对象拥有的势力范围就是它们的作用域,定义在它们之中的变量对所有其他内层作用域都是可见的,即共享,所以开发者们都非常讨厌在全局定义变量,这继承自上面所说的最小特权原则的思想,为安全起见,定义在全局作用域里的变量越少越好,于是一个叫做全局污染的话题由此引发。全局作用域在运行时会由引擎创建,不需要我们自己来实现。局部作用域:Local Scope与全局作用域相对的概念就是局部作用域,或者叫本地作用域。局部作用域就是在全局作用域之下创建的任何内层作用域,可以说我们定义的任何函数和块级作用域都是局部作用域,一般在用来与全局作用域做区别的时候才会采用这种概括说法。在开发中,我们主要关心的是使用函数作用域来实现局部作用域的这一具体方式。公有作用域:Public Scope公有作用域存在于模块中,它是提供项目中所有其他模块都可以访问的变量和方法的范围或命名空间。公私作用域的概念与模块化开发息息相关,我们通常关心的是定义在公私作用域中的属性或方法。模块化提供给程序更多的安全性控制,并隐蔽内部实现细节,但是要让程序很好的实现功能,我们有访问模块内部作用域中数据的需要。从作用域链的查找机制可知,外层作用域是无法访问内层作用域变量的,而JavaScript 中公私作用域的概念也不像其他编程语言中那么完整,不能通过词法直接定义公有和私有作用域变量,所以闭包成为了模块化开发中的核心力量。闭包实现了在外层作用域中访问内层作用域变量的可能,其方法就是在内层函数里再定义一个内层函数,用来保留对想要访问的函数作用域的内存引用,这样外层作用域就可以通过这个保留引用的闭包来访问内层函数里的数据了。通过下面两段代码的执行结果就能看出区别:function a () { var aa = ‘aa’ function b () { var bb = ‘bb’ } b() console.log(bb)}a()控制台报错:Uncaught ReferenceError: bb is not defined,因为函数 b 在运行完后就从执行栈里出栈了,其内存引用也被内存回收机制清理掉了。function a () { var aa = ‘aa’ function b () { var bb = ‘bb’ return function c () { console.log(bb) } } var c = b() console.log(c())}a()而这段代码中用变量 c 保留了对函数 b 中返回的函数 c 的引用,函数 c 又根据词法作用域法则,能够进入函数 b 的作用域查找变量,这个引用形成的闭包就被保存在函数 a 中变量 c 的值中,函数 a 可以在任何想要的时候调用这个闭包来获取函数 b 里的数据。此时这个被返回的变量 bb 就成为了暴露在函数 a 的作用域范围内,定义在函数 b 里的公有作用域变量。更加通用的实现公有作用域变量或 API 的方式,称为模块模式:var a = (function a () { var aa = ‘aa’ function b () { var bb = ‘bb’ console.log(bb) } return { aa: aa, b: b }})()console.log(a.aa)a.b()使用闭包实现了一个单例模块,输出了共有变量 a.aa 和 共有方法也称 API 的 a.b。私有作用域:Private Scope相对于公有作用域,私有作用域是存在于模块中,只能提供由定义模块直接访问的变量和方法的范围或命名空间。要澄清一个关于私有作用域变量的的误会,定义私有作用域变量,不一定是要完全避免被外部模块或方法访问,更多时候是禁止它们被直接访问。大多时候可以通过模块暴露出的公有方法来间接地访问私有作用域变量,当然想不想让它被访问或者如何限制它的增删改查就是开发者自己掌控的事情了。接着上述公有作用域的实现,来看看私有作用域的实现。var a = (function a () { var bb = ‘bb’ var cc = ‘c’ function b () { console.log(bb) } function c () { cc = ‘cc’ console.log(cc) } return { b: b, c: c }})()a.b()a.c()在模块 a 中定义的属性 bb 和 cc 都是无法直接通过引用来获取的。但是模块暴露的两个方法 b 和 c,分别实现了一个查找操作和修改操作,间接控制模块中上述两个私有作用域变量。作用域与This:Scope vs This在对作用域是什么的理解中,最大的一个误区就是把作用域当作 this 对象。一个铁打的证据是函数作用域的确定是在词法分析时,属于编译阶段,而 this 对象是在运行时动态绑定到函数作用域里的。另一个更明显的证据是当函数调用时,它们内部的 this 指的是全局对象,而不是函数本身, 想必所有开发者都踩过这一坑,能够理解作用域与 this 本质上的区别。从这两点就可以肯定决不能把作用域与 this 等同对待。this 到底是什么?它跟作用域有很大关系,但具体留到以后再讨论吧。在此之前我们先要与作用域成为好朋友。参考文献:ReferenceYou Don’t Know JS: Scope & ClosuresUnderstanding Scope in JavaScriptUnderstanding ECMAScript 6Everything you wanted to know about JavaScript scopeUnderstanding scope and visibilityJavaScript 的 this 原理Stack的三种含义TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED ...

January 30, 2019 · 3 min · jiezi