类型、值和变量

258 阅读17分钟

编程语言中,能够表示并操作的值的类型叫做数据类型(type)。

JS的数据类型有两类:

  • 原始类型。数字、字符串、布尔值,以及两个特殊的原始值null、undefined
  • 对象类型。属性的集合。每个属性由键值对构成。
    • 数组(Array),带编号的值的有序集合
    • 函数(Function)。是相关联的可执行代码的对象,通过调用函数来运行科执行代码,并返还运算结果。
      • 构造函数(constructor)。用于初始化(使用new运算符)一个对象。
    • 日期(Date)定义了代表日期的对象。
    • 正则(RegExp)定义了正则表达式的对象。
    • 错误(Rrror)类定义了那行表示javascript程序中运行时错误和语法错误对象。

JavaScript类型

  • 原始类型 VS 对象类型
  • 可以拥有方法的类型 VS 不能拥有方法的类型。
  • 可变(mutable)VS 不可变(immutable)类型。对象和数组属于可变类型

其它特点

  • JavaScript解析器有自己的内存管理机制,可以自动对内存进行垃圾回收。
  • 原始类型可以拥有自己的方法,只有null和undefined是无法拥有方法的值。
  • JS可以自由地进行数据类型转换。
  • JS的变量是无类型的(untyped),变量可以被赋予人和类型的值。
  • JS采用语法作用域。

数字

JavaScript不区分整数和浮点数,所有数字均用浮点数值表示。

标准使用IEEE 754标准定义的64位浮点格式表示,实际操作中是32位。

整数直接量

  • 十进制
  • 十六进制:0x或0X开头

浮点型直接量

  • 小数
  • 科学计数法 e、E

算数计算

  • 加法运算符(+)
  • 减法运算符(-)
  • 乘法运算符(*)
  • 除法运算符(/)
  • 求余(求整除后的余数)运算符(%)

其它复杂的运算

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.random()   // 生成一个大于等于0小于1.0的伪随机数
Math.PI    // π: 圆周率
Math.E     // e: 自然对数的底数
Math.sqrt(3)    // 3的平方根
Math.pow(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)    // e的三次幂

算术运算在溢出(overflow)、下溢(underflow)或被零整除时不会报错

  • 上溢:超出了JavaScript所能表示的上限,结果为一个特殊的无穷大(infinity)值或无穷小(-Infinity)表示
  • 下溢:当运算结果无限接近于零并超出JavaScript所能表示的最小值时,被称为下溢,用零来表示。
    • MIN_VALUE 是最小的数(接近 0,但不是负数)。它的近似值为 5 x 10-324。
    • MAX_VALUE 是最大的数。它的近似值为 1.7976931348623157 x 10308。
  • 被零整除 除以0的结果是一个非数字(not-a-number)值,NaN。无穷大除以无穷大、给任意负数作开方运算或者算术运算符与不是数字或无法转换为数字的操作数都用NaN表示。
Infinity    // 将一个可读/写的变量初始化为infinity
Number.POSITIVE_INFINITY  //最大值。同样的值,只读
1/0      // Infinity
Number.MAX_VALUE + 1   //计算结果不是Infinity
Number.NEGATIVE_INFINITY  // 该表达式表示了负无穷大
-Infinity
-1/0
-Number.MAX_VALUE - 1
NaN      // 将一个可读/写的变量初始化为NaN
Number.NaN    //同样的值,但是只读
0/0      // 计算结果是NaN
Number.MIN_VALUE/2   // 发生下溢:计算结果为0
-Number.MIN_VALUE/2   // 负零
-1/Infinity    // 同样是负零
-0

用isNaN()判断是否是NaN。

isFinite(),在参数不是NaN、Infinity或-Infinity的时候返回true

var zero = 0;   // 正常的零值
var negz = -0;   // 负零值
zero === negz   // => true: 正零值和负零值相等
1/zero === 1/negz   // => false: 正无穷大和负无穷大不等

实数有无数个,但JavaScript通过浮点数的形式只能表示其中有限个数。也就是说,在JavaScript中使用实数的时候,常常只是一个真实值的一个近似表示。

JavaScript采用了IEEE-754浮点数表示法,这是一种二进制表示法,可以精确的表示如1/2、1/8和1/1024、这样的分数,但是我们常用的分数是十进制分数1/10、1/100等。二进制浮点数表示法并不能准确地表示类似0.1这样简单的数。

