4.4.4 块与语句的值

严格来说,所有语句在执行之后都会有“结果”,但是只有语句被正确执行并且返回一个“有效的值(value)”时,这个Value才能被其他计算过程使用。

4.4.4.1 语句的执行状态

JS中的代码出错都是以“某某语句出错”的形式展现出来的,因为,事实上,所谓“异常”就是一个在语句层面捕捕获处理的语义对象

包括throw在内,所有简单语句中的所有四个子句都有自己的执行状态

  • 继续:语句处于循环中并被continue子句指示返回指定位置继续;
  • 中断:语句处于switch或者循环中,并被break子句指示终止;
  • 返回:语句从函数中返回并带有某个Value(包括默认返回的undefined)

除此之外,所有语句都使用称为normal状态的结果(Result)来返回自己的值

4.4.4.2 语句无值

确实有一些语句是无值的,其中包括所有的声明语句、debugger语句、导入导出语句,以及空语句和空快语句

eval作用于结果无知的语句时,它将返回undefined,但是,并不能反过来判断此语句是否为无值,一个准确且有效的判断方法是利用下边这项特性

  • 在一个语句块的返回值中,无值语句将被忽略

也就是说,如果语句无值,那么把它放在一批语句的末端时,将不会影响该语句的结果

4.4.4.3 语句有值

大多数语句都是有值的

但是所谓的“形式分块”让语句的值变成了更加复杂的讨论北京,从表面上来看,例如条件语句if,可以简单的理解为

  • 如果语句失败,那么它的结果是一个包含了throw状态的异常对象
  • 如果语句成功,那么它的结果必然是then或者else分支中被正确执行的语句的值。

但是if(false); else;then与else分支都是空语句,而空语句都是无值的,那么表明if语句最终——由分支语句决定的——也是无值的吗?

ECMAScript规定,在所有逻辑语句或有形式分块的语句中

  • 子句或块返回无值时,逻辑语句本身将以undefined返回
  • continue与break只改变语句的执行流程,对返回的值无影响

4.4.5 标签语句与块

大多数语句都可以添加一个标签而变成“标签化语句”,例如:aa:100; // 100

这里的aa就是一个标签,即使这个标签不被其他语句或语法引用,或者该标签的存在毫无意义,它在语法上也是合法的

当标签语句能作用于循环语句时,该标签可以被循环语句“内部的”breakcontinue子句引用。

aa: while(true) break aa;
// undefined

其中,类似break xxx这样的带标签的break子句主要用于多重循环中,在一般常见的switch语句的块中是不使用标签化的break的。当然如果使用,也是合法的

标签语句是独立的语法元素,因此可以和变量名或者其他标识符名字重名

4.5 组织形式分块的方法

所谓形式分块,它的作用就是定义一个“块级别的”作用域

意味着JS会为该分块创建作用域。称为词法作用域。

4.5.1 词法作用域

代码分块带来的语法效果是信息隐蔽——变量或成员的可见性问题,可见性的区间称为作用域,当这个域是通过静态词法分析得出的时候,就被称为词法作用域

词法环境是词法作用域这个概念的运行期实现:代码在物理行上、显式地进行形式分块,并在引擎执行期映射成词法环境记录,以便将“块级别的”词法作用域实例化

4.5.1.1 不存在“级别1:表达式”

在JS中没有“表达式级别的”词法作用域

4.5.1.2 级别2:语句

变量“存在于”某个语句级别的作用域,是指该变量被创建出来之后,在脱离了创造它的(单个或者连续的)表达式之后,仍然可以下(或仅在)所在语句中的作用域中被访问

在JS for循环阔号中定义的变量仍可在外边被访问到,说明早期的JS在设计上以及引擎实现上都没有“语句级别的”词法作用域,只支持函数级别的变量作用域。但是ES6之后使用let声明就不可以在外边访问到,就是“块级作用域”

对于没有形式分块的单一语句是没有块级作用域的意义的

if(false) let x = 100;
// SyntaxError: Lexical declaration connot appear in a signle-statement...

4.5.1.3 级别3:函数

当使用函数调用运算符”()”时。JS会为函数构建一个词法环境,以便将该函数的形式分块实例化为一个函数

对于语句内的声明要提升到语句之外的函数或者全局作用域,看起来和var一样

之所以在函数内可以声明子集的函数,而在语句内却无法声明函数,是因为函数的名字是一个变量名,而不是一个词法名字

箭头函数也是有自己的作用域的

4.5.1.4 级别4:模块

模块词法作用域所对应的形式分块是由模块的export声明决定的,执行环境将预先扫描这些声明并进行词法分析,然后会在实例化时创建一个自有的词法环境来登记它们,但是实例化或成却是在执行import声明来导入该模块才发生的,用以绑定全部的顶层声明

所有的导入都将使用本地名字,然后就变成了只读的变量

在模块的词法作用域中访问this总是得到undefined

4.5.1.5 级别5:全局

JS全局环境中一共存在三种用来等级名字的组件,包括词法作用域、变量作用域和全局对象

所有变量声明的名字都在变量作用域登记,用户代码通过“访问不存在的变量名”导致的名字创建作为全局对象的属性登记的,其他的let/count等声明导致的登记,才是发生在全局的词法作用域中的

4.5.2 执行流程及其变更

术语命令式来自命令和动作,这种计算模型就是基于冯诺依曼体系中的编程语言能得到的运算效果的本质:顺序执行

