开场白
================
作为WEB世界的第一语言,从事网络应用开发的我当然要把JS作为主要语言来学习喽!而且,Javascript的功能越来越强大,能干的事越来越多(几乎能干任何事),足以媲美主流语言。
万丈高楼平地起,这篇文章的目的在于打好javascript语言(以下简称JS)基础,为以后学习众多库和框架,应用它们、修改它们(主要的程序开发方式)铺路。
ECMAscript是javascript的语言规范和核心(严格来讲,Javascript是ECMAscript的一种实现,并且还包含DOM和BOM两部分内容)。这里以ECMAscript 第5版,即ISO/IEC 16262:2011为蓝本来学习。ECMAscript 6 虽然已经出现,但很多浏览器还不支持。感觉版本6就是把大量需要外部库支持的功能整合进原生JS了。我的逻辑是“保持语言核心尽可能简单,扩展功能尽量采用外部专用工具”,所以,还是以第5版为基础。
http://www.ecma-international.org网站上公布了这个规范的全文,以下内容均出自对这个规范和Node.org官方文档的学习。(ECMA5中文版见http://www.w3.org/html/ig/zh/wiki/ES5)
主要的学习教材还有阮一峰的《Javascript标准教程》(http://javascript.ruanyifeng.com/),在此鸣谢。
对于Javascript,最精辟的概括是“一门为web而生的面向对象的脚本语言“。它说明了JS的三大特点“与WEB紧密关联”,“面向对象”,“脚本语言”。其深刻含义需要慢慢体会。JS还有一大特点是强烈依赖宿主环境(Hosting environment),包括浏览器和服务器端。很多功能是依靠宿主环境才能实现的,所以,学习JS离不开浏览器和服务器端执行环境。
这篇学习笔记的一个目的是取出javascript最精华的子集,规范它的用法。
开发环境
================
传统上JS是运行在浏览器里的,其实是由浏览器里的解释器来解释运行。但在浏览器里调试程序实在是不太方便。好在Node.js横空出世,我们可以让JS脱离浏览器运行了。实际上node.js和chrome等浏览器都是通过V8来解释运行JS的。
小快灵的SublimeText就可以搭配NodeJS来做JS解释器。在SublineText中用Tool菜单-build system-new build system,在打开的build配置文件中写上
{
"cmd": ["node", "$file"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"selector": "source.js"
}
保存为一个名字,比如NodeJs,就可以了。以后,任何打开的JS文件用Ctrl-B就可以自动解释输出了。
V8,就像它的名字一样,是马力十足的JS发动机(引擎)。V8是google开发的JS引擎,使用C++开发,并在谷歌chrome/chromium浏览器中使用。在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序。V8中实现的ECMAScript中指定 ECMA - 262 ,第3版, 可跨平台运行(支持x86、IA - 32或ARM处理器)。V8可以独立运行,也可以嵌入到任何C/C++应用程序。项目托管在Google Code上[http://code.google.com/p/v8] ,基于BSD协议,任何组织或个人可以将其源码用于自己的项目中。
关于V8,我们以后有很多移植的工作要做,现在先不去管它,只要知道它是我们的“高级语言翻译”,能把我们写的JS代码翻译成机器语言让计算机执行就可以了。
node.js是JS的运行环境,对Google V8引擎的封装和优化,提供了很多API,让你可以使用V8以及其他很多功能,只要能够部署node.js的地方(它也是跨平台的)我们都可以放心大胆地使用JS了。
好了,假设我们已经装好了Sublime Text和Node.js,打开Sublime Text,随便写个test.js文件,如console.log("Hello world"); 点击“运行”,就可以看到亲切的“Hello world"显示在下面了(调用了node.js执行 node test.js)。当然,我们也可以在命令行中运行node test.js,同样会得到这个输出。好了,开启我们的JS之旅吧。
打印输出
=================
第一课,我们先学学如何显示内容。这要用console.log()语句。如console.log("Hello world");在命令行输出hello world。
注意,每个JS的语句最后都要有个分号(;),这是JS的结束符,解释器会凭它判断一个语句的结束。
console.log是以后我们最常用到的语句,它的作用是在标准输出中显示内容。这其实是node.js的一个功能,不是JS里固有的。还有console.err、console.time等玩法,我们先不管。只要知道可以用console.log("......“)显示内容就可以了。在调试程序的过程中,我们经常要用它打印出程序运行的状态。
如果要换行呢?用console.log("Hello\n“,"World"),你会看到打印出两行文字
Hello
World
你甚至可以用下面这行代码打印出一首诗来:
console.log(" 长亭外,古道边,\n","芳草碧连天。\n","一壶浊酒笛声残,\n","夕阳山外山。");
长亭外,古道边,
芳草碧连天。
一壶浊酒笛声残,
夕阳山外山。
如果我们引入变量,就可以实现动态输出的效果。
var a ="Bob";
console.log("Hello", a);
这将输出“Hello Bob"。
在console.log语句中,使用+号可以连接字符串。如:
console.log("Hello"+"World");
将输出“HelloWorld"。
注释
===================
用//开头的语句都被当作注释而不会被执行。
多行注释可以用
/*
.......
*/
在调试程序的过程中,我们经常要临时屏蔽掉某段程序的运行,这时可以用/* */来把这段程序注释掉。
变量与赋值
======================
上面讲到了用var a = "Bob" 实现动态替换,这里a就是变量,var a = "Bob"就是变量的赋值语句。当然,你可以简单地声明变量var a;而先不赋值(这时a的值是undefined)。
变量在计算机程序中是极为重要的概念,它有两个作用:为我们操作的对象起个名字;在内存中为它开辟一个空间存放它的值。
如果你接触过C/C++或其他编程语言,你知道变量有很多类型,int,long,float,char等,这和变量存放在内存中的空间有关。Javascript是“弱类型语言”(也有number,string等数据类型,但没有编译器在编译时检查类型错误),所有变量都可以用var来声明。这使我们不用费脑筋预先规定变量的类型。在JS中,所有数字都是64位浮点数(足够长),所有字符和字符串都是由16位字符组成的字符串。
在JS中也没有“常量”的概念,因为常量的意义在于“常量表达式在编译时求值而不在运行时求值”,而JS不是编译型语言,所以不需要常量。当然,你可以用一个简短的名字来代表一个反复用到的数字或字符串。
JavaScript允许省略var,直接对未声明的变量赋值。也就是说,var a = 1 与 a = 1,这两条语句的效果相同。但是由于这样的做法很容易不知不觉地创建全局变量(尤其是在函数内部),所以建议总是使用var命令声明变量。
严格地说,var a = 1 与 a = 1,这两条语句的效果不完全一样,主要体现在delete命令无法删除前者。不过,绝大多数情况下,这种差异是可以忽略的。
var 变量名=值 是最简单的变量赋值语句(其中的等号一定要念做“赋值为”而不是“等于”,因为这不是逻辑语句中的相等),如果要声明多个变量,或为多个变量赋值,可以这样写:
var a,b,c,d; 或
var a=1, b=2, c=3, d=4; //最好不要写成a=b=c=d=1这种形式,既不灵活,也不安全。
最好在所有函数的一开始就声明所有变量(不但好找,而且避免后面使用了未声明的变量造成全局变量污染)并赋一个初始值,象这样:
function f() {
var a = 0,
b = 0,
sum = a + b;
//函数体
console.log("sum =", sum);
}
f();
像这种初始化变量同时初始化值的做法是很好的。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是undefined)和增加代码的可读性。在你看到代码后,你可以根据初始化的值知道这些变量大致的用途,例如是要当作对象呢还是当作整数来使。
所有教授javascript的书都谈到要避免全局变量污染。这是因为在JS中所有未定义的变量都不会抛出错误而直接被当成全局变量。全局变量有可能重名造成逻辑错误(特别是当你使用了第三方库的时候)。另外一个避免全局变量的原因是可移植性。如果你想你的代码在不同的环境下(主机下)运行,使用全局变量如履薄冰,因为你会无意中覆盖你最初环境下不存在的主机对象(所以你原以为名称可以放心大胆地使用,实际上对于有些情况并不适用)。因此,最佳实践是仅使用局部变量,即在函数内部声明的变量,它的作用域仅限于函数内部。你大可写
var f = function() {
var a = 1,
b = 2,
sum = a + b;
//函数体
console.log("sum =", sum);
};
var g = function() {
var a = 1,
b = 2,
sum = a + b;
//函数体
console.log("sum =", sum);
};
f();
g();
这里的同名变量a,b,sum由于在不同的函数内,所以不会冲突。
但有时必须要用到全局变量怎么办呢?比如需要一个多个函数共用的变量,它必须在所有函数外面被声明。我们可以使用“名称空间”。其实就是先定义一个以程序本身为名的全局对象,把要用到的全局变量都作为它的属性,这些属性可以在以后的任何函数中使用。
var MYAPP = { "id":0, "name":"John"};
//这里MYAPP指向的是顶层对象。
function f() {
MYAPP.id = 3;
console.log(MYAPP.id);
}
function g() {
MYAPP.id += 5;
MYAPP.name = "Creg";
console.log(MYAPP.id, MYAPP.name);
}
f();
g();
console.log("MYAPP.id=", MYAPP.id, "MYAPP.name=", MYAPP.name);
你会发现每个函数中都单独操作了MYAPP.id和MYAPP.name,而且结果保持到了最后。在实践中,MYAPP的名字最好换成你的专有名称。
标识符与起名规范
===============
标识符(identifier)是用来识别具体对象的一个名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript语言的标识符对大小写敏感,所以a和A是两个不同的标识符。
标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript引擎遇到非法标识符,就会报错。
简单说,标识符命名规则如下:
第一个字符可以是任意Unicode字母,以及美元符号($)和下划线(_)。
第二个字符及后面的字符,还可以用数字。
JavaScript有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。
另外,还有三个词虽然不是保留字,但是因为具有特别含义,也不应该用作标识符:Infinity、NaN、undefined。
简单说,我们最好用英文字母组合作为变量名,推荐采用“驼峰写法”,如myVariable,dataCollection1,越是拼写完整,越容易帮你记住变量的意义。
对象名字有约定采用首字母大写形式,如Animal;函数名字采用下横线连接两个单词,如json_parse。
语句
=================
JavaScript程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。
语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句:
var a = 1 + 3 ;
这条语句先用var命令,声明了变量a,然后将 1+3 的运算结果赋值给变量a。
“1+3”叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,后者则是为了得到返回值。凡是JavaScript语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。
语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。函数体、循环体的后面不用加分号,因为它们只是表述,不是完整的语句。注:虽然现代的JS实现了自动在句尾加分号的功能,但为避免错误,还是写上分号为好。
var a = 1 + 3 ; var b = "abc";
分号前面可以没有任何内容,JavaScript引擎将其视为空语句。
表达式不需要分号结尾。一旦在表达式后面添加分号,则JavaScript引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。
1 + 3;
"abc";
上面两行语句有返回值,但是没有任何意义,因为只是返回一个单纯的值,没有任何其他操作。
末尾是否加分号,可以总结为:一般语句要加分号,函数体、循环结构、条件语句和单纯表达式后不加分号。
条件语句
=====================
学了语句,那么我们就写些语句吧。首先,请记住“===”是“相等”的意思,“!==”是代表不相等。
计算机的智能主要体现在能够根据不同条件执行不同的动作。这基于条件语句。
JavaScript提供if结构和switch结构,完成条件判断。
(1)if 结构
if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。
if (expression)
statement
上面是if结构的基本形式。需要注意的是,expression(表达式)必须放在圆括号中,表示对表达式求值。如果结果为true,就执行紧跟在后面的statement(语句);如果结果为false,则跳过statement。
if (m === 3)
m += 1;
上面代码表示,只有在m等于3时,才会将其值加上1。
这种写法要求statement只能有一个语句。如果想将多个语句放在statement之中,必须在if的条件判断之后,加上大括号。
if (m === 3) {
m += 1;
}
建议总是在if语句中使用大括号,因为这样方便插入语句。注意,if体后面并不加分号。
2)if...else结构
if代码块后面,还可以跟一个else代码块,表示括号中的表示式为false时,所要执行的代码。
if (m === 3) {
// then
} else {
// else
}
上面代码判断变量m是否等于3,如果等于就执行if代码块,否则执行else代码块。
对同一个变量进行多次判断时,多个if...else语句可以连写在一起。注意,不是elseif。
if (m === 0) {
// ...
} else if (m === 1) {
// ...
} else if (m === 2) {
// ...
} else {
// ...
}
else代码块总是跟随离自己最近的那个if语句。
条件语句可以嵌套。如
if (m !== 1) {
if (n === 2) {
console.log('hello');
} else {
console.log('world');
}
}
为避免程序没有反应,建议每个if语句都要跟else。
(3)switch结构
多个if...else连在一起使用的时候,可以转为使用更方便的switch结构。
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}
上面代码根据变量fruit的值,选择执行相应的case。如果所有case都不符合,则执行最后的default部分。需要注意的是,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构。
为避免程序没有反应,建议每个switch语句都要跟default。
循环语句
=========================
计算机的强大,在于能够不知疲倦地飞速执行众多重复的任务。这靠的是循环语句。
循环语句用于重复执行某个操作,它有多种形式。
(1)while循环
While语句包括一个循环条件,只要该条件为真,就不断循环。
while (expression)
statement
while语句的循环条件是一个表达式(express),必须放在圆括号中。语句(statement)部分默认只能写一条语句,如果需要包括多条语句,必须添加大括号。
while (expression){
statement
}
下面是while语句的一个例子。
var i = 0;
while (i<100){
console.log('i当前为:' + i);
i++;
}
上面的代码将循环100次,直到i等于100为止。
(2)for循环
for语句是循环命令的另一种形式,也是更常用的一种形式。
for(initialize; test; increment){ //注意,中间使用;分隔
statement
}
它分成三步:
初始化(initialize):确定循环的初始值,只在循环开始时执行一次;可以用var来声明一个初始变量。
测试(test):检查循环条件,只要为真就进行后续操作;
递增(increment):完成后续操作,然后返回上一步,再一次检查循环条件。
下面是一个循环打印数组每个元素的例子。
for (var i=0; i < arr.length; i++) { //变量i可以直接声明在这里 i++推荐写作i+=1
console.log(arr[i]);
}
所有for循环,都可以改写成while循环。所以说for语句是while语句的一种紧凑写法。
var i = 0;
while (i < arr.length) {
console.log(arr[i]);
i++;
}
for...in循环
for...in 语句用于对数组或者对象的属性进行循环操作。
for ... in 循环中的代码每执行一次,就会对数组的元素或者对象的属性进行一次操作。
语法:
for (变量 in 对象)
{
在此执行代码
}
“变量”用来指定变量,指定的变量可以是数组元素,也可以是对象的属性。
下例将打印出各种车的名字:
var x;
var mycars = new Array();
mycars[0] = "Saab";
mycars[1] = "Volvo";
mycars[2] = "BMW";
for (x in mycars)
{
console.log(mycars[x]);
}
下例将逐一打印车(对象)的各种属性名:
var x;
var mycar = {
"name" : "Saab",
"engine": "V8",
"type": "Sedan",
"Country": "Sweden"
};
for (x in mycar)
{
console.log(x);
}
(3)do...while循环
do...while循环与while循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。
do {
statement
} while(expression);
不管条件是否为真,do..while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号不能省略。
(4)跳转语句:break语句和continue语句
break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行。选择哪一个取决于后面是否还要继续循环。
break语句用于跳出代码块或循环。
var i = 0;
while (i<100){
console.log('i当前为:',i);
i++;
if (i === 10) break; //意思是终止循环
}
上面代码只会执行10次循环,一旦i等于10,就会跳出循环。不再继续执行后面的循环。
continue语句用于立即终止本次循环,返回循环结构的头部,开始下一次循环。
var i = 0;
while (i<100){
i++;
if (i%2===0) continue; //意为“跳过”
console.log('i当前为:' + i);
}
上面代码只有在i为奇数时,才会输出i的值。如果i为偶数,则直接进入下一轮循环。
如果存在多重循环,不带参数的break语句和continue语句都只针对最内层循环跳转。那么,如果要跳出上层循环怎么办呢?
(5)标签(label)
JavaScript语言允许,语句的前面有标签(label)。标签通常与break语句和continue语句配合使用,跳出特定的循环(因为JS语言中没有goto语句)。
top:
for (var i=0;i<30;i++){
for (var j=0;j<30;j++){
if (i===10 && j===15) break top;
console.log("i=",i,"j=",j);
}
}
上面代码为一个双重循环区块,加上了top标签(注意,top不用加引号)。当满足一定条件时,使用break语句加上标签名,直接跳出双层循环。如果break语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
continue语句也可以与标签配合使用。
top:
for (var i=0;i<3;i++){
for (var j=0;j<3;j++){
if (i===1 && j===1) continue top;
console.log("i="+i+",j="+j);
}
}
// i=0,j=0
// i=0,j=1
// i=0,j=2
// i=1,j=0
// i=2,j=0
// i=2,j=1
// i=2,j=2
上面代码在满足一定条件时,使用continue语句加上标签名,直接进入下一轮外层循环。在本例中把i=1 J=0后面的本轮循环都跳过了。如果continue语句后面不使用标签,则只能进入下一轮的内层循环。
错误捕捉与显示
======================
在执行JS程序的过程中,各种错误会阻断程序的执行,显示在后台命令行中。我们可以把容易出错的部分加上try...catch语句,这样错误会显示在程序的输出中并不会影响后续语句的执行。
如下例:
console.log("Hi, this is 1st statement.");
foo.bar();
console.log("Hi, this is 2nd statement.");
因为foo.bar()这个函数没有定义,当显示完第一个"Hi, this is 1st statement."后就会报错停下来。而如果我们这样写:
console.log("Hi, this is 1st statement.");
try {
foo.bar();
} catch (err) {
console.log(err.name + " : " + err.message);
}
console.log("Hi, this is 2nd statement.");
就会连续执行,尽管中间会输出ReferenceError:foo is not defined.
这可以用在程序的调试中。
数据类型
=========================
我们说javascript是弱类型语言,但不是没有数据类型。JavaScript的值的类型共有六个类别和两个特殊值。
六个类别的数据类型又可以分成两组:原始类型(primitive type)和合成类型(complex type)。
原始类型包括三种数据类型。
数值(number) //使计算机可以处理数值运算
字符串(string) //使计算机可以处理人类语言
布尔值(boolean) //使计算机可以处理逻辑
“数值”就是整数和小数(比如1和3.14),“字符串”就是由多个字符组成的文本(比如"Hello World"),“布尔值”则是true(真)和false(假)两个特定值。
合成类型也包括三种数据类型。
对象(object)
数组(array)
函数(function)
对象和数组是两种不同的数据组合方式,而函数其实是处理数据的方法。JavaScript把函数当成一种数据类型,可以像其他类型的数据一样,进行赋值和传递,这为编程带来了很大的灵活性,体现了JavaScript作为“函数式语言”的本质。
这里需要明确的是,JavaScript的所有数据,都可以视为对象。不仅合成类型的数组和函数属于对象的特例,就连原始类型的数据(数值、字符串、布尔值)也可以用对象方式调用。
除了上面这六个类别,JavaScript还定义了两个特殊值null(空)和undefined(未定义)。
null和undefined
-----------------------------------------
null表示"没有对象",即该处不应该有值。典型用法是:
作为函数的参数,表示该函数的参数不是对象。
作为对象原型链的终点。
undefined表示"缺少值",就是此处应该有一个值,但是还未定义。典型用法是:
变量被声明了,但没有赋值时,就等于undefined。
调用函数时,应该提供的参数没有提供,该参数等于undefined。
对象没有赋值的属性,该属性的值为undefined。
函数没有返回值时,默认返回undefined。
布尔值
------------
布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。
下列运算符会返回布尔值:
两元逻辑运算符: && (And),|| (Or)
前置逻辑运算符: ! (Not)
相等运算符:===,!==,==,!=
比较运算符:>,>=,<,<=
如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。
undefined
null
false
0
NaN
""(空字符串)
布尔值往往用于程序流程的控制,请看一个例子。
if (""){ console.log(true);}
// 没有任何输出
上面代码的if命令后面的判断条件,预期应该是一个布尔值,所以JavaScript自动将空字符串,转为布尔值false,导致程序不会进入代码块,所以没有任何输出。
需要特别注意的是,空数组([])和空对象({})对应的布尔值,都是true。
if ([]){ console.log(true);}
// true
if ({}){ console.log(true);}
// true
更多关于数据类型转换的介绍,参见《数据类型转换》一节。
typeof
-------------------
typeof运算符可以返回一个值的数据类型,可能有以下结果:
(1)原始类型
数值、字符串、布尔值分别返回number、string、boolean。
typeof 123 // "number"
typeof "123" // "string"
typeof false // "boolean"
(2)函数
函数返回function。
// 定义一个空函数
function f(){}
typeof f
// "function"
(3)undefined
undefined返回undefined。
typeof undefined
// "undefined"
利用这一点,typeof可以用来检查一个没有声明的变量,而不报错。
v
// ReferenceError: v is not defined
typeof v
// "undefined"
实际编程中,这个特点通常用在判断语句。
// 错误的写法
if (v){
// ...
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined"){
// ...
}
(4)其他
除此以外,都返回object。
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"
从上面代码可以看到,空数组([])的类型也是object,这表示在JavaScript内部,数组本质上只是一种特殊的对象。另外,null的类型也是object,这是由于历史原因造成的,为了兼容以前的代码,后来就没法修改了,并不是说null就属于对象,本质上null是一个类似于undefined的特殊值。
数值
============================
在计算机中要处理的主要对象是数值和字符串,对应的主要工作是数值计算和语言处理。因此,在任何一门程序语言中都必须对数值的处理加以详细规定。
整数和浮点数
JavaScript内部,所有数字统一以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相等的,而且1加上1.0得到的还是一个整数,不会像有些语言那样变成小数。
1 === 1.0 // true
1 + 1.0 // 2
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
0.1 + 0.2 === 0.3
// false,实际等于0.30000000000000004
0.3 / 0.1
// 2.9999999999999996
(0.3-0.2) === (0.2-0.1)
// false,左边等于0.09999999999999998,右边等于0.1
数值精度
根据国际标准IEEE 754,64位浮点数格式的64个二进制位中,第0位到第51位储存有效数字部分,第52到第62位储存指数部分,第63位是符号位,0表示正数,1表示负数。
因此,JavaScript提供的有效数字的精度为53个二进制位(IEEE 754规定有效数字第一位默认为1,再加上后面的52位),也就是说,绝对值小于等于2的53次方的整数都可以精确表示。
Math.pow(2, 53) // 54个二进制位
// 9007199254740992
Math.pow(2, 53) + 1
// 9007199254740992
Math.pow(2, 53) + 2
// 9007199254740994
Math.pow(2, 53) + 3
// 9007199254740996
Math.pow(2, 53) + 4
// 9007199254740996
从上面示例可以看到,大于2的53次方以后,整数运算的结果开始出现错误。所以,大于等于2的53次方的整数,都无法保持精度。
Math.pow(2, 53)
// 9007199254740992 (900兆)
9007199254740992111
// 9007199254740992000
上面示例表明,大于2的53次方以后,多出来的有效数字(最后三位的111)都会无法保存,变成0。
数值范围
上面说了整数最大能表示到2的53次方。另一方面,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则JavaScript能够表示的数值范围为2的1024次方到2的-1023次方(开区间),超出这个范围的数无法表示。这个数字足够大了,因为全宇宙填满沙子,沙子的数量大约10的100次方,还不到2的400次方(《从一到无穷大》)。
如果指数部分等于或超过最大正值1024,JavaScript会返回Infinity(关于Infinity的介绍参见下文),这称为“正向溢出”;如果等于或超过最小负值-1023(即非常接近0),JavaScript会直接把这个数转为0,这称为“负向溢出”。事实上,JavaScript对指数部分的两个极端值(11111111111和00000000000)做了定义,11111111111表示NaN和Infinity,00000000000表示0。
数值的表示法
JavaScript的数值有多种表示方法,可以用字面形式直接表示,也可以采用科学计数法表示,下面是两个科学计数法的例子。
123e3 // 123000
123e-3 // 0.123
以下两种情况,JavaScript会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。
(1)小数点前的数字多于21位。
1234567890123456789012
// 1.2345678901234568e+21
123456789012345678901
// 123456789012345680000
(2)小数点后的零多于5个。
0.0000003 // 3e-7
0.000003 // 0.000003
正常情况下,所有数值都为十进制。如果要表示十六进制的数,必须以0x或0X开头,比如十进制的255等于十六进制的0xff或0Xff。如果要表示八进制数,必须以0开头,比如十进制的255等于八进制的0377。由于八进制表示法的前置0,在处理时很容易造成混乱,有时为了区分一个数到底是八进制还是十进制,会增加很大的麻烦,所以建议不要使用这种表示法。
烦,所以建议不要使用这种表示法。
特殊数值
JavaScript提供几个特殊的数值:0, NaN和Infinity。
正零和负零
严格来说,JavaScript提供零的三种写法:0、+0、-0。它们是等价的。
-0 === +0 // true
0 === -0 // true
0 === +0 // true
但是,如果正零和负零分别当作分母,它们返回的值是不相等的。
(1/+0) === (1/-0) // false
上面代码之所以出现这样结果,是因为除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的(关于Infinity详见后文)。
NaN
(1)含义
NaN是JavaScript的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
5 - 'x'
// NaN
上面代码运行时,会自动将字符串“x”转为数值,但是由于x不是数字,所以最后得到结果为NaN,表示它是“非数字”(NaN)。
另外,一些数学函数的运算结果会出现NaN。
Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN
0除以0也会得到NaN。
0 / 0 // NaN
需要注意的是,NaN不是一种独立的数据类型,而是一种特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。
typeof NaN // 'number'
(2)运算规则
NaN不等于任何值,包括它本身。
NaN === NaN // false
由于数组的indexOf方法,内部使用的是严格相等运算符,所以该方法对NaN不成立。
[NaN].indexOf(NaN) // -1
NaN在布尔运算时被当作false。
Boolean(NaN) // false
NaN与任何数(包括它自己)的运算,得到的都是NaN。
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
(3)判断NaN的方法
isNaN方法可以用来判断一个值是否为NaN。
isNaN(NaN) // true
isNaN(123) // false
但是,isNaN只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN,所以最后返回true,这一点要特别引起注意。也就是说,isNaN为true的值,有可能不是NaN,而是一个字符串。
isNaN("Hello") // true
// 相当于
isNaN(Number("Hello")) // true
出于同样的原因,对于数组和对象,isNaN也返回true。
isNaN({}) // true
isNaN(Number({})) // true
isNaN(["xzy"]) // true
isNaN(Number(["xzy"])) // true
因此,使用isNaN之前,最好判断一下数据类型。
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}
判断NaN更可靠的方法是,利用NaN是JavaScript之中唯一不等于自身的值这个特点,进行判断。
function myIsNaN(value) {
return value !== value;
}
Infinity
(1)定义
Infinity表示“无穷”。除了0除以0得到NaN,其他任意数除以0,得到Infinity。
1 / -0 // -Infinity
1 / +0 // Infinity
上面代码表示,非0值除以0,JavaScript不报错,而是返回Infinity。这是需要特别注意的地方。
Infinity有正负之分。
Infinity === -Infinity // false
Math.pow(+0, -1) // Infinity
Math.pow(-0, -1) // -Infinity
运算结果超出JavaScript可接受范围,也会返回无穷。
Math.pow(2, 2048) // Infinity
-Math.pow(2, 2048) // -Infinity
由于数值正向溢出(overflow)、负向溢出(underflow)和被0除,JavaScript都不报错,所以单纯的数学运算几乎没有可能抛出错误。
(2)运算规则
Infinity的四则运算,符合无穷的数学计算规则。
Infinity + Infinity // Infinity
5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0
Infinity减去或除以Infinity,得到NaN。
Infinity - Infinity // NaN
Infinity / Infinity // NaN
Infinity可以用于布尔运算。可以记住,Infinity是JavaScript中最大的值(NaN除外),-Infinity是最小的值(NaN除外)。
5 > -Infinity // true
5 > Infinity // false
(3)isFinite函数
isFinite函数返回一个布尔值,检查某个值是否为正常值,而不是Infinity。
isFinite(Infinity) // false
isFinite(-1) // true
isFinite(true) // true
isFinite(NaN) // false
上面代码表示,如果对NaN使用isFinite函数,也返回false,表示NaN不是一个正常值。
var x = 0.5;
for(var i =0;i<25;i++) x = x*x;
x // 0
上面代码对0.5连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript就直接将其转为0。
至于具体的最大值和最小值,JavaScript提供Number对象的MAX_VALUE和MIN_VALUE属性表示(参见《Number对象》一节)。
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324
与数值相关的全局方法
parseInt方法
parseInt方法可以将字符串或小数转化为整数。如果字符串头部有空格,空格会被自动去除。
parseInt("123") // 123
parseInt(1.23) // 1
parseInt(' 81') // 81
如果字符串包含不能转化为数字的字符,则不再进行转化,返回已经转好的部分。
parseInt("8a") // 8
parseInt("12**") // 12
parseInt("12.34") // 12
如果字符串的第一个字符不能转化为数字(正负号除外),返回NaN。
parseInt("abc") // NaN
parseInt(".3") // NaN
parseInt("") // NaN
parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制。
parseInt(1000, 2) // 8,表示2进制的1000就是8.
parseInt(1000, 6) // 216
parseInt(1000, 8) // 512
这意味着,可以用parseInt方法进行进制的转换。
parseFloat方法
parseFloat方法用于将一个字符串转为浮点数。
如果字符串包含不能转化为浮点数的字符,则不再进行转化,返回已经转好的部分。
parseFloat("3.14");
parseFloat("314e-2");
parseFloat("0.0314E+2");
parseFloat("3.14more non-digit characters");
上面四个表达式都返回3.14。
parseFloat方法会自动过滤字符串前导的空格。
parseFloat("\t\v\r12.34\n ")
// 12.34
如果第一个字符不能转化为浮点数,则返回NaN。
parseFloat("FF2") // NaN
parseFloat("") // NaN
上面代码说明,parseFloat将空字符串转为NaN。
这使得parseFloat的转换结果不同于Number函数。
parseFloat(true) // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN
字符串
============================
字符串就是若干个排在一起的字符,放在单引号或双引号之中。在JS中没有字符的类型,要表示一个字符,只需创建包含一个字符的字符串即可。
'a'
'abc'
"abc"
用单引号表示的字符串,内部可以使用双引号;用双引号表示的字符串,内部可以使用单引号。推荐使用双引号。
'key="value"'
"It's a long journey"
在单引号字符串内,使用单引号(或者在双引号字符串内,使用双引号),必须在内部的单引号(或者双引号)前面加上反斜杠,用来转义。
'Did she say \'Hello\'?'
"Did she say \"Hello\"?"
字符串默认只能写在一行内,分成多行将会报错。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
var longString = "Long \
long \
long \
string";
longString
// "Long long long string"
上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行,效果与写在同一行完全一样。
但是,这种写法有两个注意点,首先,它是ECMAScript 5新添加的,老式浏览器(如IE 8)不支持,其次,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
连接运算符可以连接多个字符串,用来生成一个新字符串。
'c' + 'a' + 't' = 'cat'
转义
反斜杠在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些:
\0 代表没有内容的字符(\u0000)
\b 后退键(\u0008)
\f 换页符(\u000C)
\n 换行符(\u000A)
\r 回车键(\u000D)
\t 制表符(\u0009)
\v 垂直制表符(\u000B)
\' 单引号(\u0027)
\" 双引号(\u0022)
\\ 反斜杠(\u005C)
\XXX 用三位八进制数(0到377)代表一些特殊符号,比如\251表示版权符号。
\xXX 用两位十六进制数(00到FF)代表一些特殊符号,比如\xA9表示版权符号。
\uXXXX 用四位十六进制的Unicode编号代表某个字符,比如\u00A9表示版权符号。
下面是最后三种字符的特殊写法的例子。
"\251" // "©"
"\xA9" // "©"
"\u00A9" // "©"
如果非特殊字符前面使用反斜杠,则反斜杠会被省略。
"\a"
// "a"
上面代码表示a是一个正常字符,前面加反斜杠没有特殊含义,则反斜杠会被自动省略。
如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前需要再加一个反斜杠,用来对自身转义。
"Prev \\ Next"
// "Prev \ Next"
字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(从0开始)。
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
'hello'[1] // "e"
如果方括号中的数字超过字符串的范围,或者方括号中根本不是数字,则返回undefined。
'abc'[3] // undefined
'abc'[-1] // undefined
'abc'["x"] // undefined
但是,字符串与数组的相似性仅此而已。实际上,字符串是类似数组的对象,且无法改变字符串之中的单个字符。
var s = 'hello';
delete s[0];
s
// "hello"
s[1] = 'a';
s
// "hello"
s[5] = '!';
s
// "hello"
上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。
length属性返回字符串的长度,该属性也是无法改变的。
s.length; //5
字符集
==========================
JavaScript使用Unicode字符集,也就是说在JavaScript内部,所有字符都用Unicode表示。ECMAScript 3要求使用Unicode 2.1或以上版本,ECMAScript 5则要求使用Unicode 3及以上版本。
不仅JavaScript内部使用Unicode储存字符,而且还可以直接在程序中使用Unicode,所有字符都可以写成"\uxxxx"的形式,其中xxxx代表该字符的Unicode编码。比如,\u00A9代表版权符号。
var s = '\u00A9';
s // "©"
每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为2个字节。
但是需要注意的是,UTF-16有两种长度:对于U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,U+1D306对应的字符为