var x = 0.3 -0.2; //x=0.09999999999999998
var y = 0.2 - 0.1; // y=0.1
x == y    //false
x == 0.1   //false
y == 0.1   //true

在任何使用二进制浮点数的编程语言中都会有这个问题。

时间

var then = new Date(2015, 0, 1); // 2015年1月1日  
var later = new Date(2015, 0, 1, 17, 10, 30); // 同一天, 当地时间5:10:30pm, 
var now = new Date(); // 当前日期和时间  
var elapsed = now - then; // 日期减法:计算时间间隔的毫秒数 
later.getFullYear() // => 2011  
later.getMonth() // => 0: 从0开始计数的月份 
later.getDate() // => 1: 从1开始计数的天数  
later.getDay() // => 5: 得到星期几, 0代表星期日,5代表星期一 
later.getHours() // =>当地时间17: 5pm  
later.getUTCHours() // 使用UTC表示小时的时间,基于时区

字符串

字符串(string)是一组由16位值组成的不可变的有序序列,每个字符通常来自于Unicode字符集。

字符串直接量,是由单引号或双引号括起来的字符序列。由单引号定界的字符串中可以包含双引号,由双引号定界的字符串中也可以包含单引号。

可以换行写

"one \
long \
line"

当使用单引号来定界字符串时,需要格外小心英文中的缩写和所有格写法,比如can't和O'Oracle's。因为撇号和单引号是同一个字符,所以必须使用反斜线(\)来转义所有的撇号。

其它转义字符

  • '\xA9' - 版权号©

字符串的其它API

var s = "hello, world" // 定义一个字符串 
s.charAt(0) // => "h": 第一个字符  
s.charAt(s.length - 1) // => "d": 最后一个字符 
s.substring(1, 4) // => "ell":第24个字符 
s.slice(1, 4) // => "ell": 同上 
s.slice(-3) // => "rld": 最后三个字符 
s.indexOf("l") // => 2: 字符l首次出现的位置 
s.lastIndexOf("l") // => 10:字符l最后一次出现的位置  
s.indexOf("l", 3) // => 3:在位置3及之后首次出现字符l的位置 
s.split(", ") // => ["hello", "world"] 分割成子串 
s.replace("h", "H") // => "Hello, world": 全文字符替换 
s.toUpperCase() // => "HELLO, WORLD"

在JavaScript中字符串是固定不变的,类似replace()和toUpperCase()的方法都返回新字符串,原字符串本身并没有发生改变

字符串可以当做只读数组,除了使用charAt()方法,也可以使用方括号来访问字符串中的单个字符(16位值)

匹配模式

String和RegExp对象均定义了利用正则表达式进行模式匹配和查找与替换的函数。

尽管RegExp并不是语言中的基本数据类型,但是它们依然具有直接量写法

/^HTML/ //匹配以HTML开始的字符串
/[1-9][0-9]*/ // 匹配一个非零数字,后面是任意个数字
/\bjavascript\b/i // 匹配单词"javascript",忽略大小写

字符串同样具有可以接收RegExp参数的方法

var text = "testing: 1, 2, 3"; // 文本示例  
var pattern = /\d+/g // 匹配所有包含一个或多个数字的实例 
pattern.test(text) // => true: 匹配成功  
text.search(pattern) // => 9: 首次匹配成功的位置  
text.match(pattern) // => ["1", "2", "3"]: 所有匹配组成的数组 
text.replace(pattern, "#"); // => "testing: #, #, #" 
text.split(/\D+/); // => ["","1","2","3"]: 非数字字符截取字符串

布尔值

这个类型只有两个值,保留字true和false。

比较语句的结果通常是布尔值。

常用语JavaScript的控制语句中。

任意JavaScript的值都可以转换为布尔值。下面这些值会被转换成false:

undefined 
null 
00  
NaN
"" // 空字符串

布尔值包含toString()方法,因此可以使用这个方法将字符串转换为“true”或“false”

null和undefined

null是JavaScript语言的关键字,它表示一个特殊值,常用来描述“空值”。对null执行typeof运算,结果返回字符串“object”,也就是说,可以将null认为是一个特殊的对象值

