JavaScript权威指南 第三章 类型、值和变量

第三章 类型、值和变量

3.1概述与定义

JavaScript类型可以分为两类:原始类型和对象类型。
原始类型包括数值、文本字符串(也称字符串)和布尔真值(也称布尔值),null,undefined。
在JavaScript中,任何不是数值、字符串、布尔值、符号、null和undefined的值都是对象。对象(也就是对象类型的成员)是属性的集合,其中每个属性都有一个名字和一个值(原始值或其他对象)。有一个非常特殊的对象叫全局对象。

普通JavaScript对象是一个命名值的无序集合。这门语言本身也定义一种特殊对象,称为数组。数组表示一个数字值得有序集合。JavaScript语言包括操作数组得特殊语法,而数组本身也具有区别于普通对象的行为。

JavaScript与静态语言更大的区别在于,函数和类不仅仅是语言的语法,它们本身就是可以被JavaScript程序操作的值。与其他JavaScript非原始值一样,函数和类也是特殊的对象。

JavaScript接收器会执行自动垃圾收集。这意味着JavaScript程序员通常不用关心对象或其他值的析构与释放。当一个值无法触达时,或者说当程序无法以任何方式引用这个值时,解释器就知道这个值已经用不到了,会自动释放它占用的内存(JavaScript程序员有时候需要留意,不能让某些值在不经意间存续过长时间后仍可触达,从而导致它们无法被回收)。

JavaScript支持面向对象的编程风格。粗略地说,这意味着不用定义全局函数去操作不同类型的值,而是由这些类型本身定义操作值的方法。比如说对数组元素排序,不用把数组传给一个sort()函数,而是可以调用数组a的sort()方法。

在JavaScript中,只有null和undefined是不能调用方法的值

JavaScript的对象类型是可修改的,而它的原始类型是不可修改的。可修改类型的值可以改变,比如JavaScript程序可以修改对象属性和数组元素的值。数值、布尔值、符号、null和undefined是不可修改的,以数值为例,修改它是没有意义的。字符串可以看作字符数组,你可以期望它们是可修改的。但在JavaScript中,字符串也是不可修改的。虽然可以按索引访问字符串中的字符,但JavaScript没有提供任何方式去修改已有字符串的字符。

JavaScript可以自由转换不同类型的值。比如,程序期待一个字符串,而你提供了一个数值,这个数值会自动转换为字符串。而如果你在一个期待布尔值的地方使用了非布尔值,JavaScript也会相应地把它转换成布尔值。

相等操作符(==)会进行类型转换,严格相等操作符( ===)不会。

3.2 数值

Number用于表示整数和近似实数。
JavaScript使用IEEE754(Java、c++和绝大多数现代化编程语言都使用这种格式表示double类型的数值)定义的64位浮点数表示数值,这意味着JavaScript可以表示的最大整数是± 1.797 693 1 34 862 3 1 5 7x 10308,最小整数是 ±5x10(-324)

JavaScript的这种数值格式可以让我们准确表示-253到253之间的所有整数(含首尾值)。如果你的数值超过了这个范围,那可能会在末尾的数字上损失一些精度。

3.2.1 整数字面量

在JavaScript程序中,基数为10的整数可以直接写成数字序列。

JavaScript也支持十六进制(基数是16的)值。十六进制字面量以0x或0X开头,后跟一个十六进制数字字符串。十六进制数字是数字0到6和字母a(或A)到f(F)。

在ES6及之后的版本中,也可以通过二进制(基数为2)或八进制(基数为8)表示整数,分别使用前缀0b和0o(或0B和0O);

二进制:0b10101  //=>21:(1*1+0*2+1*4+0*8+1*16)
八进制:0o377    //=>255:(7*1+7*8+3*64)

3.2.2 浮点数字面量

浮点字面量可以包含小数点,它们对实数使用传统语法。实数值由数值的整数部分、小数点和数值的小数部分组成。

3.2.2 JavaScript中的算术

JavaScript还通过Math对象的属性提供了一组函数和常量,以支持更复杂的数学计算:

Math.pow(2,53)       //=>9007199254740992:2的53次方
Math.round(.6)       //=>1.0:舍入到最接近的整数
Math.ceil(.6)        //=>1.0:向上舍入到一个整数
Math.floor(.6)       //=>0.0:向下舍入到一个整数
Math.abs(-5)         //=>5:绝对值
Math.max(x,y,z)      //返回最大的参数
Math.min(x,y,z)      //返回最小的参数
Math.PI              //Π:圆周率
Math.E               //e:自然对数的底数
Math.sqrt(3)         //=>3**0.5:3的平方根
Math.pow(3,1/3)      //=>3**(1/3):3的立方根
Math.sin(0)          //三角函数:还有Math.cos、Math.atan等
Math.log(10)         //10的自然对数
Math.log(100)/Math.LN10  //以10为底100的对数
Math.log(512)/Math.Ln2   //以2为底512的对数
Math.exp(3)              //Math.E的立方

ES6在Math对象上又定义了一批函数:

Math.cbrt(27)         //=>3:立方根
Math.hypot(3,4)       //=>5:所有参数平凡和的平方根
Math.log10(100)       //=>2:以10为底的对数
Math.log2(1024)       //=>10:以2为底的对数
Math.log1p(x)         //(1+x)的自然对数;精确到非常小的x
Math.expm1(x)         //Math.exp(x)-1;Math.log1p()的逆运算
Math.sign(x)          //对<、==或>0的参数返回-1,0或1
Math.imull(2,3)       //=>6:优化的32位整数乘法
Math.clz32(0xf)       //=>28:32位整数中前导0的位数
Math.trunc(3.9)       //=>3:剪掉分数部分得到整数
Math.fround(x)        //舍入到最接近的32位浮点数
Math.sinh(x)          //双曲线正弦,还有Math.cosh()和Math.tanh()
Math.asinh(x)         //双曲线反正弦,还有Math.acosh()和Math.atanh()

