2.7 运算符的二义性

存在二义性的语法元素

运算符 含义 其他含义
+ 增值运算符
正值运算符
链接运算符
数值字面量声明
(正值、负值或指数形式)
( ) 函数调用运算符
分组运算符
new运算符的形式参数表
(某些语句中的语法符号)
(分组运算符常用作强制优先级)
?: 条件运算符 :号有声明标签的含义
:号有声明switch分支的含义
:号有声明对象成员的含义
, 连续运算符 参数分隔符
对象、数组声明分隔符
[] 解构赋值
数组下标
对象成员存取
数组字面量声明
* 幂运算符
乘法运算符
yield委托
生成器函数声明
{ } 解构赋值 复合语句
函数的代码体
字面量风格的对象声明
; 空语句
语句分隔符
类声明分隔符
(分号可以被回车和文末符代替)
in 属性检查 循环语句的语法元素
async 箭头函数声明有语法歧义

2.7.1 加号”+”的二义性

加号可以表示字符串链接、数字取正值的一元运算、数值表达式的求和运算,字符串和数字求和容易出现二义性

如果表达式中存在字符串则优先按字符串链接进行运算

当加号执行的是值运算的时候,当对象x参与运算时,将调用x.valueOf()来确定操作数的类型T如果仍不能满足要求,则尝试x.toString(),这样的复杂的类型转换逻辑导致实际的操作结果变得难以预测

2.7.2 括号”()”的二义性

阔号最常见的形式之一就是作为函数声明中的”虚拟参数表“

但是某些情况下只作为”传值参数表“而不表达函数调用的含义。到目前为止只出现在new关键字的使用中:new关键字用于创建一个对象实例并负责调用该构造器函数,如果有一对阔号指示的参数表,则在调用构造器的时候传入该参数表

也用于循环语句中,会有将表达式转换为布尔值的作用,在with(x)中会将x转化为对象、

还可以用于强制表达式运算

var str = ('please input a string', 1000)

这里由于连续运算符,的返回值是最后一个表达式的值,所以结果是1000

最后一种作为函数\方法的调用运算符

无论break (label)看起来如何合理,也会被引擎识别为语法错误,因为标签是不可以交给括号运算符处理的操作,数标签与操作数属于两个各自独立的、可重复(而不发生覆盖)的标识符系统

2.7.3 冒号”:”与标签的二义性

冒号有三种语法作用:声明对象字面量的成名和声明标签,以及在switch语句中声明一个分支,冒号还具有一个运算符的含义:在三元表达式中,表示条件为false时的表达式分支,冒号的二义性问题集中在标签声明和对象成员声明的识别上,下一节讲

2.7.4 大括号”{}”的二义性

大括号有六种作用。在所有场景中,它都是作为词法/语法符号来使用的

2.7.4.1 复合语句/语句块

第一种比较常见,表示”复合语句“

// 示例一:表示标签后的复合语句
myLabel: {
  // ...
}
// 实例二:在其他语句中表示复合语句
if(condition) {
  // ...
}
else {
  // ...
}

由于语句末尾大括号前后都可以省略;号,因此下边的代码就值得回味了

// 示例三:复合语句中的表达式语句
{1,2,3}

复合语句中的,被理解为连续运算符,所以省略了一个分号,但是当它与一个解构赋值模板来比较时,就会发现语法解析上的困难了

# 左侧是赋值模板
> let {a, b} = {a: 100, b:1000}
# 如下是对象声明
> {a, b}
{a: 100, b: 1000}
#如下是语句
> {a, b}
1000

2.7.4.2 声明对象字面量

