类型

101 阅读7分钟

基础认识

之前就大致说过JS的几种类型。抽象一点讲,所谓的类型是值的内部特征,它定义了值的行为,以此区别其他的不同类型的值。JS中的类型知识,说复杂不复杂,但是其中的类型转换却可以是今人头疼的。

内置类型

JS中有七种内置类型,分别是

  • 空值(null)
  • 未定义(undefined)
  • 布尔值( boolean)
  • 数字(number)
  • 字符串(string)
  • 对象(object)
  • 符号(symbol,ES6 中新增) 对象之外的其他类型成为“基本类型”

使用typeof可以用来查看值的类型,但是有一个类型是特殊的

typeof null === 'object' //true

这个原因之前也说过,主要是JS底层二进制表示对象的前三位是000,而null全都都是000,所以判断成对象,正确的做法是

let a = null;
(!a && typeof a === "object"); // true

null本身就是一个“假值”,再加上typeof的判断就可以了。

来看看其他的

 typeof function a(){ /* .. */ } === "function"; // true

你会奇怪,function 难道不是内置类型么。但其实,它是Object的子对象,具体讲,函数是可调用的“对象”,这是由其内部的[[Call]]属性决定的。那函数既然是对象,那它就可以拥有属性。

function fn (a,b) {}
console.log(fn.length) //2

没想到吧,函数竟然也有长度,没错,这leng属性就是其声明的参数个数。所以是2。那么还有一种类型--数组是否也是特殊类型么?

typeof [1,2,3] === 'object' //true

显示不是,数组也是对象的一种子类型,数组是按数字顺序来索引的,length表示其元素的个数。

JS的变量是没有类型的, 因为它们可以随时持有任何类型的值,所以值才是有类型的。当变量未赋值时,此时typeof的结果是undefined,但是undefined表不是undeclared(未声明)。

  • 变量在作用域中声明了,但是没赋值,是undefined。
  • 没有在作用域中声明过的变量,叫做undeclared 有意思的是 typeof对没有定义的变量判断也是 undefined
var a;
typeof a; // "undefined"
typeof b; // "undefined"

这是因为typeof有一个特殊的安全防范机制。避免报错的影响。还有一点,当宿主环境是浏览器的情况下,还可以用window. 来避免报错,未声明的变量默认会挂载在window上。

  • 数组 数组是JS中常见的使用类型,里面可以收纳各种类型的值,字符串,数字,对象,甚至是数组。数组声明后可以直接赋值,并不需要设定大小,这个大小会跟着变化。但是如果预先设置大小或者在某个位置插入值,就会变成“稀疏”数组,产生很多空白单元,
let c = []
c.length = 10;
c[3] = 3
console.log(c[1]) //undefined

数组通过数组进行索引,当然如果是字符串的键值也行,但是这不算在length里面。

let a =[];
a[0] = 1;
a['fn'] = 2;
a.lenght  //1

需要注意的是,如果字符串键值是可以强制转换成10进制的话,它会被当成索引

let a = [];
a['12'] = 12;
a.lenth //13

类数组 例如arguments对象,Dom元素列表等都是类数组,有数组的形状没有数组的方法。正常有以下几种方式

slice();
var arr = Array.prototype.slice.call(arguments) //返回一个数组的副本。

ES6Array.from()
var arr = Array.from( arguments );
  • 字符串 字符串和数组很像,有很多和数组同名的方法,如indexOf,concat等。但是数组是不可变的,那些函数并不会改变其原始值,而是返回并创建一个新的字符串。而数组的函数大部分是在其原始值上面修改的。

数组和字符串可以相互转换

let a = 'hahahaha'
a.split('') //转为数组
.... //可进行一些数组的方法操作
.join('') //数组再转为字符串
  • 数字 JS的数字是包含"整数"和带小数的十进制数。但是JS又没有真正意义上的整数,它无法就是没有带小数的点的十进制数而已,所以40.0等于40。

JS的数字类型是基于IEEE754标准来实现的,这个标准通常被称为”浮点数“,而JS用的是“双精度”格式,即64位二进制。

toFixed() 可指定小数部分返回的显示位数。返回的值是指定数字的字符串形式。数字常量可以调用一些数字的方法

// 无效语法:
42.toFixed( 3 ); // SyntaxError
// 下面的语法都有效: 
(42).toFixed( 3 ); // "42.000" 
0.42.toFixed( 3 ); // "0.420" 
42..toFixed( 3 ); // "42.000"

42.tofixed(3)是无效语法,因为 . 被视为常量42. 的一部分,所以没有通过 . 来运行toFixed方法 。而最后一个42..没有这个问题是因为第一个 . 是number的一部分,而第二个 .就是属性访问符。