通常认为null是它自有类型的唯一一个成员,它可以表示数字、字符串和对象是“无值”的。

如果要查询对象属性或数组元素的值时返回undefined则说明这个属性或元素不存在。如果函数没有返回任何值,则返回undefined。引用没有提供实参的函数形参的值也只会得到undefined。undefined是预定义的全局变量(它和null不一样,它不是关键字),它的值就是“未定义”。

typeof运算符得到undefined的类型,则返回“undefined”

全局对象

全局对象的属性是全局定义的符号,

当JavaScript解释器启动时(或者任何Web浏览器加载新页面的时候),它将创建一个新的全局对象,并给它一组定义的初始属性:

• 全局属性,比如undefined、Infinity和NaN
• 全局函数,比如isNaN()、parseInt()
• 构造函数,比如Date()、RegExp()、String()、Object()和Array()
• 全局对象,比如Math和JSON

包装对象

JavaScript对象是一种复合值:它是属性或已命名值的集合。通过“.”符号来引用属性值。

字符串既然不是对象,为什么它会有属性呢?只要引用了字符串s的属性,JavaScript就会将字符串值通过调用new String(s)的方式转换成对象,这个对象继承了字符串的方法,并被用来处理属性的引用。一旦属性引用结束,这个新创建的对象就会销毁。

同字符串一样,数字和布尔值也具有各自的方法:通过Number()和Boolean()构造函数创建一个临时对象,这些方法的调用均是来自于这个临时对象。null和undefined没有包装对象:访问它们的属性会造成一个类型错误。

存取字符串、数字或布尔值的属性时创建的临时对象称做包装对象

通过typeof运算符可以看到原始值和其包装对象的不同。

原始值和引用

原始值是不可更改的

var s = "hello"; // 定义一个由小写字母组成的文本 
s.toUpperCase(); // 返回"HELLO",但并没有改变s的值 
s // => "hello": 原始字符串的值并未改变

原始值的比较是值的比较

对象是可变的:

var o = { x:1 }; // 定义一个对象  
o.x = 2; // 通过修改对象属性值来更改对象 
o.y = 3; // 再次更改这个对象,给它增加一个新属性 
var a = [1, 2, 3] // 数组也是可修改的 
a[0] = 0; // 更改数组的一个元素 
a[3] = 4; // 给数组增加一个新元素

通常将对象称为引用类型(referencetype)。对象值都是引用(reference),对象的比较均是引用的比较:当且仅当它们引用同一个基对象时,它们才相等。

var a = []; // 定义一个引用空数组的变量 
var b = a; // 变量b引用同一个数组 
b[0] = 1; // 通过变量b来修改引用的数组 
a[0] // => 1: 变量a也会修改  
a === b // => true:a和b引用同一个数组,因此它们相等

数字

类型转换

原始值转换为数字转换为字符串转换为逻辑
false0"false"false
true1"true"true
00"0"false
11"1"true
"0"0"0"true
"000"0"000"true
"1"1"1"true
NaNNaN"NaN"false
InfinityInfinity"Infinity"true
-Infinity-Infinity"-Infinity"true
""0""false
"20"20"20"true
"twenty"NaN"twenty"true
[ ]0""true
[20]20"20"true
[10,20]NaN"10,20"true
["twenty"]NaN"twenty"true
["ten","twenty"]NaN"ten,twenty"true
function(){}NaN"function(){}"true
{ }NaN"[object Object]"true
null0"null"false
undefinedNaN"undefined"false

对象到原始值的转换

  • 转换和相等性
null == undefined // 这两值被认为相等 
"0" == 0 // 在比较之前字符串转换成数字 
0 == false // 在比较之前布尔值转换成数字  
"0" == false // 在比较之前字符串和布尔值都转换成数字
  • 显式类型转换
Number("3") // => 3  
String(false) // => "false" 或使用 false.toString()
Boolean([]) // => true 
Object(3) // => new Number(3)

除了null或undefined之外的任何值都具有toString()方法,这个方法的执行结果通常和String()方法的返回结果一致。

些运算符会做隐式的类型转换

x + "" // 等价于String(x)  
+ x // 等价于 Number(x).也可以写成 x-0
!! x // 等价于 Boolean(x). 注意是双叹号