当大括号用作对象声明时,它的字面量声明部分其实是一个字面量风格的单值表达式,我们可以考虑使用以下方法让单值表达式编程语句,但它并不总是可行的,因为{在语法解析的时候被优先作为语句块的开始符号,所以要实现类似效果需要让引擎将这一部分代码按表达式解析

// 使用分号的表示法
{a: 1,b: 2}
// 使用复合语句的表示法
{{a: 1,b: 2}}

// 先强制作为连续运算语句,然后将对象字面量理解为单值表达式
0, {a: 1,b: 2}

接下来会变得复杂一点

if(true) {
  entry: 1
}

if语句后边的语句可能是以下三者之一:

  • 一个单行语句
  • 一个表达式(语句)
  • 一个由大括号包起来的复合语句

正确的理解仍然是”语句优先“,所以大括号变成了复合语句,所以entry:1变成了标签,最后的输出时1

但是用户仍然可以使用强制表达式运算的方式得到对象字面量

if(true)({
  entry: 1
})

2.7.4.3 函数声明

大括号的第三种用法,是声明函数字面量时的语法符号

作为参考,下面这段代码的语法歧义是阔号运算符导致的,而不是大括号除了问题

function foo() {
  // ...
}(1,2);

2.7.4.4 结构化异常

大括号也是结构化异常处理的语法符号,大括号在结构化异常中是语法符号,因此不能用单行语句来代替

2.7.4.5 模板中的变量引用

由于模板内不存在语句,所以大括号就不会被理解成语句,而是一个对象,这也是eval和模板最大的不同

2.7.4.6 解构赋值

解构赋值利用赋值表达式左右运算数的不同来消除二义性。从语法设计的角度来讲,赋值表达式左侧的运算数是一个引用,而右侧是一个值

  • 将右侧的结果赋值给左侧的引用来存储
  • 如果左侧的被引用对象没有存储能力,就抛出异常
    8 = 8;
    // 向一个"值"赋值的行为是引用异常,不是语法分析期的错误
    在赋值模板中不可以使用字符串模板

赋值模板还可能出现函数的参数界面或者try语句所隐含的异常对象声明中

2.7.5 逗号”,”的二义性

逗号既可以是语法分隔符,又可以是运算符,在它作为“联系运算符”的效果是运算表达式并返回结果值

a = (1,2,3)
// 如果没有阔号,会优先进行赋值运算
a = 1,2,3
// 返回3,同时a被赋值为1

var a = 1,2,3
// 但是如果作为变量声明的话就会出现错误,因为逗号被理解为var声明分隔多个变量的语法分隔符,而不是连续运算符

2.7.6 方括号”[]”的二义性

下边的代码有语法错误么

a = [ [1] [1]];

并没有语法错误,但是我们几乎不理解这行代码的意义,但是可以被JS理解,a会被赋值为undefined,也就是说,右边部分作为表达式,可以被运算出一个结果,只有一个元素的数组,该元素为undefined

同理

array = [['pop'],['push']['length']]
// => [['pop'],1]

不过这样的二义性还不够复杂,下边的例子呈现了下面三个语法二义性带来的恶果

  • 方括号可以被理解成数组声明或者下标存取
  • 方括号还可以理解为对象成员存取
  • 逗号可以被理解成语法分隔符或者连续运算符
var table = [
  ['A',1,2,3]
  ['B',4,5,6],
  ['c',6,7,8]
]
/*
[
  undefined,
  ['c',6,7,8]
]
*/

它的第二行并没有被理解成一个数组,也没有理解成数组元素的存取,相反,被理解成了四个表达式的连续运算,因为['A',1,2,3]是一个数组对象,所以后边的[]被理解为“对象属性存取运算符”,那么规则就变成了这样

  • 如果其中运算的结果是整数,则用于下标存取
  • 如果其中运算的结果是字符串,则用于对象成员存取

所以['B',4,5,6]就得到了[6],所以['A',1,2,3]['B',4,5,6]的结果是undefined。

2.7.7 语法设计中对二义性的处理

额,,说明了以下语法解析经常会遇到一些神奇的东西,就很多二义性的问题需要处理。语法分析器:我太难了

给出了一个好玩的例子

{ 100 : a = 100 } = { 100 : a = 100}

书上说会得到有趣的结果,但是我只得到了报错,,,嘤嘤嘤

第三章 JavaScript的面向语言特性

3.1 面向对象编程的语法概要

3.1.1 对象声明和实例创建

通常可以使用字面量创建对象,或者用new关键字创建新的对象实例,也可以通过宿主程序来添加自己的构造器,并用new关键字来创建它们的对象实例,并允许用户在脚本代码中持有和使用它

3.1.1.1 使用构造器创建对象实例

构造器是创建和初始化对象一般性的方法,需要通过new运算符让构造器产生对象实例,构造器可以是普通函数也可以是JS内置的或者宿主程序拓展的构造器——按照管理,首字母大写

在语法中,参数表为空和没有参数表是一样的,所以下边两种写法等价,但是如果参数不为空,则视为构造参数,这种情况下构造函数不是一个普通意义上的函数,因此这里不能直接将其理解为函数参数列表

obj = new MyObject();
obj = new MyObject;

JS将在构造器函数执行过程中传入new运算所产生的实例,并将该实例作为this对象引用传入,就可以通过修改this对象引用的成员来完成对象构造阶段的初始化对象实例

也可以只将构造器作为普通的函数来使用,当返回值不是值类型的时候,将替代实例返回,值类型会被忽略,返回实例

JS中还会设计一些其他“初始化对象实例”的方法。但更多的牵涉到原型继承的问题,所以如下内容安排到3.2中去讨论

  • 通过构造原型实例来初始化
  • 通过Object.create()并使用属性描述符得方式来创建对象并初始化

3.1.1.2 声明对象字面量

使用字面量声明比使用构造函数要方便, 声明的时候“属性名”可以用字符串、数字或者一般标识符,只有在特殊的情况才使用字符串,通常指

  • 使用的标识符不满足JS对标识符的规则
  • 特殊的、强调的属性名

在“属性名”的声明中也允许使用[],可以包含一个符号,或者可计算表达式的值来作为名字,这个值只能是symbol或者string类型的,否则将尝试转化成为string类型的值,也可以直接引用变量名作为属性值,同时使用该变量的值作为属性值,而且这在with语句中也是可用的

这些语法带来了简洁而灵活的对象字面量声明方式,并有效的利用了其他已存在的变量名,此外,还可以用对象展开语法来引用对象的成员(而非对象自身)

方法声明是新的特定语法(而不是省略掉function关键字的函数声明),还是匿名的函数

profile = {
  method() {
    // ...
  }
}

也可以加上set/get,用来表示这是属性存取方法

某些类的对象实例也可以使用它特有的字面量声明语法,具体来说就是数组、正则表达式,另外,空对象也是以字面量null的形式存在(当然也可以看场常量、或者语法关键字)

3.1.1.3 数组及其字面量

可以使用new运算来创建一个数组

// 创建一个指定长度的数组
arr = new Array(10)

当参数只有一个并且是数值类型的时候,创建出来的会是一个长度为数值,每个元素都是undefined的空数组,其余情况每个值将作为数值的内容按顺序放入,但是这样就不如直接用字面量声明了

用字面量来声明数组时,数组可以是异质的(数组元素的类型可以不同),交错的(数组元素可以是不同维度的数组)。数组的这种交错性使它看起来像是“多维的”,事实上不过是“数组的数组”这种嵌套属性

想要获取交错数组的某个下标分量的方法

var arr = [,[,,[,,,"abc"]]]

// 正常的访问
arr[1][2][3]

// 或者模板赋值语法
var {1:{2:{3:x}}} = arr
x // => "abc"

也可以使用数值字符串作为下标来访问数组成员,但这时语义上却有所不同,这种情况下是将数组作为对象来进行“名-值”存取的

不但数组可以作为对象使用,反过来,某些对象也可以用作数组,这称为“类数组”。例如:arguments

所有数组都是可迭代对象,但是类数组不一定可迭代

3.1.1.4 正则表达式及其字面量

正则表达式的语法为/expression pattern/flags

正则表达式的flag和元字符建议去百度学习,这里就不抄写了

另外注意匹配转义符号\需要转义\\

3.1.1.5 在对象声明中使用属性存取器

除了上述的基础语法之外,还可以在字面量声明中使用属性的“存取器”,也称为读写器(get/setter),具有对象方法的一切性质,只不过通常被称为存取器函数。

get propName() {
  // ...
}
set propName(newValue) {
  // ...
}

仅在ES5的严格模式下,属性声明和该属性的存取器声明不能同时出现,解析器会认为声明了两个相同名字的属性,抛出一个无效属性的异常

3.1.2 使用类继承体系

使用new运算从“构造器”创建对象时,构造器既可以是一般函数,也可以是从ES6开始支持的“类”,这种类本质上是一种声明构造器的方式,因而所谓类继承,其实也是传统原型继承模式的一种表现方式

3.1.2.1 声明类和继承关系

声明一个类本质上就是声明一个构造器函数,其基本语法为:

class className [extends parentClass] {
  constructor() {
    // ...
  }
}

当父类(super)是内置的Object()时,extends Object可以省略,当不需要指定构造过程时,constructor(){...}声明也可以省略,这样一来,下边三个声明在语义上就是等效的

// 最简单的类声明
class MyObject()

// 等价于(采用构造函数声明风格)
function MyObject() {}

// 或等价于(采用变量声明风格)
var MyObject = new Function;

对于使用class关键字的类声明过程来说,最明显的收益就是可以使用extends来声明父类

class MyObjectEx extends MyObject {
  // ...
}

这基本代替了原先在构造函数声明风格中的如下代码:

// 与如下效果类似
MyObjectEx.prototype = new MyObject();
MyObjectEx.prototype.contructor = MyObjectEx;

class引导的整个类声明(包括类表达式)代码块总是工作在严格模式中,意味着extends声明也同样处于严格模式之下,用extends声明的父类是一个表达式(的结果值),因此事实上是说,该表达式将运行在严格模式中。

// 在extends中的表达式,例如字面量风格的函数表达式,是处于严格模式中的
class MyObject extends function x() {xyz = 123;} {
  // ...
}

new MyObject()
// ReferenceError: xyz is not definded

最后,在将一般函数用作构造器时,函数体就是构造过程本身,而在使用class关键字时,该构造过程就被独立出来使用特定的方法进行声明,即constructor

3.1.2.2 声明属性

使用函数作为构造器时需要通过原型来声明对象实例的属性,例如

// 构造函数声明风格
function MyObject(){}

// 在原型上声明属性
MyObject.prototype.aName = 'value';

而对于使用class关键字的类声明过程来说,这些声明都可以采用特定的关键字或者语法来声明:

// 类声明风格
class MyObject {
  // 声明属性的读方法
  get aName() {
    // ...
  }
  // 声明属性的写方法
  set aName(value) {
    // ...
  }
  // 声明方法
  aMethod() {
    // ...
  }
}

如果在上例中只有get或者set之一,那么这个属性就是只读或者只写的,并且你只能使用这种方法来声明类的存取属性,如果打算声明一般属性,那么仍然需要直接操作这个类的原型

class MyObject{}
// 在原型中声明属性
MyObject.prototype.aName = 'value'
// 示例
var obj = new MyObject();
console.log(obj.aName) // value

3.1.2.3 调用父类构造方法

在上例中,如果我们为MyObject派生一个子类MyobjectEx那么可以知道的是,子类是以父类的一个实例为原型的

关键字extends为你实现了类似的逻辑

// 作用类似于如下
MyObjectEx.prototype = new MyObject()
MyObjectEx.prototype.constructor = MyObjectEx

我们也知道,如果构造器MyObject是支持参数的,那么由于extends只声明了继承关系,因而无法传递给构造函数所需要的参数,要实现这个,就需要在子类的构造函数中调用父类的构造函数,JS规定这个构造方法可以是父类,也可以是父类的构造器——在整个类声明过程中可以使用super关键字来进行访问,例如:

// 访问父类的构造方法
class MyObjectEx extends Myobject {
  constructor() {
    super(x, y)
  }
}

在这种情况下使用super会默认传入当前所构造的实例作为父类可以访问的this引用,也就是说,new运算所创建的对象实例将在当前类和父类(整个继承链)中传递,在这个效果上,它于如下使用构造函数的传统方式是类似的(注意下例中的MyObject()也必须声明为传统的构造器,即普通函数):

function MyObjectEx() {
  // super(x,y) 将实现为类似的代码
  MyObjct.apply(this, (x,y))
}
MyObjectEx.prototype = new MyObject()
MyObjectEx.prototype.constructor = MyObjectEx

于是就可以在构造方法中操作this实例了,注意在使用this之前们总是需要显示的调用super以便在当前构造方法中获取this实例,如果没有extends,难么不调用super也是可以的

3.1.2.4 调用父类方法

在类声明语法中可以直接调用父类方法,这需要用到super.XXX引用

// 声明基类上的foo方法
class MyObjct {
  foo(x) {
    // ...
  }
}
// 使用继承
class MyObjectEx extends MyObject {
  foo(x, y) {
    super.foo(x); // 调用父类同名方法
  }
  bar(x) {
    super.foo(x); // 调用父类方法
  }
}

super.XXX是属性引用,而super.XXX()是将属性作为方法调用的语法,关于这一点的细节,看3.3.2.3 super对一般属性的意义

在使用super.XXX调用父类方法的时候也会隐式的传入当前this的引用,这个和在构造函数中使用super是一样的

3.1.2.5 类成员(类静态成员)

在类声明语法中也可以使用static关键字来声明类静态成员,包括静态方法和属性,并且它们在子类中也是作为类成员继承的

// 声明类的静态方法和属性
class MyObject {
  static get aName() {
    return 10;
  }
  static foo() {
    console.log(super.toString());
  }
}

class MyObjectEx extends MyObject {}

// 访问类静态成员
console.log(MyObject.aName)
// 调用类静态方法
MyObject.foo();
// 子类可以继承
MyObjectEx.foo();

/*
10
class MyObject {
  static get aName() {
    return 10;
  }
  static foo() {
    console.log(super.toString());
  }
}
class MyObjectEx extends MyObject {}
*/

访问类静态成员的时候不需要创建对象实例,因为它是类自有的成员,如果是方法(包括静态方法和属性存取方法),那么也是可以使用super的。指示在其中调用super或者super.XXX的时候,this会绑定到类(构造函数本身)——因为这个时候并没有创建对象实例

事实上,类静态成员也可以直接声明为“类、构造器函数”的成员,除了不能使用super以外,this也没有绑定到类,并没有特别的不同

// 于上例等效的声明
class MyObjct {}
MyObject.aName = 10;
MyObject.foo = function() {
  console.log(Object.toString.call(MyObject));
}
// 访问类静态成员
console.log(MyObject.aName);
// 调用类静态方法
MyObject.foo();


语言   js      js 基础 绿宝书

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