程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2021-10-04

发布于2021-10-05 20:57     阅读(1101)     评论(0)     点赞(17)     收藏(0)


第4章 表达式与操作符

表达式是一个可以被求值并产生一个值的JavaScript短语。直接嵌入在程序中的常量是最简单的表达式。变量名也是简单的表达式,可以求值之前赋给它的值。
复杂表达式由简单表达式构成。
基于简单表达式构建复杂表达式最常见的方式是使用操作符。操作符以某种方式组合其操作数的值(通常有两个),然后求值为一个新值。以乘法操作符为例。表达式xy求值为表达式x和y值的积。

4.1 主表达式

最简单的表达式称为主表达式,即那些独立存在,不再包含更简单的表达式的表达式。JavaScript中的主表达式包括常量或字面量值、某些语言关键字和变量引用

字面量是可以直接嵌入在程序中的常量值。例如:

1.23           //数值字面量
"hello"        //字符串字面量
/pattern/      //正则表达式字面量

JavaScript的一些保留字也是主表达式:

true           //求值为布尔值true
false          //求值为布尔值false
null           //求值为null值
this           //求值为当前对象

与其他关键字不同,this不是常量,它在程序中的不同地方会求值不同的值。this是面向对象编程中使用的关键字。在方法体中,this求值为调用方法的对象。

第三种主表达式是变量、常量或全局对象属性的引用:

i             //求值为变量i的值
sum           //求值为变量sum的值
undefined     //全局对象undefined属性的值

当程序中出现任何独立的标识符是,JavaScript假设它是一个变量或常量或全局对象的属性,并查询它的值。如果不存在该名字的变量,则求值不存在的变量会导致抛出ReferenceError。

4.2 对象和数组初始化程序

对象和数组初始化程序也是一种表达式,其值为新创建的对象或数组。这些初始化程序表达式有时候也被称作对象字面量和数组字面量与真正的字面量不同,它们不是主表式,因为它们包含用于指定属性或元素值的子表达式

数组初始化程序是一个包含在方括号内的逗号分隔的表达式列表。数组初始化程序的值是新创建的数组。

数组初始化程序中的元素表达式本省也可以是数组初始化程序,这意味着以下表达式可以创建嵌套数组:

let matrix=[[1,2,3],[4,5,6],[7,8,9]]

数组初始化程序中的元素表达式在每次初始化程序被求值时也会被求值。这意味着数组初始化程序表达式每次求值的结果可能不同。

对象初始化程序的最后一个表达式后面可以再跟一个逗号,而且这个逗号不会创建未定义元素。不过,通过数组访问表达式访问最后一个表达式后面的索引一定会求值为undefined。

对象初始化程序表达式与数组初始化表达式类似,但方括号变成了花括号,且每个子表达式前面多了一个属性名和一个冒号:

let p={x:2.3,y:-1.2}     //有两个属性的对象
let q={}                 //没有属性的空对象
q.x=2.3;q.y=-1.2;        //现在q拥有了跟p一样的属性

在ES6中,对象字面量拥有了更丰富的语法。对象字面量可以嵌套。
例如:

let rectangle={
      upperLeft:{x:2,y:2},
      lowerRight:{x:4,y:5}
};

4.3 函数定义表达式

函数定义表达式定义JavaScript函数,其值为新定义的函数。某种意义上来说,函数的定义表达式也是“函数字面量”,就想对象初始化程序是“对象字面量”一样。
函数定义表达式可以包含函数的名字。也可以使用函数语句而非函数表达式来定义。在ES6及之后的版本中,函数表达式可以使用更简洁的“箭头函数”语法。

4.4 属性访问表达式

属性访问表达式求值为对象属性或数组元素的值。JavaScript定义了两种访问属性的语法:

expression.identifier
expression[expression]

第一种属性访问语法是表达式后跟一个句点和一个标识符。其中,表达式指定对象,标识符指定属性名。第二种属性访问语法是表达式(对象或数组)后跟另一个位于反括号中的表达式。第二个表达式指定属性名或数组元素的索引。
eg:

let o={x:1,y:{z:3}}
let a=[o,4,[5,6]]
o.x
=>1
o.y.z
=>3
o["x"]
=>1
a[1]
=>4
a[2]["1"]
=>6
a[0].x
=>1

