3.1 数据类型
JavaScript实际上有6种数据类型;
布尔值,只有
true
和false
两个值数值,比如
81
和4.21
文本,JavaScript里称之为
字符串
特殊值
undefined
特殊值
null
对象
要了解一种数据类型,不仅要看它包含什么值,还要知道能对这种数据类型执行什么操作。比如,对数值可以执行四则运算,取模求幂等,对字符串可以执行去空格、切分、反序和大写首字符操作。本章将介绍JavaScript的所有数据类型,以及与这些类型相关的各种操作。
3.2 布尔值
在编程中,true
和false
是经常出现的两个值。
var open = true
var ready = false
var gameOver = false
var friendy = true
var enabled = true
这两个值有时候也会以比较结果的形式出现。所谓比较,就是计算一个值是等于===
、不等于!==
、小于<
、小于等于<=
、大于>
,还是大于等于>=
另一个值。
alert(137===5); // false
alert(8!==3.0); // true
alert(2<=2); // true
var x = 16 > 8;
alert(x); // true
这些值叫做布尔值。对布尔值可以执行&&
(与)、||
(或)及!
(非或叫取反)操作。假如x
和y
是布尔值:
x && y
只有在x和y都为真的情况下为真。即真真为真,短路:因比较运算为左结合运算,若x
为假,&&
后面表达式不执行x || y
y或x有一个为真的情况下即为真。即一真为真,短路:若x
为真,则短路跳出,||
后面表达式不执行!x
只有在x
非真情况下为真
代码补充:
console.log(4 < 5 && 15 === 6); // false
console.log(1 === 2 || 15 > -5); // true
console.log(!(3 <= 10)); // false
console.log(true && 3+2) // result:5 第一个表达式为真,则对第二表达式计算
console.log(false && 3+2) // result:false 第一个表达式为假,则短路跳出,不计算第二表达式
console.log(false || 5+5) // result:10 第一个表达式为假,继续检查,执行第二表达式
console.log(true || 5+5) // result:true 第一个表达式为真,短路跳出,不计算第二表达式
小练习
!(true && !false && true) || false // false
false || true && false // false
证实或证否:如果
x
和y
保存的都是布尔值,那么!(x && y)
一定等于(!x == !y)
第三题,可画图,排列组合出所有可能便可证为实,总之&&
:一旦有假则为假,||
:一旦有真则为真
3.3 数值
接下来介绍数值类型。在JavaScript中,数值就跟你想象一样,该怎么写就怎么写:1729、3.141592或者299792458。如果两个数值之间有一个E(或e),那么整个数就等于前面那个数乘以10后面那个数次幂(就是科学计数法- -
)
3.3.1 数值运算
适用于数值的操作符包括+、-、*、/和%(求余也叫取模)。
针对数值可以执行的其他运算还有
Math.floor(x)
取得小于等于x的最大正整数Math.ceil(x)
取得大于等于x的最小整数Math.sqrt(x)
对x开方Math.random(x)
得到一个大于等于0,小于1的数-
Math.pow(x,y)
得到的是x的y次方Math.floor(2.99); // 2 Math.floor(-2.99); // -3 Math.ceil(3.99); // 4 Math.ceil(-3.99); // -3 Math.sqrt(100); // 10 Math.pow(2,4); // 16 Math.random(); // x大于等于0,小于10
3.3.2 大小和精度限制
JavaScript的数值与大多数编程语言的数值一样,不同于我们日常所见的理想化数值。首先,它们收到计算设备固定大小的物理元件的限制。因此,存在一个最大的数值(在JavaScript里这个值约为1.79e308
)和一个最小的数值(-1.79e108
)。任何计算得到超过最大数值或者小于最小数值,都会转化为特殊值Infinity
或者-Infinity
。数值除了存在大小的限制,还存在精度限制,计算得到无法精确表达的数值时,会转换成最接近的可表示的数值。
console.log(12157692622039623539); // 12157692622039624000
console.log(12157692622039623539+1); // 12157692622039623539
console.log(1e200 === 1e200+1); // true
console.log(4.18e-1000); // 0
console.log(0.1+0.2); // 0.30000000000000004
console.log(0.3 == 0.1+0.2) // false
很多脚本不会涉及这类似近似性问题,有的即便涉及也可以容忍。但在某些情况下,精度达不到要求真的会导致问题(比如金融方面的数值)。因此,我们应该对什么情况下可能出现精度不够的问题有个心里预期。如以下几点,
可表示的数值密集度集中在0左右,事实上有一多半都介于1和-1之间;离0越远,就越稀疏。
所有介于正负9e15之间的数都可以精确表示,在这个范围之外,只有部分整数可以表示。
涉及非常大的数值、非常小的数值或者非整数的计算,经常会导致不准确的结果。
如果需要用到最大可表示的值,可以调用表达式Number.MAX_VALUE。最小:-Number.MIX_VALUE保存着大于0的最小可表示值,即-2e1074
3.3.3 NaN
NaN这个特殊值代表的是“Not a Number”,他会在数学计算得到了非数学意义上的结果时出现:
console.log(0/0);
console.log(Infinity*Infinity);
console.log(Infinity-Infinity);
console.log(NaN+16);
console.log(NaN === NaN); // false
NaN不等于任何值,也不等于NaN
检测一个值是否为数值,可用 isNaN()
方法。
console.log(0/0 === NaN); // false
console.log(isNaN(0/0)); // NaN
console.log(isNaN(2.398472398)); // false
console.log(isNaN(NaN)); // true
console.log(isNaN(Infinity)); // false
3.3.4 十六进制数值
JavaScript中的非负数也可以用十六进制记号表示。十六进制数值的计数规则是:0、1、2、3、4、5、6、7、8、9、A、B、C、D、F、10、11、12、......、19、1A、1B、......、1F...。如果想用十六进制表示整数,在数值前面加上0x
:
alert(0x9) // 9
alert(0x9FA) // 2554
alert(-0xCafe) // -51966
alert(0xbad) // 2989
采用十六进制记号不能表示分数,也不能使用科学计数法。
某些版本的JavaScript实现把以0开头的整数当做八进制数值:0、1、2、3、...、7、10、11、...、17、20、21、...。
如下:
alert(07) // 7
alert(011) // 9
alert(-02773) // -1531
3.4 文本
3.4.1 字符、符号与字符集
字符就是有名字的符号,比如:
加号
斯拉夫文字小型字母TSE
黑色的象棋骑士
梵文字母OM
MUSICAL SYMBOL DERMATA BELOW
不要混淆字符和符号,符号是字符的表现形式。比如,符号:K
拉丁字母大写:K
希腊大写字母:K(KAPPA)
梵文大写字母:K(KA)
类似的,符号:Σ
希腊大写字母:Σ(SIGMA)
求和号
符号:φ
带斜线的拉丁大写字母:O
直径
空集
字符集由一组特定的字符组成,其中每个字符都有唯一的编号,叫码点(codepoint)。与大多数语言一样,JavaScript使用unicode字符集。unicode字符集一般用十六进制为每个字符编码。
要了解全部码点,可访问Unicode码点
为什么要知道码点,因为在JavaScript中可以通过他们输出键盘上没有的字符。比如,下面的码点可以输出字符串"Привет"
"\u041fu0440\u0438u0432\u0435\u0442"
每个字符以u开头,后跟代表该字符码点的四位十六进制数字。此外,也可以用x开头跟两位十六进制数字表示字符。比如,字符串"o1É"可以有以下两种表示法:
"o1\xc9"
"o1\u00c9"
有些字符不会显式出来,所以必须用使用码点表示法。比如"从左到右标记"(\u200e)、"从右到左标记"(\u200f)和"零宽度非中断空白"(\ufeff)"。其中,前两个字符会出现在混合了从左到右阅读的文字(英语,西班牙语)与从右到左(希伯来和阿拉伯语)阅读的文字的文档中。
转义序列:
如:
\' 单引号
\" 双引号
\xhh hh是两位十六进制值,相应字符的码点
\uhhhh hhhh是四位十六进制值,也是相应字符码点
\n、\t、\b、\f、\r、\v 换行符、制表符、退格符、进纸符、回车符、制表符
\ 反斜杠本身
反斜杠不仅用于通过码点来表示字符,而且也用于与后续字符组织成所谓的转义序列。
换行符n导致后面的字符出现在下一行,t制表符表示按列对齐文本。如:
alert("1.\tf3\te5\n2.\tg4\t\u265bh4++")
u加码点的方式只在JavaScript代码中有效。如果想在HTML文档显示字符,则需要在码点两侧分别加上和分号;
字符 JavaScript表示法 HTML表示法
黑旗皇后 \u265b ♛
骰子1 \u2680 ⚀
骰子2 \u2681 ⚁
藏文 \u0f5c ཛྷ
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<style type="text/css">
div#die {
font-size: 800%;
}
</style>
</head>
<body>
<button id="roller">Roll</button>
<div id="die"></div>
<script type="text/javascript">
document.getElementById("roller").onclick = function () {
document.getElementById("die").innerHTML =
"ɨ"+Math.floor(Math.random()*6)+";"
}
</script>
</body>
</html>
一个掷骰子的程序,随机生成0~6中一个数。因为骰子六面的字符在Unicode中的码点为2680~2685。
由于程序非常短,所以才把程序嵌入到了HTML文档中。如果代码比较长,请把它们转移到单独的文件中。样式规则也一样,因为这里只有一条规则,我们才把它放到了文档头部。大型页面中,所有样式规则也应该保存在一个单独的文件汇总。这样,文档的结构、样式、行为才能各安其位,相互分离。在软件工程中,关注点分离是一个非常重要的思想。
3.4.2 字符串操作
JavaScript支持很多种字符串操作,比如查询字符串长度(即字符数)、字母的大小写转换以及替换字符串中某一部分,等等。
console.log("Hello, there".length); // 12
console.log("Hello,there".toLowerCase()); // hello,there
console.log("Hello,there".toUpperCase()); // HELLO,THERE
console.log("Hello,there".replace("ello","i")); // Hi,there
Sometimes,你可能想知道某个字符在字符串的哪个位置,或者某个位置上是哪个字符。JavaScript字符串的第一索引为0,第二个字符的索引为1,第三个为2,以此类推。要知道字符串s中位置为p的字符,可以使用表达s.charAt(p)。要定位字符串中文本的位置,可以使用indexOf和lastIndexOf。表达式s.substring(x,y)会得到字符串s中,从位置x开始直到(但不包含)位置y的所有字符。
"Some text".charAt(2) // "m"
var str = "Some text";
str[str.length-1]; // "t" 可获取任意字符串最后一个字符(包含空格)
"Some text".indexOf("me") // 2 返回某个指定的字符串值在字符串中首次出现的位置
"Some text".lastIndexOf("e") // 6 返回一个指定的字符串值最后出现的位置,从后向前搜索
alert("Some text".substring(3,0)) // "e te" 返回上标(包括)至下标(不包括)之间的字符
操作符+
用于拼接两个字符串....略
String 对象的方法 slice()、substring() 和 substr() (不建议使用)都可返回字符串的指定部分。slice() 比 substring() 要灵活一些,因为它允许使用负数作为参数。slice() 与 substr() 有所不同,因为它用两个字符的位置来指定子串,而 substr() 则用字符位置和长度来指定子串。
substr(start,length)
必需。一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置
可选。子串中的字符数。必须是数值。如果省略了该参数,那么返回从 stringObject 的开始位置到结尾的字串
var str = "Hello World";
console.log(str.substr(-3,5)) // "rld" 倒数第3个字符开始的5个字符
console.log(str.substr(1,-1)) // "" length参数必须为正整数
console.log(str.substr(1,4)) // "ello" 正数第2个字符开始的4个字符
console.log(str.substr(2)) // "llo World" 正数第2个字符开始到结尾所有字符
Test中,参数start可为负整数,与W3C描述不同,待验证
slice(start,end)
要抽取的片断的起始下标。如果是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符,-2 指倒数第二个字符,以此类推
紧接着要抽取的片段的结尾的下标。若未指定此参数,则要提取的子串包括 start 到原字符串结尾的字符串。如果该参数是负数,那么它规定的是从字符串的尾部开始算起的位置
var str = "Hello World";
console.log(str.slice(1,4)) // "ell" 第2个字符,到第5个字符的前一个字符,参数end本身并不包含
console.log(str.slice(-5,-2)) // "Wor" 倒数第5个字符,到倒数第二个字符前一个字符,和正数同理
console.log(str.slice(3)) // "lo World" 不指定参数end,提取到结尾字符串
console.log(str.slice(-(str.length-1))) // "ello World" 倒数也同上
substring(start,stop)
必需。一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置
可选。一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1。如果省略该参数,那么返回的子串会一直到字符串的结尾
var str = "Hello World";
console.log(str.substring(2,7)); // "llo W" 从索引为2到索引为7之间的字符(不包括索引7的字母)
console.log(str.substring(-3,2)); // "He" start参数不识别负数,从头部开始选择
console.log(str.substring(3)) // "lo World" 索引为3的字符后所有字符,因为未指定stop参数
console.log(str.substring(4,-2)) // "Hell" 选择从头部首字符至索引为4的字符前一个字符,stop参数不能识别负数
3.5 undefined与null
一般来说,我们都希望通过编程得到一些实际效果,比如某个东西的成本多少(数值),游戏中某个玩家是否处于活动状态(布尔值)或者你辅导员的名字(字符串)。不过,在某些情况下,我们也需要知道某个数据不存在,或者某个数据不可靠。就以辅导员为例,有几个问题来表示:
我有个辅导员,她名叫Alice
我压根就没有辅导员
我可能有也可能没有辅导员,我真的不知道
我不知道自己有没有辅导员,不过我不介意让别人知道这件事
JavaScript为表示第二种情况提供了null,为第三种情况提供了undefined。
var supervisor = "Alice"; // 辅导员是Alice
var chief = null; // 肯定没有辅导员
var assistant = undefined; // 可能有助理
3.6 对象
3.6.1 对象基础
在JavaScript中,所有不是布尔值、数值、字符串、null和undefined的值,都是对象。对象有属性,属性有值。属性名可以是字符串(位于引号内),也可是非负整数(0、1、2...)。属性值也可以是对象,可以定义复杂的数据结构。对象字面量是一种定义的表达式,如:
var dress = {
size:4,
color:"green",
brand:"DKNY",
price:834.95
};
var Location = {
latitude:31.131013,
longitude:29.976977
};
var part = {
"serial number":"367DRT2219873X-785-11P",
description:"air intake manifold",
"unitr cost":29.95
};
var p = {
name:{first:"Sean",last:"O' Brien"},
country:"Ireland",
birth:{year:1981,month:2,day:17},
kidNames:{1:"Ciara",2:"Bearach",3:"Mairead",4:"Aisling"}
};
定义对象之后,可以使用点或方括号读取属性的值。
p.country; // "Ireland"
p["country"]; // 方括号内属性名必须加引号
p:birth.year; // 1981
p.birth["year"]; // 1981
p["birth"].year; // 1981
p["birth"]["year"]; // 1981
p.kidNames[4]; // "Aisling"
p["kidNames"][3] // "Aisling"
用点号访问属性的方式虽然简洁,却不能用于读取以整数命名的属性(比如不能用"a.1"),在ES3中也不能读取以JavaScript保留字命名的属性。这时候,就要使用方括号表示法(a[10]、a["var"])。方括号表示法还适用于包含空格及其他非字母的属性(part["serial number"]),对象属性并非一成不变,可以随时给对象添加或删除属性,如:
var dog = {}; // 将一个对象引用赋值给变量
dog.name = "Karl"; // 添加属性1
dog.breed = "Rottweiler"; // 添加属性2
console.log(delete dog.name); // 删除属性2,返回true
console.log(dog.name); // undefined
在使用JavaScript开发Web应用,构成网页的元素就是带有属性的对象。下面这个小程序演示这一点,它通过每2秒随机变换一次笑脸的位置在实现笑脸在浏览器中的跳跃。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScript Temperature Converter</title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<style type="text/css">
#face {
font-size: 500%;
position: absolute;
}
</style>
</head>
<body>
<div id="face">☺</div>
<script type="text/javascript">
var style = document.getElementById("face").style;
var move = function () {
style.left = Math.floor(Math.random()*500)+"px";
style.top = Math.floor(Math.random()*400)+"px";
}
setInterval(move,2000);
</script>
</body>
</html>
这里的笑脸就是字符U+263a,嵌在了ID为face的div的元素中。作为对象,这个元素有一个style属性,而这个属性本身也是一个对象,又有
position、left和top等属性。这段脚本通过setinterval每2000毫秒执行一次move函数。样式属性left和top的值都是字符串,有几种格式,其中一种就是288px这样的属性值,表示相对于窗口的偏移量。这个脚本每次运行move函数都会重新设定样式的值。JavaScript检测到样式变化就会刷新浏览器窗口。除非你关闭浏览器窗口或者打开了其他网页,否则这个程序会一直运行。
小练习:
什么情况下必须使用方括号表访问对象的属性?
使用了不能作为标识符的属性名的情况
将变量的值作为属性名使用的情况
将表达式的求值结果作为属性名使用的情况
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
// 自行使用console.log()或者alert()调用试验
var test = {
1:"one",
name:"name",
var:"var",
'2 333':2333,
"foo-bar":5,
x:"x"
};
test["1"]; // "one"
test.1; // Unexpected number错误
test[1]; // Unexpected number错误
test["name"]; // "name"
test.name; // "name"
test.var; // "var"
test["var"]; // "var"
test["2 333"]; // 属性名含有空格必须用方括号
// 属性名不能使用数字!
test.foo-bar; // 含有横杠的属性名点运算会引起错误,会解析为:obj.foo减去bar,引发错误
test["foo-bar"]; // 5
// 下面展示将变量的值作为属性名使用的情况
var key = "name";
test[key]; // "name" 方括号内的变量存储的值依旧是引号包裹的字符串,方括号正常访问
</script>
</body>
</html>
如下代码会输出什么,为什么?
var pet = {
name:"Oreo",
type:"Rat"
};
alert(pet[name]);
Answer:方括号内的值是属性名的字符串,缺少引号,将搜索name变量,但是并不存在此变量及相应的值,所以将弹出undefined
3.6.2 理解对象引用
对象与其他五种值(数值、字符串、布尔值、null、undefined)是不同的,其他五种值统称基本类型值,对象与他们的区别主要表现在两方面。虽然说起来有点严肃,但明白这两点区别对于正确高效地使用对象至关重要,必须牢记。第一点:
对象表达式的值并非对象本身,而是一个指向对象的引用。
如图示,基本类型值直接存储在变量中,而对象不是。对象的值中存储的是指向对象的引用(变量b)。
由于对象的值其实是引用,所以把一个对象赋值给一个变量,实际上会产生该对象引用的一个副本,而不会赋值对象本身。换句话说,对象赋值不会产生新对象。想想看,对象的赋值与基本类型值赋值过程并没有不同。变量间的赋值就是把保存一个盒子里的东西赋值一份再保存到另一个盒子中,而该盒子中存储的可能是数值,也可能是一个引用。下图表示基本类型赋值和对象赋值,可以想一想。
对象与其他类型值的第二个重要的区别是:
对同一个对象字面量的每次求值,都会产生一个新对象。
如上:脚本声明了三个变量,创建了两个对象。虽然两个对象拥有相同的属性,每个属性的值也相同,但它们却是两个不同的对象,因此这个脚本会创建两个对象。
关于变量只保存对象的引用而非对象这一点,不仅在赋值的时候有所体现,在等同性测试的时候也会有所体现。这两种情况下,我们都必须搞明
白。对于测试表达式x===y
,我们想知道x
和y
中等值是否相同。
var a = {x:1,y:2}; // 声明对象字面量1
var b = a; // 将变量a引用对象的引用赋值给变量b
var c = {x:1,y:2}; // 声明对象字面量2
alert(a===b); // true
alert(a===c); // false
// 1. 即使两个对象的内部结构相同,但是比较结果:false
// 2. 相等运算,比较的是引用的对象是否来自同一个对象,a、b变量引用同一个对象,全等运算为:true
// 3. 值得一提的是,a,b变量操作过程中,并没有产生新对象
// 再次强调!对象相等运算比较的是,引用对象来源是否是同一个对象
如下,一个对象可以同时被多个变量引用,因此通过其中任何一个变量都可以修改对象的属性,也都可以查看修改后的结果。
小练习:
-
对表达式
{x:1,y:2}=== {x:1,y:2}
求值,解释结果。false:使用{}大括号就已经声明了对象字面量,本质上和上面的变量a,c引用的对象是相同的,所过结果为假。
3.6.3 对象原型
每个JavaScript对象都有一个暗藏的链接指向自己的原型对象(prototype),如果你读取的的属性不在对象本身上,那么JavaScript就会进一步查询这个对象的原型对象。如果在这个原型对象上也没找到,还会进一步查询原型对象的原型对象,以此类推。这个原型链最后一个对象的暗藏链接,应该指向null值。如果整个原型链都没有你想读取的属性,那么你会看到一个错误。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
var protoCircle = {
x:0,
y:0,
radius:1,
color:"black",
};
var c1 = Object.create(protoCircle);
c1.x = 4;
c1.color = "green";
console.log(c1.radius); // 1
// 注意 c1.y === 0,c1.radius === 1 (继承的属性)
</script>
</body>
</html>
实际上,这里基于原型创建c1对象使用的是Object.create()
。这是ES5定义的操作,在老版本JavaScript引擎中,还有一种基于原型创建对象爱的技术,后面会继续讨论。
在需要定义大量相似对象时,原型是非常有用的。下图又基于一个原型创建了两个新对象。其中一个完全没有自己的属性,因而它的属性完全继承原型对象。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
var protoCircle = {
x:0,
y:0,
radius:1,
color:"black",
};
var c1 = Object.create(protoCircle);
c1.x = 4;
c1.color = "green";
var c2 = Object.create(protoCircle);
c2.x = 4;
c2.radius = 15;
var c3 = Object.create(protoCircle);
c3.color = "c3.orange";
console.log(c3.color); // orange 此处键值对是对c3对象自有属性创建,不是继承属性创建
delete c3.__proto__.radius; // 可通过_proto_指向原型,这里删除了原型的radius属性
console.log(c1.radius); // undefined
console.log(protoCircle.radius); // undefined
</script>
</body>
</html>
小练习:
说说什么是自有属性,什么是继承属性。
字面意思先简单理解,自有属性在自身,直接访问,不需通过原型链。继承属性继承自原型,通过_proto_
向上查询
如果运行
protoCircle.radius = 5
,那么访问c1.radius
和c2.radius
结果发生什么?
c1.radius = 5
,c2.radius = 15
,原型的属性改变,则凡是继承它属性的都会受到影响。c1,c2对象均继承自原型,但是c2存在自有属性,并未查询原型,所以读取的是自有属性。
3.6.4 自引用对象
对象的属性可以引用自身,两个对象也可以通过属性相互作用。但这种情况下,光靠对象字面量无法描述了。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
var one = {name:"obj-one"};
var two = {name:"obj-two",Link:one};
one.Link = two;
alert(two.Link.name) // "obj-one"
</script>
</body>
</html>
于是先使用对象字面量创建对象的部分属性,然后在通过赋值方式定义其他属性。当然也可以为彼此赋值。
3.7 数组
数组是一种特殊的对象,它的属性是从0开始的连续非负整数,而且有一个名为length
的对应属性。之所以说数组特殊,主要是因为不能使用常规的对象字面量来创建它,必须使用另一种专用语法:
var a = [];
var b =[8,false,[[null,9]]];
var numbers = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six"
];
这种专用语法实际上是用来创建属性0,1,2...,还有length
的。当然,length属性也很特殊:为它赋值可以扩大或缩小数组,那么新增属性会被设定为undefined。另外,在超过数组长度的某个位置上赋值也可以扩大数组。
var a = [9,3,2,1,3]; // a[0]值为9,a.length等于5
a[20] = 6; // a[5]到a[19]的值都是undefined,a[20]说明有20 + a[0]个值,length:21
alert(a.length); // 21
a.length = 50; // a[21]到a[49]的值都是undefined
a.length = 3; // a现在是[9,3,2]
基于某个分隔符拆分(split)字符串、切割(slice)现有数组都可以创建新数组,另外把两个数组拼接(concat)起来也可以创建新数组。反之,把数组元素连接(join)起来可以创建一个由同一个分隔符分隔的字符串:
var s = "A red boat";
var a = s.split(" "); // a是["A","red","boat"]
var b = [9,3,2,1,3,7];
var c = b.slice(2,5); // c是[2,1,3]
var d = c.concat(a); // d是[2,1,3,"A","red","boat"]
console.log(d.join("|")) // "2|1|3|A|red|blat"
注意,使用slice切割数组b中从位置2到位置5的元素,返回数组是[ b[2],b[3],b[4] ]
。换句话说,切割得到的数组包含位于切割起点的元素,不
包含位于切割终点的元素;和substring
截取字符串的操作相同。a.slice(x,y),则返回a[x]和a[y-1]构成的数组。还有另外两种slice操作:
var a = [9,4,1,7,8];
var b = a.slice(2); // a[2]开始到最后
var c = a.slice(); // 所有索引,得到a的副本
拆分字符串的split
操作不会修改原字符串、切割(slice
)、拼接(concat
)和连接(join
)数组元素也不会修改原数组。不过。确实有一些操作会修改数组对象本身。比如,可以使用push
在数组末尾、使用unshift
在数组开头添加元素;反之,可以使用pop
在数组末尾、使用shift
在数组开头删除元素。另外,还可使用reverse
反转数组元素的顺序,使用sort
对数组元素进行排序。这些会修改数组本身的操作为可变操作(mutator)。
var a = []; // a是长度(length)为0的数组
var b = [3,5]; // length:2
b.push(2); // [3,5,2]
b.unshift(7); // [7,3,5,2]
a.push(3,10,5); // [3,10,5]
a.reverse(); // [5,10,3]
a.pop(); // [5,10]
a.shift(); // [10]
b.push(a[0],1); // [7,3,5,2,10,1]
b.sort(); // [1,10,2,3,5,7]
b.reverse(); // [7,5,3,2,10,1]
JavaScript默认是将所有的数组元素都当成字符串来排序的,就算数组中包含数值、布尔值、对象或别的数据也一样。既然是按字符串排序,那排序标准就是字母表,因此字符串"1"小于"10",后者有小于"2"(就如同"foot"小于"football",后者又小于"goal")。实际上也可以按照数值进行排序,具体后面章节会介绍。
把数组放到对象字面量中,或者反之,可以创造出任何复杂的数组结构:
var song = {
title:"In My Head",
track_number:10,
album:"Rock Steady",
artist:"No Doubt",
authors:["Gwen Stefani","Tony Kannal","Tom Dumont"],
duration:205
};
var triangle = [{x:0,y:0},{x:3,y:-6},{x:-4,y:-1.5}];
实践中,我们会使用对象(单独)描述一种具体的事物,如裙子、人、歌曲和坐标点等等,会用数组描述一组事物,如一张专辑中的歌曲或一个多边形的顶点。数组也不是构造数据集合的唯一方式。
小练习:
把变量song和triangle用图表示出来,包括引用的对象。(PS大法...这书实在啊,教JS顺便复习PS基础操作:)
请把变量a引用的数组在执行下列操作之后的结果用图示形式画出来:
var a = [1,2,3,4];a.unshift(a.pop());
答:图就免了,pop()
;会删除数组末尾元素并返回,之后被unshift()
接收,添加至数组头部,需要注意的是,由于数组长度不变,删除末尾元素后,只是a[3]的值变为undefined,所以最后a数组内元素是:[4,1,2,3,undefined]
3.8 类型转换
3.8.1 弱类型
目前,接触到的从操作符,如:
布尔值 && 布尔值
布尔值 || 布尔值
!布尔值
-数值
数值+,-,*,/,%数值
Math.sqrt(ceil,floor,PI,pow,random...)(数值)
字符串+字符串
字符串.toUpperCase()
字符串.indexOf(数值)
对象[字符串]
...
如果让操作符搭配一个或多个"错误的"的类型会怎么样?
这里是数值:
7 * false // 0
7 * true // 7
7 * "5" // 35
7 * " " // 0
7 * "dog" // NaN
7 * null // 0
7 * undefined // NaN
7 * {x:1} // NaN
这里是布尔值:
!5 // false
!0 // true
!"dog" // false
!"" // true
!" " // false
!null // true
!undefined // true
!{x:1} // false
这里是字符串:
"xyz"+false // "xyzfalse"
"xyz"+true // "xyztrue"
"xyz"+7 // "xyz7"
"xyz"+null // "xyznull"
"xyz"+undefined // "xyzundefined"
"xyz"+{x:1} // xyz[object Object]
"xyz"+[1,2,3] // xyz1,2,3
通过以上实验,可以看出操作符搭配错误的类型,不仅不会报错,还会正常显示。通过类型转换以有意义的方式处理了错误类型的值。
转换为数值:
false
被转换为0
,true
被转换为1
,字符串为转换成可能的值,null
被转换成0
;如果无法把字符转换成数值,则转换成NaN
。对象x
会调用x.valueOf()
;转换为布尔值:
0
、空字符串(""
)、null
、undefined
、NaN
。其他值都被转换成true
。false一般称为假值,true则为真值;转换为字符串:JavaScript会按常理处理,如例子所示。只是对象x会调用
x.toString()
,具体后面会讨论。
关于valueOf和toString的详细解释,&&与||其实他们并不是真的需要比较布尔值,后续继续讨论。
由于存在隐式数据类型转换,JavaScript被称为弱类型编程语言。在强类型编程语言中,由错误类型值构成的表达式会导致错误。如果脚本里含有这种"病句",要么不会被允许执行,要么干脆直接停止工作,要么会在问题表达式被求值时抛出异常。
有时候,这种自动类型转换会造成一些意外。比如isNaN
有时会产生一些难以理解的行为,我们知道它的作用是判断某个值是不是非数值(Not a Number)。一般来说,我们会觉得布尔值、字符串、null会被判定为非数值,但实验表明并非如此。
isNaN(true); // false,因为true转换成了1
isNaN(null); // false,因为null转换成了0
isNaN("water"); // true,很明显
isNaN("100"); // false,因为"100"转换成了100
看来,应该把isNaN
的作用解读为"不能转换成数值"。再比如数值与字符串间的转换,也是不可避免会碰到的:
var x = prompt("Enter a number");
var y = prompt("Enter another number");
alert(x+y); // 结果是拼接后的字符串,而非数学意义上的加法
每次提示都输入2,结果是22,因为对prompt
求值的结果总是字符串,而+操作符又同时适用于数值和字符串。如果提示框显示x-y
,执行的那就是数值减法。因为-
在JavaScript只能用于数值,所以得到的字符串都会先被转换成数值。当然,乘法和除法也是"安全的"。但不管怎样,在数值和字符串相遇的时候,你总得自己多加小心才是。
3.8.2 显式转换
鉴于字符串与数值间的转换容易出问题,很多JavaScript程序员倾向于在代码中显式地实现字符串到数值的转换。
如:
"3.14"-0; // 3.14
"3.14"*1; // 3.14
("3.14"/1); // 3.14
+"3.14"; // 3.14 [速度最快]
Number("3.14"); // 3.14 [清除,速度最慢]
parseFloat("3.14"); // 3.14
前三个表达式使用了-
、*
、/
,作为数值操作符,它们会在执行计算(在这里都是无效计算)之前把字符串转换成数值。第四个表达式也使用了一个数值操作符,叫做一元加。把它放到数值前面,不会产生任何操作,而与之对应的一元减操作符就不一样了。
+4 // 4 [一元加]
-4 // -4 [一元减]
由于一元加需要一个数值参与计算,因此如果它后面是一个字符串,JavaScript就会把这个字符串转换成数值。使用+把字符串转换成数值的做法显得有点神秘,但这种技术很方便,而且也并不少见。类似这种的编程方法被称为:习语,对外人并不显见,必须习而得之。
var x = +prompt("Enter a nunber");
var y = +prompt("Enter another nunber");
alert(x+y); // 算术加法
那么Number(s)
和parseFloat(s)
中s如果是字符串,结果又会怎么样?
var x = Number(prompt("Enter....number"));
var y = Number(prompt("Enter another...ber"));
alert(x+y); // 算术加法
不过,很多程序员并不使用这种方式,因为效率低:JavaScript引擎在运行以上代码时会额外多做一些工作,导致脚本执行速度降低,内存占用增多。究其原因,就是Number生成的并非基本类型值,而是一个包装着数值的对象,这个对象在被当成数值使用时会把数值拿出来。对象比基本类型值更让JavaScript引擎费劲,一方面创建对象需要分配空间,而在不需要对象时,还要销毁对象。然而,有时候可读性确实比效率更重要,也可以使用Number。
parseFloat(string);
还可以使用parseFloat及parseInt显式把字符串转换成数值。转换从字符串开头开始,但不一定转换整个字符串,而且首尾的空格会被忽略。
console.log(parseFloat("23.9")); // 23.9
console.log(parseFloat("5.663E2")); // 566.3
console.log(parseFloat(" 8.11 ")); // 8.11
console.log(parseFloat("52.3xyz")); // 52.3
console.log(parseFloat("xyz52.3")); // NaN
console.log(parseFloat("3 .5 .6")); // 3
console.log(parseFloat("123456 454")); // 123456
// 首尾空格忽略
// 除了+、-、数字、小数点、科学计数法(E或e)这些符号,其他的字符本身以及后面的所有字符都会被忽略
// 如果参数字符串第一个字符串不能被解析为数字,会直接返回NaN
W3C官方说明:
parseFloat 是全局函数,不属于任何对象。
parseFloat 将它的字符串参数解析成为浮点数并返回。如果在解析过程中遇到了正负号(+ 或 -)、数字 (0-9)、小数点,或者科学记数法中的指数(e 或 E)以外的字符,则它会忽略该字符以及之后的所有字符,返回当前已经解析到的浮点数。同时参数字符串首位的空白符会被忽略。
如果参数字符串的第一个字符不能被解析成为数字,则 parseFloat 返回 NaN。提示:您可以通过调用 isNaN 函数来判断 parseFloat 的返回结果是否是 NaN。如果让 NaN 作为了任意数学运算的操作数,则运算结果必定也是 NaN。
parseInt(string, radix);
parseInt得到的数值没有小数部分,其实parseInt中的Int就是integer,整数的意思。数值中含有小数的叫浮点数(Why?)
console.log(parseInt("23.9")); // 23
console.log(parseInt("5.663E2")); // 5
console.log(parseInt(" 8.11 ")); // 5
console.log(parseInt("52.3xyz")); // 52
console.log(parseInt("xyz52.3")); // NaN
使用parseInt可以转换基数为2到36的任何数值,进制转换。
console.log(parseInt("75EF2",16)); // 483058
console.log(parseInt("50",8)); // 40
console.log(parseInt("110101",2)); // 53
console.log(parseInt("hello",30)); // 14167554
console.log(parseInt("36",2)); // NaN
W3C官方说明:
当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数(前提)。
举例,如果
string
以 "0x" 开头,parseInt()
会把string
的其余部分解析为十六进制的整数。如果string
以 0 开头,那么 ECMAScript v3 允许 >parseInt()
的一个实现把其后的字符解析为八进制或十六进制的数字。如果 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。
3.8.3 松散相等操作符
因为JavaScript是弱类型的,所以在执行+-*/<等计算时,要确保数据的类型匹配,就算是相等操作符,也会发生同样的类型转换。
console.log(false === 0) // false 严格相等比较(类型不转换)
console.log(false == 0) // true 相等比较(类型转换)
JavaScript提供两种相等测试机制,一种会执行隐式类型转换,一种不会。
相等操作符===
当且仅当两个表达式的值相同,类型相同时才会返回true
(除了null)。而!==
的结果自然与===
截然相反。这两种相等操作符叫做严格相等操作符。
另一种测试机制==
和!=
被人喻为严格操作符的"邪恶表亲"。==
在测试之前会不顾一切地转换表达式类型,因此尽量不要使用它。
不过,==
的转换是有意义的,只是要记住比难。JavaScript官方规范对此有详细描述,但我们这里可以简短归纳。
-
要确定
x == y
的结果,JavaScript会尝试对它们进行类型转换,以便比较。如果x和y中有一个字符串,有一个数值,JavaScript会把字符串转换成数值。
如果一个是布尔值,另一个不同,JavaScript会把布尔值转换成数值。
如果是一个对象,另一个是字符串是或数值,JavaScript会把对象转换成字符串或者数值。
最后,
undefined == null
、null == undefined
。待寻找原因。
由于上述情况复杂,一般更倾向于使用严格相等操作符
===
和!==
。虽然松散相等操作符==
和!==
可以像前面讨论的那样在代码提供一些快捷操作,即自动帮我们实现字符串到数值的转换,但转换结果很多情况下无法预见,规则很难牢记。
使用
===
和!==
,尽量不使用==
和!=
。
3.9 typeof
操作符
有时候,你可能必须某个值的类型。JavaScript有一个古怪的typeof操作符,能够返回关于表达式类型的字符串描述。说它古怪,是因为它的返回值有时候可信,有时候又不可信。
typeof 101.3; // number
typeof false; // boolean
typeof "dog"; // string
typeof {x:1,y:2}; // object
typeof undefined; // undefined
typeof null; // object
typeof [1,2,3]; // object
typeof alert; // function
typeof (typeof 1); // string
null
的类型没有里有是object
。数组的类型返回object
倒可以理解,毕竟数组是对象。可为什么函数的类型又不是了呢?函数也是一种对象啊。
小练习:
-
测试
typeof Infinity
和typeof NaN
console.log(typeof Infinity); // number console.log(typeof NaN); // number
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。