类型
大多数开发者认为, 像JavaScript这种动态语言是没有类型
(type)
的, 让我们来看看 ES5.1 规范对此是如何界定的;
- 本规范中运算法则所操纵的值均有相应的类型, 定义了所有可能出现的类型, ECMAScript类型又进一步细分为
语言类型
和规范类型
- ECMAScript语言中所有的值都有一个对应的语言类型, ECMAScript语言类型包括
Undefined Null Boolean String Number 和 Object
在你不知道的JS中, 这样来定义'类型': 对语言引擎和开发人员来说, 类型是值得内部特征, 它定义了值的行为, 以使其区别于其他值.
1.1 类型
要正确合理的进行类型转换, 必须掌握 JavaScript 中的各个类型及其内在行为, 几乎所有的 JavaScript 程序都会涉及某种形式的强制类型转换.
1.2 内置类型
JavaScript 有七种内置类型:
- 空值 (null)
- 未定义 (undefined)
- 布尔值 (boolean)
- 数字 (number)
- 字符串 (string)
- 对象 (object)
- 符号 (symbol, ES6新增)
// 使用 typeof 运算符查看值的类型, 它返回类型的字符串值
typeof undefined === 'undefined' // true
typeof true === 'boolean' // true
typeof 42 === 'number' // true
typeof '42' === 'string' // true
typeof {a: 1} === 'object' // true
// ES6 新加入的类型
typeof symbol() === 'symbol' // true
typeof null === 'object' // true
// 上面的类型使用 typeof 返回值有同名的字符串与之对应, 但 null返回的是 'object', 正确的应该是 'null', 这个 bug 由来已久, 也许永远不会修复
// 需要使用复合条件检测 null 值的类型
const a = null
(!a && typeof a === 'object') // true
typeof function a(){ /* .. */ } === 'function' // true
// function(函数) 实际上是 object 的一个'子类型'. 具体说: 函数是'可调用对象', 它有一个内部属性[[Call]], 该属性使其可以被调用
typeof [1, 2, 3] === 'object' // true
// 数组也是对象的一个子类型
1.3 值和类型
JavaScript 中的变量是没有类型的, 只有值才有. 变量可以随时持有任何类型的值
1.3.1 undefined 和 undeclared
- 变量在未持有值的时候为
undefined
var a;
typeof a; // 'undefined'
var b = 42;
var c;
b = c
typeof b; // 'undefined'
typeof c; // 'undefined'
大多数开发者倾向于将 undefined
等同于 ubdeclared(未声明)
, 但在 JavaScript
中它们完全是两回事
- 已在作用域中声明但还没有赋值的变量, 是
undefined
- 还没有在作用域中声明过的变量, 是
undeclared
var a;
a // undefined
b // ReferenceError: b is not defined ==> undeclared
令人苦恼的是:
typeof a; // 'undefined'
typeof b; // 'undefined' ==> 并没有报错, typeof 有一个特殊的安全防护机制(阻止报错)
1.3.2 typeof Undeclared
该安全防护机制在浏览器中运行的JavaScript代码来说还是很有帮助的, 因为多个脚本文件会在共享的全局命名空间中加载变量
// TODO1
顶层的全部变量声明 var DEBUG = true 只在 debug.js文件中才有, 而该文件只在开发和测试才被加载到浏览器, 生产环境不予加载, 问题是如何在程序中检查全局变量 DEBUG 才不会出现 ReferenceError错误,
这时 typeof 的安全防护机制就成为了我们的帮手
// 这样会抛出错误
if(DEBUG) console.log('debug is start')
// 这样安全
if(typeof DEBUG !== 'undefined') console.log('debug id start')
// 对内建的API也有用,
if(typeof atob === 'undefined') atob = function() { /* Todo... */ }
值
数组(array) 字符串(string) 和数字(number) 是一个程序最基本的组成部分, 但在JavaScript 中, 却有不同的表现, 值介绍几个内置类型, 深入了解并合理利用它们
2.1 数组
和其它强类型语言不同的是, 在 JavaScript 中, 数组可以容纳任何类型的值
let a = [1, '2', [3]]
a.length // 3
a[0] === 1 // true
a[2][0] === 3 // true
对数组声明即可向其中添加值, 不需预先设定大小
let a = []
a.length // 0
a[0] = 1
a[1] = '2'
a[2] = [3]
a.length // 3
!!! 注意
使用 delete 可以将单元从数组中删除, 但是, 单元删除后, 数组的 length 属性并不会发生变化
2.1.1 类数组
有时需要将类数组(一组通过数字索引的值)转换为真正的数组
// DOM 查询操作会返回 DOM 元素列表 && 通过 arguments 对象(类数组) 将函数的参数当做列表来访问
// 工具函数 slice(...) 经常被用于这类操作
function foo() {
let arr = Array.prototype.slice.call(arguments)
arr.push('bam')
console.log(arr)
}
foo('baz', 'bar') // ['baz', 'bar', 'bam']
// ES6 Array.from(...) 也能实现
let arr = Array.from(arguments)
2.2 字符串
字符串经常被当成字符数组. 字符串的内部实现究竟有没有使用数组并不好说, 但JavaScript中的字符串和字符数组并不是一回事, 只是看起来像.
let a = 'foo'
let b = ['f', 'o', 'o']
// 它们都是类数组, 都有length属性 以及indexOf() concat()方法
a.legnth // 3
b.length // 3
a.indexOf('o') // 1
b.indexOf('o') // 1
let c = a.concat('bar') // 'foobar'
let d = b.concat(['b', 'a', 'r']) // ["f", "o", "o", "b", "a", "r"]
// 但并不意味它们都是 '字符数组'
a[1] = 0 // a ==> 'foo'
b[1] = 0 // b ==> ['f', 0, 'o']
// JavaScript 中字符串是不可变的, 而数组是可变的
字符串不可变是指字符串的成员函数不会改变其原始值, 而是创建并返回一个新的字符串. 数组的成员函数都是在其原始值进行操作.
-
许多数组函数用来处理字符串很方便, 虽然字符串没有这些函数, 但可以通过 '借用' 数组的非变更方法来处理字符串:
a.join // undefined a.map // undefined let c = Array.prototype.join.call(a, '-') // "f-o-o" let d = Array.prototype.map.call(a, function(v) { // "F.O.O." return v.toUpperCase() + '.' }).join('')
-
另一个不同点在于字符串反转, 数组有一个字符串没有的可变更成员函数(reverse)
Array.prototype.reverse.call(a) // 不能借用了
2.3 数字
JavaScript 只有一种数值类型: number(数字), 包括 "整数" 和带小数的十进制, 之所以加引号, 因为JavaScript没有真正意义上的整数.
javaScript 中的 "整数" 就是没有小数的十进制数, 所以 42.0 等同于 "整数" 42
2.3.1 数字的语法
JavaScript中的数字常量一般用于十进制表示.
const a = 42;
const a = 42.3
数字前面的 0 可以忽略, 后面的 0 也可以忽略
const a = 0.42
const b = .42
const c = 42.0
const d = 42
特别大和特别小的数字默认用指数格式显示, 与 toExponential()
函数输出结果相同
const a = 5E10 // a ==> 50000000000
a.toExponential(); // "5e+10"
数字值可以调用 Number
const a = 42.59
a.toFixed(0) // 43
a.toFixed(1) // 42.6
2.3.2 较小的数值
二进制浮点数最大的问题, 是会出现如下情况
0.1 + 0.2 === 0.3 // false
在处理带有小数的数字时需要注意, 很多程序只需要处理整数, 此时使用JavaScript的数字类型是绝对安全的.
那怎样判断0.1 + 0.2和 0.3
是否相等
- 最常见设置一个误差范围值, 通常称为'机械精度', 对js来说, 这个值通常是
2^-52
- ES6开始,该值定义在
Number.EPSLION
中, 可以直接使用// ES6 之前 if(!Number.EPSLION) { Number.EPSLION = Nath.pow(2, -52) } function numbersCloseEnoughEqual(a, b) { return Math.abs(a - b) < Number.EPSLION } let a = 0.1 + 0.2 let b = 0.3 numbersCloseEnoughEqual(a, b) // true
2.3.3 整数的安全范围
数字的呈现方式决定了"整数"的安全值范围远小于
Number.MAX_VALUE
能够被 "安全" 呈现的最大整数是
2^53 - 1
, 即9007199254740991
, 在ES6中被定义为Number.MAX_SAFE_INTEGER
, 最小整数是-9007199254740991
, 在ES6中被定义为Number.MIN_SAFE_INTEGER
2.3.4 整数检测
要检测一个整数是否是整数, 可以使用ES6的 Number.isInteger()
方法
Number.isInterger(42) // true
Number.isInteger(42.000) // true
Number.isInteger(42.3) // false
ES6之前的版本 polifill Number.isInteger()
方法
if(!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == 'number' && num % 1 == 0
}
}
检测一个值是否是安全的整数, 可以使用ES6的Number.isSafeInteger()
方法
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Math.pow(2, 53)) // fale
Number.isSafeInteger(Math.pow(2, 53) - 1) // true
2.4 特殊的值
JavaScript 数据类型有几个特殊的值需要开发人员特别小心
2.4.1 不是值的值
undefined
类型只有一个值, 即undefined
. null
类型也只有一个值, 即null
. 它们的名称即是类型又是值
- null 指空值
(empty value)
==> 从未赋值 - undefined 指没有值
(misssing value)
==> 曾赋值, 但目前没有值
2.4.2 undefined
在非严格模式, 可以为全局标识符 undefined 赋值 (设计实在欠考虑)
function foo() {
undefined = 2 // 非常糟糕
}
function foo() {
'use strict'
undefined = 2 // TypeError
}
永远不要重新定义 undefined
void 运算符
表达式 void __
没有返回值, 因此返回结果是 undefined
. void
并不改变表达式的结果, 只是让表达式不返回值
var a = 42
console.log(void a, a) // undefined 42
按照惯例我们用void 0
来获得undefined
(主要源于C语言). void 0
| void 1
和 undefined 并没有实质上的区别
总之, 如果要将代码中的值(如表达式的返回值) 设为 undefined, 就可以使用 void.
2.4.3 特殊的数字
数字类型有几个特殊的值:
-
不是数字的数字
-
NaN
指 '不是一个数字', (无效数值, 失败数值, 可能更准确)
var a = 2 / 'foo' // NaN typeof a === 'number' // true
- NaN 是一个'警戒值', 用于指出数字类型中的错误的情况, 即 '执行数学运算没有成功, 这是失败后返回的结果'
NaN !== NaN // true
-
-
无穷数
var a = 1 / 0 // Infinity var b = -1 / 0 // -Infinity
零值
- JavaScript有一个常规的
0
和一个-0
var a = 0 / -3
var b = 0 * -3
2.4.4 特殊等式
ES6中新加入一个工具方法
Object.is()来判断两个值是否绝对相等
, 可以用来处理上述所有情况
var a = 2 / 'foo'
var b = -3 * 0
Object.is(a, NaN) // true
Object.is(b, -0) // true
Object.is(b, 0) // false
能使用 ==
和 ===
时尽量不要使用 Object.is()
, 因为前者效率更高, 更为通用, Object.is() 主要用来处理哪些特殊的相等比较
2.5 值和引用
- 在许多编程语言中, 赋值和参数传递可以通过
值复制(value-copy)
或者引用复制(reference-copy)
- JavaScript中没有指针, 引用的工作机制也不尽相同. 在 JavaScript 中变量不可能成为指向另一个变量的引用.
- JavaScript引用的指向是值, 如果一个值有10个引用, 这些引用指向同一个值,
它们之间没有引用/指向关系
- JavaScript 对值和引用的赋值/传递在语法上没有区别, 完全根据值的类型决定
let a = 2
let b = 2 // b 是 a的一个副本
b++
a // 2
b // 3
let c = [1, 3 ,5]
let d = c // d 是 [1, 3, 5] 的一个引用
d.push(7)
c // [1, 3, 5, 7]
d // [1, 3, 5, 7]
简单值(即标量基本类型值, scalar primitive) 总是通过值复制的方式来赋值/传递, 包括
null undefined string number boolean symbol
复合值(compound value) --对象(包括数组和封装对象) 和函数, 则总是通过引用复制的方式来赋值/传递
-
由于引用指向的是值本身而非变量, 所以一个引用无法更改另一个引用的指向
var a = [1, 2, 3] var b = a a // [1, 2, 3] b // [1, 2, 3] b = [4, 5, 6] a // [1, 2, 3] b // [4, 5, 6] // b = [4, 5, 6] 并不影响 a 指向值 [1, 2, 3]. 除非 b 不是指向数组的引用, 而是指向 a 的指针, js不存在这种情况
-
函数参数的困惑
function foo(x) { x.push(4) x; // [1, 2, 3, 4] x = [4, 5, 6] x.push(7) x; // [4, 5, 6, 7] } let a = [1, 2, 3] foo(a) a; // 是[1, 2, 3, 4] 不是[4, 5, 6, 7] // 向函数传递 a 的时候,实际是将引用 a 的一个副本赋值给 x, 而 a 仍指向 [1, 2, 3] // 在函数中我们可以引用 x 来更改数组的值, 但 x = [4, 5, 6] 并不影响 a 的指向, 所以a仍指向[1, 2, 3, 4]
- 我们不能通过引用 x 来更改引用 a 的指向, 只能更改 a 和 x 共同指向的值
function foo(x) { x.push(4) x; // [1, 2, 3, 4] x.length = 0; // 清空数组 x.push(4, 5, 6, 7) x; // [4, 5, 6, 7] } let a = [1, 2, 3] foo(a) a; // 是[4, 5, 6, 7] 不是[1, 2, 3, 4] // x.length = 0 和 x.push(...) 没有创建新数组. 而是改变当前数组
- 我们无法自行决定使用值复制还是引用复制, 一切由值的类型来决定
- 通过值复制的方式来传递复合值(如数组), 需要为其创建一个复本, 这样传递的就不是原始值:
foo(a.slice())