JavaScript 的类型系统
当前 ECMAScript 标准定义了 8 种数据类型,包括 7 个原始(primary)类型,还有一个是对象类型。
Null 和 Undefined 类型
据说在最初设计 JavaScript 时,像 Java 一样,只设置了 null 表示“无”。根据 C 语言的传统,null 可以自动转为 0。
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。
Number(null) // 0
5 + null // 5
因此,他又设计了一个 undefined。区别是这样的:null 是一个表示“空”的对象,转为数值时为 0;undefined 是一个表示"此处无定义"的原始值,转为数值时为 NaN。
Number(undefined) // NaN
5 + undefined // NaN
Number 类型
存储结构
在 JavaScript 内部,所有数字都是以 64 位浮点数形式储存,即使整数也是如此。
因此 1 与 1.0 是相同的,是同一个数
1 === 1.0 // true
字面量
数值字面量支持十进制、八进制、十六进制以及科学计数法。
// 十进制:无前缀,或者有前缀0但是后面有8或9
var num1 = 21;
var num2 = 0789; // 这是十进制,因为这里有8和9
// 八进制:
// 使用前缀0表示(ES5的严格模式不再允许使用该写法)。
var num3 = 0123;
// 使用前缀0o或0O表示(ES6引入的新写法)。
var num4 = 0o123;
// 十六进制:使用前缀0x或0X表示。
var num5 = 0xff;
// 科学计数法
var num6 = 25e2; // 2500
var num7 = 25e-2; // 0.25
// 二进制:使用前缀0b或0B表示(ES6引入的新写法)。
var num8 = 0b111110111; // 503
前缀 0 表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法。
特殊数值
NaN
NaN 表示“非数字”(not a number),主要用在将字符串解析成数值出错等场合。
5 - 'x' // NaN
0 / 0 // NaN
Infinity
Infinity 表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非 0 数值除以 0,得到 Infinity。
Infinity 有正负之分,Infinity 表示正的无穷,-Infinity 表示负的无穷。
Bigint 类型
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于 2 的 1024 次方的数值,JavaScript 无法表示,会返回 Infinity。
// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024) // Infinity
ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题,这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
为了与 Number 类型区别,BigInt 类型的数据必须添加后缀 n。
const a = 2172141653n;
const b = 15346349309n;
// BigInt 可以保持精度
a * b // 33334444555566667777n
String 类型
字符串是包含零个或多个字符的序列。
字面量
// 字符串需放在单引号或者双引号之中。
var str1 = 'abc';
var str2 = "abc";
// 单引号字符串内部,可以使用双引号。双引号字符串内部可以使用单引号。
var str1 = 'key = "value"'
var str2 = "It's a long journey"
// 在单(双)引号字符串内部使用单(双)引号,需用反斜杠来进行转义。
var str1 = 'Did she say \'Hello\'?'
var str2 = "Did she say \"Hello\"?"
// 当字符串太长时,可以使用反斜杠进行折行,效果与写在同一个行完全一样。
// 需注意的是:
// 1. 从第二行开始,不要添加多余的空格,否则这些空格也会成为字符串的一部分。
// 2. 反斜杠后边必须是换行符,不能有其他字符(比如空格)。
var longString = 'Long \
long \
long \
string';
模板字符串
使用 + 号可以将多个字符串拼接起来。但是如果有很多变量时,使用 + 就比较麻烦了。
因此,ES6 引入了模板字符串。模板字符串(template string)是增强版的字符串,用反引号(`)标识,可以使用 ${} 嵌入变量或表达式。
// 使用拼接符
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
// 使用模板字符串
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
转义
对于那些有特殊含义的字符(比如单引号)或者没有字符实体(比如换行符)的字符,可以采用下面两种方法来表示:
- 转义符(反斜杠)+ 普通字符
- 转义符 + Unicode 码点
转义符 + Unicode 码点的形式同样可以表示其他普通字符。
另外,Unicode 码点也可以使用三位八进制(000 到 377)或者两位十六进制(00 到 FF)表示。但这两种方法只能表示 256 个字符。
字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从 0 开始)。但是,无法改变或者删除字符串内的单个字符。
字符串长度
length 属性返回字符串的长度,该属性也是无法改变的。
var s = 'hello';
s.length // 5
s.length = 3;
s.length // 5
字符集
JavaScript 使用 Unicode 字符集。每个字符在 JavaScript 内部都是以 16 位(即 2 个字节)的 UTF-16 格式储存。
Symbol 类型
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。为了从根本上防止属性名的冲突,ES6 引入了 Symbol 类型。
Symbol 类型是一种新的原始数据类型,它表示独一无二的值。Symbol 值可以用作对象的属性名
Symbol 值通过 Symbol 函数生成。该值不是对象,因此 Symbol 函数前不能使用 new 命令,否则会报错。
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。该参数可以是原始类型,也可以是对象,并且不会对 Symbol 值产生影响。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
let s3 = Symbol('foo');
s1 === s3 // false
Symbol 值可以显式转为字符串或者布尔值:
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
let sym = Symbol();
Boolean(sym) // true
!sym // false
Symbol 值作为对象属性名时,不能用点运算符,只能使用方括号。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
Object 类型
简单的说,在 JavaScript 中对象就是一组属性(property)的集合。
属性包含属性名(key)和属性值(value)以及属性的一些特征(比如:属性是否可被修改、属性是否可被遍历等)组成。
对象字面量
对象的字面量由一组键值对和 {} 构成,其中键和值之间由冒号(:)分隔。
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。如果键名是数值,会被自动转为字符串。
// 这是一个空的对象
var obj = {};
// 创建一个Student对象
var student = {
name: 'Tom',
age: 12
}
构造函数
JavaScript 语言使用构造函数(constructor)作为对象的模板,通过 new 命令执行构造函数来生成实例对象。比如:Object 就是一个构造函数。
// 创建一个空的对象
var obj = new Object();
// 定义一个Student构造函数
var Student = function(){
this.name = 'Tom',
this.age = 12
};
// 创建一个Student对象实例
var s = new Student();
Class(类)
ES6 引入了 Class(类)这个概念,作为对象的模板,通过 class 关键字,可以定义类。
// 定义一个Student类
class Student {
// 构造方法,生成对象实例时被调用
constructor(name, age) {
this.name = name;
this.age = age;
}
// 定义“类”的方法时,前面不需要添加function关键字
toString() {
return '(name: ' + this.name + ', age: ' + this.age + ')';
}
}
// 创建一个Student对象实例
var s = new Student('Tom', 12);
对象的分类
ECMAScript 规范明确定义了各种对象的类别,包括:
- 常规对象(ordinary object)拥有 JavaScript 对象所有的默认行为。
- 特异对象(exotic object)的某些内部行为和默认的有所差异。
- 标准对象(standard object)是 ECMAScript 6 中定义的对象,例如 Array, Date 等,它们既可能是常规也可能是特异对象。
- 内置对象(built-in object)指 JavaScript 执行环境开始运行时已存在的对象。标准对象均为内置对象。
数据类型判断
使用 typeof 运算符可以判断一个值的数据类型。
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function',函数也是对象
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
null 的类型是 object,这是由于历史原因造成的。1995 年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑 null,只把它当作 object 的一种特殊值。后来 null 独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null 返回 object 就没法改变了。
相关资料
《JavaScript 语言精髓与编程实践》
JavaScript 全栈教程
JavaScript 教程
ES6 入门教程
Understanding ECMAScript 6(简体中文版)
ECMAScript® 2020 Language Specification
作者:lingyundu