JavaScript中提供了专门的函数和方法用来做更加精确的数字到字符串(number-to-string)和字符串到数字(string-to-number)的转换。

var n = 17;  
binary_string = n.toString(2); // 转换为 "10001" 
octal_string = "0" + n.toString(8); // 转换为 "021" 
hex_string = "0x" + n.toString(16); // 转换为 "0x11"
var n = 123456.789; 
n.toFixed(0); // "123457" 
n.toFixed(2); // "123456.79" 
n.toFixed(5); // "123456.78900" 
n.toExponential(1); // "1.2e+5" 
n.toExponential(3); // "1.235e+5" 
n.toPrecision(4); // "1.235e+5" 
n.toPrecision(7); // "123456.8" 
n.toPrecision(10); // "123456.7890" 

parseInt()只解析整数,而parseFloat()则可以解析整数和浮点数

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*2 + 1) 
parseInt("ff", 16); // => 255 (15*16 + 15) 
parseInt("zz", 36); // => 1295 (35*36 + 35) 
parseInt("077", 8); // => 63 (7*8 + 7) 
parseInt("077", 10); // => 77 (7*10 + 7) 
  • 对象转换为原始值

所有的对象(包括数组和函数)都转换为true。

对象到字符串(object-to-string)和对象到数字(object-to-number)的转换是通过调用待转换对象的一个方法来完成的 toString() valueOf()

对象到字符串的转换过程:

  1. 如果对象具有toString()方法,则调用这个方法。如果它返回一个原始值,JavaScript将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。需要注意的是,原始值到字符串的转换在表3-2中已经有了详尽的说明。
  2. 如果对象没有toString()方法,或者这个方法并不返回一个原始值,那么JavaScript会调用valueOf()方法。如果存在这个方法,则JavaScript调用它。如果返回值是原始值,JavaScript将这个值转换为字符串(如果本身不是字符串的话),并返回这个字符串结果。
  3. 否则将抛出一个类型错误异常。

对象到数字的转换过程:

  1. 如果对象具有valueOf()方法,返回一个原始值,则JavaScript将这个原始值转换为数字(如果需要的话)并返回这个数字。
  2. 如果对象具有toString()方法,返回一个原始值,则JavaScript将其转换并返回。
  3. 否则,JavaScript抛出一个类型错误异常。

JavaScript中的“+”运算符可以进行数学加法和字符串连接操作。如果它的其中一个操作数是对象,则JavaScript将使用特殊的方法将对象转换为原始值,而不是使用其他算术运算符的方法执行对象到数字的转换。

“+”和“==”转换包含日期对象时。对象到原始值的转换基本上是对象到数字的转换(首先调用valueOf()),日期对象则使用对象到字符串的转换模式。

12 + (new Date())    //"12Wed Mar 08 2017 11:23:06 GMT+0800 (中国标准时间)"
var now = new Date(); // 创建一个日期对象  
typeof(now + 1) // => "string": "+"将日期转换为字符串 
typeof(now - 1) // => "number": "-"使用对象到数字的转换 
now == now.toString() // => true: 隐式的和显式的字符串转换 
now > (now - 1) // => true: ">"将日期转换为数字

变量作用域

  • 全局变量拥有全局作用域。
  • 函数内声明的变量只在函数体内有定义。它们是局部变量,作用域是局部性的。函数参数也是局部变量,它们只在函数体内有定义。

花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而JavaScript中没有块级作用域。JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为声明提前(hoisting),即JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部,看一下如下代码:

var scope = "global"; 
function f() {
    console.log(scope); // 输出"undefined",而不是"global" 
    var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
    console.log(scope); // 输出"local" 
} 

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。

var truevar = 1; // 声明一个不可删除的全局变量 
fakevar = 2; // 创建全局对象的一个可删除的属性 
this.fakevar2 = 3; // 同上  
delete truevar // => false: 变量并没有被删除 
delete fakevar // => true: 变量被删除 
delete this.fakevar2 // => true: 变量被删除

JavaScript全局变量是全局对象的属性,这是在ECMAScript规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当做跟函数调用相关的某个对象的属性。规范称该对象为“声明上下文对象”(declarative environment record)。

每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当JavaScript需要查找变量x的值的时候(这个过程称做“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。

当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。