概述
众所周知, JavaScript 是一门弱类型语言, 不对变量进行类型强制, 变量可以随时持有任何类型的值, 所以在 JavaScript 中, 类型对于我们开发人员来说可以理解为值的内部特征, 类型定义了值的行为, 以使其能够区别于其他值.
JavaScript 中共有七种内置数据类型, 包括基本类型和对象类型.
基本类型
基本类型分为以下六种:
- string(字符串)
- boolean(布尔值)
- number(数字)
- symbol(符号)
- null(空值)
- undefined(未定义)
string ,number ,boolean 和 symbol 这四种类型统称为原始类型(Primitive), 表示不能再细分下去的基本类型; symbol 表示独一无二的值, 通过 Symbol 函数调用生成, 由于生成的 symbol 值为原始类型, 所以 Symbol 函数不能使用 new 调用; null 和 undefined 通常被认为是特殊值, 这两种类型的值唯一, 就是其本身.
对象类型
对象类型 ( object ) 也称引用类型, 以此和 JavaScript 的基本类型区分开来. 对象在逻辑上是属性的无序集合, 是存放各种值的容器. 对象值存储的是引用地址, 所以和基本类型值不可变的特性不同, 对象值是可变的.
声明一个对象通常有以下几种方式:
- const obj = {
- } // 字面量形式, 推荐
- const obj = new Object() // new 调用
- const obj = Object() // 与 new 调用相同
- cosnt obj = Object.create(null) // 空对象
包装对象
我们知道对象拥有属性和方法. 但比如字符串这种基本类型值不属于对象为什么还拥有属性和方法呢? 实际上在引用字符串的属性或方法时, 会通过调用 new String() 的方式转换成对象, 该对象继承了字符串的方法来处理属性的引用, 一旦引用结束, 便会销毁这个临时对象, 这就是包装对象的概念.
不仅仅只是字符串有包装对象的概念, 数字和布尔值也有相对应的 new Number() 和 new Boolean() 包装对象. null 和 undefined 没有包装对象, 访问它们的属性会报类型错误.
字符串, 数字和布尔值通过构造函数显式生成的包装对象, 既然属于对象, 和基本类型的值必然是有区别的, 这点可以通过 typeof 检测出来.
- typeof 'seymoe' // 'string'
- typeof new String('seymoe') // 'object'
一切皆是对象?
我们经常听到诸如 "JavaScript 中一切皆是对象" 的论断, 并且能够指出部分理由(或者说误导性假象):
所谓例如字符串, 数字等基本类型值 "拥有" 属性和方法
typeof null 结果是 object
可调用的函数也属于对象(子类型)
......
但经过上述的内容, 我们应该很容易反驳 "JavaScript 中一切皆是对象" 这一错误的论断.
数据类型判定
上文中我们了解了 JavaScript 中的数据类型, 那么如何去判断一个值属于什么数据类型呢?
判断一个值属于那种数据类型共有三种方式, 分别为 typeof,instanceof 和 Object.prototype.toString(), 我们分别来看.
typeof
一般通过 typeof 操作符来判断一个值属于哪种基本类型.
- typeof 'seymoe' // 'string'
- typeof true // 'boolean'
- typeof 10 // 'number'
- typeof Symbol() // 'symbol'
- typeof null // 'object' 无法判定是否为 null
- typeof undefined // 'undefined'
根据以上可以看出, 只有 null 的判定会有误差.
如果使用 typeof 操作符对对象类型及其子类型, 譬如函数 (可调用对象), 数组(有序索引对象) 等进行判定, 则除了函数都会得到 object 的结果.
- typeof {
- } // 'object'
- typeof [] // 'object'
- typeof(() => {
- }) // 'function'
由于无法得知一个值到底是数组还是普通对象, 显然通过 typeof 判断具体的对象子类型远远不够.
instanceof
通过 instanceof 操作符也可以对对象类型进行判定, 其原理就是测试构造函数的 prototype 是否出现在被检测对象的原型链上.
- [] instanceof Array // true
- ({
- }) instanceof Object // true
- (()=>{
- }) instanceof Function // true
注意: instanceof 也不是万能的.
举个例子:
- let arr = []
- let obj = {
- }
- arr instanceof Array // true
- arr instanceof Object // true
- obj instanceof Object // true
在这个例子中, arr 数组相当于 new Array() 出的一个实例, 所以 arr.__proto__ === Array.prototype, 又因为 Array 属于 Object 子类型, 即 Array.prototype.__proto__ === Object.prototype, 所以 Object 构造函数在 arr 的原型链上. 所以 instanceof 仍然无法判断优雅的判断一个值到底属于数组还是普通对象.
还有一点, 可能有人会说 Object.prototype.__proto__ === null, 岂不是说 arr instanceof null 也应该为 true, 这个语句其实会报错提示右侧参数应该为对象, 这也印证 typeof null 的结果为 object 真的只是个 bug .
Object.prototype.toString()
Object.prototype.toString() 可以说是判定 JavaScript 中数据类型的终极解决方法了, 具体用法请看以下代码:
- Object.prototype.toString.call({
- }) // '[object Object]'
- Object.prototype.toString.call([]) // '[object Array]'
- Object.prototype.toString.call(() => {
- }) // '[object Function]'
- Object.prototype.toString.call('seymoe') // '[object String]'
- Object.prototype.toString.call(1) // '[object Number]'
- Object.prototype.toString.call(true) // '[object Boolean]'
- Object.prototype.toString.call(Symbol()) // '[object Symbol]'
- Object.prototype.toString.call(null) // '[object Null]'
- Object.prototype.toString.call(undefined) // '[object Undefined]'
- Object.prototype.toString.call(new Date()) // '[object Date]'
- Object.prototype.toString.call(Math) // '[object Math]'
- Object.prototype.toString.call(new Set()) // '[object Set]'
- Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
- Object.prototype.toString.call(new Map()) // '[object Map]'
- Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
我们可以发现该方法在传入任何类型的值都能返回对应准确的对象类型. 用法虽简单明了, 但其中有几个点需要理解清楚:
该方法本质就是依托
Object.prototype.toString()
方法得到对象内部属性 [[Class]]
传入原始类型却能够判定出结果是因为对值进行了包装
null 和 undefined 能够输出结果是内部实现有做处理
类型转换
将值从一种类型转换为另一种类型被称为 "类型转换", 在 JavaScript 中类型转换都属于强制类型转换. 更进一步, 我们可以借用《You Don't Know JS》作者在中卷提到的观点, 将强制类型转换分为隐式强制类型转换和显式强制类型转换. 某些操作符产生的副作用等不明显的转换就是隐式转换, 我们能够从代码中看到哪些地方进行了转换就是显式转换.
- let a = 10
- let b = a + '' // 隐式强制类型转换
- var c = String(a) // 显式强制类型转换
上述代码中, 对于变量 b 来说, a 被强制转换为字符串类型与 ''拼接, 这个过程是" 隐式 "的, 而对于变量 c 而言, 主动调用 String 构造函数进行类型转换是" 显式 " 的. 但隐式和显式仍然是相对的, 如果你自己清楚数字与字符串相加, 数字会被强制转换为字符串这个规则, 那么它就是显式的, 反之同理.
JavaScript 中, 强制类型转换总是会返回基本类型的值, 比如字符串, 数字, 布尔值, 不会返回对象和函数.
转换为字符串
ES 规范定义了一些抽象操作 (即仅供内部使用的操作) 和转换规则来进行强制类型转换, ToString 抽象操作就负责处理非字符串到字符串的强制类型转换.
转换规则:
null 转换为'null'
undefined 转换为 undefined
true 转换为'true',false 转换为'false'
数字转换遵循通用规则, 极大极小的数字使用指数形式
普通对象除非自定义 toString() 方法, 否则返回内部属性 [[Class]], 如上文提到的 [object Object]
对象子类型的 toString() 被重新定义的则相应调用返回结果
- String(null) // 'null'
- String(undefined) // 'undefined'
- String(true) // 'true'
- String(1) // '1'
- String(-1) // '-1'
- String(0) // '0'
- String(-0) // '0'
- String(Math.pow(1000,10)) // '1e+30'
- String(Infinity) // 'Infinity'
- String(-Infinity) // '-Infinity'
- String({
- }) // '[object Object]'
- String([1,[2,3]]) // '1,2,3'
转换为数字
ToNumber 抽象操作负责处理非数字类型转换为数字类型.
转换规则:
null 转换为 0
undefined 转换为 NaN
true 转换为 1,false 转换为 0
字符串转换时遵循数字常量规则, 转换失败返回 NaN
对象类型会被转换为相应的基本类型值, 如果得到的值类型不是数字, 则遵循以上规则强制转换为数字
为了将值转换为基本类型值, 规范定义了 ToPrimitive 内部操作, 该操作首先检测该值是否存在 valueOf() 方法, 有且返回的值为基本类型值则用此值返回或继续转换, 没有则检测是否存在 toString() 方法, 有且返回基本类型值则用此值返回或继续转换, 没有则报错.
转换为布尔值
ToBoolean 抽象操作负责处理非布尔类型转换为布尔类型.
转换规则:
可以被强制强制类型转换为 false 的值: null,undefined,false,+0,-0,NaN 和 ''
假值列表以外的值都是真值
总结
JavaScript 中的数据类型总共只有七种, 包括六种基本类型和对象类型, 对象类型拥有很多子类型, 也可以说是 JavaScript 的内置对象. 判断一个值属于那种数据类型有三种方式. JavaScript 中的数据类型转换只会返回基本类型值, 所以基本存在转换为字符串, 数字和布尔值三种情况, 转换的更多具体细节, 本文不做讨究.
参考资料
- You Don't Know JS: Types & Grammar
- http://2ality.com/2011/11/improving-typeof.html
玩转 JavaScript 系列
写作是一个学习的过程, 尝试写这个系列也主要是为了巩固 JavaScript 基础, 并尝试理解其中的一些知识点, 以便能灵活运用. 如果有错误或者不严谨的地方, 请务必给予指正, 十分感谢!
整个系列会持续更新, 不会完结.
全目录
1. 玩转 JavaScript 之 数据类型
来源: https://juejin.im/post/5c2efc6c6fb9a049db734473