第三章类型,值和变量
计算机程序通过操作值(数值3.14)或文本(如"hello world")来工作,编程语言中这些可以表示操作的值被称为类型
,而一门语言支持的类型集支持的类型集也是这门语言最基本的特征.
程序在需要吧某个值保存下来以便将来使用时,会把这个值赋给(或存入)变量,变量有名字,程序可以通过这些名字来引用
值.
变量的工作方式则是一门编程的另一个基本特征.
本章讲解js中类型,值和变量.首先从概念和一些定义开始.
3.1 概述与定义
js类型可以分为两类:原始类型和对象类型.
js的原始类型
包括数值,文本字符串(也称字符串)和布尔真值(也称布尔值)
.本章将用很大的篇幅专门讲解js中的数值,类型和字符串类型,
js中的特殊值null
和undefined
也是原始值,但是他们不是数值,字符串或布尔值.这两个值通常被认为是各自特殊类型的唯一成员,将在3.5章节进行介绍.
ES6新增了一种特殊的类型symbol(符号),用于对语言进行拓展而不破坏向后兼容性.
在js中,任何不是数值,字符串,布尔值,符号,null和undefined的值都是对象.
对象(也就是对象类型的成员)是属性的集合,其中每个属性都有一个名字和一个值(原始值或其他对象).有一个非常特殊的对象叫全局对象,后面学习.
普通js对象是一个命名值的无序集合.这门语言本身也定义一种特殊的对象,称为数组.数组表示一个数字值的有序集合.js语言包括操作数组的特殊语法,而数组本身也具有区别于普通对象的行为.
除了基本的对象和数组之外,js还定义了其他一些有用的对象类型,
Set
对象表示一组值的结合,
Map
对象表示键与值的映射.
各种"定型数组"(typed array)
类型便于对字节数组和其他二进制数据进行操作.
RegExp
类型表示文本模式,可以实现对字符串的复杂匹配,搜索和替换操作,
Date
类型表示日期和时间,支持基本的日期计算.
Error
及其类型表示js代码运行期间可能发生的错误.
所有这些类型将在11章介绍.
js
与静态语言的更大的差别在于,函数和类不仅仅是语言的语法,他们本身就是可以被js程序操作的值,与其他js非原始值一样,函数和类也是特殊的对象.第八和九章将详细介绍.
在内存管理方面,js解释器会执行自动垃圾回收.这意味着js程序员通常不用关心对象或其他值的析构与释放.
当一个值无法触达时,或者说当程序无法以任何方式引用这个值 时,解释器就知道这个值已经用不到了,会自动释放它占用的内存(js程序员有时候需要留意,不能让某些值在不经意间存续过长时间后任然可以触达,从而导致他们无法被回收)
js支持面向对象的编程风格,粗略的说,这意味着不用定义全局函数去操作不同类型的值.而是由于这些类型本身定义操作值的方法.比如对数组元素排序,不用把数组传给一个sort()函数,而是可以调用数组a的sort方法:
a.sort() // sort(a)的面向对象版本
第九章将介绍如何定义方法.从技术角度来讲,只有js对象才有方法,但是数值,字符串,布尔值和符号表现的似乎他们也有方法,在js中,只有null和undefined是不能调用方法的值.
js的对象类型是可以修改的(mutable),而他的原始类型是不可修改的(immutable).可修改类型的值可以改变,比如js程序可以修改对象属性和数组元素的值.数值,布尔,符号,null和undefined是不可修改的,以数值为例,修改他们是没有意义的.字符串可以看成字符数组,你可能期望他们可以是修改的,但是在js中,字符串也是不可以修改的,虽然可以按照索引访问字符串中的字符,但是js没有提供任何方法区修改已有字符串的字符.可修改值与不可修改值的区别后面介绍.
js可以自由的转换不同类型的值,比如,程序期待一个字符串,而你提供了一个数值,这个数值会自动转换为字符串,而如果你在一个布尔值的地方使用了非布尔值,js也会响应的把它转换为布尔值,这种自动转换的规则3.9章解释
js这种自由的值转换会影响对相等的定义,而相等操作符会根据3.9.1节描述进行类型转化(不过在实践中,相等操作符已经被弃用,取而代之的是不会做类型转换的严格相等操作符===.关于细节后面四章介绍)
常量和变量可以让我们在程序中使用名字来引用值,常量使用const
声明,变量使用let
(或在较老的js中使用var
)声明.
js常量和变量是无类型
的(untyped),声明并不会限定要赋值何种类型的值.变量声明和赋值将在3.10章介绍.
下面详细介绍
3.2 数值
js的主要数值类型Number用于表示整数和近似实数.
js使用IEEE754标准定义的64位浮点格式表示数值,这意味着js可以表示的最大整数是(+/-)1.7976931349823177*10308,最小整数是(±)5*10-324.IEEE754详解(最详细简单有趣味的介绍)_明月几时有666的博客-CSDN博客
js的这种数值格式可以让我们准确表示-9007199254740992(-253)到9007199254740992(253)之间的所有整数(含首尾值).如果你的数值超出了这个范围,那可能会在末尾的数值上损失一些精度.但是要注意,js中的某些操作(如四章介绍的数组索引和位操作)是以32位整数计算的.如果想准确的表示更大的整数可以参考3.2.5章节…
当数值真正出现在js
程序中,就叫做数值字面量(numeric literal).js
支持几种形式的数值字面量,.后面章节会介绍.注意,任何数值字面量前面都可以加上一个减号(-)将数值变成负值.
3.2.1整数字面量
在js
程序中,基数为10的整数可以直接写成数字序列.例如:
0
3
1000000
除了基数为10的整数字面量之外,js
也支持十六进制(基数是16的)值.十六进制字面量以0x
或0X
开头,后跟一个十六进制数字字符串.十六进制数字是数字0到9和字母a(A),到f(或F),a到f表示10到15.下面是十六进制整数字面量的例子:
0xff // 255:(16*16+15)
0xBADCAFE // 195939070
在es6
以及之后的版本中,也可以通过二进制(基数为2)或八进制(基数为8)表示整数,分别使用前缀0b和0o
ob10101 // 21
0o377 // 255 3*64+7*8+7*1
3.2.2浮点字面量
浮点字面量可以包含小数点.他们对实数使用传统句语法.实数值有数值的整数部分,小数点和数值的小数部分组成.
浮点字面量也可以指数计数法表示,即实数值后面可以跟一个字母e(E),跟一个可选的加号或减号,在跟几个整数指数.这种计数法表示的是实数值乘以10的指数次幂.
更简洁的语法形式为:
[digits][.digits][(E|e)[(+|-)]digits]
例如:
3.14
2446.6789
6.02e23 // 6.02*10^23
1.454545E-32 // 1.454545*10^-32
数值字面量中的分隔符
可以用下划线将数值字面量分割为容易看清的数字段:
let billion = 1_00_000
从2020年,数值字面量中这样添加下滑线还没有称为正式的js标准,但是这个特性已经进入标准化流程的后期,而且已经被所有浏览器以及node实现
3.2.3javascript中的算术
js程序使用语言提供的算术操作符来操作数值,包括加法+,减法-,*,/,%.
es2016增加了取幂**.这些操作符以及更多的操作符将在4章介绍
除了上述基本的操作符,js还通过Math对象的属性提供了一组函数和常量,一支持更为复杂的数学计算:
Math.pow(2,53) // 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,yz) // 返回最小的值
Math.random() // 伪随机数x 0<=x<1.0
Math.PI
Math.E // 自然对数的底数
Math.sqrt(3) // 3的平方根 3**0.5
Math.pow(3,1/3) // 3**(1/3) 3的立方根
Math.sin(0) // 三角函数,还有cos,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.log1p(x)//(1+x)的自然对数,精确到非常小的x
Math.expm1(x)// Math.exp(x)-1,Math.log1p()的逆运算
Math.sign(x)//对<,==或>0的参数返回-1,0或1
Math.imul(2,3)// 6 优化的32位整数乘法
Math.clz32(0xf)//28 32为整数前导零的位数
Math.trunc(3.9) // 3 减掉分数部分得到整数
Math.fround(x) // 舍入到最接近的32位浮点数
Math.sinh(x)// 双曲线正弦还有Math.cosh,Math.tanh()
Math.asinh(x)// 双曲线反正弦,还有Math.acosh(),Math.atabh()
js中的算术在遇到上溢,下溢出,或被零整除时不会发生错误.在数值操作的结果超过最大可表示数值时(上溢),结果是一个特殊的无穷值Infinity.类似的当某个赋值的绝对值超过了最大可表示负数的绝对值时候,结果是负无穷-infinity.这两个无穷值的行为跟我们预期的一样:任何数加减乘除无穷值结果还是无穷值(只是符号可能相反)
下溢出发生在数值操作结果比最小可表示数值更加接近0的情况下,此时,js返回0,如果下溢出来自负数,js返回一个被称为"负零"的特殊值.这个值与常规的零几乎无法区分,js程序员极少需要检测他.
被零除在js中不是错误,只会简单的返回无穷或负无穷,不过有一个例外:0除以0是没有意义的值(NaN),这个操作的结果是一个特殊的"非数值"(NaN.Not a Number)此外,无穷除以无穷,负数平方根或者用无法转换为数值的非数值作为算术操作符的操作数,结果都是NaN
js预定义了全局常量Infinity和NaN以对应正无穷和非数值.这些值也可以通过Number对象的属性获取:
Infinity // 因为太大而无法表示的正数
Number.POSITIVE_INFINITY // 同上
1/0 // 同上
Number.MAX_VALUE * 2 // 同上 溢出
-Infinity // 因为太大而无法表示的负数
Number.NEGATIVE_INFINITY // 同上
-1/0 // 同上
-Number.MAX_VALUE*2 //同上
NaN // 非数值
Number.NaN // 同上.写法不同
0/0 // NaN
Infinity/Infinity // 同上
Number.MIN_VALUE/2 // 0 下溢出
-Number.MIN_VALUE/2 // -0
-0
// ES6定义了下列Number属性
Number.parseInt()// 同全局函数parseInt
Number.parsFloat()// 同上
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-1
Number.EPSILON // 2**-52 // 数值与数值之间最下的差
非数值在js中有一个不同寻常的特性:他与任何值比较都不相等,也不等于自己.这意味着不能通过x === NaN来确定某个变量x的值是NaN,相反,此时,必须写成x != x或Number.isNaN(x).这两个表达式当且仅当x与全局常量NaN具有相同值的时候才返回true.
全局函数isNaN()与Number.isNaN()类似.他会在参数是NaN时,或者在参数是无法转换为数值的非数值时返回true.相反的函数Number.isFinite()在参数不是NaN,Infinity或-Infinity时返回true.全局isFinite()函数在参数是有限数字或者可以转换为有限数时返回true.
负零值也有点不同寻常,他与正零值相等(即便使用js的严格相等比较),这意味着除了作为除数使用,几乎无法区分这两个值
let zero = 0;// 常规的零
let negz = -0// 负零
zero = negz // true
1/zero === 1/negz
3.2.4二进制浮点数与舍入错误
实数有无限多个,但是js的浮点格式只能表示其中有限个(切确的说是18437736874454810627个).这意味着在通过js操作实数时,数值表示的经常是实际数值的近似值.
js(以及所有现代编程语言)使用的IEEE-754浮点表示法是一种二进制表示法,这种表示法可以精确的表示1/2,1/8,1/100,等等,.二进制浮点数表示法无法精确表示哪怕0.1这么简单的数
如虽然js数值有足够大的精度,能够非常近似的表示0.1,但是无法精确的表示,可能导致一些问题,
let x = .3-.2// 30美分-20美分
let y .2-.1
x===y //false
x === .1 // false
y === .1 // true
由于舍入错误,.3和.2近似值的差与.2和.1近似值的插入并不相等,这并不是js独有的问题,而是所有使用二进制浮点数的编程语言共同的问题.同样,也要注意代码中x和y的值及其接近,他们也都及其接近正确的值.这个计算得到的值完全能够满足任何需要,切记不要视图比较他们的想等性.
如果浮点近似值对你的程序而言是个问题,可以考虑使用等量整数,
例如:计算与钱数有关的数值时候,可以使用整数形式的美分,而不是零点几美元
3.2.5通过BigInt表示任意精度整数
es2020为js定义了一种新的数值类型,BigInt这种数值类型的值是整数,之所以增加这个类型,只要是为了表示64位整数,这对于兼容很多其他语言和API是必须的.但是BigInt值可能有数千甚至数百万个数值,可以满足对大数的需求(不过,BigInt实现并不适合加密,因为他没有考虑防止防止时序攻击)
BigInt字面量写作一串数字后跟小写字母n.默认情况下,基数是10,但可以通过前缀0b,0O,0X来表示二进制,八进制,十六进制Bigint:
1234n
0b1111n
0o777n
ox90000000n
可以用BigInt()函数把常规js数值或字符串转换为BigInt值:
BigInt(Number,MAX_INTEGER) // .....n
let string = "1"+"0".repeat(100);// 1后跟着100个零
Bigint(string)//10n**100n
BigInt的算术运算与常规js数值的元素运算类似,只不过除法会丢弃余数并且向下(向零)舍入:
100n+200n // 300n
300n-200n//100n
虽然标准的加减乘取余平方,可以用于BigInt,但不能混用BigInt操作数和常规数值操作数.咋一看这个规则有点怪,但是实际上是合理的.如果一种数值类型比另一种更加通用,则比较容易定义混合操作数的计算并返回更加通用的类型…但是上述两种类型都不比另一种更加通用:BigInt可以表示超大数,因此它比常规数值更加通用,但是bigInt只能表示整数,这样看常规js数值类型反而更通用,这个问题无论如何也解决不了,因此js搁置了这个问题,只是简单的不允许在使用算术操作符混用这个两种类型的操作数.
相对来说,比较操作符运混合操作数类型(关于和=的区别,参考3.9.1章)
1<2n // true
2>1n;// true
0==0n// true
为操作符(4.8.1章节介绍)通常用于BigInt操作数,单Math对象的任何函数都不接受BigInt操作数.
3.2.6日期和时间
js为表示和操作与日期及相关的数据而定义了简单的Date类,js的Date是对象,但也有表示形式,即自1970年1月1日起至今的毫秒数,也叫时间戳.
let timestamp = Date.now();// 当前时间的时间戳
let now = new Date();// 当时时间的日期对象
let ms = now.getTime()// 转换为毫秒时间戳
let ISO= now.toISOString();// 转换为标准格式的字符串
Date类及其方法11.4节有详细介绍,但在3.9.3节探讨js类型转换时,我们也会提高Date对象.
3.3文本
js中表示文本的类型是String,即字符串,字符串是16位值的不可修改的有序序列,其中每个值都表示一个Unicode字符.字符串的length属性是它包含的16位值的个数.js的字符串(以及数组)使用基于零的索引,因此第一个16为值的索引是0,第二个值的索引是1,依次类推.空字符串是长度为0的字符串.js没有表示当个字符串元素的专门类型.要表示一个16位值,使用长度为1的字符串即可.
字符.码点.和JavaScript字符串
js使用Unicode字符集的utf-16编码,因此js字符串是无符号16位值的序列.最常用的Unicode字符串(即"基本多语言平台"中的字符)的码点(codepoint)是16位的,可以用字符串中的一个元素来表示.码点超出16位的Unicode字符使用UTF-16规则编码为两个16位值的序列(称为surrogate pair,即"代理对").这意味着长度为2(两个16位值),的js字符串可能表示的只是一个Unicode字符:
let euro = "ä" \u{e4}
let love = "💙"
euro.length // 1 这个字符串是一个16位的元素
love.length // 2 "💙"的utf-16编码是 两个16位的js字符串
JavaScript的字符串操作方法一般操作的是16位数值,而不是字符串.换句话说,他们不会特殊对待代理对,不对字符串进行归一化,甚至不保证字符串是格式正确的utf-16.
但是在es6中,字符串是可迭代的,如果对字符串使用for/of循环或…操作符迭代的字符而不是16位值
3.3.1字符串字面量
要js程序中包含字符串,可以把字符串放到一对匹配的单引号,双引号或反引号(``)中.双引号字符和反引号可以出现在有单引号定界的字符串中,同理由双引号和反引号定界的字符串里也可以包含另外两种引号,下面是几个字符串字面量的例子:
""
'stesing'
'name="myform"'
"wouldn't you prefer O'Reilly's book>?"
"dddd'ggaga'"
`"防辐射服是",刚`
使用反引号定界字符串是es6的特性,允许在字符串字面量中包含(或插入)js表达式.3.3.4章节将介绍这种表达式插值语法.
js最早的版本要求字符串字面量必须写在一行,使用+操作符把单行字符串拼接成长字符串的js代码随处可见,到了es5,我们可以在每行末尾加一个反斜杠()从而把字符串字面量写到多行上.这个反斜杠和它后面的行终结符都不属于字符串字面量.如果需要在单引号或双引号中包含换行符,需要使用字符串序列\n(下一节讲述).es6的反引号语法支持跨行字符串,而行终结符也是字符串字面量的一部分:
// 写在一行但是表示两行的字符串
'two\nlines'
// 写在三行但是表示一行的字符串
"one\long\line"
// 写在两行,实际上也是两行的字符串:
`嘎嘎嘎
刚个`
注意,在使用单引号定界符字符串时,必须注意英文中的缩写和所有格,比如cant’t和O’Reilly中的单引号.因为这里的撇号就是单引号,所以必须使用反斜杠字符()"转义"单引号中出现的所有撇号(下一节讲述转义)
在客户端js编程中,js代码中可能包含HTML代码的字符串,而html代码中可能包含js代码.与js类型HTML使用单引号或双引号来定界字符串.为此,如果要将js和html代码混合,最好js和HTML使用不同的引号.
<button onclick="alert('thank you')">Click me</button>
3.3.2字符串字面量中的转义序列
反斜杠在js字符串中有特殊的作用:他与后面的字符组合在一起,可以在字符串中表示一个无法直接表示的字符,例如:\n是一个表示换行的转义序列
前面还提到一个例子\'
,表示单引号(或撇号)字符.这种转义序列在以单引号定界字符串时,可以用来在字符串中包含撇号.之所以称之为转义序列,就是反斜杠转换了通常意义上单引号的含义.转义之后,他不在表示字符串结束.而是表示撇号:'you\'re right.it can\'t be a quota
表中列出了js中的转义序列以及他们表示的字符,其中3个转义序列是通用可以指定十六进制数字形式的Unicode字符长编码来表示任何字符,例如\xA9表示版权字符,其中包含十六进制数字形式的Unicode编码.类似的,\u表示通过4位十六进制数值指定任意的Unicode字符,如果数字包含在一对花括号中,则是1到6位数字,例如:\u03c0
表示π,\u{1f600}
表示"开口笑"表情.
\0 | NUL字符(\u0000) |
---|---|
\b | 退格符(\u0008) |
\t | 水平制表符(/u0009) |
\n | 换行符(\n000A) |
\v | 垂直制表符(\u000B) |
\f | 进纸符(\n000C) |
\r | 回车符(\u000D) |
\" | 双引号(\u0022) |
\' | 单引号或者撇号(\u0027) |
\\ | 反斜杠(\u005C) |
\xnn | 有2位十六进制数字nn指定的Unicode字符 |
\unnn | 有4位十六进制数字nnnn指定的Unicode字符 |
\u{n} | 有码点n指定的Unicode字符,其中n是介于0和10FFFF之间1到6位十六进制数组(ES6) |
如果字符\
位于任何表示上表之外的字符串前面,则这个反斜杠会被忽略,(当然语言的版本将来可能变化).例如,\#
等同于#.最后,如前所述,es5允许把反斜杠放在换行符前面从而将一个字符串字面量拆成多行.
3.3.3使用字符串
拼接字符串是js的内置特性.如果对数值使用+操作符,那数值会相加.如果对字符串使用+操作符,那字符串将会拼接起来(第二个在第一个后面)
let mes = "hello"+"world"
let greetiong = "welcom to my blog"+" "+name
可以使用标准的全等===和不全等!==操作符比较字符串.只有当两个字符串具有完全相同的16位值的序列时才相等.字符串也可以使用<,<=.>,>=操作符来比较.字符串比较是通过比较16位值完成的(要了解更多关于更可靠的地区相关字符串的比较,可以参考11.7.1节)
要确定一个字符串的长度(即字符串包含的16位值的个数),可以使用字符串的length属性:s.length
处了length属性之外,js还提供了操作字符串的丰富API:
let s = "hello world";
s.substring(1,4);// ell
s.slice(1,4)// ell
s.slice(-3)// rld最三个字符
s.split(",")// ["hello","world"],从定界符处拆开
// 搜索字符串
s.indexOf("l") /// 2
s.indexOf("l",3)//3 位置3后第一个"l"的位置
s.indexOf("zz")// -1
s.lastIndexOf("l")// 10 最后一个字母l的位置
// Es6及之后的版本中的布尔值搜索函数
s.startsWith("hello")// true
s.endsWith("!")// false.不是以他结尾
s.includes("or")// true 包含子串or
// 创建字符串的修改版本
s.replace("llo","ya") // hey world
s.toLowerCase() // 小写
s.toUpperCase() // 大写
s.normalize()// Unicode NFC归一化:ES6新增
s.normalize("NFD")// NFD归一化,还有"NFKC"和"NFKD"
// 访问字符串中的个别(16位值)字符
s.charAt(0)//h
s.charAt(s.length-1)d
s.charCodeAt(0) // 72 指定位置的16位数值
s.codePointAt(0)// 72 ES6.适用于码点大于16位的情形
// ES2017新增的字符串填充函数
"x".padStart(3) // " x"在左侧添加空格,让字符串长度变成3
"x".padEnd(3)// "x "在右侧添加空格,让字符串长度变成3
"x".padStart(3,"*")// "**x" 在左侧添加星号
"x".padEnd(3,"-")// "x--"
// 删除空格函数.trim()是es5就有的,其他是Es2019增加的
" test ".trim() // "test"
" test ".trimStart() // "test "
" test ".trimEnd() // " test"
// 未分类字符串方法
s.concat("!")// hello world! 可以使用+代替
"<>".repeat(5)// "<><><><><>"
记住js中的字符串是不可修改的,像replace()和toUpperCase()这样的方法都返回新的字符串,他们并不会调用他们的字符串.
字符串也可以被当成只读数组,使用方括号而非charAt()方法访问字符串中个别的字符(16位值)
let s = "hello,world"
s[0] // h
s[s.length-1]// d
3.3.4模板字面量
在ES6及之后的版本,字符串字面量可以用反引号来定界:
let s = `hello world`;
不过,这不仅仅是一种新的字符串字面量语法,因为模板字面量可以包含任意js表达式.反引号中字符串字面量最终值的计算,涉及对其中包含的所有表达式求值,将这些表达式的值转化为字符串,然后在把这些字符串与反引号中的字面量组合:
let name = "red"
let greeting =`hello ${name}`
位于${和对应的}之间的内容都被当做js表达式来解释.而位于这对还花括号之外的则是常规字符串字面量.括号内的表达式会被求值,然后转换为字符串并插入模板中,替换美元符号,花括号以及花括号中的所有内容.
模板字面量可以包含任意数量的表达式可以包含任何常规字符串可以出现的转义字符,也可以跨任意多行而无须特殊转义.下面的模板字面量包含4个js表达式,1个Unicode转义序列和至少4个换行符(表达式的值也可能包含换行符):
let errorMessage = `\
\u2718 test failure at ${failename}:${linenumber}:
${exception.message}
Stack trace:
${exception.stack}
`;
这里第一行末尾的反斜杠转义了第一个换行符,因此最终字符串的第一个字符是Unicode字符✘(\u2718)而非换行符
标签化模板字符串
模板字面量有一个强大单不常用的特性:如果在开头的反引号前面有一个函数名(标签),那么模板字面量中的文本和表达式的值将作为参数传给这个函数.“标签化模板字面量”(tagged template literal)的值就是这个函数的返回值.这个特性可以用于先对某些值进制HTML或SQL转义,然后再把他们插入文本.
ES6提供了一个内置的标签函数:String.raw().这个函数返回反引号中未经处理的文本,即不会处理任何反斜杠转义:
`\n`.length // 1"字符串中只包含一个换行符
String.raw`\n`.length // 2 一个反斜字符和一个字母n
注意:即使标签化模板字面量的标签部分是函数,在调用这个函数时候也没有圆括号.在这种非常特别的情况下,反引号字符充当开头和末尾的圆括号.
可以自定义模板标签函数是js的非常强大的特性.这些函数不需要返回字符串,可以被当成构造函数使用,就像为语言本身了一种新的字面量语法一样.14.5节
3.3.5模式匹配
js定义了一种被称为正则表达式(或RegExp)的数据类型,用于描述和匹配文本中的字符串模式.RegExp不是js中的基础类型,但具有类似数值和字符串的字面量语法,因此他们有时候看起来像是基础类型.正则表达式字面量的语法很复杂,他们定义的API也没那么简单.,11.3节将详细讲述这些内容,由于RegExp很强大,且常用与文本处理,因此本节将简单介绍一下.
一对斜杠之间的文本构成正则表达式字面量.这对斜杆中的第二个后面也可以跟一个或多个字母,用于修改模式的含义.例如:
/^HTML/;// 匹配字符开头的字母HTML
/[1-9][0-9]*/;// 匹配非0数字,后面跟着任意数字
/\bjavasript\bb/i;// 匹配JavaScript这个词不区分大小写
RegExp对象定义了一些有用的方法,而字符串也有接受RegExp参数的方法,例如
let test = "testing:1,2,3";// 示例文本
let pattern = /\d+/g;// 配一个或多个数字
pattern.test(test) // true
text.search(pattern) //9 第一个匹配项的位置
text.match(pattern) //["1","2","3"]所以匹配项的数组
text.replace(pattern,"#");//"testing:#,#,#"
text.split(/\D+/)// ["","1","2","3"]// 基于非数值拆开
3.4布尔值
布尔值表示真假
js的任何值都可以转换为布尔值,下面这些值都会被转换为(因此可以被用作)布尔值false:
undefined
null
0
-0
NaN
""// 空字符串
所有其他值,包括所有对象(和数组)都转换为(可以被用作)布尔值true,false和可以转换为它的6个值有时候也被称为假性值(falsy),而所有其他的值则被称为真性值(truthy).在任何js期待布尔值的时候,假性值都可以当做false,而真性值都可以当做true.
例如:假设变量o要么保存一个对象,要么值是null,可以通过一个if语句向下面这样检查o是否为空
if(o!==null)//
使用不全等操作符!==比较o和null,求值结果要么是true要么是false,不过,也可以省略比较,直接依赖null是假性值而对象是真性值这个事实:
if(o)//
布尔值有一个toString()方法,用于将自己转换为字符串"true"或"false".除此之外,布尔值再没有其他有用的方法了.除了这个极其简单的API,还有三种重要的布尔值操作符
&&操作符执行布尔与操作,当且仅当两个操作数都为真时,求值结果才为真.任何一个操作数为假的,结果都为假.||操作符执行布尔或操作,任意一个操作数为真,求值结果就是真.一元的!操作符执行布尔非操作,如果假则结果为true,如果为真则结果false.4.10节将全面详细介绍这个操作符
3.5null和undefined
null是一个语言关键字,求值为一个特殊值.,通过用于表示某个值不存在,对null使用typeof操作符返回字符串"object".表明将null看成一种特殊对象,表示"没有对象".但是在实践中,null通常被当作它自己类型的唯一成员,可以用来表示数值,字符串以及对象"没有值".多数编程语言都有一个与js的null等价的值,比如NULL,nil或None;
js中的undefined也表示值不存在,但是undefined表示一种跟深层次的不存在,具体来说,变量的值未初始化的时候就是undefined.在查询不存在的对象属性或数组元素时也会得到undefined.此外.没有明确返回值的函数返回的值是undefined.没有传值的函数参数的值也是undefined.undefined是一个预定义的全局常量(而非想null那样的语言关键字,不过在实践中这个区别并不重要),这个常量的初始化值就是undefined.对undefined应用typeof操作符会返回"undefined".表示这个值是该特殊类型的唯一成员.
抛开细微的差别,null和undefined都可以表示某个值不存在,经常被混用.相等操作符认为他们相等(要区分他们,必须使用全等操作符=).因为他们俩都是假性值,在需要布尔值的情况下,他们都可以当做false使用.null和undefined都没有属性或方法.实时上,使用.或[]访问这两个值的属性或方法会导致TypeError
我们认为可以用undefined表示一种系统级别,意料之外的或类似错误的没有值,可以用null表示程序级别,正常或意料之中的没有值.实际编码中,我们会尽量避免使用null和undefined,如果需要给某个变量或属性赋这样一个值,或者需要想函数传入或从函数总返回这样一个值,我通常使用null.有些人避免是用null而用undefined.
3.6符号
符号(Symbol)是ES6新增的一种原始类型,用作非字符串的属性名.要理解符号,需要了解js的基础类型Object是一个属性的无序集合,其中每个属性都有一个名字和一个值.属性名通常是(在ES6之前一直必须是)字符串.但在ES6和之后的版本中,符号也可以作为属性名:
let strname = "string name"
let symname = Symbol("propname")// 可以用作属性名的符号
typeof strname // string
typeof symname // symbol
let o = {}
o[strname] = 1 //
o[symname] = 2 // 使用符号定义一个属性
o[strname] // 1访问字符串名字的属性
o[symname]// 2访问符号名字的属性
Symbol类型永远没有字面量语法.要获取一个Symbo值,需要调用Symbol()函数**.这个函数永远不会返回相同的值**,即使每次传入的的参数都一样.这意味着可以将调用Symbol()取得到符号值安全的用于为对象添加新属性,**而无须担心可能重写已有的同名属性.**类似的,如果定义了符号属性但没有共享相关符号,也可以确信程序中的其他代码不会意外重写这个属性.
实践中,符号通常用作一种语言扩展机制.ES6新增了for/of循环(5.4.4节)和可迭代对象(12章),为此就需要定义一种标准机制让类可以实现,从而把自身变得可迭代.但选择任何特定的字符串作为这个迭代器方法的名字都有可能被破坏已有的代码.为此,符号名应运而生.正如12章会介绍,Symbol.Iterator是一个符号值,可用作一个方法名,让对象变得可以迭代.
Symbol()函数可选地接受一个字符串参数,返回唯一的符号值.如果提供了字符串参数,那么调用返回符号值的toString()方法得到的结果中会包含该字符串,不过要注意**,以相同的字符串调用两次Symbol()会产生两个完全不同的符号值.**
let s = Symbol("sym_x")
let b = Symbol("sym_x")
s.toString()// "Symbol(sym_x)"
符号值唯一有趣的方法就是toString().不过还应该知道两个与符号相关的函数.在使用符号时,我们有时希望他们对代码是私有的,从而可以确保你的代码的属性永远不会与其他代码的属性发生冲突.但是有时我们也希望定义一些可以与其他代码共享的符号值.例如.我们定义了某种扩展,希望别人的代码也可以使用,就像前面提到的Symbol.Iterator机制一样.
为了定义一些可以与其他代码共享的符号值,js定义了一个全局符号注册表.Symbol.for()函数接受一个字符串参数,返回一个与该字符串关联的符号值.如果没有符号与该字符串关联,则会创建并返回一个新符号,否则,就会返回已有的符号.换句话说,Symbol.for()和Symbol()完全不同:Symbol()永远不会返回相同的值,而在以相同的字符串调用时Symbol.for()始终返回相同的值.传给Symbol.for()的字符串会出现在toString()(返回符号值)的输出中.而且,这个字符串也可以通过将返回的符号传给Symbol.keyFor()来得到:
let s = Symbol.for("shared");
let b = Symbol.for("shared")
s === b // true
s.toString() // "Symbol(shared)"
Symbol.keyFor(s) // "shared"
3.7全局对象
前几节,解释了js的原始类型和值.本书会各用一章节来介绍对象类型(对象,数组和函数),但现在必须介绍一个非常重要的对象值:全局对象.全局对象的属性是全局性定义的标识符,可以在js程序的任何地方使用.js解释器启动后(或每次浏览器加载新页面时),都会创建一个新的全局对象并为其添加一组初始的属性,定义了:
- undefined,Infinity和NaN这样的全局常量;
- isNaN(),parseInt()(3.9.2节)和eval()(4.12节)这样的全局函数;
- Date(),RegExp(),String(),Object()和Array()(见3.9.2节)这样的构造函数;
- Math和JSON(见6.8章)这样的全局对象
全局对象的初始化属性并不是保留字,但他们应该都被当成保留字.本章已经介绍过其中的一些全局属性了,剩下的更多属性将在本书其他章节介绍.
在Node中,全局对象有一个名为global的属性,其值为全局对象本身,因此Node程序始终可以通过global来引用全局对象.
在浏览器中,Window对象浏览器窗口中的所有js代码而言,充当了全局对象的角色.这个全局的Window对象有一个自引用的window属性,可以引用全局对象.Window对象定义了核心全局属性,也定义了其他一些特定于浏览器和客户端js的全局值.15.13节介绍的工作线程有自己不同的全局对象(不是Window).工作线程中的代码可以通过self来引用他们的全局对象.
ES2020最终定义了globalThis,作为在任何上下文引用全局对象的标准方式,所有现代浏览器和Node都实现了这个特性.
3.8不可修改的原始值与可修改的对象引用
js中的原始值(undefined,null,布尔值,数值和字符串)与对象(包括数组和函数)有一个本质的区别.原始值是不可修改的,即没有办法改变原始值.对于数值和布尔值,这一点很好理解:修改一个数值的值没有什么用.可是,对于字符串,这一点就不好理解了.因为字符串类似字符数组,我们或许认为可以修改某个索引位的字符.事实上,js不允许这么做.所有看起来返回一个修改后字符串的字符串方法,实际上返回的都是一个新字符串.例如:
let s = "hello"
s.toUpperCase() // 返回HELLO
s // hello
原始值是按值比较的,即两个值只有在他们的值相同的时候才是相同的.对于数值,布尔值,null和undefined来说,这句话有点绕,其实很好理解,例如,在比较两个不同的字符串时,当且仅当这两个字符串长度相同,并且每个索引的字符也相同的时,js才认为他们相等.
对象不同于原始值,对象是可以修改的,即他们的值可以改变
let = {x:1}
o.x = 2
o.y = 3
let a = [1,2,3]
a[0] = 1
a[0]
对象不是按值比较的,两个不同的对象即使拥有完全相同的属性和值,他们也不相等.相同,两个不同的数组,即使每个元素都相同,顺序也相同,他们也不相等.
let o ={x:1};,p = {x:1}
o === p // false
let a = [],b=[]
a === b // false
对象有时候被称作引用类型(reference type),以去区别于js的原始类型.基于这一术语,对象值就是引用,对象是按引用比较的.换句话说,两个对象值当且仅当他们引用同一个底层对象时,才是相等的.
let a = []
let b = a
b[0] = 1
a === b // true
从上面的代码可以看出,把对象(或数组)赋值给一个变量,其实是在赋值引用(内存地址的引用) ,并不会创建对象的新副本.如果想创建对象或数组的副本,必须显示赋值对象的属性或数组的元素,下面使用for循环(5.4.3节)演示了这个过程:
let a = ["a","b"]
let b = [];
for(let i = 0;i<a.length;i++){
b[i] = a[i]
}
let c = Array.from(b);// ES6中可以使用Array.from()复制数组
类似的,如果要**比较两个不同的对象或数组,**必须比较他们的属性或元素,以下代码定义了一个比较两个数组的函数:
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类型转换
js对待自己所需值的类型非常灵活.这一点我们在介绍布尔值已经看到了.js需要一个布尔值,而你可能提供了其他类型的值,js会根据需要转化这个值.有些值(真性值)转换为true,有些值(假性值)转换为false.对其他类型也是如此:如果js想要字符串,他就会把你提供的任何值都转换为字符串如果想要数值,他也会尝试把你给的值转化为一个数值(如果无法进行有意义的转化就转换为NaN)
10 + "object" // 10object
"7" * "4" //20 两个字符串都转换为数值
let n = 1-"x" // NaN
n+"objects" // NaN objects
下表总结了js类型之间的转化关系.表中加粗的内容是可能让人觉得意外的转换目标.空格单元格表示没有转换必要,因此什么操作也不会发生.
值 | 转换为字符串 | 转化为数值 | 转化为布尔值 |
---|---|---|---|
undefined | "undefined" | **NaN** | false |
null | "null" | **0** | false |
true | "true" | **1** | |
false | "false" | **0** | |
""(空字符串) | **0** | **false** | |
"1.2"(非空,数值) | 1.2 | true | |
"one"(非空,非数值) | NaN | true | |
0 | "0" | false | |
-0 | "0" | false | |
1(有限,非零) | "1" | true | |
Infinity | "Infinity" | true | |
-Infinity | "-Infinity" | true | |
NaN | "NaN" | **false** | |
{}(任何对象) | 3.9.3章节 | 参见3.9.3 | true |
[]空数组 | "" | 0**** | **true** |
[9](一个数值元素) | "9" | **9** | true |
['a'](任何其他数组) | 使用join()方法 | NaN | true |
Function(){}(任何函数) | 3.9.3章节 | NaN | true |
表中展示的原始值到原始值的转换相对容易理解.转换为布尔值的情况在3.4讨论过了.转换为字符串的情况对所有原始值都是有明确定义的.转换为数值就稍微有点微妙了,可以解析数值的字符串会装换为对应的数值.字符串开头和结尾可以有空格,但开头或末尾任何属于数值字面量的非空格字符,都会导致字符串到数值的转换NaN.有些数值转换的结果可能会让人不可思议,比如true转换为1,而false和空字符串都转换为0
Number("123a") // NaN
对象到原始值的转换要复杂一些,3.9.3节介绍
3.9.1转换与相等
js有两个操作符用于测试两个值是否相等,一个严格相等操作符===,如果两个值不是同一种类型,那么这个操作符就不会判定他们相等.但由于js在类型转换上很灵活,所有他也定义了==操作符.这个操作符判定相等的标准相当灵活.比如说,下列所有的比较的结果都是true:
null == undefined //true
"0" == 0
0 == false
"0" === false
4.9.1节解释了为判定两个值是否相等,==操作符都做了那些转化
但要记住,一个值可以转换为另一个值并不意味着 两个值是相等的,比如,如果undefined用在理论期待布尔值的地方,那么他会被转化为false.但并不意味着undefined == false。js操作符和语句期待不同类型的值,因此会执行以这些类型为目标类型的转换. if语句将undefined转换为false,但==操作符永远不会将其操作符转换为布尔值.
3.9.2显示转换
尽管js会自动执行很多类型的转换,但有时候我们也需要进行显示转换,或者有意进行显示转换以保证代码清晰.
执行显示类型转换的最简单方法就是使用Boolean(),Number()和String()函数:
Number("3") // 3
String(false) // "false"
Boolean([]) // true
除了null和undefined之外的所有值都有toString()方法,这个方法返回的结果通过与String()函数返回的结果相同.
顺便说一下,Boolean(),Number()和String()函数也可以被当做构造函数通过new关键字来使用.如果你这样使用他们,那么会得到一个与原始布尔值,数值和字符串值类似的"包装"对象.这种包装对象是早期js的历史遗存,已经没有必要在使用他们了
某些js操作符会执行隐式类型转换,有时候,可以利用这一点完成类型转换.如果+操作符有一个操作数是字符串,那么它会把另一个操作数转换成字符串.一元操作符+会吧自己的操作数转为数值.而一元操作符!会把自己的操作数转换为布尔值,然后在取反.这些事实导致我们常常会在某些代码中看到如下类型转换的用法:
x+"" // String(x)
+x // Number(x)
x-0 // Number(x)
!!x // Boolean(x)// 注意两次取反
格式化和解析数值是计算机常见的错误来源,而js为数值到字符串和字符串到数值的转换提供了函数和方法,能够对转换进行更精确的控制.
Number类定义的toString()方法接受一个可选的参数,用于指定一个基数或底数.如果不指定这个参数,转换的默认基数为10.当然也可以按照其他基数(2到36)来转换数值.例如:
let n = 17;
let binary = "0b"+n.toString(2)// binary == "0b10001"
let octal = "0b"+n.toString(8)// "0o21"
let hex = "0x"+n.toString(16) // "0x11"
在使用金融或科学计数时,可能需要控制转换后得到额字符串的小数位的个数或有效数字的个数,或者需要控制是否采用指数计数法.Number类为这些数值到字符串的转换定义3中方法,toFixed()把数值转换为字符串时可以指定小数点后面的位数.这个方法不使用指数计数法.toExponential()使用指数计数法将数值转换为字符串,结果是小数点前1位小数点后为指定位数(意味着有效数字个数比你指定的值多一位).toPrecision()按照指定的有效数字个数将数值转换为字符串.如果有效数字个数不足以显示数值的整数部分,他会使用指数计数法,注意:这三种方法必要时都会舍去末尾的数字或者补零.
let n = 123456.789
n.toFixed(2)//'123456.79'
n.toExponential(1)//'1.2e+5'
n.toExponential(3)//'1.235e+5'
n.toPrecision(4)//'1.235e+5'
n.toPrecision(10)//'123456.7890'
n.toPrecision(15)//'123456.789000000'
除了这里介绍数值格式化,Intl.NumberFormat类定义了一个更通用,更国际化的数值格式化方法.详见11.7.1节
如果把字符串传给Number()转换函数,它会尝试把字符串当成整数或浮点数字面量来解析.这个函数只能处理基础为10的整数**,不允许末尾出现**无关字符,parseInt()和parseFloat()函数(都是全局函数,不是任何类方法)则更灵活一点.parseInt()只解析整数,而parseFloat()即解析整数也解析浮点数.如果字符串以0X或0x开头,parseInt()会将其解析为十六进制数值.parseInt()和parseFloat()都会跳过开头的空格,尽量多的解析数字字符,忽略后面的无关字符.如果第一个非空格字符不是有效的数值字面量,他们会返回NaN:
parseInt("3 gga") // 3
parseFlat(" 3.14aa")//3.14
parseInt("3.14") // 3
parseInt("0xFF") // 255.
parseFloat(".1")// 0.1
parseInt("0.1") // 0
parseInt(".1")//NaN整数不能以.开头
parseFloat("$12.100")//NaN数值不能以$开头
parseInt()接受可选的第二个参数,用于指定要解析数值的底(基)数,合法的值是2到36
parseInt("11",2)// 3:(1*2+1)
parseInt("ff",16)// 255:(1*2+1)
parseInt("077",8)// 63
3.9.3对象到原始值转换
前几节解释了如果将一种类型的值显示转换为另一种类型,也解释了js中原始类型值之间的隐式转化.本节介绍js进对象转换为原始值时遵循的复杂规则,这些规则冗长,晦涩,建议先看3.10章节.
js对象到原始值转换的复杂性,主要原因在于某些对象类型不止有一种原始值的表示.比如,Date对象可以用字符串表示,也可以用时间戳表示,js规范定义了对象到原始值转换的三种基本算法:
- 偏字符串:该算法返回原始值,而且只要可能就返回字符串
- 偏数值:该算法返回原始值,而且只要可能就返回数值
- 无偏好:该算法不倾向于任何原始值类型,而是有类定义自己的转化规则,js内置类型除了Date类都实现了偏数值算法.Date类实现了偏字符串算法
这些对象到原始值算法的实现将在本节最后再解释.这里我们需要想了解一下这些算法在js中过得用法
对象转换为布尔值
对象到布尔值的转换很简单:所有对象都转换为true.注意,这个转换不需要使用前面介绍的对象到原始值的转换算法,而是直接适用于所有对象.包括空数组,甚至包括new Boolean(false)这样的包装类.
对象转换为字符串
在将对象装换为字符串时,js首先使用偏字符串算法将他转换为一个原始值,然后将得到的原始值在转换为字符串,如有必要按上面转换表中的规则执行.
这种转换会发生在把对象传给一个接受字符串参数的内置函数时,比如将String()作为转换函数,或者将对象插入模板字面量中(3.3.4节)时就会发生这种转换.
对象转换为数值
当需要吧对象转换为数值的时候,js首先使用偏数值算法将他转换为一个原始值,然后将得到的原始值再转换为数值,如有必要按转换表中规则执行.
接受数值参数的内置js函数和方法都以这种方式将对象转换为数值,而除数值操作符之外的多数(下面情况),js操作符也按照这种方式把对象转换为数值.
操作符转换特例
操作符将在第四章详细介绍.在此,我们只介绍那些不遵循上述基本的对象到字符串或对象到数值转换规则的操作符特例.
首先,js中的+操作符执行数值加法和字符串拼接.如果一个操作数是对象,那js会使用无偏好算法将对象转换为原始值.如果两个操作数据都是原始值.则先会检查他们的类型,如果有一个参数是字符串,则把另一个原始值也转换为字符串并拼接两个字符串,否则,把两个参数都转换为数值并把他们相加.
其次 ==和!=操作符以允许类型转换的宽松方式执行相等和不相等测试.如果一个操作数是对象,另一个操作数是原始值,则这两个操作符会使用无偏好算法将对象转换为原始值,然后再比较两个原始值.
最后,关系操作符<,<=,>和>=比较操作数的顺序,即可以比较数值,也可以比较字符串.如果操作数总有一个对象,则会使用偏数值算法将对象转换为原始值.不过要注意,与对象到数值转不同,这个偏数值算法返回的原始值不会再会转换为数值
注意,Date对象的数值表示是可以使用<和>进行有意义的比较,但他的字符串表示则不行,对于Date对象,无偏好算法会将其转换为字符串,而js中这两个操作符会使用偏数值算法的事实以为这我们可以比较两个Date对象的顺序.
toString()和valueOf()方法
所有对象都会继承两个在对象到原始值转换时使用的方法,在接下来解释偏字符串,偏数值和无偏好转换算法前,我们必须先了解这两个方法.
第一个方法toString()的任务是返回对象的字符串表示.默认情况下,toString()方法不会返回特别的值(14.4.3节会用到这个默认值)
({x:1,y:2}).toString() // "[object object]"
很多类都定义了自己特有的toString()版本,比如,
Array类的toString()方法会将数组的每个元素转换为字符串,然后再使用逗号作为分隔符将他们拼接起来,
Funtion类的toString()方法会将用户定义的函数转换成js源代码的字符串.
Date类定义的toString()方法返回一个人类友好(且js可解析)的日期和时间字符串.
RegExp类定义的toString()方法将RegExp对象转换为一个看起来像RegExp字母量的字符串:
[1,2,3].toString()// 1,2,3
(function(x){f(x);}).toString()//"function(x){f(x);}"
/\d+/g.toString() // "/\\d+/g"
let d = new Date(2020,0,1)
d.toString()// 'Wed Jan 01 2020 00:00:00 GMT+0800 (中国标准时间)'
另一个对象转换函数叫valueOf().这个方法的任务并没有太明确的定义,大体上可以认为他是把对象转换为代表对象的原始值(如果存在这样一个原始值).对象是复合值,且多数对象不能真正通过一个原始值表示,
因此valueoOf()方法默认情况下只返回对象本身,而非返回原始值,
String,Number和Boolean这样的包装类定义的valueOf()方法也只是简单地返回被包装的原始值.
Array,Function和RegExp简单的继承默认方法.在这些类型的实例上调用valueOf()会返回对象本身.
Date对象定义的valueOf()方法返回日期内部表示形式:自1970年1月1日至今的毫秒数:
let d = new Date(2010,0,1);// January 1 2010,(Pacific time)
d.valueOf() // 1262333280000
对象到原始值转换算法
解释完toString()和valueOf()方法后,现在我们可以大致的解释前面三个对象到原始值转换算法的的实现
- 偏字符串算法首先尝试toString()方法.如果这个方法有定义且返回原始值,则js使用该原始值(即使这个值不是字符串).如果toString()不存在,或者存在但返回对象,则js尝试valueOf()方法,如果这个方法存在且返回原始值,则js使用改值,否则转换失败,TypeError
- 偏数值算法与偏字符串算法类似,只不过先尝试valueOf()方法,再尝试toString()方法
- 无偏好算法取决于被转换对象的类,如果是一个Date对象,则js使用偏字符串算法,如果是其他类则js使用偏数值算法.
以上规则适用于所有内置js类型,也是我们所有定义类的默认规则,14.4.7节解释了如何在自定义类中定义自己的对象到原始值转换算法.
在结束类型转换的讨论之前,有必要补充说明一下.偏数值转化规则的细节,可以解释为为什么空数组会转换为数值0,而单元素数组也可以转换为数值:
Number([]) // 0
NUmber([99]) // 99
对象到数值的的转换首先需要使用偏数值算法把对象转换为一个原始值,然后再把得到的原始值转换为数值.
偏数值算法,先尝试valueOf()方法,将toString()作为备用,
[1,20].valueOf() [1,2]
Array类继承了默认的valueOf()方法,该方法不返回原始值.因此在尝试将数组转换为数值时,最终会调用toString()方法.空数组转换为空字符串,而空字符串转换为数值0.只有一个元素的数组转换为该元素对应的字符串,如果数组只包含一个数值,则该数值转换为字符串,在转换会数值.
3.10变量声明与赋值
计算机编程中最基本的一个技术就是使用名称(或标识符)表示值.绑定名字和值为我们提供了一种引用值和在程序总使用值的方式.对于绑定名字和值,我通常会说把值赋给变量.术语"变量"意味着可以为其赋值新值,也就是说变量关联的值在程序运行时可能变化.如果把一个值永久的赋给一个名字,那么可以称该名字为常量而不是变量.
在js中使用变量或常量前,必须先声明他,在es6之后的版本,这是通过let和const关键字来完成的,接下来我们将会介绍.在es6之前,变量是通过var声明的,这个关键字更加特殊一些.
3.10.1使用let和const声明
在现代js(es6之后2015年)中,变量是通过let关键字声明的:
let i;
let sum;
也可以使用一条let语句声明多个变量
let i,sum;
声明变量的同时(如果可能)也为其赋予一个初始值是个好习惯:
let message = "hello";
let i = 0,j=0,k = 0;
let x = 2,y =x*x;// 初始化语句可以使用前面声明的变量
如果在let语句中不为变量指定初始值,变量也会被声明,但在被赋值之前他的值是undefined
要声明常量而非变量,则要使用const而非let.const与let类似,区别在于const必须在声明时初始化常量;.
const H0 = 74;// 哈勃常数
const C = 299792.458;// 真空中的光速
const AU = 1.496E8;// 地球与太阳间的平均距离
顾名思义,常量的值是不能改变的,尝试给常量重新赋值会抛出TypeError.
声明常量的一个常见(但并非普遍性的)的约定是全部字母大写.如H0或HTTP_NOT_FOUND,以区别变量
何时使用const
关于使用const关键字有两种调论,一种论调是只在值基本不会改变的情况下使用const,比如物理常数,程序版号,或用于标识文件类型的字节序.
另一种论调是认为程序总很多所谓的变量实际上在程序运行时并没有改变,为此,应该全部使用const声明,然后如果发现确实需要允许值改变,在改成let.这样有助于避免因意外修改变量而导致出现的bug
在第一种情况下,我们只对那些必须不变的值使用const.另一种情况下,对任何不会变化的值使用const.作者推荐前一种思路
在第五章中,我们会学习js中的for,for/in和for/of循环语句,其中每种循环都包含一个循环变量,在循环的每次迭代中都会取得一个新值,js允许在循环语法中声明这个循环变量,这也是let的另一个常见的使用场景.
for(let i = 0;len = data.length;i<len;i++)console.log(data[i])
虽然看起来有点怪,但也可以使用const声明for/in和for/of中的这些循环"变量",只要保证在循环体内不给它重新赋值即可,此时,const声明的只是一次循环迭代期间的常量值:
for(const datum of data)console.log(datum)
变量与常量作用域
变量的作用域(scope)是程序源代码中的一个区域,在这个区域内变量有定义.通过let和const声明的变量和常量具有块作用域.这意味着他们只在let和const语句所在的代码块中有定义.js类和函数的函数体是代码块,if/else语句的语句体,while和for循环的循环体都是代码块.粗略的讲,如果变量或常量声明在一对花括号中,那这对花括号就限定了该变量或常量有定义的代码区域(当然,声明变量或常量的let或const语句之间的代码行中引用这些变量或常量也是不合法的).作为for,for/in,for/of循环的一部分的变量和常量,以循环体作为他们的作用域,即使他们实际上位于花括号外部.
如果声明位于顶级,在任何代码块外部,则称其为全局变量或常量,具有全局作用域.在node和客户端js模块中(参见10章),全局变量的作用域是定义他们的文件.但在传统客户端js中,全局变量的作用域是定义他们的HTML文档,换句话说,如果有
重复声明
在同一个作用域中使用多个let或const声明同一个名字是语法错误.在嵌套作用域中声明同名变量是合法的(尽管实践中最好不要这么做):
const x = 1;// 声明x为全局常量
if(x===1){
let x = 2;
console.log(x)// 2
}
console.log(x)//1
let x = 3// 错误重复声明
声明与类型
如果你使用过静态类型语言(C或JAVA).可能认为变量声明的主要目的是为变量指定可以赋予它的值的类型.但我们也看到了,js的变量声明与值的类型无关.js变量可以保存任何类型的值.例如:在js中,给一个变量赋一个数值,然后再给他赋一个字符串是合法的:
let i= 10;
i = "len"
3.10.2使用var的变量声明
在es6之前的js中,声明变量的唯一方式是使用var关键字,无法声明常量.var的语法和let语法相同:
var x;
var data = [],count = data.length
for(var i=0;i<count;i++)console.log(data[i])
虽然var和let有着相同的语法,但他们也有着重要的区别.
- 使用var声明的变量不具有块作用域,这种变量的作用域仅限于包含函数的函数体,无论他们在函数中嵌套层次有多深.
- 如果在函数体外部使用var,则会声明一个全局变量.但通过var声明的全局变量与通过let声明的全局变量有一个重要区别,通过var声明的全局变量被实现为全局对象(3.7节)的属性.全局对象可以通过globalThis引用.因此如果你在函数外部写了var x =2;就相当于写了globalThis.x = 2;不过要注意,这么类比并不完全恰当.因为通过全局var创建的这个属性不能使用delete操作符(4.13.4节)删除.通过let和const声明的全局变量和常量不是全局对象的属性.
- 与通过let声明的变量不同,使用var多次声明同名变量合法.而且有var变量具有函数作用域而不是块级作用域,这种重新声明实际上是很常见的.变量i经常用于保存整数值.特别是经常用作for循环的索引变量.在有多个for循环的函数中,每个循环通常都以for(var i=0;…开头**.因为var并不会把这些变量的作用域限定在循环体内**,每次循环都会(无害的)重新声明和重新初始化同一个变量.
- var声明的一个最不寻常的特性是作用域提升(hoisting).在使用var声明变量时,该声明会被提升,到包含函数的顶部,.因此对使用var声明的变量,可以在包含函数内部的任何地方使用而不会报错.如果初始化代码尚未运行,则变量的值可能是undefined,但在初始化之间是可以使用变量而不报错的(这会成为bug的来源,也是let要纠正的一个最重要的错误特性,如果使用let声明了一个变量,但试图在let运行前使用该变量则会导致错误而不是得到undefined的值)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="div">1</div>
<div class="div">2</div>
<div class="div">3</div>
<div class="div">4</div>
<div class="div">5</div>
<script>
let div = document.querySelectorAll(".div");
for (var i = 0; i < div.length; i++) {
div[i].addEventListener("click",(e)=>{
console.log(i);// 点击的时候 5 5 5 5 5
})
}
</script>
</body>
</html>
使用未声明的变量
在严格模式下(5.6.3节),如果试图使用未声明的变量,那代码运行时会触发引用错误.
但在严格模式外部,如果将一个值赋给尚未使用let,const或var声明的名字,则会创建一个新全局变量.而且无论这个赋值语句在函数或代码块中被嵌套了多少次,都会创建一个全局变量.而且,无论这个赋值语句在函数或代码中被嵌套多少次,都会创建一个全局变量.这肯定不是我们想要的,非常容易招致缺陷,这也是推荐严格模式一个最好的理由.
以这种意外方式创建的全局变量使用var声明的全局变量,都定义全局对象的属性,但与通过恰当的var声明的属性不同,这些属性可以通过delete操作删除.
a = 100
3.10.3解构赋值
ES6实现了一种复合声明与赋值语法,叫做解构赋值(destruring assignment).在解构赋值中,等号右手端的值是数组或对象("结构化"的值),左手端通过模拟数组或对象字面量语法指定一个或多个变量.在解构赋值发生时候,或从右侧的值中提取(结构)出一个或多个值,并保存到左侧列出的变量中.解构赋值可能最常用于在const,let或var声明语句中初始化变量,但也可以在常规赋值表达式中使用(给已声明的变量赋值),而且正如8.3.5节将会介绍的,解构也可以在定义函数参数时使用.
下面是结构数组值的一段示例代码:
let [x,y] = [1,2];
[x,y] = [x+1,y+1];
[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(theta)]
}
let [r,theta] = toPolar(1.0,1.0);// r == Math.sqrt(2);theta = Math.PI/4
let [x,y] = toCartesian(r,theta);[x,y] == [1.0,1.0]
前面我们看到了,可以在js的各种for循环中声明变量和常量.同样也可以这个上下文中使用变量解构赋值.
let o = {x:1,y:2}
for(const [name,value] of Object.entries(o)){
console.log(name,value);// 打印x1和y2
}
解构赋值左侧变量的个数不一定与右侧数组中元素的个数相同,左侧多余的变量会被设置为undefined,而右侧多余的值会被忽略.左侧的变量列表可以包含额外的逗号,已跳过右侧的有些值:
let [x,y] = [1] // x == 1 y == undefined
[x,y] = [1,2,3]; //
[,x,y] = [1,2,3,4];// x ==2;y==4
在解构赋值时,如果你想把所有未使用或剩余的值收集到一个变量中,可以在左侧最后一个变量名前面加上3个点(…):
let [x,...y] = [1,2,3,4];// y == [2,3,4]
8.3.2节中还会看到以这种方式使用3个点,但那时是用于把函数所有剩余参数收集到一个数组中.
结构赋值可用于嵌套数组.此时.赋值的左侧看起来也应该像一个嵌套的数组字面量:
let [a,[b,c]] = [1,[2,2,5],3] // a==1;b==2;c==2.5
数组结构的一个强大特性是它并不要求必须数组!实际上,赋值的右侧可以是任何可迭代的对象(参见12章),任何可以在for/of循环(5.4.4节)中使用的对象也可以被解构:
let transparent = {r:0.0,g:0.0,b:0.0,a:1.0}// 一个rgba颜色对象
let {r,g,b} = transparent;// r == 0.0;g==0.0;b==0.0
下面这个例子展示了如果吧Math对象的全局函数赋值到变量中,这样可以简化需要大量三角计算的代码:
const {sin,cos,tan} = Math;
注意,代码中Math对象的属性远远不止解构赋值给个别变量的这3个,那些没有提到名字的属性都被忽略了.如果赋值的左侧包含一个不是Math属性的变量名,该变量将被赋值undefined.
在上面每个对象解构的例子中,我们都选择了与要解构对象的属性一致的变量名.这样可以保持语法简单且容易理解,但是这并不是必须的.对象解构赋值左侧的每个标识符都可以是一个冒号分割的标识符对,其中第一个标识符是要解构其值的属性名,.第二个标识符是要把值赋给他的变量名:
const {cos:cosine,tan:tangent} = Math;
我发现如果变量名和属性名不一样,对象结构语法会变得过于复杂,反而导致用处不大.所有这种情况下通常不会使用简写形式,.如果你选择使用,要记住属性名一定是在冒号左侧,无论是在对象字面量中,还是在对象解构赋值的左侧.
在使用嵌套对象,对象的数组,或数组的对象时,解构赋值甚至会变得复杂,但是都是合法的:
let points = [{x:1,y:2},{x:3,y:4}];//两个坐标点对象的数组
let [{x:x1,y:y1},{x:x2,y:y2}] = points;//结构到四个变量中
(x1 === 1&&y1===2&&x2===3&&y2===4)//true
类似这样的复杂解构赋值语法既难写又难理解,甚至还不如使用类型let x1 = points.p1[0];这样的传统代码更简单易懂/
理解复杂解构
如果你发现自己维护的代码中使用了复杂的解构赋值,可以通过一些规律来应对这种复杂性.首先,想象一下常规(单值)赋值.在赋值之后,你可以从赋值的左侧取得变量名,然后在自己的代码中作为表达式使用,这个表达式会被求值为赋给他的值.解构赋值其实也是一样的.解构赋值的做出看起来像是一个数组字面量或对象字面量(6.2.1和6.10章节).在赋值之后,左侧也类似于有一个有效的数组字面量或对象字面量.为验证你写的解构赋值是正确的,可以尝试在另一个赋值表达式的右侧使用解构赋值的左侧:
// 先定义一个数据结构并进行复杂的结构赋值
let points = [{x:1,y:2},{x:3,y:4}];
let [{x:x1,y:y1},{x:x2,y:y2}] = points;
// 通过翻转赋值的两端来验证你的解构语法
let points2 = [{x:x1,y:y1},{x:x2,y:y2}] // points2 ==points
小结
本章要点
- 如何在js种编写及其操作数值和文本
- 使用原始类型:布尔值,Symbol,null和undefined
- 不可修改的原始类型和可修改的引用类型之间的区别
- 隐式将值从一种类型转换到另一种类型
- 如何声明和初始化常量和变量(包括结构赋值),以及作用域