本篇文章主要介绍 JavaScript 中几个常用的内置值类型。
1. 数组
JavaScript 中,数组可以容纳任意类型的值,可以是 string
、number
、object
,也可以是其他数组(多维数组),声明数组后加入值也不需要预先设定数组的长度大小。
1.1 稀疏数组
稀疏数组(sparse array)是指索引不连续,数组长度大于元素个数的数组,通俗说就是含有空白或空缺单元的数组。
1.1.1 稀疏数组生成方式
// 构造函数声明一个没有元素的数组
var a = new Array(5); // [empty × 5]
// 指定的索引值大于数组长度
var a = [];
a[5] = 4; // [empty × 5, 4]
// 指定大于元素个数的数组长度
var a = [];
a.length = 5; // [empty × 5]
// 数组直接量中省略值
var a = [0, , , ,]; // [0, empty × 3]
// 删除数组元素
var a = [0, 1, 2, 3, 4];
delete a[4]; // [0, 1, 2, 3, empty]
1.1.2 empty
和 undefined
稀疏数组在控制台中的表示如下:
var a = new Array(5);
console.log(a); // [empty × 5]
empty × 5
即表示数组有 5 个空缺单元。但是 empty
并非 JavaScript 的基本数据类型,当尝试访问数组中的元素时,JavaScript 会返回一个 undefined
,这是因为 JavaScript 引擎在发现元素缺失时会临时赋值 undefined
。
console.log(a[0]); // undefined
但是 empty
和 undefined
表示的并不是一个含义,empty
表示的是当前数组元素没有值,空缺的;而 undefined
则表示当前元素是存在值的,并且值是 undefined
。如下:
var a = new Array(5);
var b = [undefined, undefined, undefined];
a.forEach(i => { console.log(i) }); // 无 log 输出,无元素不会执行回调
b.forEach(i => { console.log(i) }); // undefined undefined undefined
1.1.3 稀疏数组的遍历
-
forEach
、filter
、some
、every
方法
这些方法在遍历到稀疏数组的缺失元素时,回调函数不会执行。
var a = [1,,,,];
a.forEach(i => { console.log(i) }); // 只会打印一次 1
for...in
for-in
语句只会遍历对象的可枚举属性,不会遍历稀疏数组中的缺失元素。
var a = [1,,,,5];
for (var i in a) { console.log(a[i]) }; // 1 5
-
for...of
、for
循环、find
、finedIndex
方法
for...of
和 for 循环都会将空缺元素的值替换为 undefined。find
、finedIndex
是通过 for 循环实现的,所以同 for 循环。
var a = [1,,,,5];
for (var i of a) { console.log(i) }; // 1 undefined undefined undefined 5
-
includes
方法
includes
方法则比较特殊,可以理解为当数组为空时,只会返回 false;而当数组非空(指长度不为0的数组,其中包括全部元素都缺失的数组),且函数调用参数为空时会返回 true。
var a = [1,,,,];
var b = new Array(5);
var c = [];
a.includes(); // true
b.includes(); // true
c.includes(); // false
-
map
方法
不会遍历缺失元素,但返回的结果具有与源数组相同的长度和空隙。
var a = [1,,,,5];
a.map(i => i); // [1, empty × 3, 5]
-
sort
方法
不会遍历缺失元素,数组能正常排序,同时会返回与源数组相同的长度。
var a = [5,,,,1];
a.sort(); // [1, 5, empty × 3]
-
join
方法
缺失元素占的坑还是会被保留。
var a = new Array(5);
a.join(); // ",,,,"
1.1.4 稀疏数组转密集数组
可以通过如下两个方法实现,转换规则是将空缺元素使用 undefined
代替:
// 稀疏数组
var a = new Array(5);
Array.apply(null, a); // ES5 [undefined, undefined, undefined, undefined, undefined]
Array.from(a); // ES6 [undefined, undefined, undefined, undefined, undefined]
1.1.5 稀疏数组特性
稀疏数组跟密集数组相比具有以下特性:
- 访问速度慢
- 内存利用率高
这与 V8 引擎构建 JS 对象的方式有关。V8 访问对象有两种模式:字典模式 和 快速模式。
稀疏数组使用的是字典模式,也称为散列表模式,该模式下 V8 使用散列表来存储对象属性。由于每次访问时都需要计算哈希值(实际上只需要计算一次,哈希值会被缓存)和寻址,所以访问速度非常慢。另一方面,对比起使用一段连续的内存空间来存储稀疏数组,散列表的方式会大幅度地节省内存空间。
而密集数组在内存空间中是被存储在一个连续的类数组里,引擎可以直接通过数组索引访问到数组元素,所以速度会非常快。
如下是一个 jsperf 测试:
// Sparse Array
var a = [];
a[10000] = 1;
a.forEach(function(){});
// Dense Array
var b = Array.from(a);
b.forEach(function(){});
可见密集数组的访问性能明显比稀疏数组的高,因此建议日常编码中能避免稀疏数组的尽量避免。
1.2 类数组
类数组的两个条件:
具有:指向对象元素的数字索引下标以及
length
属性告诉我们对象的元素个数不具有:诸如 push、forEach 以及 indexOf 等数组对象具有的方法
1.2.1 类数组:NodeList
如下,通过 querySelectorAll
获取到的 NodeList
,有 length,可以通过下表访问到具体的元素,不能调用数组的方法。所以它就是一个类数组。
const arrayLike = document.querySelectorAll("div");
console.log(Object.prototype.toString.call(arrayLike)); // [object NodeList]
console.log(arrayLike.length); // 127
console.log(arrayLike[0]);
// <div id="js-pjax-loader-bar" class="pjax-loader-bar"></div>
console.log(Array.isArray(arrayLike)); // false
arrayLike.push("push");
// Uncaught TypeError: arrayLike.push is not a function(…)
1.2.2 类数组对象
如下就是通过一个对象创建出来的类数组。
const arrayLikeObj = {
length: 2,
0: "This is Array Like Object",
1: true,
};
1.2.3 类数组函数
const arrayLikeFunc1 = function () {};
console.log(arrayLikeFunc1.length); // 0
const arrFunc1 = Array.prototype.slice.call(arrayLikeFunc1, 0);
console.log(arrFunc1, arrFunc1.length); // [], 0
1.2.4 类数组转化为数组
// 数组slice方法
Array.prototype.slice.call(arrLike);
// Array.from
Array.from(arrLike);
2. 字符串
字符串和数组的确很相似,它们都是类数组,都有 length
属性以及 indexOf(..)
和 concat(..)
方法
var a = "foo";
var b = ["f", "o", "o"];
a.length; // 3
b.length; // 3
a.indexOf("o"); // 1
b.indexOf("o"); // 1
a.concat("bar"); // foobar
b.concat(["b", "a", "r"]); // ["f","o","o","b","a","r"]
JavaScript 中字符串是不可变的,而数组是可变的。
var a = "foo";
var b = ["f", "o", "o"];
a[1] = "O";
b[1] = "O";
a; // "foo",不可改变
b; // ["f","O","o"],可变
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
var a = "foo";
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
有些数组非变更的函数(不会改变原数组),可以用来处理字符串,如下:
var a = "foo";
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call(a, "-");
var d = Array.prototype.map
.call(a, function (v) {
return v.toUpperCase() + ".";
})
.join("");
c; // "f-o-o"
d; // "F.O.O."
3. 数字
JavaScript 只有一种数值类型:number
,它可以表示整数和带小数的十进制数。
JavaScript 的数字类型是基于 IEEE 754 标准实现,并且使用的是双精度格式,即 64 位二进制。
双精度浮点格式(64 位):1 位符号位、11 位有效数字位和** 52 位指数位**。
由于 JavaScript 的数字值可以使用 Number
对象进行封装,因此可以直接调用 Number.prototype
中的方法。
(42).toFixed(3); // "42.000"
(0.42).toFixed(3); // "0.420"
(42).toFixed(3); // "42.000"
如下是无效语法,因为 .
被看作为 42.
的一部分,所以没有 .
属性访问符来调用 toFixed
方法。即 .
运算符会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。
42.toFixed( 3 ); // SyntaxError
但是下面的语法是有效的(注意其中的空格):
(42).toFixed(3); // "42.000"
十六进制:0xf3
or 0Xf3
八进制:0o363
or 0O363
二进制:0b11110011
or 0B11110011
以上进制的前缀尽量使用小写字母。
4.1 较小的数字
从数学角度下面的计算应该为 true,但是结果为 false。这是因为数值的运算都会转换为二进制,而小数部分的二进制有些数字无法精准表示出来,在有限的位数下,就会进行误差取舍,所以导致最终结果的不精确。
0.1 + 0.2 === 0.3; // false
解决此问题最常见的做法就是设置一个误差范围值,通常是 2^-52
,这个值在 JavaScript 中被定义在Number.EPSILON
中,或者使用 Math.pow(2,-52)
:
function numbersCloseEnoughToEqual(n1, n2) {
return Math.abs(n1 - n2) < Number.EPSILON;
}
numbersCloseEnoughToEqual(0.1 + 0.2, 0.3); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false
4.2 整数的安全范围
能够被“安全”呈现的最大整数是 2^53 - 1
,即 9007199254740991
,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER
。
最小整数是 -9007199254740991
, 在 ES6 中 被 定 义 为 Number.MIN_SAFE_INTEGER
。
超过此范围的值,应该转换为字符串展示,或者借助相关的工具库。
对于数位操作,最大支持 32 位的数字,超过 32 位的将会被忽略
5. 原生函数
JavaScript 的内建函数(built-in function),也叫原生函数(native function),常用的原生函数如下:
String
、Number
、Boolean
、Array
、Object
、Fuction
、RegExp
、Date
、Error
、Symbol
。
5.1 内部属性 [[Class]]
所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]]
,可以看作为一个内部的分类。这个属性一般无法直接访问,一般通过 Object.prototype.toString
来查看,如下:
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(/regex-literal/i); // "[object RegExp]"
5.2 封装对象
由于基本类型没有属性和方法,需要通过封装对象才能访问,此时 JavaScript 会自动为基本类型值包装一个封装对象:
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
如果需要经常用到这些字符串属性和方法,比如在 for 循环中使用 i < a.length,那么从 一开始就创建一个封装对象也许更为方便,这样 JavaScript 引擎就不用每次都自动创建了。
但实际证明这并不是一个好办法,因为浏览器已经为 .length
这样的常见情况做了性能优化,直接使用封装对象来“提前优化”代码反而会降低执行效率。
一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什 么时候应该使用封装对象。换句话说,就是应该优先考虑使用 "abc" 和 42 这样的基本类型 值,而非 new String("abc")
和 new Number(42)
。
5.3 拆封对象
如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数:
var a = new String("abc");
var b = new Number(42);
var c = new Boolean(true);
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
在需要用到封装对象中的基本类型值的地方会发生隐式拆封,如下:
var a = new String("abc");
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"
5.4 构造函数
关于数组(array)、对象(object)、函数(function)和正则表达式,我们通常喜欢以常 量的形式来创建它们。实际上,使用常量和使用构造函数的效果是一样的(创建的值都是通过封装对象来包装)。
5.4.1 Array(...)
构造函数 Array()
不要求带 new 关键字,不带的时候会自动补上。因此 Array(1, 2, 3)
和 new Array(1, 2, 3)
的
效果是一样的。
需要注意的是,Array
只有一个数字参数的时候,创建出来的是该数字长度的空数组,多个数字的时候,创建的则是拥有这些数字的数组。
5.4.2 Object(...)、Function(...)、RegExp(...)
除非万不得已,否则尽量不要使用 Object(..)/Function(..)/RegExp(..)
5.4.3 Date(...) 和 Error(...)
创建日期对象必须使用 new Date()
。Date(...)可以带参数,用来指定日期和时间,而不带 参数的话则使用当前的日期和时间。
构造函数 Error(...)(与前面的 Array() 类似)带不带 new 关键字都可。
5.4.4 Symbol(...)
Symbol 比较特殊,不能带 new 关键字,否则会出错。
6. 对象
6.1 语言 BUG null
通过 typeof null
的结果是 object
,但是 null
本身是一个基本类型,这是 JavaScript 语言中的一个 BUG。
这是因为不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object
类型,
null 的二进制表示全部都是 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
6.2 对象属性的存在性
6.2.1 属性访问
通过属性访问返回值是否是 undefined,可以判断属性是否存在,但是这个属性也有可能存储的就是 undefined,此时就没法区分了。
6.2.2 in
操作符
in
操作符会检查属性是否在对象及其 [[Prototype]]
原型链中。
var myObject = { a: 2 };
"a" in myObject; // true
"b" in myObject; // false
这里需要注意的是,如果对数组进行使用 in
操作符时,检查的不是数组的值,而是数组的下标。
6.2.3 hasOwnProperty
方法
hasOwnProperty(..)
只会检查属性是否在对象中,不会检查 [[Prototype]]
链。
var myObject = { a: 2 };
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
6.2.4 propertyIsEnumerable
方法
propertyIsEnumerable(..)
会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable: true
。
6.2.5 Object.keys(..)
和 Object.getOwnPropertyNames(..)
这两个方法都只会查找对象直接包含的属性。
6.2.6 可枚举性
从下面代码可以看出来,myObject.b
确实存在并且有访问值,但是却不会出现在 for..in
循环中。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。
var myObject = {};
Object.defineProperty(
myObject,
"a"
// 让 a 像普通属性一样可以枚举 { enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
"b" in myObject; // true
myObject.hasOwnProperty("b"); // true
// .......
for (var k in myObject) {
console.log(k, myObject[k]); // "a" 2
}