但是程序执行总会有意外,我们需要改变“顺序执行”的流程

GOTO会带来大量的问题,所以在每个词法作用域设计类似GOTO的语句,例如break

4.5.2.1 级别1:可能的逃逸

没有任何有效的方式可在表达式中实现类似GOTO的效果。

通过制造“异常”可以在表达式计算过程中产哼一个向外层的中断。但与GOTO不同的是,这并非表达式专属的,而是普通的、一般意义上的、可被捕获的异常

4.5.2.2 级别2:“break

在循环中可以使用break和continue来改变循环流程,break;会直接跳出当前循环,continue:跳过当前循环并进入下一次循环

break <label>会跳出label指明的作用域

var i = 0;
my_label: {
  i ++;
  while(true) {
    break my_label;
  }
  i = 0;
}
console.log(i) // 1

4.5.2.3 级别3:return子句

  • break label;不能跨越函数的词法作用域
  • return 子句可以跨越任何语句的作用域而退出函数

函数外边套标签,函数内部的break无法访问到外边的标签

4.5.2.4 级别4:动态模块与Promise中的流程控制

模块有自己的词法作用域,但没有相关流程控制语句,静态加载的模块并没有执行流程的概念,对于动态模块来说,他的作用域将会“被包含于”一个Promise对象的执行环境中,类似在new Promise(func)语句中的func中

因此这种情况下也能针对模块进行流程控制,例如在Promise.then中响应onRejected,或者调用promise.catch()和promise.finally(),然而这些响应函数的调用都不会回到:模块,因此也就不存在所谓的:模块作用域中的GOTO语句

4.5.2.5 级别5:throw语句

首先应该注意到try...catch...finally语法结构的词法作用域是语句,而throw是全局。我们称呼throw时,用的是”语句“,而不是”子句“

尽管throw也是一个流程变更的语句,然而其作用域却是脚本引擎全局。

4.5.3 词法作用域之间的相关性

结构化的语言中的作用域是互补相交的,只存在平行或者嵌套两种相关性

  • 相同级别的词法作用域可以互相嵌套
  • 高级别的词法作用域能够包含低级别的词法作用域
  • 低级别的词法作用域不能包含高级别的词法作用域,由于不存在包含关系,因此语言实现时,一般处理称语法上的违例或者强制解释为平行关系
// 代码块一
if(true) {
  // ...
  // 代码块二
  function foo() {
    // ...
  }
}

// 等效于
// 代码块一
if(true) {
  //...
}
// 代码块二
function foo() {
  // ...
}

4.5.4 执行流程变更的内涵

  • 词法作用域是互不相交的,正是作用域互不相交的特性构造了代码结构化的层次,并消除了一些错误隐患
  • 词法作用域间可以存在平行或包含关系。高级别可以嵌套低级别的词法作用域,反之则不成立
  • 高级别的流程变更子句(或语句)可以跨越低级别的作用域,反之则不成立

4.6 层次结构程序设计

JS的对象其实只是属性包,所谓属性,本质上是另一个”名字系统“,在这个新的名字系统中,仍然存在着变量或词法标识符类似的作用域问题,并且也受相似的规则约束

封装、继承与多态是面向对象解决”结构性问题“的三种具体手段

  • 封装与继承是对数据(即对象性质)的结构化,分别展示数据的内聚与外延
  • 多态是对象性质在继承关系上的表现

当对象性质分成属性与方法之后,逻辑(方法)之于数据(属性)的可见性问题也就出现了,并且这种可见性必然在内聚与外延的两个方向上同时出现

4.6.1 属性的可见性

属性是对象的自有属性,方法也是属性的一种,它们是平等的,由于在传统上,JS并不存在”对象或类“这样的作用域,因此在对象方法中访问属性,与在其他域、其他环境中访问属性没有区别:

  • 得到该属性的引用,然后
  • 使用属性存取运算符得到属性值

并且由于所有的属性都是公开的,所以任何时候、任何环境下只要能获得这个对象的(的引用),就可以无差别的访问它的全部属性

符号具有隐匿名字的特性,但并不能隐匿可见性。当一个对象使用某个符号作为属性名时,用户代码至多也只是不知道其符号名而已

另一个与属性可见性相关的设计时属性描述符中的Enumerable性质,它影响属性在for…in语句中的可见性效果,但它并不实际影响属性的可访问性,所以与符号类似,也只能起简单的隐匿作用

4.6.2 多态的逻辑

在传统的JS面向对象中,多态特性是非常脆弱的,主要表现为类型识别,包括instanceof关键字以及后来加入的isPrototypeOf方法,从ES6之后还加入了super属性,才彻底解决了实现多态时,调用父类方法的问题

4.6.2.1 super是对多态逻辑的绑定

super明确地指向继承关系中的父级,然而我们知道JS的继承树是可以重置的,访问super是一个动态的逻辑,而不是一个确定的成员

4.6.2.2 super是一个作用域相关的绑定

super关键字是上下文受限的,并且是词法作用域(静态)绑定的,这个关键字只能使用在类声明(含类表达式)和对象字面量风格的声明中,并且只能出现在方法或静态方法声明内部。这意味着super必须作用于某个方法的“函数作用域”中。且如果该方法时类声明中的方法,则该函数作用域还将是“类作用域”的子集作用域

4.6.3 私有作用域的提出

从ES6提出类继承以来,类的声明中只能包括方法和存取器属性

TS中包含私有成员声明语法



语言   js      js 基础 绿宝书

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!