JavaScript中的算术在遇到上溢出、下溢出或被0除时不会发生错误。在数值操作的结果超过最大可表示数值时(上溢出),结果是一个特殊的无穷值(Infinity)。类似地,当某个负数地绝对值超过了最大可表示负数地绝对值时,结果是负无穷值(-Infinity)。

下溢出发生在数值操作地结果比最小可以表示数值更接近0地情况下(例如:Number.MIN_Value/2)。此时,JavaScript返回0。如果下溢出来自负数,JavaScript返回一个被称为“负零”的特殊值。这个值与常规的零几乎完全无法区分,JavaScript程序员极少需要检测它。

被0除在JavaScript中不是错误,只会简单的返回无穷或负无穷。不过有一个例外:0除以0是没有意义的值,这个操作的结果是一个特殊的“非数值”(NaN,Not a Numbwe)。此外,无穷除无穷、负数平方根或者根本无法转换为数值的非数值作为算术操作符的操作数,结果也都是NaN

JavaScript预定义了全局变量Infinity和NaN以对应正无穷和非数值。这些值也可以通过Number对象的属性获取:

Infinity                             //因为太大而无法表示的正数
Number.POSITIVE_INFINITY             //同上
1/0                                  //Infinity;溢出
Number.MAX_VALUE*2                   //因为太大而无法表示的正数
-Infinity                            //因为太大而无法表示的负数
Number.NEGATIVE_INFINITY             //同上
-1/0                                 //Infinity;溢出
-Number.MAX_VALUE*2                  //因为太大而无法表示的负数

NaN                                  //非数值
Number.NaN                           //同上,写法不同
0/0                                  //=>NaN
Infinity/Infinity                    //=>NaN

Number.MIN_VALUE/2                   //=>0:下溢出
-Number.MIN_VALUE/2                  //=>-0:负0
-1/Infinity                          //=>-0:也是负0

//ES6定义了下列Number属性
Number.parseInt()                    //同全局parseInt()函数
Number.parseFloat()                  //同全局parseFloat()函数
Number.isNaN(x)                      //判断x是不是NaN
Number.isFinite(x)                   //判断x是数值还是无穷
Number.isInteger(x)                  //判断x是不是整数
Number.isSafeInteger(x)              
Number.MIN_SAFE_INTEGER              //-(2**53-1)
Number.MAX_SAFE_INTEGER              //2**53
Number.EPSILON                       //2**-52:数值和数值之间最小的差

非数值与任何值都不相等,包括本身,因此常通过x!=x或Number.isNaN(x)。这两个表达式单且仅当x与全局常量NaN具有相同值时才返回true。

负零值等于正零值(即使使用JavaScript的严格相等比较),这意味着除了作为除数使用,几乎无法区分这两个值。

let zero=0;              //常规的0
let nagz=-0;             //负零
zero===negz              //=>true:零等于负零
1/zero===1/negz          //=>false:Infinity!=-Infinity

3.2.4 二进制浮点数与舍入错误

实数有无限多个,但JavaScript的浮点格式只能表示其中有限个。这意味着在通过JavaScript操作实数时,数值表示的经常是实际数值的近似值

JavaScript(以及所有现代编程语言)使用IEEE-754浮点表示法是一种二进制表示法,这种表示法可以精确地表示如1/2、1/8和1/1024等分数。然而,我们常用的分数是十进制分数:1/10、1/100,等等。二进制浮点数表示法无法精确到哪怕0.1这么简单的数。

虽然JavaScript数值有足够大的精度,能够非常近似地表示0.1,但无法精确地表示。这可能导致一些问题,比如以下代码:

let x=.3-.2   //30美分减20美分
=>undefined
let y=.2-.1   //20美分减10美分
=>undefined
x===y
=>false      
x===.1
=>false
y===.1
=>true

由于舍入错误,.3和.2近似值的差与.2-.1近似值的差并不相等。这是所有使用二进制浮点数的编程语言共同的语言。

注意代码中X和y的值极其接近,它们也都极其接近正确的值。这个计算得到的值完全能够满足任何需要,切记不要试图比较它们的相等性。

如果浮点近似值对你的程序而言是个问题,可以考虑使用等量整数。例如,计算与钱数有关的数直时可以使用整数形式的美分,而不是零点几美元。

3.2.5 通过BigInt表示任意精度整数

ES2020为JavaScript定义了一种新的数值类型BigInt。

原因:

为了表示64位整数,这对于兼容很多其他语言和API是非常必要的。但BigInt值可能有数千甚至数百万个数字,可以满足对大数的需求(不过,BigInt的实现并不适合加密,因为它们没有考虑时序攻击)。

BigInt字面量写作一串数字后跟小写字母n。默认情况下,基数是10,但可以通过前缀0b、0o和0x来表示二进制、八进制和十六进制BigInt;

BigInt值得算术运算与常规JavaScript的算术运算符类似,只不过除法会丢弃余数并且会向下(向零)舍入:

1000n+2000n     //=>3000n
3000n-2000n     //=>1000n
2000n*3000n     //6000000n
3000n/997n      //3n:商是3
3000n%997n      //9n:余数是92n**131071n)-1n   //有39457位数字的梅森素数

不能混用BigInt操作数和常规数值操作数,如果一种数值类型比另一种更通用,则比较容易定义混合操作数的计算结果并返回更通用的类型。但上述两种类型都不比另一种更通用:BigInt表示超大值,因此它比常规数值更通用,但是BigInt只能表示整数,这样看常规JavaScript数值类型反而更通用。这个问题无论如何无法解决,因此JavaScript搁置了这个问题,只是简单地不允许在使用算术操作符时混用这两种类型地操作数

Math对象的任何函数都不接收BigInt操作数。

3.3 文本

JavaScript中表示文本的类型是String,即字符串。字符串是16位值得不可修改的有序序列,其中每个值都表示一个Unicode字符。字符串的length属性是它包含的16位值的个数。JavaScript的字符串(以及数组)使用基于0的索引,因此第一个16位值得个数是0,第二个值的索引是1。JavaScript没有表示单个字符元素的专门类型。要表示一个16位值,使用长度为1的字符串即可。