42 .toFixed( 3 ) //中间隔了空格也行

浮点数问题

0.1 + 0.2 === 0.3; // false

这个问题不光JS有,很多使用了IEEE754标准的语言也有这个问题。其本质原因是 二进制浮点数中的0.1和0.2并不精确,相加的结果是 0.30000000000000004这样的接近数字
当然绝大部分程序处理的都是整数的操作,最大也不过百亿万亿,所以其实不用担心JS的精度问题,此时的JS的数字类型是安全的。

那么如何判断 0.1 + 0.2 和 0.3 相等呢

常见的方法是设置一个误差范围值,通常称为“机器精度”,这个值通常是 2^-52 (2.220446049250313e-16)。

ES6之前可以使用Math.pow(2,-52),ES6则可以使用Number.EPSILON

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.EPSILON 这个值表示了1 与可表示的大于 1 的最小的浮点数之间的差值,大约也就是2^-52,所以当数字相加之和 和这个值之间的误差小于Number.EPSILON时,就可以判定两数相等了,因为误差太小了。

JS的安全整数范围在 Number.MAX_VALUE和Number.MIN_SAFE_INTEGER之间,就是2^53 - 1 到 -2^53 - 1,也就是9007199254740991~ -9007199254740991

整数的检测方法有:ES6的Number.isInteger()或者polyfill方法

if (!Number.isInteger) {
 Number.isInteger = function(num) {
     return typeof num == "number" && num % 1 == 0;
 };
}

特殊值

undefined类型只有一个值,就是undefined,null类型也只有一个值,即null。两者很像,但又有点区别

  1. null 指空值,undefined 指没有值
  2. nul不是标识符,不能用来做变量使用和赋值,但是undefined可以做变量和赋值

NaN,无效数值,失败数值。表示不是一个数字。但其本身确是数字类型typeof NaN === "number,一般用指执行数学运算没有成功,是失败的返回结果。它的特殊点不止于此,它不等于任何值,哪怕是自身。

NaN != NaN  //true,

ES6的Number.isNaN(..)可以用来判断是否是NaN

简单标量基本类型值(字符串和数字等)通过值复制来赋值/传递,而复合值(对象等) 通过引用复制来赋值 / 传递。JavaScript 中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值。
简单来讲就是基本类型复制后,新值的改变不影响旧值,而复杂类型的复制,新值的个数改变或者属性改变会影响旧的对象。

let a = 2;
let b = a;
b++
console.log(a) //2
console.log(b) //3


var c = [1,2,3];
var d = c;
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]


var a = [1,2,3];
var b = a;
b = [4,5,6]; 
a; // [1,2,3] 
b; // [4,5,6]
//这就是JS中的引用时指向值的说法,b其实也是指向[1,2,3]a没啥关系,后续 b 变了是它本身引用的值变了,这并不影响a的引用

原生函数

JS 有很多常见的原生函数

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()——ES6 中新加入的! 原生函数可以被当做构造函数使用,
var a = new String( "abc" );
typeof a;  //'object'

这里创建出来的是封装了基本类型值的封装对象。

而这些 typeof 返回值是‘object’的对象内部都有一个属性[[class]],这个属性无法直接访问,一般通过Object.prototype.toString()来查看。

Object.prototype.toString.call([1,2,3]) //[object Array]
Object.prototype.toString.call(/regex-literal/i) //[object RegExp]

Object.prototype.toString.call( null );[object null]
Object.prototype.toString.call( undefined );[object undefined]

Object.prototype.toString.call( "abc" );[object String]
Object.prototype.toString.call( 42 );[object Number]

这个属性基本是和创建这个对象的原生构造函数一致,null和undefined 虽然没有构造函数,但是内部属性任然还是其类型值,而像一些基本类型则会被各自的封装对象自动包装,进而判断出值。

基本类型值一般没有 .length 或者 .toString的方法,这都是JS自动为其做的对象包装。但是如果想要得到封装对象的值的话,可以使用 .valueOf()方法

var a = new String( "abc" );
a.valueOf() //abc

大部分情况下,基本不会用或者少用原生构造函数构造新值,主要是因为这样的操作比较繁琐,而且执行效率不高。

但是像 Date() 和 Error() 这是需要构造函数来做的,Date()不带new的话会返回当前日期的字符串值,而错误类型则只能使用Error来生成。

Symbol:ES6新增的基本类型,意思是 符号,符号是具有唯一性的特殊值(并非绝对),用来命名对象属性不容易导致重名。

symbol同原生构造函数来构造的时候不能带 new 关键字,这样会报错。它一般以静态形式出现,代码和控制台都无法查看和访问它的值。大部分用于私有或特殊属性。