💉JavaScript类型部分
type
status
date
slug
summary
tags
category
icon
password
Status
Javascript 作为典型的动态语言,为了提高程序健壮性,在运行时作类型检测是必不可少的。
Javascript 是一种类型松散的动态类型语言,这意味,你可以随时改变 JavaScript 变量的类型,并且由类型不一致的错误仅在运行时抛出。在网络通信过程,你无法保证接收的数据类型与预期一致,是否会导致系统出现未定义行为。
因此,在编写成熟、可控的 JavaScript 程序,我们需要对变量的类型进行检测。
内置类型
首先来谈谈 JavaScript 内置的数据类型,包括原始数据类型或者叫值类型,对应的,有引用数据类型。
原始数据类型:
- number
- string
- boolean
- undefined
- null
- symbol
- bigint
引用数据类型:
- Function
- Array(TypedArray)
- Object
- RegExp
- Math
- Date
- Map
- Set
- …
对于引用数据类型,由于 ECMA 不断扩充其标准,无法一一列出。自定义的类型和 BOM、DOM
中使用的接口及类型通常都是继承自 Object 的引用数据类型,且其具体类型可以通过下述各方法获取。
检测方法
类型检测常见的方法有以下:
- typeof
- instanceof
- constructor
- Object.prototype.toString.call
- Duck typing
typeof 判断基本类型
使用运算符
typeof
返回的是类型名,结果仅包括以下 7 种:number、string、boolean、undefined、symbol、object、function。null 类型和大部分引用类型都不能用
typeof
直接判断。注意:用 typeof 判断 null、Array、Date、RegExp 等类型结果均为”object”。
null 被判为对象类型的原因:在判断数据类型时,是根据机器码低位标识来判断的,而 null 的机器码标识为全 0,而对象的机器码低位标识为 000。所以
typeof null
的结果被误判为object
。instanceof 判断引用数据类型
instanceof
用于检查构造函数的prototype
属性是否出现在某个实例对象的原型链上,需要注意的是,对于原始数据类型,__proto__
属性是不存在的,即无法查找原型链,除非使用对应的构造函数去创建实例。也许有人会说
num1.__proto__===Number.prototype
结果是 true 啊,这是因为包装器的原因,对原始数据类型直接调属性是不符合 instanceof 的定义和行为的。当然,对于简单判断原始类型的场景,我们除了 typeof,也可以使用这种判断原型的方法。对 null 和 undefined,因为不存在所谓原型链,也没有所谓包装器,没法用判断原型的方法去判断。
另外,虽然
instanceof
能够判断出 arr 是 Array 的实例,但它“同时也是”Object 的实例,因为instanceof
会在原型链一直寻找判断,找到则返回 true。这对判断一个未知引用类型并不友好。对于这种情况,可以换用 constructor 进行判断。
instanceof 的大概过程,可以自己验证一下:
注意不同 window 或 iframe 间的对象检测不能使用 instanceof。
题外话:在 ES6 中添加了 symbol 类型,目的是避免对象的属性都是字符串键导致冲突,我个人对 symbol 之所以要添加的理解是,与 Proxy 和 Reflect 添加目的一样,是为了开放 JavaScript 语言的底层,供开发者去编写一些元编程的黑魔法提供便利。
比如 Symbol 的各种静态方法可以修改一些方法的行为,针对
instanceof
,有Symbol.hasInstance
方法,能修改判断某对象是否为某构造器的实例的行为。constructor 判断类型
访问实例对象的
constructor
属性会返回其构造函数,因此判断构造函数名来得到类型,如" ".constructor === String
。无法用
constructor
判断 null 和 undefined,但可以避免使用instanceof
时 arr
的原型对象既可以为 Array 也可以是 Object。Object.prototype.toString.call
toString
是 Object 类型的原型方法,任何继承 Object 类型的对象都拥有 toString 方法。所有
typeof
返回为”object”
的对象都有一个内部属性[[class]]
,这个内部属性无法直接访问,一般通过Object.prototype.toString
来查看。如果对象的 toString 方法没有重写的话,直接在对象上调用 toString 方法,可以得到当前对象的
[[Class]],其格式为
"[object Xxx]"
,其中 Xxx 为对象的类型。但除了 Object 类型的对象外,其他类型调用 toString 方法返回的都是其内容的字符串,所以我们需要使用
call
或者apply
来改变toString
方法的执行上下文。Object.prototype.toString.call
可以判断 null,但通常用val===null
来判断是否为 null。修改对象的
Symbol.toStringTag
属性可以修改它的Object.prototype.toString.call
的结果,因此[[class]]
并不是完全不可改的。基于 WebIDL 规范,浏览器正在向所有 DOM 原型对象添加
Symbol.toStringTag
属性。Duck Typing
在程序设计中,Duck Typing(鸭子类型)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
在鸭子类型中,关注点在于对象的行为(能做什么),而不是关注对象的具体类型。
我们可以编写一个函数,它的参数为一个 Duck 类型的对象,并调用它的”走”和”叫”方法。实际使用时,该函数可以接收任意类型的对象,并调用它的”走”和”叫”方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的”走”和”叫”方法的对象都可被函数接受。
翻译过来就是,判断一个对象是否是数组,可以看这个对象是否拥有 push()等方法。TypeScript 的类型保护(Type Guard,或者译为类型守卫)就可以使用这种方法来缩小类型范围。
类型隐式转换
类型转换规则
JS 种的类型转换,只有如下三种情况:
- 转为布尔值
- 转为数字
- 转为字符串
以下是各种数据类型之间相互转换的表格:
原始值类型 | 目标值类型 | 结果 |
boolean | 布尔值 | 本身值,false 就为 false ,true 就为 true |
number | 布尔值 | 0 和 NaN 为 false 否则都为 true |
string | 布尔值 | 除了空字符串为 false 其他都为 true |
undefined、null | 布尔值 | false |
symbol | 布尔值 | true |
对象 | 布尔值 | true |
数组 | 布尔值 | true |
原始值类型 | 目标值类型 | 结果 |
boolean | 字符串 | true: 'true';false: 'false' |
number | 字符串 | 数字字符串 |
string | 字符串 | 字符串本身值 |
undefined、null | 字符串 | 抛错 |
symbol | 字符串 | symbol 字符串 |
对象 | 字符串 | '[object Object]' |
数组 | 字符串 | 空数组转为空字符串,否则转为由逗号拼接每一项的字符串 |
原始值类型 | 目标值类型 | 结果 |
boolean | 数字 | true 转换为 1,false 转换为 0 |
number | 数字 | 数字本身 |
string | 数字 | 除了都是数字组成的字符串,能转换成数字外,其他都是 NaN |
null | 数字 | 0 |
undefined | 数字 | NaN |
symbol | 数字 | 抛错 |
对象 | 数字 | NaN |
数组 | 数字 | 空数组转换为0;只有一项(数字)的数组转换为这个数字;只有一项(空字符串、undefined、null)的数组转换为0;除上述以外情况的数组转换为 NaN |
触发 JS 隐式转换的先决条件
在下面两种情况下,将会触发 JS 的隐性类型转换机制。 1. 当使用
==
、&&
、||
等逻辑操作符进行判断时 2. 当使用 + - * /
四则运算符进行操作时下面就
==
和四则运算两种情况,来分析一下内部的转化机制:1. 使用
==
操作符进行判断时先来看下在使用
==
进行判断时,隐式转换的内部机制,判断步骤如下:两个操作数类型一样的情况:
- 如果两个操作数是同类基本类型值,则直接比较
- 如果两个操作数是同类引用类型值,则比较内存地址
两个操作数类型不一样的情况:
- 如果有一个操作数是布尔值,则将这个布尔值转换为数字再进行比较。
- 如果有一个操作数是字符串,另一个操作数是数字,则将字符串转换成数字再进行比较
- 如果有一个操作数是引用类型的值,则调用该实例的
valueOf
方法,如果得到的值不是基本类型的值,再调用该实例的toString
方法,用得到的基本类型的值按照前面的规则进行匹配对比。
以上逻辑转换成流程图:
特殊情况:
null == undefined
判断为true
null
和undefined
无法转换为基本类型值
NaN != NaN
判断为true
,事实上,NaN
更像一个特例,谁都不等于
2. 使用
+
进行判断时- 两个操作数都为数字时直接运行加法操作
- 若有一方为字符串,则将两个操作数都转换成字符串,进行字符串拼接操作。
true + true
/false + false
/null + null
转换为数字进行加法运算
undefined + undefined
进行加法运算,结果为NaN
3. 使用除 + 号以外的四则运算符判断时
直接进行数学运算,行就行,不行就直接
NaN
,简单粗暴。类型转换的经典面试题目
[] == ![]
输出结果是?
{} == !{}
输出结果是?
1 + '1'
输出结果是?
true + true
输出结果是?
4 + []
输出结果是?
4 + {}
输出结果是?
4 + [1]
输出结果是?
4 + [1, 2, 3, 4]
输出结果是?
'a' + + 'b'
输出结果是?
第一题的转化逻辑如下:
第二题的转化逻辑如下:
后面的题目:
- Twikoo