码点超过16位的Unicode字符串使用UTF-16规则编码为两个16位值的序列。这意味着一个长度为2(两个16位值)的JavaScript字符串可能表示的只是一个Unicode字符:
在这里插入图片描述

3.3.1 字符串字面量

要在JavaScript程序中包含字符串,可以把字符串放在一对匹配的单引号、双引号或者反引号(’、"或`)中。
使用反引号定界字符串是ES6的特性,允许在字符串字面量中包含(或插入)JavaScript表达式。

JavaScript最早的版本要求字符串字面量必须写在一行,使用+操作符把单行字符串拼接成长字符串的JavaScript代码随处可见。到了ES5,我们可以每行末尾加一个反斜杠(\)从而把字符串字面量写到多行上。这个反斜杠和它后面的行终结符都不属于字符串字面量。如果需要在单引号或双引号字符串中包含换行符,需要使用字符序列\n(下一节讲述)。ES6的反引号语法支持跨行字符串,而行终结符也是字符串字面量的一部分。

//写在一行但表示两行的字符串:
'two\nlines'

//写在三行但只有一行的字符串:
"one\
long\
line"

//写在两行实际上也是两行的字符串:
`the new line character at the end of this line
is included literally in this string`

注意,在使用单引号定界字符串时,必须注意英文中的缩写和所有格,比如can’t 和 O’Reilly中的单引号。因为这里的撇号就是单引号,所以必须使用反斜杠字符(\)“转义”单引号中出现的所有撇号。

如果要将JavaScript和HTML代码混合在一起,最好JavaScript和HTML分别使用不同的引号。

3.3.2 字符串字面量的转义序列

反斜杠在JavaScript字符串中有特殊的作用:它与后面的字符组合在一起,可以在字符串中表示一个无法直接表示的字符。之所以称为转义序列,就是反斜杠转换了通常意义上的含义。

下列表中列出了JavaScript中的转义序列及它们表示的字符。其中3个转义序列是通用的,可以指定十六进制形式的Unicode字符编码来表示任何字符。例如,\xA9表示版权符号,其中包含十六进制数字形式的Unicode编码。类似地,\u表示通过4位十六进制数字指定地任意Unicode字符,如果数字包含在一对花括号中,则是1到6位数字。例如,\u03c0表示字符Π,\u{1f600}表示“开口笑”表情符号。
在这里插入图片描述
如果字符\位于如何表3-1之外的前面,则这个反斜杠会被忽略。

3.3.3 使用字符串

拼接字符串是JavaScript的一个内置特性。对字符串使用+符号,那字符串会拼接起来。

可以使用标准的全等=== 和不全等 !==操作符比较字符串。只有这两个字符串具有完全相同的16位值得序列时才相等。字符串也可以使用<、<=、>和>=操作符来比较。字符串比较是通过比较16位值完成的。

通过length属性来确定一个字符串的长度。

除了length属性之外,JavaScript还提供了操作字符串的丰富API:

//取得字符串的一部分
let s="Hello,world"
s.substring(1,4)
=>"ell"      //=>"ell":第2~4个字符
s.slice(1,4)
=>"ell"      //同上
s.slice(-3)
=>"rld"      //最后三个字符        
s.split(',')
=>(2) ["Hello", "world"]    //从定界符处拆开

//搜索字符串
s.indexOf("l")
=>2         //第一个字符“l”的位置
s.indexOf("l",3)
=>3         //3后第一个“l”的位置
s.indexOf("zz")
=>-1        //s不包含字串“zz” 
s.lastIndexOf("l")
=>9         //最后一个字母l的位置

//ES6之后的布尔值搜索函数
s.startsWith("hell")
=>true     //s以hell开头
s.endsWith("!")
=>false    //s不以!结尾
s.includes("or")
=>true     //s包含子串“or”

//创建字符串修改版本
s.replace("llo","ya")
=>"heya,world" 
s.toLowerCase()
=>"hello,world"
s.toUpperCase()
=>"HELLO,WORLD"
s.normalize()      
=>"hello,world"      //Unicode归一化,ES6新增
s.normalize("NFD")
=>"hello,world"      //NFD归一化,还有“NFKC”和“NFKD”

//访问字符串中的个别(16位值)字符
s.charAt(0)
=>"h"
s.charAt(s.length-1)
=>"d"
s.charCodeAt(0)
=>104
s.codePointAt(0)
=>104

//ES2017新增的字符串填充函数
"x".padStart(3)
=>"  x"              //在左侧添加空格,让字符串长度变成3
"x".padEnd(3)
=>"x  "              //在右侧添加空格,让字符串长度变成3
"x".padStart(3,"*")
=>"**x"              //在左侧添加*,让字符串长度变成3
"x".padEnd(3,"*")
=>"x**"              //在右侧添加*,让字符串长度变成3

//删除空格函数。trim()是ES5就有的,其他是ES2019添加的
" test ".trim()
=>"test"                    //删除开头和末尾的空格
" test ".trimStart()
=>"test "                   //删除左侧空格,也叫trimLeft
" test ".trimEnd()
=>" test"                   //删除右侧空格,也叫trimRight

//未分类字符串方法
s.concat("!")               
=>"Hello,world!"            //可以用“+”代替
s.repeat(5)
"Hello,worldHello,worldHello,worldHello,worldHello,world"   //拼接n次,ES6新增

==JavaScript中的字符串都是不可修改的。==像replace()和toUpperCase()这样的方法都返回一个新的字符串,它们并不会修改调用它们的字符串。

字符串也可以看成只读数组,使用方括号而非charAt()方法访问字符串中个别的字符。

3.3.4 模板字面量

在ES6及之后的版本中,字符串字面量可以用反引号来定界。

模板字面量可以包含任意的JavaScript表达式。反引号中字符串字面量最终值得计算,涉及对其中包含得所有表达式求值、将这些表达式的值转换成字符串,然后再把这些字符串与反引号中的字面量组合:
eg:

let name='helei'
let greeting=`Hello ${name}`
=>"Hello helei"

位于${和对应的}之间的内容都被当成JavaScript表达式来解释。而位于这对花括号之外的则是常规字符串字面量。括号内的表达式会被求值,然后转换成字符串并插入模板中,替换美元符号、花括号以及花括号中的所有内容。

模板字面量可以包含任意数量的表达式,可以包含任何常规字符串中可以出现的转义字符,也可以跨任意多行而无须特殊转义。下面的模板字面量包含4个JavaScript表达式、1个Unicode转义序列和至少4个换行符(表达式的值也可能包含换行符):

let errorttessage = `\
\u2718 Test failure at  ${filename}:${linenumber}:
${exception.message}
Stack trace:
${exception.stack}
`;

这里第一行末尾的反斜杆转义了第一个换行符,因此最终字符串的第一个字符Unicode字符(\u2718)而非换行符。

标签化模板字面量

模板字面量有一个强大但不太常用的特性:如果在开头的反引号前面有一个函数名(标签),那么模板字面量中的中文和表达式的值将作为参数传给这个值。“标签化模板字符量”的值就是这个函数的返回值。这个特性可以用于先对某些值进行HTML或SQL转义,然后再把它们插入文本。

ES6提供了一个内置标签函数:String.raw()。这个函数返回反引号中未经处理的文本,即不会处理任何反斜杠转义:

`\n`.length
=>1
String.raw`\n`.length
=>2

注意,即使标签化模板字面量的标签部分是函数,在调用这个函数时也没有圆括号。在这种非常特别的情况下,反引号字符充当开头和末尾的圆括号。

3.3.5 模式匹配

JavaScript定义了一种被称为正则表达式(或RegEXP)的数据类型,用于描述和匹配文本中的字符串模式。RegExp不是JavaScript中的数据类型,但具有类似数值和字符串的字面量语法,因此它们有时候看起来像是基础类型。

一对斜杆之间的文本构成正则表达式字面量。这对斜杆中的第二个后面也可以跟一个或多个字母,用于修改模式的含义。例如:

/^HTML/;                 //匹配字符串开头的字母HTML
/[1-9][0-9]*/            //匹配非0数字,后面跟着任意数字
/\bjavascript\b/i        //匹配"javascript"这个词,不区分大小写

RegExp对象定义了一些有用的方法,而字符串也有接收RegExp参数的方法,例如:

let text='texting:1,2,3';
let pattern=/\d+/g
pattern.test(text)
=>true                    //存在匹配项
text.search(pattern)
=>8                       //第一个匹配项的位置
text.match(pattern)
=>["1", "2", "3"]         //所有匹配项的数组
text.replace(pattern,'#')
=>"texting:#,#,#"               
text.split(/\D+/)
=>["", "1", "2", "3"]     //基于非数字拆分

3.4 布尔值

布尔值表示真或假、开或关、是或否。这个类型只有两个值:true和false.
JavaScript的任何值都可以转换成布尔值。下面这些值都会被转换成false:
undefined、null、0、-0、NaN、""(空字符串)

所有其他值,包括所有对象(和数组)都转换为(可以被用作)布尔值true。false和可以转换成它的6个值有时候也被称为假性值(falsy),而所有其他值则被称为真性值(truthy)。在任何JavaScript期待布尔值的时候,假性值都可以当作false,真性值都可以当作true。
布尔值有一个toString()方法,可用于将自己转换成字符串“true”或“false”。除此之外,布尔值再没有其他有用的方法了。

3.5 null和undefined

null是一个语言关键字,求值为一个特殊值,通常用于表示某个值不存在。对null用typeof操作符返回字符串“object”,表明可以将null看成一种特殊对象,表示“没有对象”。但在实践中,null通常被当作它自己类型的唯一成员,可以用来表示数组、字符串以及对象“没有值”
JavaScript中的undefined也表示值不存在,但undefined表示一种更深层次的不存在。具体来说:

  1. 变量的值未初始化时就是undefined
  2. 在查询不存在的对象属性或数组元素时也会得到undefined
  3. 没有明确返回值的函数返回的值是undefined,没有传值的函数参数的值也是undefined。

undefined是一个预定义的全局变量(而非像null那样的语言关键字,不过在实践中这个区别并不重要),这个常量的初始值就是undefined。对undefined应用typeof操作符会返回“undefined”,表示这个值是该特殊类型的唯一成员

抛开细微的差别,null和undefined都可以表示某个字不存在,经常被混用。相等操作符== 认为它们相等(要区分它们,必须使用全等操作符 ===) 。因为它们两个都是假性值,在需要布尔值的情况下,它们都可以当作false使用。null和undefined都没有属性和方法,使用.或[]访问这两个值的属性或方法会导致TypeError

我认为可以哟波undefined表示一种系统级别、意料之外或类似错误的没有值,可以用null表示程序级别、正常或意料之中的没有值。

3.6符号

符号(Symbol)是ES6新增的一种原始类型,用作非字符串的属性名要理解符号,需要了解JavaScript的基础类型Object是一个属性的无序集合,其中每个属性都有一个名字和一个值。属性名通常是(ES6之前一直是)字符串。但在ES6和之后的版本中,符号也可以作为属性名:

let strname="string name"               //可以用作属性名的字符串
let symname=Symbol("propname")          //可以用作属性名的符号
typeof strname                          
=>"string"                              //strname是字符串
typeof symname
=>"symbol"                              //symname是符号
let o={}
o[strname]=1                            //使用字符串定义一个属性
=>1
o[symname]=2                            //使用符号名定义一个属性 
=>2                                      
o[strname]                              //访问字符串名字的属性
=>1
o[symname]                              //访问符号名字的属性                       
=>2

==Symbol类型没有字面量语法,要获取一个Symbol值,需要调用Symbol()函数。这个函数永远不会返回相同的值,即使每次传入的参数都一样。==这意味着可以调用Symbol()取得的符号安全地用于为对象添加新属性,而无须担心可能重写已有的同名属性。类似地如果定义了符号属性但没有共享相关符号,也可以确信程序中地其他代码不会重写这个属性。

实践中,符号通常用作一种语言扩展机制,ES6新增了for/of循环和可迭代对象,为此就需要定义一种标准地机制让类可以实现,从而把自身变得可迭代。但选择任何特定地字符串作为这个迭代器方法地名字都有可能破坏已有地代码,为此,符号应运而生。正如第十二章会介绍的,Symbol.iterator是一个符号值,可用作一个方法名,让对象变得可迭代。

Sympol()函数可选地接收一个字符串参数,返回唯一的符号值。如果提供了字符串参数,那么调用返回符号值的toString()方法得到的结果中会包含该字符串。不过要注意,以相同的字符串调用两次Symbol()会产生两个完全不同的符号值。

let s=Symbol("sym_x")
let s1=Symbol("sym_x")
s.toString()
=>"Symbol(sym_x)"
s1.toString()
=>"Symbol(sym_x)"

符号值唯一有趣的方法就是toString()。不过,还应该知道两个与符号相关的函数。在使用符号是,我们有时希望它们对代码是私有的,从而可以确保你的代码的属性永远不会与其他代码的属性发生冲突。但有时我们也希望定义一些可以与其他代码共享的符号值。

为了定义一些可以与其他代码共享的符号值,JavaScript定义了一个全局符号注册表。Symbol.for()函数接收一个字符串参数,返回一个与该字符串关联的符号值。如果没有符号与该字符串关联,则会创建并返回一个新符号;否则,就会返回已有的符号。换句话说,Symbol.for()与Symbol()完全不同:Symbol()永远不会返回相同的值,而在以相同的字符串调用时Symbol.for()始终返回相同的值传给Symbol.for()的字符串会出现在toString()(返回符号值)的输出中。而且,这个字符串也可以通过将返回的符号传给Symbol.keyFor()来得到

let s=Symbol.for("shared")
let t=Symbol.for("shared")
s===t
=>true
s.toString()
=>"Symbol(shared)"
Symbol.keyFor(t)
=>"shared"

3.7 全局对象

全局对象的属性是全局定义的标识符,可以在JavaScript程序的任何地方使用。JavaScript解释器启动后(或每次浏览器加载新页面时),都会创建一个新的全局对象并为其添加一组初始的属性,定义了:

  • undefined、Infinity和NaN这样的全局常量
  • isNaN()、parseInt()和eval()这样的全局函数
  • Date()、RegExp()、String()、Object()和Array()这样的构造函数
  • Math和JSON这样的全局对象

全局地下的初始属性并不是保留字,但它们应该都被当成保留字。

在Node中,全局对象有一个名为global的属性,其值为全局对象本身,因此在Node程序中始终可以通过global来引用全局对象。

在浏览器中,Window对象对浏览器窗口中的所有JavaScript代码而言,充当了全局对象的角色,这个全局的Window对象有一个自引用的window属性,可以引用全局对象。Window对象定义了核心全局属性,也定义了其他一些特定于浏览器和客户端JavaScript的全局值。15.13节介绍的工作线程有自己不同的全局对象(不是window)。工作线程中的代码可以通过self来引用它们的全局对象。

ES2020最终定义了globalThis作为任何上下文引用全局对象的标准方式。2020年初,所有现代浏览器和Node都实现了这个属性。

3.8 不可修改的原始值与可修改的对象引用

JavaScript中的原始值(undefined、null、布尔值、数值和字符串)与对象(包括数组和函数)有一个本质的区别。原始值是不可修改的,即没有办法改变原始值。对于数值和布尔值,这一点很好理解:修改一个数值的值没有什么用。可是,对于字符串,这一点就不太好理解了。因为字符串类似字符数组,我们或许认为可以修改某个索引位置的字符。事实上,JavaScript并不允许这么做。所有看起来返回一个修改后字符串的字符串方法,实际上返回的都是一个新字符串。
例如:

let s='hello'
s.toUpperCase()
=>"HELLO"
s
=>"hello"      //原始字符串并未改变

原始值是按值比较的,即两个值只有在它们的值相同的时候才是相同的。对于数值、布尔值、null和undefined来说,这化听起来确实有点绕。其实很好理解,例如,在比较两个不同的字符串时,当且仅当这两个字符串长度相同并且每个索引的字符也相同时,JavaScript才认为它们相等。

对象不同于原始值,对象是可修改的,即它们的值可以改变:

let o={x:1}
o.x=2
=>2
o.x=3
=>3
let a=[1,2,3]
a[0]=0
=>0
a[3]=4
=>4
a
=>[0, 2, 3, 4]

对象不是按值比较的,两个不同的对象即使拥有完全相同的属性和值,它们也不相等。同样,两个不同的数组,即使每个元素都相同,顺序也相等,它们也不相等:

let a={x:1},b={x:1}
a===b
=>false                   //不同的对象永远也不会相等
let c=[],d=[]
c===d
=>false                   //不同的数组也永远不会相等

类似地,如果需要比较两个不同的对象和数组,必须比较它们的属性或元素。
eg:

function equalArrays(a,b){
       if(a===b) return true;
       if(a.length!==b.length) return false;
       for(let i=0;i<a.length;i++){
          if(a[i]!==b[i]) return false;
       }
       return true;
}

3.9 类型转换

JavaScript对待自己所需值的类型非常灵活。JavaScript会根据需要转换这个值。

eg:

10+" objects"
=>"10 objects"
"7"*"4"
=>28
let n=1-'x'            //n==NaN;字符串'x'无法转换成数值
n+" objects"        
=>"NaN objects"        //=>'NaN objects':NaN转换成字符串'NaN'    

在这里插入图片描述
在这里插入图片描述
转换成数值有些微妙,可以解析为数值的字符串会转化为对应的数值。字符串开头和末尾可以有空格,但开头或末尾任何不属于数值字面量的非空格字符,都会导致字符串到数值的转换产生NaN。有些数值转换的结果可能会让人不可思议,比如true转换为1,而false和空字符串都转换为0。

eg:

'3 '*4
=>12
' 3'*4
=>12
'e3'*4
=>NaN

3.9.1 转换与相等

一个是严格相等操作符( ===) ,如果两个值不是同一种类型,那么这个操作符就不会判定它们相等。一个是 ==,它的比较相对灵活,会进行类型转换。

3.9.2 显式转换

尽管JavaScript会自动执行很多类型的转换,但有时我们也需要进行显式转换,或者有意进行显式转换以保证代码清晰。
执行显示类型转换的最简单方法就是使用Boolean()、Number()和String()函数。
除null和undefined之外的所有值都有toString()方法,这个方法返回的结果通常与String()函数返回的结果相同
某些JavaScript操作符会执行隐式类型转换,有时候可以利用这一点完成类型转换。如果+操作符有一个操作数是字符串,那它会把另一个操作数转换为字符串。一元操作符+会把自己的操作数转换成数值。而一元操作符!会把自己的操作数转换成布尔值,然后再取反。这些事实导致我们常常再某些代码中看到如下类型转换的用法:

let y
y+""
=>"undefined"
+y
=>NaN
y-0
=>NaN
!!y                    
=>false

格式化和解析数值是计算机程序中常见的错误来源,而JavaScript为数值到字符串和字符串到数值的转换提供了特殊函数和方法,能够针对转换进行更加精确的控制。

Number类定义的toString()方法接收一个可选的参数,用于指定一个基数或底数,如果不指定这个参数,转换的默认基数为10。当然也可以按照其他基数(2到36)来转换数值。例如:

let n=17
let binary="0b"+n.toString(2)
binary
=>"0b10001"
let octal="ob"+n.toString(8)
octal
=>"ob21"
hex="0x"+n.toString()
=>"0x17"

在使用金融或科学数据时,可能需要控制转换后得到的字符串的小数位的个数或者有效数字的个数,或者需要控制是否采用指数计数法。Number类为这些数值到字符串的转换定义了3种方法。

  1. toFixed()把数值转换成字符串时可以指定小数点后面的位数。这个方法不使用指数计数法。
  2. toExponential()使用指数计数法将数值转换成字符串,结果是小数点前1位,小数点后为指定位数(意味着有效数值个数比你的指定的值多一位)
  3. toPrecision()按照指定的有效数字个数将数值转换成字符串。如果有效数字个数不足以显示数值的整体部分,它会使用指数计数法。、

注意,这三种方法必要时都会舍去末尾的数字或者补0。

如果把字符串传给Number()转换函数,它会尝试把字符串当成整数或浮点数字面量来解析。这个函数只能处理基数为10的整数,不允许末尾出现无关字符。parseInt()和parseFloat()函数(都是全局函数,不是任何类的方法)则更灵活一些。parseInt()只解析整数,而parseFloat()既解析整数也解析浮点数。如果字符串以0x或0X开头,parseInt()会将其解析为十六进制数值。parseInt()和parseFloat()都会跳过开头的空格,尽量多地解析数字字符,忽略后面地无关字符。如果第一个非空格字符不是有效的数值字面量,它们会返回NaN:

parseInt("3 blind mice")
=>3
parseFloat(" 3.14 meters")
=>3.14
parseInt("-12.34")
=>-12
parseInt("0xFF")
=>255
parseInt("0xff")
=>255
parseInt("-0xFF")
=>-255
parseFloat(".1")
=>0.1
parseInt("0.1")
=>0
parseInt(".1")
=>NaN                         //整数不能以‘.’开头
parseFloat("$72.47")
=>NaN                         //数值不能以‘$’开头

parseInt()接收可选的第二个参数,用于指定解析数值的底(基)数,合法的2到36。例如:

parseInt("11",2)
=>3            //1+1*2
parseInt('ff',16)
=>255          //15+15*16
parseInt('zz',36)
=>1295         //35+35*36
parseInt('077',8)
=>63           //7+7*8
parseInt('077',10)
=>77          //7+7*10

3.9.3 对象到原始值转换

JavaScript对象到原始值转换的复杂性,主要原因在于某些对象类型不止一种原始值的表示。比如,Date对象可以用字符串表示,也可以用时间戳表示。JavaScript规范定义了对象到原始值转换的3种基本算法。

  • 偏字符串
    该算法返回原始值,而且只要可能就返回字符串
  • 偏数值
    该算法返回原始值,而且只要可能就返回数值
  • 无偏好
    该算法不倾向于任何原始值类型,而是由类定义自己的转换规则。JavaScript内置类型除了Date类都实现了偏数值算法。Date类实现了偏字符串算法。
对象转换成布尔值

对象到布尔值的转换很简单:所有对象都转换为true。注意,这个转换不需要使用前面介绍的对象到原始值的转换算法,而是直接适用于所有对象。包括空数组,甚至包括new Boolean(false)这样的包装对象

对象转换成字符串

在将对象转换成字符串时,JavaScript首先使用偏字符串算法将它转换成一个原始值,然后将得到的原始值再转换成字符串
这种转换会发生再把对象传给一个接收字符串参数的内置函数时,比如将String()作为转换函数,或者将对象插入模板字面量中时,就会发生这种转换。

对象转换成数组

当需要把对象转换成数值时,JavaScript首先使用偏数值算法将它转换成一个原始值,然后将得到的原始值再转换成数值
接收数值类型的内置JavaScript函数和方法都以这种方式将对象转换成数值,而除数值操作符之外的参数JavaScript也按照这种方式把对象转换成数值

操作符转换特例

首先,JavaScript中的==+==操作符执行加法和字符串拼接。

  • 如果一个操作数是对象,那JavaScript会使用无偏好算法将对象转换成原始值。
  • 如果两个操作数都是原始值,则会先检查它们的类型。
  • 如果有一个参数是字符串,则把另一个原始值也转换为字符串并拼接两个字符串。
  • 否则,把两个参数都转换成数值并把它们相加。

其次, ==和!=操作符以允许类型转换的宽松方式执行相等和不相等测试。如果一个操作数是对象,另一个操作数是原始值,则这两个操作符会使用无偏好算法将对象转换成原始值,然后再比较两个原始值。

最后,关系操作符<、<=、>和>=比较操作数的顺序,既可以比较数值,也可以比较字符串。如果操作数中有一个是对象,则会使用偏数值算法将对象转换成原始值。不过要注意,与对象到数值的转换不同,这个偏数值算法返回的原始值不会再被转换成数值。

注意,Date对象的数值表示是可以使用<和>进行有意义的比较的,但它的字符串表示则不行,对于Date对象,无偏好算法会将其转换成字符串,而JavaScript中这两个操作符会使用偏数值算法的事实意味着我们可以比较两个Date对象的顺序。

toString()和valueOf()方法

所有对象都会继承两个在对象到原始值转换时使用的方法。

第一个方法toString()的任务是返回对象的字符串表示。默认情况下,toString()方法不会返回特别的值:

({x:1,y:2}).toString()
=>"[object Object]"

很多类都定义了自己特有的toString()版本。

  • Array类的toString()方法会将数组的每个原始转换成字符串,然后再使用逗号将它们拼接起来。
  • Function类的toString()方法会将用户定义的函数转换成JavaScript源代码的字符串。
  • Date类定义的toString()方法返回一个人类友好(且JavaScript可解析)的日期和时间字符串。
  • RegExp类定义的toString()方法会将RegExp()对象转换成一个看起来像RegExp字面量的字符串。
({x:1,y:2}).toString()
=>"[object Object]"
[1,2,3].toString()
=>"1,2,3"
(function(x) { f(x); }).toString()
=>"function(x) { f(x); }"
/\d+/g.toString()
=>"/\\d+/g"

另一个对象转换函数叫valueOf()。这个方法的任务并没有太明确的定义,大体上可以认为它是吧对象转换为代表对象的原始值(如果存在这样一个原始值)。对象是复合值,且多数对象不能真正通过一个原始值来表示,因此valueOf()方法默认情况下只返回对象本身,而非返回原始值

  • String、Number和Boolean这样的包装类定义的valueOf()方法也只是简单地返回被包装的原始值
  • Array、Function和RegExp简单地继承默认方法。在这类型地实例上调用valueOf()会返回对象本身。
  • Date对象定义的valueOf()方法返回日期的内部表示形式:自1970年1月1日至今的毫秒数。
对象到原始值转换算法

解释完toString()和valueOf()方法后,现在我们可以大致地解释前面三个对象到原始值转换算法的实现了。

  • 偏字符串算法首先尝试toString()方法,如果这个方法有定义且返回原始值,则JavaScript会使用该原始值(即使这个原始值不是字符串)。如果toString()不存在,或者存在但返回对象,则JavaScript尝试valueOf()方法。如果这个方法存在且返回原始值,则JavaScript使用该值。否则,转换失败,报TypeError。
  • 偏数值算法与偏字符串算法类似,只不过先尝试valueOf()方法,再尝试toString()方法。
  • 无偏好算法取决于被转换对象的类。如果是一个Date对象,则JavaScript使用偏字符串算法。如果是其他类型的对象,则JavaScript使用偏数值算法。

以上规则适用于所有内置JavaScript类型,也是我们所有自定义类的默认规则。

偏数值转换规则的细节可以解释为什么空数组会转换成数值0,而单元素数值也可以转换为数值:
对象到数值的转换首先使用偏数值算法把对象转换成一个原始值,然后再把得到的原始值转换成数值。偏数值算法首先尝试valueOf()。Array类继承了默认的valueOf()方法,由于该方法返回对象本身,因此最终会调用toString()方法。空数组转换成空字符串。而空字符串转换成数值0。只有一个元素的数组转换成该元素对应的字符串。如果数组只包含一个数值,则该数值先转换为字符串,再转换回数值。

3.10 变量声与赋值

3.10.1 使用let和const声明

在现代JavaScript中,变量是通过let关键字声明的,也可以使用一条let语句声明多个变量:

let i;
let sum;
let i,sum;

声明变量的同时(如果可能)也为其赋予一个初始值是个好的编程习惯。

如果在let语句中不为变量指定初始值,变量也会被声明,但在被赋值之前它的值是undefined。

要声明常量而非变量,则要使用const而非let。const与let类似,区别在于const必须声明时初始化常量:

const H0=74                       //哈勃常数(km/s/Mps)
const C=299792.458                //真空中的光速(km/s)
const AU=1.496E8                  //天文单位:地球与太阳间的平均距离(km)

顾名思义,常量的值是不能改变的,尝试给常量重新赋值会抛出TypeError。

声明常量的一个常见(但非普遍性)的约定是全部字母大写,如H0或HTTP_NOT_FOUND,以区别于变量。

变量与常量作用域

变量的作用域是程序源代码中一个区域,在这个区域内变量由定义。通过let和const声明的变量和常量具有块作用域。这意味着它们只在let和const语句所在的代码块中有定义

  • JavaScript类和函数的函数体是代码块
  • if/else语句的语句的语句体是代码块
  • while和for循环的循环体是代码块

粗略地讲,如果变量或常量声明在一对花括号中,那这对花括号就限定了该变量或常量有定义的代码区域(在声明变量或常量的let或const语句之前的代码行中引用这些变量或常量也是不合法的)。作为for、for/in或for/of 循环的一部分声明的变量和常量,以循环作为它们的作用域,即使它们实际上位于花括号外部。

如果声明位于顶级,在任何代码块外部,则称其为全局变量或常量,具有全局作用域。在Node和客户端JavaScript模块中,全局变量的作用域是定义它们的文件但在传统客户端JavaScript中,全局变量的作用域是定义它们的HTML文档。换句话说,如果

重复声明

在同一个作用域中使用多个let或const声明同一个名字是语法错误。在嵌套作用域中声明同名变量是合法的(尽管实践中千万不要这么做)。

声明和类型

如果你使用过静态类型语言(如C或Java),可能认为变量声明的主要目的是为变量指定可以赋予它的值的类型。但我们看到了,JavaScript的变量声明与值得类型无关(有一些JavaScript扩展,如TypeScript和Flow ,允许通过类似x:number = 0;的语法在声明变量时指定类型。),JavaScript变量可以保存任何类型的值。

3.10.2 使用var的变量声明

在ES6之前的Javascript中,声明变量的唯一方式是使用var关键字,无法声明常量。var的语法与let的语法相同。

虽然var和let有相同的语法,但它们也有重要的区别。

  • 使用var声明的变量不具有块作用域。这种变量的作用域仅限于包含函数的函数体,无论它们在函数中嵌套的层次有多深。
  • 如果在函数体外部使用var,则会声明一个全局变量。但通过var声明的全局变量与通过let声明的全局变量有一个重要的区别。通过var声明的全局变量被实现为全局对象的属性。全局对象可以通过globalThis引用。因此,如果你在函数体外部写了var x=2;,就相当于写了globalThis.x=2;。不过要注意,这么类比并不完全恰当。因为通过全局var创建的这个数值不能使用delete操作符。通过let和const声明的全局变量和常量不是全局对象的属性。
  • 与通过let声明的变量不同,使用var多次声明同名变量是合法的。而且由于var变量具有函数作用域而不是块作用域,这种重新声明实际上是很常见的。变量i经常用于保存整数值,特别是经常用作for循环的索引变量。在有多个for循环的函数中,每个循环通常都以for(var i=0;…开头。因为var并不会把这些变量的作用域限定在循环体内,每次循环都会(无害地)重新声明和初始化同一个变量)。
  • var声明的一个最不同寻常的特性是作用域提升。在使用var声明变量时,该声明会被提高至包含函数的顶部。因此对使用var声明的变量,可以在包含函数内部的任何地方使用而不会出错。如果初始化代码尚未运行,则变量的值可能是undefined,但在初始化之前是可以使用变量而不保错的(这会成为一个bug来源,也是let要纠正的一个最重要的错误特性。如果使用let声明了一个变量,但试图在let语句运行前使用该变量则会导致错误,而不是得到undefined值)。

3.10.3 解构赋值

ES6实现了一种符合声明与赋值语法,叫作解构赋值。在解构赋值中,等号右手端的值是数组或对象(“解构化的值”),而左手端通模拟数组组或对象字面量语法指定一个或多个变量。在解构赋值发生时,会从右侧的值中提取(解构)出一个或多个值,并保存到左侧列出的变量中。解构赋值可能最常用与在const、let或var声明语句中初始化变量,但也可以在常规赋值表达式中使用(给已声明的变量赋值)。

下面是解构数组值的一段示例代码:

let [x,y]=[1,2]     //相当于let x=1,y=2
[x,y]=[x+1,y+1]
=>[2, 3]            //相当于x=x+1,y=y+1
[x,y]=[y,x]
=>[3, 2]            //交换两个变量的值
[x,y]
=>[3, 2]            //递增和交换后的值

解构赋值让使用返回数组的函数变得异常便捷:

//将[x,y]坐标转换成[r,theta]极坐标
function toPolar(x,y){
    return [Math.sqrt(x*x+y*y),Math.atan2(y,x)];
}

//将极坐标转换成笛卡尔坐标
function toCartesian(r,theta){
    return [r*Math.cos(theta),r*Math.sin(thera)];
}

let [r,theta]=toPolar(1.0,1.0)
let [x,y]=toCartesian(r,theta)             

前面我们看到了,可以在JavaScript的各种for循环中声明变量和常量。同样也可以在这个上下文中使用变量解构赋值。下面这段代码循环遍历了一个对象所有属性的名/值对,并使用解构赋值将两个元素的数组转换为单个变量:

let o={x:1,y:2}
for(const [name, value] of Object.entries(o)) {
      console.log(name, value); 
}

解构赋值左侧变量的个数不一定与右侧数组中的个数相等。左侧多余的变量会被设置为undefined,而右侧多余的值则会被忽略。左侧的变量列表可以包含额外的逗号,以跳过右侧的某些值:

let [x,y]=[1]
[x,y]=[1,2,3]
=>[1, 2, 3]
[,x,,y]=[1,2,3,4]
=>[1, 2, 3, 4]
x
=>2
y
=>4

数组解构的一个强大特性是它并不要求必须是数组!实际上,赋值的右侧可以是任何可迭代对象,任何可以在for/of循环中使用的对象也可以被解构

let [first,...rest]='Hello'
first
=>"H"
rest
=>(4) ["e", "l", "l", "o"]

解构数组在右侧是对象值的情况下也可以执行。此时,赋值的左侧看起来就像一个对象字面量,即一个包含在花括号内的括号分隔的变量名列表:

let transparent={r:0.0,g:0.0,b:0.0,a:1.0}
let {r,g,b}=transparent
r
=>0
g
=>0
b
=>0

下面这个例子展示了如何把Math对象的全局函数复制到变量中,这样可以简化大量三角计算的代码:

//相当于const sin=Math.sin,cos=Math.cos,tan=Math.tan
const {sin,cos,tan}=Math;

注意,代码中Math对象的属性远远不止解构赋值给个别变量的这3个。那些没有提到名字的属性都被忽略了。如果赋值的左侧包含一个不是Math数组的变量名,该变量将被赋值undefined。

在上面每个对象解构的例子中,我们都选择了与要解构对象的属性一致的变量名。这样可以包含语法简单且任意理解,但这并不是必需的。对象解构赋值左侧的每个标识符都可以是一个冒号分隔的标识符对,其中第一个标识符是要解构其值的属性名,第二个标识符是要把值赋给它的变量名:

// 相当于 const cosine = Math.cos, tangent = Math.tan;
const ( cos: cosine, tan; tangent ) = Math;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值