概念(了解)
计算机程序的运行需要对值(value)进行操作。
在编程语言中,能够表示并操作的值的类型称做数据类型(type),编程语言最基本的特性就是能够支持多种数据类型。
当程序需要将值保存起来以备将来使用时,便将其赋值给(将值“保存”到)一个变量(variable)。变量是一个值的符号名称,可以通过名称来获得对值的引用。
js 数据类型“一览众山小”(掌握)
本书在此章节开头写了一个很好的概括,我看过许多博文和视频,他们都没有讲得如此的清楚,大多数人都是把重心放在了 api 上面,然而作者在这里并没有讲 api,真正的讲了数据类型,让我对 js 有了一个整体的认知,在我学习 python 等其他语言时,我发现它们的数据设计都是大同小异的,读了这篇文章真的是让我更容易学习计算机语言,这个概括在后面每一章开头都不会重现的,有许多人看着冗长就直接跳过了,非常可惜。因此本段不会做太多的删减。
原始类型
JavaScript 的数据类型分为两类:原始类型(primitive type)和对象类型(object type)。JavaScript 中的原始类型包括数字、字符串和布尔值。
JavaScript 原始类型中有两个特殊的原始值:null(空)和 undefined(未定义),它们不是数字、字符串和布尔值。它们通常分别代表了各自特殊类型的唯一的成员。
对象类型
javaScript 中除了数字、字符串、布尔值、nu11 和 undefined 之外的就是对象了。对象(object)是属性(property)的集合,每个属性都由“名/值对”(值可以是原始值,比如数字、字符串,也可以是对象)构成。
数组
普通的 JavaScript 对象是“命名值”的无序集合。JavaScript 同样定义了一种特殊对象一一数组(array),表示带编号的值的有序集合。JavaScript 为数组定义了专用的语法,使数组拥有一些和普通对象不同的特有行为特性。
函数
JavaScript 还定义了另一种特殊对象一一函数。函数是具有与它相关联的可执行代码的对象,通过调用函数来运行可执行代码,并返回运算结果。和数组一样,函数的行为特征和其他对象都不一样。JavaScript 为使用函数定义了专用语法。对于 JavaScript 函数来讲,最重要的是,它们都是真值,并且 JavaScript 可以将它们当做普通对象来对待。
类对象
如果函数用来初始化(使用 new 运算符)一个新建的对象,我们称之为构造函数(constructor)。每个构造函数定义了一一类(class)对象一一由构造函数初始化的对象组成的集合。类可以看做是对象类型的子类型。除了数组(Array)类和函数(Function)类之外,JavaScript 语言核心定义了其他三种有用的类。日期(Date)类定义了代表日期的对象。正则(RegExp)类定义了表示正则表达式(一种强大的模式匹配工具,在第 10 章会讲到)的对象。错误(Error)类定义了那些表示 JavaScript 程序中运行时错误和语法错误的对象。可以通过定义自己的构造函数来定义需要的类。
内存管理
JavaScript 解释器有自己的内存管理机制,可以自动对内存进行垃圾回收(garbage collection)。这意味着程序可以按需创建对象,程序员则不必担心这些对象的销毁和内存回收。当不再有任何引用指向一个对象,解释器就会知道这个对象没用了,然后自动回收它所占用的内存资源。
面向对象
JavaScript 是一种面向对象的语言。不严格地讲,这意味着我们不用全局的定义函数去操作不同类型的值,数据类型本身可以定义方法(method)来使用值。例如,要对数组 a 中的元素进行排序,不必要将 a 传入 sort()函数,而是调用 a 的一个方法 sort():
a.sort();//sort(a)的面向对象的版本
第 9 章将会讲述方法的定义。从技术上讲,只有 JavaScript 对象才能拥有方法。然而,数字、字符串和布尔值也可以拥有自己的方法。在 JavaScript 中,只有 null 和 undefined 是无法拥有方法的值。
可变类型和不可变类型
JavaScript 的类型可以分为原始类型和对象类型,也可分为可以拥有方法的类型和不能拥有方法的类型,同样可分为可变(mutable)类型和不可变(immutable)类型。可变类型的值是可修改的。对象和数组属于可变类型:JavaScript 程序可以更改对象属性值和数组元素的值。数字、布尔值、null 和 undefined 属于不可变类型——比如,修改一个数值的内容本身就说不通。字符串可以看成由字符组成的数组,你可能会认为它是可变的。然而在 JavaScript 中,字符串是不可变的:可以访问字符串任意位置的文本,但 JavaScript 并未提供修改已知字符串的文本内容的方法。
数据类型转换
JavaScript 可以自由地进行数据类型转换。比如,如果在程序期望使用字符串的地方使用了数字,JavaScript 会自动将数字转换为字符串。如果在期望使用布尔值的地方使用了非布尔值,JavaScript 也会进行相应的转换。JavaScript 中灵活的类型转换规则对“判断相等”(equality)的定义亦有影响。
变量和作用域
JavaScript 变量是无类型的(untyped),变量可以被赋予任何类型的值,同样一个变量也可以重新赋予不同类型的值。使用 var 关键字来声明(declare)变量。JavaScript 采用词法作用域(lexical scoping)。不在任何函数内声明的变量称做全局变量(global variable),它在 JavaScript 程序中的任何地方都是可见的。在函数内声明的变量具有函数作用域(function scope),并且只在函数内可见。
数字
范围(了解)
和其他编程语言不同,JavaScript 不区分整数值和浮点数值。JavaScript 中的所有数字均用浮点数值表示。JavaScript 采用 IEEE 754 标准定义的 64 位浮点格式表示数字,这意味着它能表示的最大值是+-1.7976931348623157×10308,最小值是+-5×10-324。
按照 JavaScript 中的数字格式,能够表示的整数范围是从-9007199254740992~
9007 199254740992(即-2^53~2^53),包含边界值。如果使用了超过此范围的整数,则无法保证低位数字的精度。然而需要注意的是,JavaScript 中实际的操作(比如数组索引,以及第 4 章讲到的位操作符)则是基于 32 位整数。
整型直接量(了解)
除了十进制的整型直接量,JavaScript 同样能识别十六进制(以 16 为基数)值。所谓十六进制的直接量是指以“0x”或“0X”为前缀,其后跟随十六进制数串的直接量。下面是十六进制整型直接量的例子:
Oxff; //15*16+15=255(十进制)
0xcafe911;
2
尽管 ECMAScript 标准不支持八进制直接量,但 JavaScript 的某些实现可以允许采用八进制(基数为 8)形式表示整数。八进制直接量以数字 0 开始,其后跟随一个由 0~7(包括 0 和 7)之间的数字组成的序列,例如:
0377; //3*64+7*8+7=255(十进制)
由于某些 JavaScript 的实现支持八进制直接量,而有些不支持,因此最好不要使用以 0 为前缀的整型直接量,毕竟我们也无法得知当前 JavaScript 的实现是否支持八进制的解析。 在 ECMAScript6 的严格模式下,八进制直接量是明令禁止的。
浮点型直接量(了解)
3.14;
2345.789;
0.333333333333333333;
6.02e23; //6.02×10^23
1.4738223e-32; //1.4738223×10^32
2
3
4
5
JavaScript 中的算术运算(掌握)
JavaScript 程序是使用语言本身提供的算术运算符来进行数字运算的。这些运算符包括加法运算符(+)、减法运算符(一)、乘法运算符(*)、除法运算符(/)和求余(求整除后的余数)运算符(%),除了基本的运算符外,JavaScript 还支持更加复杂的算术运算,这些复杂运算通过作为 Math 对象的属性定义的函数和常量来实现。
溢出(overflow)
当数字运算结果超过了 JavaScript 所能表示的数字上限(溢出),结果为一个特殊的无穷大(infinity)值,在 JavaScript 中以 Infinity 表示。同样地,当负数的值超过了 JavaScript 所能表示的负数范围,结果为负无穷大,在 JavaScript 中以-Infinity 表示。 无穷大值的行为特性和我们所期望的是一致的:基于它们的加、减、乘和除运算结果还是无穷大值(当然还保留它们的正负号)。
下溢(underflow)
下溢(underflow)是当运算结果无限接近于零并比 JavaScript 能表示的最小值还小的时候发生的一种情形。这种情况下,JavaScript 将会返回 0。当一个负数发生下溢时,JavaScript 返回一个特殊的值“负零”。这个值(负零)几乎和正常的零完全一样,JavaScript 程序员很少用到负零。
被零整除与 NaN
被零整除在 JavaScript 并不报错:它只是简单的返回无穷大(Infinity)或负无穷大(-Infinity)。但有一个例外,零除以零是没有意义的,这种整除运算结果也是一个非数字(not-a-number)值,用 NaN 表示。无穷大除以无穷大、给任意负数作开方运算或者算术运算符与不是数字或无法转换为数字的操作数一起使用时都将返回 NaN。
JavaScript 中的非数字值有一点特殊:它和任何值都不相等,包括自身。也就是说,没办法通过 x==NaN 来判断变量 x 是否是 NaN。相反,应当使用 x!=x 来判断,当且仅当 x 为 NaN 的时候,表达式的结果才为 true。函数isNaN()的作用与此类似,如果参数是 NaN 或者是一个非数字值(比如字符串和对象),则返回 true。JavaScript 中有一个类似的函数isFinite(),在参数不是 NaN、Infinity 或-Infinity 的时候返回 true。
负零值
负零值同样有些特殊,它和正零值是相等的(甚至使用 JavaScript 的严格相等测试来判断)。这意味着这两个值几乎一模一样,除了作为除数之外:
var zero = 0; //正常的零值
zero === negz; //=>true:正零值和负零值相等
1 / zero === 1 / negz; //=>false:正无穷大和负无穷大不等
2
3
二进制浮点数和四舍五入错误(掌握)
实数有无数个,但 JavaScript 通过浮点数的形式只能表示其中有限的个数(确切地说是 18437736874454810627 个)。也就是说,当在 JavaScript 中使用实数的时候,常常只是真实值的一个近似表示。
JavaScript 采用了 IEEE-754 浮点数表示法(几乎所有现代编程语言所采用),这是一种二进制表示法,可以精确地表示分数,比如 1/2、1/8 和 1/1024。遗憾的是,我们常用的分数(特别是在金融计算方面)都是十进制分数 1/10、1/100 等。二进制浮点数表示法并不能精确表示类似 0.1 这样简单的数字。
JavaScript 中的数字具有足够的精度,并可以极其近似于 0.1。但事实是,数字不能精确表述的确带来了一些问题。看下这段代码:
var x = 0.3 - 0.2; //30美分减去20美分
var y = 0.2 - 0.1; //20美分减去10美分
x == y; //=>false:两值不相等!
x == 0.1; //=>false:.3-.2不等于.1
y == 0.1; //=>true:.2-.1等于.1
2
3
4
5
JavaScript 的未来版本或许会支持十进制数字类型以避免这些舍入问题。在这之前你可能更愿意使用大整数进行重要的金融计算,例如,要使用整数“分”而不要使用小数“元”进行基于货币单位的运算。
日期和时间(了解)
JavaScript 语言核心包括 Date()构造函数,用来创建表示日期和时间的对象。这些日期对象的方法为日期计算提供了简单的 API。日期对象不像数字那样是基本数据类型。详细细节在后面章节会说到。
文本
UTF-16 编码(了解)
JavaScript 采用 UTF-16 编码的 Unicode 字符集,JavaScript 字符串是由一组无符号的 16 位值组成的序列。最常用的 Unicode 字符都是通过 16 位的内码表示,并代表字符串中的单个字符。
那些不能表示为 16 位的 Unicode 字符则遵猫 UTF-16 编码规则——用两个 16 位值组成的一个序列(亦称做“代理项对”)表示。这意味着一个长度为 2 的 JavaScript 字符串(两个 16 位值)有可能表示一个 Unicode 字符。
var p="π"; //π由16位内码表示0x03c0
var e="𝑒; //𝑒17位内码表示0x1d45;
p.length; //1
e.length; //2
//在ECMAScript 6中,有一个新的Unicode转义符,能让你指定任意的码位(不用再管是否是16位)
console.log('\u{1d452}'); //𝑒
2
3
4
5
6
字符串拆分成数行(掌握)
在 ECMAScript3 中,字符串直接量必须写在一行中,而在 ECMAScript5 中,字符串直接量可以拆分成数行,每行必须以反斜线(\)结束,反斜线和行结束符都不算是字符串直接量的内容。如果希望在字符串直接量中另起一行,可以使用转义字符\n(后续会有介绍)。
"two\nlines"//这里定义了一个显示为两行的字符串
"one\
1ong\
line" //用三行代码定义了显示为单行的字符串,只在ECMAScript5中可用
2
3
4
引号嵌套(了解)
需要注意的是,当使用单引号来定界字符串时,需要格外小心英文中的缩写和所有格写法,比如 can't 和 O'Reily's。因为撤号和单引号是同一个字符,所以必须使用反斜线(\)来转义(转义符将在下一章讲解)所有的撤号。
当 JavaScript 代码和 HTML 代码混杂在一起的时候,最好在 JavaScript 和 HTML 代码中各自使用独立的引号风格。
<button onclick="alert("Thank you")">Click Me</button>
转义字符(了解)
在 JavaScript 字符串中,反斜线(\)有着特殊的用途,反斜线符号后加一个字符,就不再表示它们的字面含义了,比如,\n 就是一个转义字符\,它表示的是一个换行符。
\o NUL字符(\u0000)
\b 退格符(\u0008)
\t 水平制表符(\u0009)
\n 换行符(\u000A)
\v 垂直制表符(\u000B)
\f 换页符(\u000C)
\r 回车符(\u000D)
\" 双引号(\u0022)
\' 撇号或单引号(\u0027)
\\ 反斜线(\u005C)
\xXX 由两位十六进制数XX指定的Latin-1字符
\uXXXX 由4位十六进制数XXXX指定的Unicode字符
2
3
4
5
6
7
8
9
10
11
12
"\n"这个转义字符则常与 alert()搭配使用。
如果一定要在 document.write()当中使用“\n”,必须包裹在 HTML 的<PRE>标记里才有作用。(一般用<br>)。
布尔值(了解)
任意 JavaScript 的值都可以转换为布尔值。下面这些值会被转换成 false:
undefined;
null;
0;
NaN;
(''); //空字符串
2
3
4
5
所有其他值,包括所有对象(数组)都会转换成 true。false 和上面 6 个可以转换成 false 的值有时称做“假值”(falsy value),其他值称做“真值”(truthy value)。
JavaScript 期望使用一个布尔值的时候,假值会被当成 false,真值会被当成 true。
null 和 undefined(掌握)
null
对 null 执行 typeof 预算,结果返回字符串“object”,也就是说,可以将 nu11 认为是一个特殊的对象值,含义是“非对象”。但实际上,通常认为 nu11 是它自有类型的唯一一个成员,它可以表示数字、字符串和对象是“无值”的。
undefined
ECMAScript5 中做了修正;undefined 在该版本中是只读的。实际上,使用“.”和“[0”来存取这两个值的成员或方法都会产生一个类型错误。nul1 和 undefined 都不包含任何属性和方法。
你或许认为 undefined 是表示系统级的、出乎意料的或类似错误的值的空缺,而 nu11 是表示程序级的、正常的或在意料之中的值的空缺。如果你想将它们赋值给变量或者属性,或将它们作为参数传入函数,最佳选择是使用 null。
联想延伸:因此 Object.prototype.__proto__ 是 null,而不是 undefined。
全局对象(掌握)
当 JavaScript 解释器启动时(或者任何 Web 浏览器加载新页面的时候),它将创建一个新的全局对象,并给它一组定义的初始属性,全局对象的属性是全局定义的符号,JavaScript 程序可以直接使用。
- 全局属性,比如 undefined、Infinity 和 NaN
- 全局函数,比如 isNaN()、parseInt()和 eval()
- 构造函数,比如 Date()、RegExp()、String()、Object()和 Array()
- 全局对象,比如 Math 和 JSON
var global = this; //定义一个引用全局对象的全局变量
包装对象(掌握)
概念
存取字符串、数字或布尔值的属性时创建的临时对象称做包装对象。
读取属性或方法
字符串既然不是对象,为什么它会有属性呢?只要引用了字符串 s 的属性,JavaScript 就会将字符串值通过调用 new String(s)的方式转换成对象,这个对象继承了字符串的方法并被用来处理属性的引用。一旦属性引用结束,这个新创建的对象就会销毁(其实在实现上并不一定创建或销毁这个临时对象,然而整个过程看起来是这样)。
同字符串一样,数字和布尔值也具有各自的方法:通过 Number()和 Boolean()构造函数创建一个临时对象,这些方法的调用均是来自于这个临时对象。nul1 和 undefined 没有包装对象:访问它们的属性会造成一个类型错误。
设置属性或方法
在读取字符串、数字和布尔值的属性值(或方法)的时候,表现的像对象一样。但如果你试图给其属性赋值,则会忽略这个操作:修改只是发生在临时对象身上,而这个临时对象并未继续保留下来。
var s="test";//创建一个字符串
s.len=4;//给它设置一个属性
var t=s.len;//查询这个属性undefined
2
3
原始值和包装对象
需要注意的是,可通过 String(),Number()或 Boolean()构造函数来显式创建包装对象:
vars="test",n=1,b=true;//一个字符串、数字和布尔值
var S=new String(s);//一个字符串对象
var N=new Number(n);//一个数值对象
var B=new Boolean(b);//一个布尔对象
2
3
4
JavaScript 会在必要时将包装对象转换成原始值,因此上段代码中的对象 S、N 和 B 常常,但不总是表现的和值 s、n 和 b 一样。“= =”等于运算符将原始值和其包装对象视为相等,但“===”全等运算符将它们视为不等。通过 typeof 运算符可以看到原始值和其包装对象的不同。
不可变的原始值和可变的对象引用(掌握)
JavaScript 中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。
值比较
原始值的比较是值的比较:只有在它们的值相等时它们才相等。对象值都是引用(reference),对象的比较均是引用的比较:当且仅当它们引用同一个基对象时,它们才相等。
类型转换(掌握)
显式类型转换
做显式类型转换最简单的方法就是使用 Boolean()、Number()、String()或 Object()函数。当不通过 new 运算符调用这些函数时,它们会作为类型转换函数并按照上表所描述的规则做类型转换:
Number('3'); //=>3
String(false); //=>"false"或使用false.toString()
Boolean([]); //=>true
Object(3); //=>new Number(3)
2
3
4
需要注意的是,除了 null 或 undefined 之外的任何值都具有 tostring()方法,这个方法的执行结果通常和 String()方法的返回结果一致。
如果试图把 null 或 undefined 转换为对象,则会像上表所描述的那样抛出一个类型错误(TypeError)。Object()函数在这种情况下不会抛出异常:它仅简单地返回一个新创建的空对象。
隐式类型转换
JavaScript 中的某些运算符会做隐式的类型转换。如果“+”运算符的一个操作数是字符串,它将会把另外一个操作数转换为字符串。一元“+”运算符将其操作数转换为数字。同样,一元“!”运算符将其操作数转换为布尔值并取反。
x +
'' + //等价于String(x)
x; //等价于Number(x).也可以写成x-0
!!x; //.等价于Boolean(x).注意是双叹号
2
3
4
对象转换为原始值
new Boolean(false)是一个对象而不是原始值,它将转换为 true。 所有的对象继承了两个转换方法。第一个是 toString(),第二个是 valueOf()。
[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 201000:00:00 CMT-0800(PST)"
2
3
4
var d=new Date(2010,0,1);//2010年1月1日(太平洋时间)
d.valueOf()//=>1262332800000
2
对象到字符串的转换
如果对象具有 toString()方法,则调用这个方法。如果对象没有 tostring()方法,或者这个方法并不返回一个原始值,那么 JavaScript 会调用 valueOf()方法。
对象到数字的转换
如果对象具有 valueof()方法,后者返回一个原始值,则 JavaScript 将这个原始值转换为数字(如果需要的话)并返回这个数字。否则,如果对象具有 toString()方法,后者返回一个原始值,则 JavaScript 将其转换并返回。
对象转换为数字的细节解释了为什么空数组会被转换为数字 0 以及为什么具有单个元素的数组同样会转换成一个数字。数组继承了默认的 value0f()方法,这个方法返回一个对象而不是一个原始值,因此,数组到数字的转换则调用 tostring()方法。空数组转换成为空字符串,空字符串转换成为数字 0。含有一个元素的数组转换为字符串的结果和这个元素转换字符串的结果一样。如果数组只包含一个数字元素,这个数字转换为字符串,再转换回数字。
作为属性的变量(掌握)
当声明一个 JavaScript 全局变量时,实际上是定义了全局对象的一个属性。
当使用 var 声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过 delete 运算符删除。
如果你没有使用严格模式并给一个未声明的变量赋值的话,JavaScript 会自动创建一个全局变量。以这种方式创建的变最是全局对象的正常的可配值属性,并可以删除它们:
var true
var=1;//声明一个不可删除的全局变量
fakevar=2;//创建全局对象的一个可删除的属性
this.fakevar2=3;//同上delete truevar//=>false:变量并没有被删除
delete fakevar//=>true:变量被删除
delete this.fakevar2//=>true:变量被删除
2
3
4
5
6
声明上下文对象
JavaScript 全局变量是全局对象的属性,这是在 ECMAScript 规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当做跟函数调用相关的某个对象的属性。ECMAScript 5 规范称为“声明上下文对象”(declarative environment record)。
作用域链(掌握)
作用域链含义
如果将一个局部变量看做是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。
每一段 JavaScript 代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。
变量解析规则
当 JavaScript 需要查找变量 x 的值的时候(这个过程称做“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为 x 的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为 x 的属性,JavaScript 会继续查找链上的下一个对象。如果第二个对象依然没有名为 x 的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性 x,那么就认为这段代码的作用域链上不存在 x,并最终抛出一个引用错误(ReferenceError)异常。
作用域链组成
在 JavaScript 的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。
在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。
在一个嵌套的函数体内,作用域链上至少有三个对象。
函数定义和调用对于作用域链
当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。
对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别一一在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。
作用域链的概念对于理解 with 语句是非常有帮助的,同样对理解闭包的概念也至关重要。