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();