你不知道的JS(中):类型与值
本文是《你不知道的JavaScript(中卷)》的阅读笔记,第一部分:类型与值。 供自己以后查漏补缺,也欢迎同道朋友交流学习。
类型
对语言引擎和开发人员来说,类型是值的内部特征,它定义了值的行为,以使其区别于其他值。
内置类型
JavaScript 有七种内置类型:
- 空值(null)
- 未定义(undefined)
- 布尔值( boolean)
- 数字(number)
- 字符串(string)
- 对象(object)
- 符号(symbol,ES6 中新增)
对于类型, we 一般使用typeof来判断,但有一些特殊情况无法准确判断,如下:
javascript
// null的类型不是null
typeof null === "object"; // true
(!a && typeof a === "object"); // null需要复合条件来判断
// function的类型不是object
typeof function a(){} === "function"; // true
// 数组也是object
typeof [1,2,3] === "object"; // true
值和类型
JS中的变量是没有类型的,只有值才有。JS不做"类型强制";
undefined 和 undeclared 已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。
内置类型小结
JS中有其中内置类型:null、undefined、boolean、number、string、object和symbol,可以使用typeof运算符来查看。但对于null、function和数组要特殊处理。
变量没有类型,但它们持有的值 have 类型。类型定义了值的行为特征。 在 JS 中它们是两码事,undefined 是值的一种,undeclared 则表示变量还没有被声明过。
值
数组(array)、字符串(string)和数字(number)是一个程序最基本的组成部分。
数组
与其他强类型语言不同,在JS中数组可以容纳任何类型的值,可以是字符串、数字、对象,甚至是其他数组(多维数组就是这么实现的):
javascript
var a = [1, '2', [3]];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
数组声明可以不预先设定大小,但使用delete删除时要注意length不会被改变。还有在创建稀疏数组时,长度会变化,没有设置的位置的值为undefined。
javascript
var a = [];
a[0] = 1;
a[2] = 3;
a[1]; // undefined
a.length; // 3
同时数组也是对象,可以使用字符串的key去获取属性
javascript
var a = [0, 1, 2];
a['2']; // 2
但也需要注意如果把字符串的数字作为索引赋值处理,会被强制转换为十进制的数字,且长度也会改变:
javascript
var a = [];
a['13'] = 22;
a.length; // 14
类数组 有时需要将类数组转换为真正的数组,一般通过数组工具函数(如indexOf、concat、forEach等)来实现; 还有函数的参数arguments也可以进行数组转化:
javascript
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
ES6也可以使用Array.from去处理:
javascript
var arr = Array.from( arguments );
字符串
字符串和数组的确很相似,它们都是类数组,都有length属性以及indexOf和concat方法。 但字符串是不可变的,数组是可变的。
数字
JS只有一种数值类型:number,包括"整数"和带小数的十进制数。JS没有真正意义上的整数。JS中的数字类型是基于IEEE754标准来实现的,该标准通常也被称为"浮点数"。JS使用的是"双精度"格式。 特别大或者特别小的数字默认使用指数格式显示:
javascript
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
较小的数值 二进制浮点数最大的问题就是较小的数值运算不精确:
javascript
0.1+0.2 === 0.3; // false
// 因为相加等于0.30000000000000004
如何来判断相等呢?最常见的方法是设置一个误差范围,通常为称为"机器精度"。JS是2^-52 (2.220446049250313e-16)。 在ES6中使用Number.EPSILON,ES6之前使用polyfill:
javascript
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
使用 Number.EPSILON 来比较两个数字是否相等
javascript
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
整数的安全范围:
数字的呈现方式决定了"整数"的安全值范围远远小于 Number.MAX_VALUE。 能够被"安全"呈现的最大整数是 2^53 - 1,即 9007199254740991,在 ES6 中 被定义为Number.MAX_SAFE_INTEGER。最小整数是 -9007199254740991,在 ES6 中被定义为Number.MIN_SAFE_INTEGER。
特殊数值
不是值的值:
- null指空值
- undefined指没有值
undefined 在非严格模式可以给undefined赋值:
javascript
function foo() {
undefined = 2; // very bad
}
在非严格和严格模式可以把undefined命名变量:
javascript
function foo() {
"use strict";
var undefined = 2; // very bad
console.log(undefined); // 2
}
foo();
void运算符 表达式void xxx没有返回值,因此返回的结果是undefined。
特殊的数字 NaN:不是一个数字。
javascript
var a = 2 / 'foo'; // NaN
typeof a === 'number'; // true
NaN 是一个"警戒值",用于指出数字类型中的错误情况,即"执行数学运算没有成功,这是失败后返回的结果"。 NaN ≠ NaN为true,它和自己不相等,是唯一一个非自反。
javascript
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
如果要判断是否是NaN,需要使用全局工具函数isNaN来判断:
javascript
var a = 2 / "foo";
isNaN(a); // true
但isNaN有个缺陷,就是检查参数是否不是NaN,也不是数字:
javascript
isNaN('foo'); // true
很明显'foo'不是数字也不是NaN,这是一个很久的bug。 ES6中我们可以使用Number.isNaN,ES6之前可以使用polyfill:
javascript
// 方法1:
if (!Number.isNaN) {
Number.isNaN = function (n) {
return typeof n === 'number' && window.isNaN(n)
}
}
// 方法2:
if (!Number.isNaN) {
Number.isNaN = function (n) {
return n !== n
}
}
无穷数 正无穷: Infinity 负无穷:-Infinity
零值 JS有0 and -0,-0也是有意义的,对负数的乘法和除法可以出现-0,加减法不行;-0的判断:
javascript
function isNegZero(n) {
n = Number(n);
return (n === 0) && (1/n === -Infinity)
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
特殊等式 ES6中新加入一个工具方法Object.is来判断俩个值是否绝对相等。
javascript
Object.is(2 / 'foo', NaN); // true
Object.is(-3*0, -0); // true
Object.is(-3*0, 00); // false
polyfill:
javascript
if (!Object.is) {
Object.is = function(v1, v2) {
// 判断是否是-0
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 判断是否是NaN
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
值和引用
JS引用指向的是值,根据值得类型来决定。基本类型是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。复合值(对象:数组和封装对象、函数)则是通过引用复制的方式来赋值/传递。
由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向.
javascript
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]
函数参数就经常让人产生这样的困惑:
javascript
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 然后
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // 是[1,2,3,4],不是[4,5,6,7]
我们向函数传递 a 的时候,实际是将引用 a 的一个复本赋值给 x,而 a 仍然指向 [1,2,3]。在函数中我们可以通过引用 x 来更改数组的值(push(4) 之后变为 [1,2,3,4])。但 x = [4,5,6] 并不影响 a 的指向,所以 a 仍然指向 [1,2,3,4]。 我们不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。 如果要将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。
javascript
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]
}
var a = [1,2,3];
foo( a );
a; // 是[4,5,6,7],不是[1,2,3,4]
如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值。例如:
javascript
foo( a.slice() )
值小结
JavaScript 中的数字包括"整数"和"浮点型"。 null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。 数字类型有几个特殊值,包括NaN(意指"not a number",更确切地说是"invalid number")、+Infinity、-Infinity 和 -0。
原生函数
JS的内建函数,也叫原生函数。常用的原生函数有:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()------ES6 中新加入的!
原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和我们设想的有所出入:
javascript
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
内部属性[[Class]]
所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]]。这个属性无法直接访问,一般通过 Object.prototype.toString(..) 来查看。例如:
javascript
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
封装对象包装
使用封装对象时有些地方需要特别注意。比如 Boolean:
javascript
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 执行不到这里
}
拆分
如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数:
javascript
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
原生函数作为构造函
Array:
javascript
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这实非明智之举:一是容易忘记,二是容易出错。
javascript
var a = new Array( 3 );
a.length; // 3
a;
Object/Function/RegExp 万不得已,不要使用这些构造函数。在实际情况中没有必要使用 new Object() 来创建对象,因为这样就无法像常量形式那样一次设定多个属性,而必须逐一设定。
Date和Error 相较于其他原生构造函数,Date(..) 和 Error(..) 的用处要大很多,因为没有对应的常量形式来作为它们的替代。 创建日期对象必须使用 new Date()。Date可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。Date主要用来获得当前的 Unix 时间戳(从 1970 年 1 月 1 日开始计算,以秒为单位)。该值可以通过日期对象中的 getTime() 来获得。 从 ES5 开始引入了一个更简单的方法,即Date.now()。对 ES5 之前我们可以使用polyfill:
javascript
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
构造函数 Error带不带 new 关键字都可。错误对象通常与 throw 一起使用:
javascript
function foo(x) {
if (!x) {
throw new Error( "x wasn't provided" );
}
// ...
}
Symbol ES6 中新加入了一个基本数据类型 ------符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。该类型的引入主要源于 ES6 的一些特殊构造,此外符号也可以自行定义。 ES6中有一些预定义符号,以Symbol的静态属性形式出现,如 Symbol.create、Symbol.iterator 等,可以这样来使用:
javascript
obj[Symbol.iterator] = function(){ /*..*/ };
我们可以使用Symbol原生构造函数来自定义符号。但它比较特殊,不能new关键字,否则会出错:
javascript
var mysym = Symbol( "my own symbol" );
mysym; // Symbol(my own symbol)
mysym.toString(); // "Symbol(my own symbol)"
typeof mysym; // "symbol"
var a = { };
a[mysym] = "foobar";
Object.getOwnPropertySymbols( a );
// [ Symbol(my own symbol) ]
原生原型
原生构造函数有自己的 .prototype 对象,如 Array.prototype、String.prototype 等。这些对象包含其对应子类型所特有的行为特征。
- String#indexOf 在字符串中找到指定子字符串的位置。
- String#charAt 获得字符串指定位置上的字符。
- String#substr、String#substring 和 String#slice 获得字符串的指定部分。
- String#toUpperCase 和 String#toLowerCase 将字符串转换为大写 or 小写。
- String#trim 去掉字符串前后的空格,返回新的字符串。以上方法并不改变原字符串的值,而是返回一个新字符串。
原生函数小结
JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性。