无论哪种属性访问表达式,位于.或位于[前面的表达式都会先求值。如果求值结果为null或undefined,则表达式会抛出TypeError,因为它们是JavaScript中不能有属性的两个值。如果对象表达式后跟一个点或一个标识符,则会对以该标识符为名字的属性求值,且该值会成为整个表达式的值。==如果对象表达式后跟位于方括号中的另一个表达式,则第二个表达式会被求值并转换成字符串。整个表达式的值就是名字为该字符串的属性的值。==任何一种情况下,如果指定名字的属性不存在,则属性访问表达式的值是undefined。

在两种属性访问表达式中,加标识符的语法更简单,都是通过它访问的属性的名字必须是合法的标识符,而且在写代码时已经知道了这个名字。如果属性中包含空格或标点字符,或者是一个数值(对于数组本身而言),则必须使用方括号语法。方括号也可以用来访问非静态属性名,即属性本身是计算结果。

4.4.1 条件式属性访问

ES2020增加了两个新的属性访问表达式:

expression?.identifier
expression?.[expression]

在JavaScript中,null和undefined是唯一两个没有属性的值。在使用普遍的属性访问表达式时,如果.或[]左侧的表达式求值为null或undefined,会报TypeError。可以使用?.或?.[]语法防止这种错误发生。

比如表达式a?.b,如果a是null或undefined,那么整个表达式求值结果为undefined,不会尝试访问属性b。如果a是其他值,则a?.b求值为a.b的值(如果a没有名为b的属性,则整个表达式的值还是undefined)。

这种形式的属性访问表达式有时候也称为“可选链接”,因为它也适用于下面这种更长的属性访问表达式链条:

let a={b:null};
a.b?/.c.d  
=>undefined

条件式属性访问也可以让我们使用?.[]而非[]。在表达式a?.[b][c]中,如果a的值是null或undefined,则整个表达式立即求值为undefined,子表达式b和c不会求值。换句话说,如果a没有定义,那么b和c无论谁有副效应,这个副效应都不会发生:

let a;              //忘记初始化这个变量了!
let index=0
try{ 
   a[index++];      //抛出TypeError
}catch(e){
   index            //抛出TypeError之前发生了递增
}
=>1
a?.[index++]
=>undefined         //因为a是undefined
index
=>1
a[index++]          //不能索引undefined
=>VM4584:1 Uncaught TypeError: Cannot read property '1' of undefined
    at <anonymous>:1:2
(anonymous) @ VM4584:1

使用?.和?.[]的条件式访问是JavaScript最新的特性之一。在2020年初,多数主流浏览器的当前版本或预览版已经支持这个新语法。

4.5 调用表达式

调用表达式是JavaScript中调用(或执行)函数或方法的一种语法。

f(0)                   //f是函数表达式,0是参数表达式
Math.max(x,y,z)        //Math.max是函数。x、y、z是参数
a.sort()               //a.sort()是函数,没有参数  

求值调用表达式时,首先求值函数表达式,然后求值参数表达式以产生参数值的列表。如果函数表达式的值不是函数,则会抛出TypeError。然后,按照函数定义时参数的顺序给参数赋值,之后再执行函数体。如果函数使用return语句返回一个值,则该值就成为调用表达式的值。否则,调用表达式的值就是undefined。

如果该表达式是属性访问表达式,则这种调用被称为方法调用。在方法调用中,作为属性访问主体的对象或数组在执行函数体时会变成this关键字的值。这样就可以支持面向对象的编程范式,即函数(这样使用时我们称其为“方法”)会附着在其所属对象上来执行操作。

4.5.1 条件式调用

在ES2020中,可以使用?.()而非()来调用函数。正常情况下,我们调用函数时,如果圆括号左侧的表达式是null或undefined或任何其他非函数值,都会抛出TypeError。而使用?.()调用语法,如果?左侧的表达式求值为null或undefined,则整个调用表达式求值为undefined,不会抛出异常。

数组对象有一个sort()方法,接收一个可选的函数参数,用来定义对数组排序的规则。在ES2020之前,如果想写一个类似sort()的这种接收可选函数参数的方法,通常需要在函数内使用if语句检查该函数参数是否有定义,然后再调用:

function square(x,log){  //第二个参数是一个可选的函数
        if(log){         //如果传入了可选的函数
            log(x);      //调用这个函数
        }
        return x*x;      //返回第一个参数的平方
}

但有了ES2020的条件式调用语法,可以简单地使用?.()来调用这个可选的函数,只有再函数定义是才会真正调用:

function square(x,log){   //第二个参数是一个可选的函数
        log?.(x);         //如果有定义则调用
        return x*x;
}

不过要注意,?.()只会检查左侧的值是不是null或undefined,不会验证该值是不是函数。因此,这个例子中的square()函数再接收带两个数值时仍然会抛出异常

与条件式属性访问表达式类似,使用?.()进行函数调用也是短路操作:如果?.左侧的值是null或undefined,则圆括号中的任何参数表达式都不会被求值:

let f=null,x=0try{ 
   f(x++);      //因为f是null所以抛出TypeError
}catch(e){
    x           //抛出异常前发生了递增
}
=>1
f?.(x++)
=>undefined     //f是null,但不会抛出异常
x
=>1             //因为短路,递增不会发生

使用?.()的条件调用表达式既适用于函数调用,也适用于方法调用。因为方法调用又涉及属性访问,所以有必要花时间确认一下自己是否理解下列表达式的区别:

o.m()        //常规属性访问,常规调用
o?.m()       //条件式属性访问,常规调用
o.m?.()      //常规属性访问,条件式调用

使用?.()的条件式调用是JavaScript最新的特性之一。在2020年初,多数主流浏览器的当前版本或预览版已经支持这个语法。

4.6 对象创建表达式

对象创建表达式创建一个新对象并调用一个函数(称为构造函数)来初始化这个新对象。对象创建表达式类似于调用表达式,区别在于前面多了一个关键字new:

new Object()
new Point(2,3)

如果在对象创建表达式中不给构造函数传参,则可以省略圆括号:

new Object
new Date

对象创建表达式的值是新创建的对象。

4.7 操作符概述

操作符在JavaScript用于算术表达式、比较表达式、逻辑表达式、赋值表达式等。

多数操作符都以+和=这样的标点符号表示。不过,有一些也以delete和instanof这样的关键字表示。关键字操作符也是常规操作符,与标点符号表示的操作符一样。只不过它们的语法没有那么简短而已。

表4-1按操作符优先级组织。换句话说,表格前面的操作符比后面的操作符优先级更高。横线分隔的操作符具有不同优先级。“结合性”中的“左”表示“从左到右”,“右”表示“从右到左”。“操作数”表示操作数的个数。“类型”表示操作数的类型,以及操作符的结果类型(->后面)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.7.1 操作数个数

操作符可以按照它们期待的操作数个数(参照数量来分类)。多数JavaScript操作符(如乘法操作符*)都是二元操作符,可以将两个表达式合成一个更复杂的表达式。换句话说,这些操作符期待两个操作数。JavaScript也支持一些一元操作符,这些操作符将一个表达式转换为另一个更复杂的表达式。表达式-x中的操作符-就是一元操作符,用于对操作数x进行负值操作。最后,JavaScript也支持一个三元操作符,即条件操作符?:,用于将三个表达式组合为一个表达式。

4.7.2 操作数与结果类型

有些操作符适用于任何类型的值,但多数操作符期待自己的操作数是某种特定类型,而且多数操作符也返回(或求值为)特定类型的值。

JavaScript操作符通常会按照需要转换操作数的类型。比如,乘法操作符*期待数值参数,而表达式"3" * “5"之所以合法,是因为JavaScript可以把操作数转换为数值。因此这个表达式的值是数值15,而非字符串"15”。也要记住,每个JavaScript值要么是“真值”要么是“假值”,因此期待布尔值操作数的操作符可以用于任何类型的操作数。

有些操作符的行为会因为操作数类型的不同而不同。最明显的,+操作符可以把数值加起来,也可以拼接字符串。类似地,比较操作符(如<)根据操作数类型会按照数值顺序或字母表顺序比较。后面对每个操作符都有详细介绍,包括它们的类型依赖,以及它们执行的类型转换。

注意,表4-1中列出的赋值操作数和少数其他操作符期待操作数类型为lval。lval即lvalue(左值),是一个历史悠久的术语,意思是“一个可以合法地出现在赋值表达式左侧的表达式”。在JavaScript中,变量、对象属性和数组元素都是“左值”。

4.7.3 操作符副效应

对类似2*3这样地简单表达式求值不会影响程序状态,程序后续地任何计算也不会被这个求值所影响。但有些表达式是有副效应的,即对它们求值可能会影响将来求值的结果。赋值操作符就是明显的例子:把一个值赋给变量或属性,会改变后续使用该变量或属性的表达式的值。类似地,递增和递减操作符++和–也有副效应,因为它们会执行隐式赋值。同样,delete操作符也有副效应,因为删除属性类似于(但不同于)给属性赋值undefined。

其他JavaScript操作符都没有副效应,但函数调用和对象创建表达式是否有副效应,取决于函数或构造函数体内是否使用了有副效应的操作符。

4.7.4 操作符优先级

表4-1中的操作符是按照优先级从高到低的顺序排列的,表中横线分组了相同优先级的操作符。操作符优先级控制操作符被执行的顺序。优先级高(靠近表格顶部)的操作符先于优先级低(靠近表格底部)的操作符执行,

来看下面这个表达式:

w=x+y*z;

其中乘法操作符*的优先级高于加法操作符+高,因此乘法计算先于加法执行。另外,赋值操作符=的优先级最低,因此赋值会在右侧所有操作符都执行之后才会执行。

操作符优先级可以通过圆括号显示改写。比如,要强制先执行上例中的加法计算,可以这样写:

w=(x+y)*z

注意,属性访问和调用表达式的优先级高于表4-1中列出的任何操作符。看下面的例子:

//my是一个由function属性的对象,function属性
//是一个函数的数组。这里调用了x号函数,并传给它
//参数y,然后再求值函数调用返回值的类型
typeof my.functions[x](y)

尽管typeof是优先级最高的操作符,但typeof操作符要基于属性访问、数组索引和函数调用的结果执行,这些操作的优先级全部高于操作符。

实践中,如果你完全不确定自己所用操作符的优先级,最简单的办法就是使用圆括号明确求值顺序。最重要的规则在于:乘和除先于加减执行,而赋值优先级很低,几乎总是最后才执行

JavaScript新增的操作符并不总是符合这个优先级模式。比如再表4-1中,??操作符比||和&&优先级低,而实际上它相对于这两个操作符的优先级并没有定义,ES2020要求混用??和||或&&时必须使用圆括号。类似地,新的幂操作符**相对于一元负值操作符也没有明确定义,因此在同时求负值和求幂时也必须使用圆括号

4.7.5 操作符结合性

操作符结合性规定了相同优先级操作的执行顺序。左结合意味着操作从左到右执行。例如,减操作符具有左结合性,因此:

w=x-y-z;

就等价于:

w=((x-y)-z);

另一方面,下列表达式:

y=a**b**c;
x=~-y;
w=x=y=z;
q=a?b:c?d:e?f:g;

就等价于:

y=(a**(b**c));
x=~(-y);
w=(x=(y=z))
q=a?b:(c?d:(e?f:g))

因为幂、一元、赋值和三元操作符具有右结合性。

4.7.6 求值顺序

操作符的优先级和结合性规定了复杂表达式中操作的执行顺序,但它们没有规定子表达式的求值顺序。==JavaScript始终严格按照从左到右的顺序对表达式求值。==例如:在表达式w=x+y*z中,子表达式w首先会被求值,再对x、y和z求值。然后将y和z相乘,加到x上,再把结果赋值给w表示的变量或属性。在表达式中使用圆括号可以改变乘法、加法和赋值的相对顺序,但不会改变从左到右的求值顺序。

求值顺序只在一种情况下会造成差异,即被求值的表达式具有副效应,这会影响其他表达式的求值。比如,表达式x递增一个变量,而表达式z会使用这个变量,此时保证x先于z被求值就很重要了

4.8 算术表达式

多数算术操作符(除了下面提到的)都可以用于BigInt操作数或常规数值,前提是不能混淆两种类型。

基本的算术操作符是**(幂)、*(乘)、/(除)、%(模:除后的余数)、+(加)和-(减)。如前所述,我们会在单独一节讨论+操作符。另外5个基本操作符都会对自己的操作数进行求值,必要时将操作数转换成数值,然后计算幂、积、商、余和差。无法转换成数值的非数值操作数则转换成NaN。如果有操作数是(或被转换成)NaN,则操作结果(几乎始终)是NaN。

** 操作符的优先级高于*、/和%(后三个的优先级又高于+和-)。与其他的操作符不同, ** 具有右结合性,即2 ** 2 ** 3相对于2 ** 8而非4 ** 3。另外,-3 ** 2这样的表达式本质上是有歧义的,取决于一元减号和幂操作的相对优先级,这个表达式可能意味着(-3) ** 2,也可能意味着-(3**2)。对于这种情况,不同语言的处理方式也不同。JavaScript认为这种情况下不写括号就是语法错误,强制你自己消除表达式的歧义。 **是JavaScript中最新的操作符,是ES2016中增加的。但Math.pow()函数在JavaScript很早的版本中就有了,它与 ** 操作符执行完全相同的操作。

/操作符用第二个操作数除第一个操作数。如果你习惯了区分整数和浮点数的编程语言,应该自带整数相除得到整数。但在JavaScript中,所有数值都是浮点数,因此所有除法操作得到的都是浮点数,比如5/2得到的是2.5而不是2。被0除得到正无穷或负无穷,而0/0求值为NaN。这两种情况都不是错误。

%操作符计算第一个操作数对第二个操作数取模的结果。换句话说,它返回第一个操作数被第二个操作数整除之后的余数。结果的符号与第一个操作数相同。例如,5%2求值为1,而-5%2求值为-1;

虽然模操作常用于整数,但也可以用于浮点数。比如,6.5%2.1求值为0.2。

6.5%2.1
0.19999999999999973

4.8.1 +操作符

二元+操作符用于计算数值操作数的和或者拼接字符串操作数:

1+2                   //=>3
"hello"+" "+"there"   //=>"hello there"
"1"+"2"               //=>"12"

如果两个操作数都是数值或字符串,+操作符执行后的结果自不必说。但除这两种情况之外的任何情况,都会涉及类型转换,而实际执行的操作取决于类型转换的结果。+操作符优先字符串拼接:只要有操作数是字符串或可以转换为字符串的对象,另一个操作数也会被转换为字符串并执行拼接操作。只有任何操作数都不是字符串或类字符串值时才会执行加法操作。

严格来说,+操作符的行为如下所示。

  • 如果有一个操作数是对象,则+操作符使用3.9.3节介绍的对象到原始值的算法把该对象转换成原始值。Date对象用toString()方法来转换,其他所有对象通过valueOf()转换(如果这个对象返回原始值)。不过,多数对象并没有valueOf()方法,因此它们也会通过toString()方法转换。
  • 完成对象到原始值的转换后,如果有操作数是字符串,另一个操作数也会被转换为字符串并进行拼接。
  • 否则,两个操作数都被转换成数值(或NaN),计算加法。

下面是几个例子:

1+2
=>3               //加法
"1"+"2"
=>"12"            //拼接
"1"+2
=>"12"            //数值转换成字符串后再拼接
1+{}
=>"1[object Object]"       //对象转换为字符串后再拼接
true+true
=>2              //布尔值转换为字符串后计算加法
2+null
=>2              //null转换为0后计算加法
2+undefined
=>NaN            //undefined转换为NaN后计算加法

最后,很重要的一点是,+操作符在用于字符串和数值时,可能不遵守结合性。换句话说,结果取决于操作执行的顺序。

例如:

1+2+" blind mice"
=>"3 blind mice"
1+(2+" blind mice")
=>"12 blind mice"

第一行没有圆括号,+操作符表现出左结合性,即两个数值先相加,然后它们的和再与字符串拼接。第二行中的圆括号改变了操作执行的顺序:数值2先于字符串拼接产生一个新字符串,然后数值1再与新字符串拼接得到最终结果。

4.8.2 一元算术操作符

一元操作符修改一个操作数的值以产生一个新值。在JavaScript中,一元操作符全部具有高优先性和右结合性。本节介绍的算术一元优先符(+、-、++和–)都在必要时将自己唯一的操作数转换为数值。注意,操作符+和-既是一元操作符,也是二元操作符。

一元算术操作符如下所示。

一元加(+)
       一元操作符将其操作数转为数值(或NaN)并返回转换后的值。如果操作数是数值,则它什么也不做。由于BigInt值不能转换为常规数值,因此这个操作符不应该用于BigInt。

一元减(-)
       当-用作一元操作符时,它在必要时将操作数转换为数值,然后改变结果的符号。

递增(++)
       ++操作符递增其操作数(也就是加1),这个操作数必须是一个左值(变量、数组元素或对象属性)。这个操作符将其操作数转换为数值,在这个数值上加1,然后将递增后的数值再赋值会这个变量、元素或属性。

==++操作符的返回值取决于它与操作数相对位置。如果位于操作数前面,则可以称其为前增操作符,即先递增操作数,再求值为操作数递增后的值。如果位于操作数后面,则可以称其为后递增操作符,即它也会递增操作数,但仍然求值为该操作数未递增的值。==看看下面两行代码,注意它们的差异:

let i=1,j=++i;
i
=>2
j
=>2
let n=1,m=n++;
n
=>2
m
=>1

这说明,表达式x++不一定等价于x=x+1。++操作符不会执行字符串拼接,而始终会将其操作数转换为数值。如果x是字符串“1”,++x就是数值2,但x+1则是字符串“11”

另外也要注意,==由于JavaScript会自动插入分号,因此不能在后递增操作符和它前面的操作数之间插入换行符。==如果插入了换行符,JavaScript会将操作数当成一条完整的语句,在它后面插入一个分号。

递减(–)
       --操作符也期待左值操作数。它会将这个操作数转换为数值,减1,然后把递减后的值再赋值给操作数。与++操作符类似,–返回的值取决于它与操作数的相对位置。如果位于操作数前面,它递减并返回递减后的值。如果位于操作数后面,它递减操作数,但返回未递减的值。在位于操作数后面时,操作数与操作符之间不能有换行符。

4.8.3 位操作符

略(看不懂),后面再做补充

4.9 关系表达式

4.9.1 相等与不相等操作符

== 和 === 操作符分别用两个相同的标准检查两个值是否相同。这两个操作数都接受任意类型的操作数。都在自己的操作数相同时返回true,不同时返回false。=== 操作符被称为严格相等操作符(或者全等操作符),它根据严格相同的定义检查两个操作数是否“完全相同”。 == 操作符被称为相等操作符,它根据更宽松的(允许类型转换的)相同定义检查两个操作数是否“相等”。

!= 和 !== 操作符测试的关系与 == 和 === 恰好相反。!=不相等操作符在两个值用 == 测试相等时返回false,否则返回true。!==操作符在两个值严格相等时返回false,否则返回true。

正如3.8节所说,JavaScript对象是按引用而不是按值比较的。对象与自己相等,但与其他对象都不相等。即使两个对象有同样多的属性,每个属性的名字和值也相同,那它们也不相等。类似地,两个数组即使元素相同、顺序相同,它们也不相等。

严格相等

严格相等操作符 === 求值其操作数,然后按下列步骤比较两个值,不做类型转换。

  • 如果两个值类型不同,则不相等。
  • 如果两个值都是null或都是undefined,则相等、
  • 如果两个值都是布尔值true或都是布尔值false,则相等。
  • 如果一个或两个值是NaN,则不相等(虽然有点意外,但NaN确实不等于如何其他值,也包括NaN本身!要检查某个值是不是NaN,使用x!==x或全局isNaN()函数)
  • 如果两个值都是数值且值相等,则相等。如果一个值是0而另一个是-0,则也相等。
  • 如果两个值是字符串且相同位置包含完全相等的16位值,则相等。如果两个字符串长度或内容不同,则不相等。两个字符串可能看起来相同,也表示同样的意思,但底层编码却使用不同的16位值序列。JavaScript不会执行Unicode归一化操作,像这样的两个字符串用 === 或 == 操作符都不会判定相等。
  • 如果两个值引用同一个对象、数组或函数,则相等。如果它们引用不同的对像,即使两个对象有完全相同的属性,也不相等。

基于类型转换的相等

相等操作符==与严格相等类似,但没有那么严格。如果两个操作数的值类型不同,它会尝试做类型转换,然后再比较。

  • 如果两个值类型相同,则按照前面的规则测试它们是否严格相等。如果严格相等,则相等。否则不相等。
  • 如果两个值类型不同,== 操作符仍然认为它们相等。此时它会使用一下规则,基于类型转换来判定相等关系。
            - 如果一个值是null,另一个值是undefined,则相等。
            - 如果一个值是数值,另一个值是字符串,把字符串转换成数值,再比较转换后的数值。
            - 如果一个值为true,把它转换为1,再比较。如果有一个值为false,把它转换为0,再比较。
            - 如果一个值是对象,另一个值是数值或字符串,先使用3.9.3描述的算法把对象转化成原始值,再比较。JavaScript内置的核心类先尝试使用valueOf(),再尝试使用toString()。但Date是个例外,这个类执行toString()转换。
            - 其他任何值的组合都不相等。

4.9.2 比较操作符

比较操作符测试操作数的相对顺序(数值或字母表顺序)。
小于(<)
       <操作符在第一个操作数小于第二个操作数是求值为true,否则求值为false。
大于(>)
       >操作符在第一个操作数大于第二个操作数时求值为true,否则求值为false。
小于等于(<=)
       <=操作符在第一个操作数小于等于第二个操作数时求值为true,否则求值为false。
大于等于(>=)
       >=操作符在第一个操作数大于等于第二个操作数时求值为true,否则求值为false。
这几个比较操作符的操作数可能是任何类型,但比较只针对数值和字符串,因此不是数值或字符串的操作数会被转换类型。

比较和转换的规则如下。

  • 如果有操作数求值为对象,该对象会按照3.9.3节的描述被转换为原始值。即如果它的valueOf()方法返回原始值,就使用这个值,否则就使用它的toString()方法返回的值。
  • 如果在完成对象到原始值的转换后两个操作数都是字符串,则使用字母表顺序比较这两个字符串,其中“字母表顺序”就是组成字符串的16位Unicode值的数值顺序。
  • 如果在完成对象到原始值的转换后至少有一个操作数不是字符串,则两个操作数都会被转换为数值并按照数值顺序来比较。0和-0被认为相等。Infinity比它本身之外的任何数都大,-Infinity比它本身的任何值都小。如果有一个操作数是(或转换后是NaN),则这些操作符都返回false。虽然算术操作符不允许BigInt值与常规数值混用,但比较操作符允许数值与BigInt进行比较。

记住,JavaScript字符串是16位整数值的序列,而字符串比较就是比较两个字符串的数值序列。Unicode定义的这个数值编码顺序不一定与特定语言或地区使用的传统校正顺序匹配。特别要注意字符串比较是区分大小写的,而所有大写ASCII字母比所有小写ASCII字母都小。如果不留意,这条规则很可能导致令人不解的结果。例如,根据<操作符,字符串“Zoo”会排在字符串“aardvark"前面。

如果需要更可靠的字符串比较算法,可以用String.localCompare()方法,这个方法也会考虑特定地区的字母表顺序。要执行不区分大小写的比较,可以使用String.toLowerCase()或String.toUpperCase()把字符串转换成全小写或全大写。

+操作符和比较操作符同样都会对数值和字符串操作数区别对待。+偏向字符串,即只要有一个操作数是字符串,它就会执行拼接操作。而比较操作符偏向数值,只有两个操作符全是字符串时才会按字符串处理:

1+2
=>3             //相加
"1"+"2"     
=>"12"          //拼接
"1"+2
=>"12"          //2会被转换成"2"
11<3 
=>false         //数值比较
"11"<"3"
=>true          //字符串比较
"11"<3
=>false         //数值比较,"11"会转换成11
"one"<3
=>false         //数值比较,"one"转换为NaN

最后,注意<=(小于或等于)和>=(大于或等于)操作符不依赖相等或严格相等操作符确定两个值是否“相等”。其中,小于或等于操作符只是简单地定义位“不大于”,而大于或等于操作符则定义为“不小于”。还有一个例外情形,即只要有一个操作数是(或可以转换为NaN),则全部4个操作符都返回false。

4.9.3 in操作符

in操作符期待左侧操作数是字符串、符号或可以转换为字符串的值,期待右侧操作数是对象。如果左侧的值是右侧的对象的属性名,则in返回true。例如:

let point={x:1,y:1}     //定义对象
"x" in point
=>true                  //对象有名为“x”的属性
"z" in point
=>false                 //对象没有名为“z”的属性
"toString" in point
=>true                  //对象继承了toString方法
let data=[7,8,9]        //数组,有元素(索引)0、1和2
"0" in data             
=>true                  //数组有元素“0”  
1 in data
=>true                  //数值会转化成字符串
3 in data
false                   //没有元素3

4.9.4 instanceof操作符

instanceof操作符期待左操作数是对象,右操作数是对象类的标识。这个操作符在左侧对象是右侧类的实例时求值为true,否则求值为false。在JavaScript中,对象类是通过初始化它们的构造函数定义的。因而,instanceof的右侧的操作数应该是一个函数。下面看几个例子:

let d=new Date()              //通过Date()构造函数创建一个新对象
d instanceof Date
=>true                        //d是通过Date()创建的
d instanceof Object
=>true                        //所有对象都是Object的实例
d instanceof Number
=>false                       //d不是Number对象
let a=[1,2,3]
a instanceof Array
=>true                        //a是个数组
a instanceof Object
=>true                        //所有数组都是对象
a instanceof RegExp
=>false                       //数组不是正则表达式

注意,所有对象都是Object的实例。instanceof在确定对象是不是某个类的实例时会考虑“超类”。如果instanceof的左侧操作数不是对象,它会返回false。如果右侧操作数不是对象的类,它会抛出TypeError。

要理解instanceof的工作原理,必须理解“原型链”。原型链是JavaScript的继承机制。为了对表达式o instanceof f 求值,JavaScript会求值f.propotype,然后在o的原型链上查找这个值。如果找到了,则o是f(或f的子类)的实例,instanceof返回true。如果f.propotype不是o原型链上的一个值,则o不是f的实例,instanceof返回false。

4.10 逻辑表达式

4.10.1 逻辑与 (&&)

&&操作符可以从不同层次来理解。最简单的情况下,在与布尔操作值共同使用时,&&对两个值执行布尔与操作:当且仅当第一个操作数为true并且第二个操作数也是true时,才返回true。如果有一个操作数是false,或者两个操作数都是false,它返回false。

&&经常用于连接两个关系表达式。

关系表达式始终返回true或false,因此在像这样使用时,&&操作符本身也返回true或false。关系操作符的优先级高于&&(以及||),因此类似这样的表达式可以不带圆括号。

但&&不要求操作数是布尔值。我们知道,JavaScript值要么是“真值”,要么是“假值”。理解&&的第二个层次是它对真值和假值执行布尔与操作。如果两个操作数都是真值,&&返回一个真值:否则(一个或两个操作数是假值),&&返回假值。在JavaScript中,期待布尔值的任何表达式或语句都可以处理真值或假值,因此&&并不总返回true或false的事实在实践中并不会导致出现问题。 `

注意,上面谈到&&返回“一个真值”或“一个假值”时并没有说明这个值是什么。对此,需要从第三个层次上来理解&&。这个操作符首先对第一个操作数即它左边的表达式求值,如果左边的值是假值,则整个表达式的值也一定是假值,因此&&返回它左侧的值,不再求它右侧的表达式。

另一方面,如果&&左侧的是真值,则整个表达式的值取决于右侧的值。如果右侧的值是真值,则整个表达式的值一定是真值:如果右侧的值是假值,则整个表达式的值一定是假值==。因此,在左侧的值是真值的时候,&&操作符求值并返回它右侧的值:==

let o={x:1}
let p=null    
o&&o.x
=>1             //o是真值,因此返回o.x的值
p&&p.x
=>null          //p是假值,因此返回它,不对p.x求值

这里关键是要理解,&&可能会(也可能不会)对右侧操作数求值。在这个代码实例中,变量p的值为null,表达式p.x如果被求值会导致TypeError。但代码中以惯用方式利用&&只在p为真值(不是null或undefined)时才对p.x求值。

&&的这种行为有时候也被称为短路,可能你也会看到有代码利用这种行为条件地执行代码。例如,一下两行JavaScript代码效果相同。

if(a===b) stop();          //只有a===b时才调用stop()
(a===b)&& stop();          //效果与上面一样

一般来说,必须注意&&右侧包含副效应(赋值、递增、递减或函数调用)的表达式。无论其副效应是否依赖左侧的值。

尽管这个操作符的工作方式比较复杂,但它最常见的用法还是对真值和假值执行布尔代数计算。

4.10.2 逻辑或(||)

||操作符对它的操作数执行布尔或操作。如果有一个操作数是真值,这个操作数就返回真值。如果两个操作数都是假值,它就返回假值。

尽管||操作符最常用作简单的布尔或操作符,但它与&&类似,也有更复杂的行为。它首先会对第一个操作数,即它左侧的表达式求值。如果第一个操作数的值是真值,||就会短路,直接返回该真值,不会再对右侧表达式求值。而如果第一个操作数的值是假值,则||会求值第二个操作符并返回该表达式的值。

与&&操作符一样,应尽量避免让右操作符包含副效应,除非是有意利用右侧表达式可能不会被求值的事实。

这个操作符的习惯用法是再一系列备选项中选择第一个真值:

//如果maxWith是真值,就使用它,否则,看看preferences
//对象。如果preferences里也没有真值,就使用硬编码的常量
let max=maxWidth||preferences.maxWith||500

注意,如果0是maxWith的有效值,则以上代码有可能有问题,因为0是个假值。此时可以使用??操作符。

在ES6之前,这个惯用法常用于在函数中给参数提供默认值:

//复制o的属性给p,返回p
function copy(o,p){
     p=p||{};      //如果没有传入对象p,使用新创建对象
     //这里是函数体
}

不过在ES6及之后的版本中,这个技巧已经没有必要了。因为默认参数可以直接写在函数定义中:function copy(o,p={}){…}。

4.10.3 逻辑非(!)

!操作符是个一元操作符,出现在操作符前面。这个操作符的目的是反转其操作数的布尔值。

与&&和||不同,!操作符将操作数转换成布尔值,然后再反转得到的布尔值。这意味着!始终返回true或false,而要取得任何值x对应的布尔值,只要对x应用这个操作符两次即可:!!(x)。

作为一元操作符,!优先级较高。如果想反转表达式p&&q的值,需要使用圆括号:!(p&&q)。有必要说一下,可以通过如下JavaScript语法来表达布尔代数的两个法则:

//德摩根定律
!(p&&q)===(!p||!q)
!(p||q)===(!p&&!q)

4.11 赋值表达式

JavaScript使用=操作符为变量或属性赋值。例如:

i=0;           //设置变量i为0
o.x=1          //设置对象o的属性x为1

=操作符期待其左操作数是一个左值,即变量或对象属性或数组元素。它期待右侧操作数是任意类型的任意值。赋值表达式的值是右侧操作数的值。作为副效应,=操作符将右侧的值赋给左侧的变量或属性,以便将来对该变量或属性的引用可以求值为这个值。

尽管赋值表达式通常很简单,但有时候你可能会看到一个大型表达式中会用到赋值表达式的值。例如,可以像下面这也再同一个表达式中赋值并测试这个值:

(a=b)===0

如果你要这样做,最好真正明白=和===操作符的区别。注意,=的优先级很低,在较大的表达式中使用赋值的值通常需要使用圆括号。

赋值操作符具有右结合性,这意味着如果一个表达式中出现多个赋值操作符,它们会从右到左求值。因此,可以通过如下带啊吗将一个值赋给多个变量:

i=j=k=0;              //把三个变量都赋值为0

4.11.1 通过操作赋值

除了常规的=赋值操作符,JavaScript还支持其他一些赋值操作符,这些操作符同组合赋值和其他操作符提供了快捷操作。例如,+=操作符执行加法和赋值操作。

+=操作符可以处理数值和字符串。对数值操作符,它执行加法并赋值;对字符串操作数,它执行拼接并赋值。

类似的操作符还有-=、*=、&=,等等。
在这里插入图片描述
多数情况下,表达式:

a op=b

(其中op是操作符)都等价于表达式:

a=a op b

在第一行,a只会被求值一次。而在第二行,它会被求值两次。这两种情况只有在a包含副效应(如函数调用或递增操作符)时才会有区别。比如,下面这两个表达式就不一样了:

data[i++]*=2
data[i++]=data[i++]*2
let data=[0,1,2,3]
let i=0
data[i++]*=2
data
=>(4) [0, 1, 2, 3]
let data=[0,1,2,3]
let i=0
data[i++]=data[i++]*2
data
=>(4) [2, 1, 2, 3]

4.12 求值表达式

与很多解释型语言一样,JavaScript有能力解释JavaScript源代码字符串,对它们求值以产生一个值。JavaScript是通过全局函数eval()来对源代码字符串求值的:

eval("3+2")             //=>5

对源代码字符串的动态求值是一个强大的语言特性,但这种特性在实际项目中几乎用不到。如果你发现自己在使用eval(),那应该好好思考一下到底是不是真需要使用它。特别地,eval()可能会成为安全漏洞,为此永远不要把来自用户输入的字符串交给它执行。对于像JavaScript这么复杂的语言,无法对用户输入脱敏,因此无法保证在eval()中安全使用。由于这些安全问题,某些Web服务器使用HTTP的“Content-Security-Policy”头部对整个网站禁用eval()。

在这里插入图片描述

4.12.1 eval()

eval()期待一个参数,如果给它传入任何非字符串值,它会简单地返回这个值。如果传入字符串,它会尝试把这个字符串当成JavaScript代码来解析,解析失败会抛出SyntaxError。如果解析字符串成功,它会求值代码并返回该字符串最后一个表达式或语句的值:如果最后一个表达式或语句没有值则返回undefined。如果求值字符串抛出异常,该异常会从调用eval()的地方传播出来。

对于eval()(在像这样调用时),关键在于它会使用它的代码的变量环境。也就是说,它会像本地代码一样查找变量的值、定义新变量和函数。如果一个函数定义了一个局部变量x,然后调用了eval(“x”),那它会取得这个局部变量的值。如果这个函数调用了eval(“var y=3;”),则会声明一个新局部变量y。另外,如果被求值的字符串使用了let或const,则声明的变量或常量或被限制在求值的局部作用域内,不会定义到调用环境中。

类似地,函数也可以像下面这样声明一个局部函数:

eval("function f(){ return x+1; }");

如果在顶级代码中调用eval(),则它操作的一定是全局变量和全局函数。

注意,传给eval()的代码字符串本身必须从语法上说的通:不能使用它向函数中粘贴的代码片段。比如,eval(“return;”)是没有意义的,因为return只在函数中是合法的,即使被求值的字符串使用与调用函数相同的变量环境,这个字符串也不会称为函数的一部分。只要这个字符串本身可以作为独立的脚本运行(即使像x=0这么短),都可以合法地传给eval()。否则,eval()将抛出SyntaxError。

4.12.2 全局eval()

之所以eval()会干扰JavaScript的优化程序,是因为它能够修改局部变量。不过作为应对,解释器也不会过多优化调用eval()的函数。那么,如果其脚本为eval()定义了别名,然后又通过另一个名字调用这个函数,JavaScript解释器该怎么做呢?JavaScript规范中说,如果eval()被以“eval”之外的其他名字调用时,它应该把字符串当成顶级全局变量来求值。被求值的代码可能定义新全局变量或全局函数,可能修改全局变量,但它不会再使用或修改调用函数的局部变量。因此也就不会妨碍局部优化。

相对而言。使用名字"eval"来调用eval()函数就叫作“直接eval”(这样就有点保留字的感觉了)。直接调用eval()使用的是上下文的变量环境。任何其他调用方式,包括间接调用,都使用全局对象作为变量环境,因而不能读,写或定义局部变量或函数(无论直接调用还是间接调用都只能通过var来定义新变量。在被求值的字符串中使用let或const创建的变量和常量会被限定在求值的局部作用域内,不会修改调用或全局环境)。

const geval=eval                    //使用另一个名字,实现全局求值
let x="global",y="global"
function f() {
 let x="local"; 
 eval("x +='changed';"); 
 return x;
}

function g() { 
  let y="local";
  geval("y+='changed';");  
  return y; 
}

f()
=>"localchanged"
y
=>"globalchanged"

注意,这种全局求值的能力不仅仅是为了适应优化程序的需求,同时也是一种极其有用的特性,可以让我们把代码字符串作为独立、顶级的脚本来执行。正如本节开始时提到的,真正需要求值代码字符串的场景非常少。假如你必须使用eval(),那很有可能应该使用它的全局求值而不是局部求值。

4.12.3 严格eval()

严格模式对eval()函数增加了更多的限制,甚至对标识符“eval”的使用也进行了限制。当我们在严格模式下调用eval()时,或者当被求值的代码字符串以“ use strict”的指令开头时,eval()会基于一个私有变量环境进行局部求值。这意味着在严格模式下,被求值的代码可以查询和设置局部变量,但不能在局部作用域中定义新变量或函数。

另外,严格模式让eval()变得更像操作符,因为“eval”在严格模式下会变成保留字。此时不能再使用新值来重写eval()函数。换句话说,通过名字“eval”来声明变量、函数、函数参数或捕获块参数都是不允许的。

4.13 其他操作符

4.13.1 条件操作符(?:)

条件操作符是JavaScript唯一一个三元操作符(有三个操作数),因此有时候也被叫作三元操作符。这个操作符有时候会被写作?:。
条件操作符的操作数可以是任意类型。第一个操作数被求值并解释为一个布尔值。如果第一个操作数的值是真值,那么就求值第二个操作数,并返回它的值。否则,求值第三个操作数并返回它的值。第二个或第三个操作数只有一个会被求值,不可能两个都被求值。
可以使用if语句实现类似的结果,但?:操作符更简洁。下面展示了它的典型应用,其中检查了变如果有定义(一个有意义的真值)就使用它,否则就提供一个默认值:

greeting="hello"+(username?username:"there")

等价于:

greeting="hello"
if(username){
   greeting+=username;
}else{
   greeting+="there"
}

4.13.2 先定义(??)

==先定义操作符??求值其先定义的操作数,如果其左操作数不是null或undefined,就返回该值。否则,它会返回右操作数的值。==与&&或||操作符类似,??是短路的:它只有在第一个操作数求值为null或undefined才会求值第二个操作数。如果表达式a没有副效应,那么a??b等价于:

(a!==null&&a!==undefined)? a:b

??是对||的一个有用的替代,适合选择先定义的操作数,而不是第一个为真值的操作数。尽管||名义上是个逻辑或操作符,习惯上也会使用它选择第一个非假值操作数:

//如果maxWith是真值,就使用它,否则,看看preferences
//对象。如果preferences里也没有真值,就使用硬编码的常量
let max=maxWidth||preferences.maxWith||500

这种习惯用法的问题在于,0、空字符串和false都是假值,但这些值在某些情况下是完全有效的。对上面的代码示例来说,maxWith如果等于0,就会被忽略,如果我们把||操作符换成??,那么对这个表达式来说,0也会成为有效的值:

//如果maxWith有定义,就使用它,否则,看看preferences
//对象。如果preferences里也没有定义,就使用硬编码的常量
let max=maxWidth||preferences.maxWith||500

??操作符与&&和||操作符类似,但优先级并不比它们更高或更低。如果表达式中混用了??和它们中的任何一个,必须使用圆括号说明先执行哪个操作:

(a ?? b) || c      //??先执行,然后执行||
a ?? (b || c)      //||先执行,然后执行??
a ?? b || c        //SyntaxError:必须有圆括号

== ??操作符是ES2020定义的,在2020年初已经得到所有主流浏览器当前和预览版的支持。这个操作符的正式名称为“缺值合并”操作符,但我没有使用这个叫法。因为这个操作符会选择自己的一个操作数,但我并没有看到它会“合并”操作数。==

4.13.3 typeOf操作符

typeof是一个一元操作符,放在自己的操作数前面,这个操作数可以是任意类型。typeof操作符的值是一个字符串,表面操作数的类型。表4-3列出了所有JavaScript值在应用typeof操作符后得到的值。
在这里插入图片描述
可以像下面这样在表达式中使用typeof操作符:

//如果value是字符串,把它包含在引号中,否则把它转换成字符串
(typeof value === "string") ? "'" +value+ "'" :value.toString

注意,如果操作数的值是null,typeof返回"object"。如果想区分null和对象,必须显式测试这个特殊值。

尽管JavaScript函数是一种对象,typeof操作符也认为函数不一样,因为它们有自己的返回值。

因为除函数之外的所有对象和数组值,typeof都求值为“object”,所以可以只用它来区分对象和其他原始类型。而要区分不同对象的类,必须使用其他方法,例如instanceof操作符、class特性,或者constructor属性。

4.13.4 delete操作符

delete是一元操作符,尝试删除其操作数指定的对象属性或数组元素。与赋值、递增和递减操作符一样,使用delete通常也是为了发挥其属性删除的副效应,而不是使用它返回的值。来看几个例子:

let o={x:1,y:2}          //先定义一个对象
delete o.x
=>true                   //删除它的属性       
"x" in o
=>false                  //这个属性不存在了
let a=[1,2,3]
delete a[2]              //删除数组的最后一个元素
=>true
2 in a                      
=>false                  //数组元素2不存在了
a.length
=>3                      //但要注意,数组长度没有变化

==注意,被删除的属性或数组元素不仅会被设置为undefined值。当删除一个属性是,这个属性就不复存在了。尝试读取不存在的属性会返回undefined,==但可以通过in操作符测试某个属性是否存在。删除某个数组元素会在数组中留下一个“坑”,并不改变数组的长度。结果数组是一个稀疏数组。

delete期待它的操作数是个左值,如果操作数不是左值,delete什么也不做,且返回true。否则,delete尝试删除指定的左值。如果删除成功则返回true,都是并非所有的属性都是可以删除的:不可配置属性(参见14.1节)就无法删除。

在严格模式下,delete的操作数如果是未限定标识符,比如变量、函数或函数参数,就会导致SyntaxError。此时,delete操作符只能用于属性访问表达式。严格模式也会在delete尝试删除不可配置(即不可删除)属性时抛出TypeError。但在严格模式之外,这两种情况都不会发生异常,delete只是简单地返回false,表示不能删除操作数。

let o={x:1,y:2}
delete o.x
=>true                    //删除对象的一个属性;返回true
typeof o.x
=>"undefined"             //属性不存在;返回“undefined”
delete o.x
=>true                    //删除不存在的属性,返回true
delete 1
=>true                    //这样做毫无意义,但是会返回true
delete o
=>false                   //不能删除变量;返回false,或在严格模式下报TypeError
delete Object.prototype
=>false                   //不可删除的属性:返回false,或在严格模式下报TypeError

4.13.5 await操作符

await是ES2017增加的,用于让JavaScript中的异步编程更自然。简单来说,await期待一个Promise对象(表示异步计算)作为其唯一操作数,可以让代码看起来像是在等待异步计算完成(但实际上它不会阻塞主线程,不会妨碍其他异步操作进行)。await操作符的值是Promise对象的兑现值。关键在于,await只能出现在已经通过async关键字声明为异步的函数中。

4.13.6 void操作符

void是一元操作符,出现在它的操作数前面,这个操作数可以是任意类型。void是个与众不同的操作符,用处不多:它求值自己的操作数,然后丢弃这个值并返回undefined。由于操作数的值会被丢弃,只有在操作数有副效应时才有必要使用void操作符。

void操作符太难解释,也很难给出一个实际的例子说明它的用法。一种情况是你要定义一个函数,这个函数什么也不返回,但却使用了箭头函数的简写语法,其中函数体是一个会被求值并返回的表达式。如果你只想对这个表达式求值,不想返回它的值,那最简单的方法就是用花括号把函数体包起来。此时,作为替代也可以使用void操作符:

let counter=0;
const increment=()=>void counter++;
increment()             
=>undefined
counter
=>1

4.13.7 逗号操作符(,)

逗号操作符是二元操作符,其操作数可以是任意类型。这个操作符会求值其左操作数,求值其右操作数,然后返回其右操作数的值。因此,下面这行代码:

i=0,j=1,k=2;

求值为2,基本上等价于:

i=0;j=1;k=2;

换句话说,逗号左侧的操作数始终会被求值,但这个值会被丢弃。而这也意味着只有当左侧表达式有副效应时才有必要使用逗号操作符。逗号操作符唯一常见的使用场景就是有多个循环变量的for循环:

//下面第一个逗号是let语句语法的一部分
//第二个逗号是逗号操作符,它让我们把两个表达式(i++与j--)
//放到了本来期待一个表达式的语句(for循环)中
for(let i=0,j=10;i<j;i++,j--){
     console.log(i+j);
}

原文链接:https://blog.csdn.net/weixin_45970219/article/details/120601530




所属网站分类: 技术文章 > 博客

作者:麻辣小龙虾

链接:http://www.qianduanheidong.com/blog/article/198121/acc46e52431b4f07ccf6/

来源:前端黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

17 0
收藏该文
已收藏

评论内容:(最多支持255个字符)