在编程语言中,能够表示并操作的值的类型称做数据类型(type),编程语言最基本的特性就是能够支持多种数据类型。
当程序需要将值保存起来以备将来使用时,便将其赋值给(将值“保存”到)一个变量(variable)。变量是一个值的符号名称,可以通过名称来获得对值的引用。
JavaScript解释器有自己的内存管理机制,可以自动对内存进行垃圾回收(garbage collection)。这意味着程序可以按需创建对象,程序员则不必担心这些对象的销毁和内存回收。当不再有任何引用指向一个对象,解释器就会知道这个对象没用了,然后自动回收它所占用的内存资源。
我们不用全局的定义函数去操作不同类型的值,数据类型本身可以定义方法(method)来使用值。例如,要对数组a中的元素进行排序,不必要将a传入sort()函数,而是调用a的一个方法sort():
a.sort(); //sort(a)的面向对象的版本
在JavaScript中,只有null和undefined是无法拥有方法的值。
1. 数字
和其他编程语言不同,JavaScript不区分整数值和浮点数值。JavaScript中的所有数字均用浮点数值表示。
64位浮点格式表示数字,这意味着它能表示的最大值是±1.7976931348623157×10308,最小值是±5×10-324。
能够表示的整数范围是从-9 007 199 254 740 992~9 007 199 254 740 992(即-253~253),包含边界值。
1.1整型
十六进制的直接量是指以"0x"或"0X"为前缀,0~9之间的数字和a(A)~f(F)之间的字母构成,a~f的字母对应的表示数字10~15。
0xff //15*16+15=255(十进制)
0xCAFE911
1.2 浮点型
3.14
2345.789
.333333333333333333
6.02e23//6.02×10的23次方
1.4738223E-32 //1.4738223×10的-32次方
1.3 算术运算
通过作为Math对象的属性定义的函数和常量来实现:
Math.pow(2,53) //=>9007199254740992:2的53次幂
Math.round(.6) //=>1.0:四舍五入
Math.ceil(0.6); //=>1.0:向上求整
Math.floor(0.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的三次幂
JavaScript中的算术运算在溢出(overflow)、下溢(underflow)或被零整除时不会报错。
1.4 Infinity和NaN
JavaScript预定义了全局变量Infinity和NaN,用来表示正无穷大和非数字值。
当数字运算结果超过了JavaScript所能表示的数字上限(溢出),结果为一个特殊的无穷大(infinity)值,在JavaScript中以Infinity表示。同样地,当负数的值超过了JavaScript所能表示的负数范围,结果为负无穷大,在JavaScript中以-Infinity表示。基于它们的加、减、乘和除运算结果还是无穷大值(当然还保留它们的正负号)。
被零整除在JavaScript并不报错:它只是简单的返回无穷大(Infinity)或负无穷大(-Infinity)。
但有一个例外,零除以零是没有意义的,这种整除运算结果也是一个非数字(not-a-number)值,用NaN表示。
无穷大除以无穷大、给任意负数作开方运算或者算术运算符与不是数字或无法转换为数字的操作数一起使用时都将返回NaN。
Infinity; //将一个可读/写的变量初始化为infinity
Number.POSITIVE_INFINITY; //同样的值,只读
1 / 0; //这也是同样的值
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;
JavaScript中的非数字值有一点特殊:它和任何值都不相等,包括自身。也就是说,没办法通过x==NaN来判断变量x是否是NaN。相反,应当使用x!=x来判断,当且仅当x为NaN的时候,表达式的结果才为true。函数isNaN()的作用与此类似,如果参数是NaN或者是一个非数字值(比如字符串和对象),则返回true。
JavaScript中有一个类似的函数isFinite(),在参数不是NaN、Infinity或-Infinity的时候返回true。
负零值同样有些特殊,它和正零值是相等的(甚至使用JavaScript的严格相等测试来判断)。这意味着这两个值几乎一模一样,除了作为除数之外:
var zero = 0; //正常的零值
var negz = -0; //负零值
zero === negz; //=>true:正零值和负零值相等
1 / zero === 1 / negz; //=>false:正无穷大和负无穷大不等
1.5 浮点数的精度
var x = 0.3 - 0.2;
var y = 0.2 - 0.1;
x == y; //=>false:两值不相等!
x == 0.1; //=>false:.3-.2不等于.1
y == 0.1; //=>true:.2-.1等于.1
由于舍入误差,0.3和0.2之间的近似差值实际上并不等于0.2和0.1之间的近似差值。这个问题并不只在JavaScript中才会出现,理解这一点非常重要:在任何使用二进制浮点数的编程语言中都会有这个问题。
要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值:
Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true
1.6 日期时间
var then = new Date(2011, 0, 1); //2011年1月1日
var later = new Date(2011, 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表示小时的时间,基于时区
2.字符串
JavaScript采用UTF-16编码的Unicode字符集,JavaScript字符串是由一组无符号的16位值组成的序列。(两个字节16个二进制位)
在JavaScript程序中的字符串直接量,是由单引号或双引号括起来的字符序列。由单引号定界的字符串中可以包含双引号,由双引号定界的字符串中也可以包含单引号。
"" //空字符串:它包含0个字符
'testing'
"3.14"
'name="myform"'
"Wouldn't you prefer O'Reilly's book?"
"This string\nhas two lines"
"π is the ratio of a circle's circumference to its diameter"
2.1 转义字符
在JavaScript字符串中,反斜线\有着特殊的用途,反斜线符号后加一个字符,就不再表示它们的字面含义了,比如,\n就是一个转义字符(escape sequence),它表示的是一个换行符。
为什么把它们叫做转义字符,反斜线可以使我们避免使用常规方式解释单引号,当单引号不是用来标记字符串结尾时,它只是一个撇号:
如果“\”字符位于没有在表3-1中列出的字符前,则忽略“\”
2.2 字符串使用
s="hello,world";
s[0] //=>"h"
s[s.length-1] //=>"d"
除了length属性,字符串还提供许多可以调用的方法
var s = "hello,world"; //定义一个字符串
s.charAt(0); //=>"h":第一个字符
s.charAt(s.length - 1); //=>"d":最后一个字符
s.substring(1, 4); //=>"ell":第2~4个字符
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()的方法都返回新字符串,原字符串本身并没有发生改变。 字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值
var s = "hello"; //定义一个由小写字母组成的文本
s.toUpperCase(); //返回"HELLO",但并没有改变s的值
s; //=>"hello":原始字符串的值并未改变
2.3 正则表达式
JavaScript定义了RegExp()构造函数,用来创建表示文本匹配模式的对象。这些模式称为“正则表达式”(regular expression)
在两条斜线之间的文本构成了一个正则表达式直接量。第二条斜线之后也可以跟随一个或多个字母,用来修饰匹配模式的含义,例如:
/^HTML/; //匹配以HTML开始的字符串
/[1-9][0-9]*/; //匹配一个非零数字,后面是任意个数字
/\bjavascript\b/i; //匹配单词"javascript",忽略大小写
RegExp对象定义了很多有用的方法,字符串同样具有可以接收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"]:用非数字字符截取字符串
3.布尔值
任意JavaScript的值都可以转换为布尔值。下面这些值会被转换成false:
undefined
null
0
-0
NaN
"" //空字符串
所有其他值,包括所有对象(数组)都会转换成true。
4. null和undefined
null是JavaScript语言的关键字,它表示一个特殊值,常用来描述“空值”。对null执行typeof运算,结果返回字符串"object",也就是说,可以将null认为是一个特殊的对象值,含义是“非对象”。但实际上,通常认为null是它自有类型的唯一一个成员,它可以表示数字、字符串和对象是“无值”的。
undefined是预定义的全局变量(它和null不一样,它不是关键字),它的值就是“未定义”。如果使用typeof运算符得到undefined的类型,则返回"undefined",表明这个值是这个类型的唯一成员。
尽管null和undefined是不同的,但它们都表示“值的空缺”,两者往往可以互换。判断相等运算符“==”认为两者是相等的(要使用严格相等运算符“===”来区分它们)。
5. 全局对象
全局对象的属性是全局定义的符号,JavaScript程序可以直接使用。当JavaScript解释器启动时(或者任何Web浏览器加载新页面的时候),它将创建一个新的全局对象,并给它一组定义的初始属性:
- 全局属性,比如undefined、Infinity和NaN。
- 全局函数,比如isNaN()、parseInt()和eval()
- 构造函数,比如Date()、RegExp()、String()、Object()和Array()
- 全局对象,比如Math和JSON
在代码的最顶级——不在任何函数内的JavaScript代码——可以使用JavaScript关键字this来引用全局对象:
var global=this; //定义一个引用全局对象的全局变量
在客户端JavaScript中,在其表示的浏览器窗口中的所有JavaScript代码中,Window对象充当了全局对象。这个全局Window对象有一个属性window引用其自身,它可以代替this来引用全局对象。
当初次创建的时候,全局对象定义了JavaScript中所有的预定义全局值。这个特殊对象同样包含了为程序定义的全局值。如果代码声明了一个全局变量,这个全局变量就是全局对象的一个属性
6. 类型转换
编程语言分为动态(类型)语言和静态(类型)语言,动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在第一次赋值给变量时,在内部将数据类型记录下来。Python、Ruby和JavaScript就是典型的动态类型语言。静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、JAVA等。
JavaScript中的取值类型非常灵活,JavaScript将根据需要自行转换类型。
10 + "objects"; //=>"10 objects".数字10转换成字符串
"7" * "4"; //=>28:两个字符串均转换为数字
var n = 1 - "x"; //=>NaN:字符串"x"无法转换为数字
n + "objects"; //=>"NaN objects":NaN转换为字符串"NaN"
- 字符串转化为数字,以数字表示的字符串可以直接转换为数字
- 布尔值转化为数字,true转换为1,false、空字符串""转换为0。
- 原始值转换为对象,调用String()、Number()或Boolean()构造函数
- null和undefined属于例外,当将它们用在期望是一个对象的地方都会造成一个类型错误(TypeError)异常,而不会执行正常的转换。
Number("3"); //=>3
String(false); //=>"false"或使用false.toString()
Boolean([]); //=>true
Object(3); //=>new Number(3)
“==”相等运算符也随相等的含义灵活多变。例如,如下这些比较结果均是true:
null == undefined; //这两值被认为相等
"0" == 0; //在比较之前字符串转换成数字
0 == false; //在比较之前布尔值转换成数字
"0" == false; //在比较之前字符串和布尔值都转换成数字
需要注意的是,除了null或undefined之外的任何值都具有toString()方法,这个方法的执行结果通常和String()方法的返回结果一致。
JavaScript中的某些运算符会做隐式的类型转换,有时用于类型转换。如果“+”运算符的一个操作数是字符串,它将会把另外一个操作数转换为字符串。一元“+”运算符将其操作数转换为数字。同样,一元“!”运算符将其操作数转换为布尔值并取反。
x + ""; //等价于String(x)
+x; //等价于Number(x).也可以写成x-0
!!x; //等价于Boolean(x).注意是双叹号
Number类定义的toString()方法可以接收表示转换基数(radix)的可选参数,如果不指定此参数,转换规则将是基于十进制。同样,亦可以将数字转换为其他进制数
var n = 17;
binary_string = n.toString(2); //转换为"10001"
octal_string = "0" + n.toString(8); //转换为"021"
hex_string = "0x" + n.toString(16); //转换为"0x11"
6.1 toFixed() toExponential() toPrecision()
- toFixed()根据小数点后的指定位数将数字转换为字符串,它从不使用指数记数法。
- toExponential()使用指数记数法将数字转换为指数形式的字符串,其中小数点前只有一位,小数点后的位数则由参数指定(也就是说有效数字位数比指定的位数要多一位)
- toPrecision()根据指定的有效数字位数将数字转换成字符串。如果有效数字的位数少于数字整数部分的位数,则转换成指数形式。
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"
6.2 parseInt() parseFloat()
parseInt()函数和parseFloat()函数(它们是全局函数,不从属于任何类的方法)。
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()可以接收第二个可选参数,这个参数指定数字进制转换的基数
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)
6.3 对象转换为原始值
对象到布尔值的转换非常简单:所有的对象(包括数组和函数)都转换为true。new Boolean(false)是一个对象而不是原始值,它将转换为true。
所有的对象继承了两个转换方法。
第一个是toString(),它的作用是返回一个反映这个对象的字符串。默认的toString()方法并不会返回一个有趣的值
({x:1,y:2}).toString(); //=>"[object Object]"
- 数组类(Array class)的toString()方法将每个数组元素转换为一个字符串,并在元素之间添加逗号后合并成结果字符串
- 函数类(Function class)的toString()方法返回这个函数的实现定义的表示方式(JavaScript源代码字符串)。
- 日期类(Date class)定义的toString()方法返回了一个可读的(可被JavaScript解析的日期和时间字符串。
- RegExp类(RegExp class)定义的toString()方法将RegExp对象转换为表示正则表达式直接量的字符串
[1, 2, 3].toString(); //=>"1,2,3"
(function(x) {
f(x);
}.toString()); //=>"function(x){\n f(x);\n}"
/\d+/g.toString(); //=>"/\\d+/g"
new Date(2010, 0, 1).toString(); //=>"Fri Jan 01 2010 00:00:00 GMT-0800(PST)"
另一个转换对象的函数是valueOf()。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单返回对象本身。日期类定义的valueOf()方法会返回它的一个内部表示:1970年1月1日以来的毫秒数。
var d = new Date(2010, 0, 1); //2010年1月1日(太平洋时间)
d.valueOf(); //=>1262332800000
日期类是JavaScript语言核心中唯一的预先定义类型,它定义了有意义的向字符串和数字类型的转换。 下面的代码展示了日期对象和“+”、“-”、“==”以及“>”的运行结果:
var now = new Date(); //创建一个日期对象
typeof (now + 1); //=>"string":"+"将日期转换为字符串
typeof (now - 1); //=>"number":"-"使用对象到数字的转换
now == now.toString(); //=>true:隐式的和显式的字符串转换
now > now - 1; //=>true:">"将日期转换为数字
7 变量声明和变量作用域
如果未在var声明语句中给变量指定初始值,那么虽然声明了这个变量,但在给它存入一个值之前,它的初始值就是undefined。
- 一个变量的作用域(scope)是程序源代码中定义这个变量的区域。
- 全局变量拥有全局作用域,在JavaScript代码中的任何地方都是有定义的。
- 在函数内声明的变量只在函数体内有定义。它们是局部变量,作用域是局部性的。
- 函数参数也是局部变量,它们只在函数体内有定义。 在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖。
var scope = "global"; //声明一个全局变量
function checkscope() {
var scope = "local"; //声明一个同名的局部变量
return scope; //返回局部变量的值,而不是全局变量的值
}
checkscope(); //=>"local"
7.1 函数作用域和变量提升
JavaScript使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
function test(o) {
var i = 0; //i在整个函数体内均是有定义的
if (typeof o == "object") {
var j = 0; //j在函数体内是有定义的,不仅仅是在这个代码段内
for (var k = 0; k < 10; k++) {
//k在函数体内是有定义的,不仅仅是在循环内
console.log(k); //输出数字0~9
}
console.log(k); //k已经定义了,输出10
}
console.log(j); //j已经定义了,但可能没有初始化
}
JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为变量提升(hoisting),即JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部
var scope = "global";
function f() {
console.log(scope); //输出"undefined",而不是"global"
var scope = "local"; //变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope); //输出"local"
}
你可能会误以为函数中的第一行会输出"global",因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置:
function f() {
var scope; //在函数顶部声明了局部变量
console.log(scope); //变量存在,但其值是"undefined"
scope = "local"; //这里将其初始化并赋值
console.log(scope); //这里它具有了我们所期望的值
}
“变量提升”这步操作是在JavaScript引擎的“预编译”时进行的,是在代码开始运行之前。
在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此
- JavaScript全局变量是全局对象的属性
- 局部变量当做跟函数调用相关的声明上下文对象(declarative environment record)的属性。
- JavaScript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。
7.2 作用域链
①每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。
当JavaScript需要查找变量x的值的时候(这个过程称做“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。
如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。
②在JavaScript的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。
③当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。
④对于嵌套函数来讲,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。