Open creeperyang opened 9 years ago
把值从一个类型转为另一个类型,通常称为类型转换("type casting"),可以是显式的,也可以是隐式的(由值怎么使用的规则强制)。
注意:虽然不明显,但类型转换的结果总是生成基础类型的值。包装不是严格意义的类型转换。
在分辨显式隐式转换前,首先了解控制转换的基本规则。ES5规范的第九章定义了一些抽象操作(也叫 "internal-only operation"),关于转换规则。我们关注ToString
,ToNumber
,ToBoolean
, ToPrimitive
4个。
ToString
非字符串转换成字符串,就由ToString
处理。
内置基础类型有规定的转换规则:null
-->"null"
,undefined
-->"undefined"
,true
-->"true"
。数字就是像我们期待那样, 但很小或很大的数字是以指数形式。
对一般对象来说,除非你指定了你自己的,默认的toString()
(位于Object.prototype.toString()
)会返回[[Class]]
(第三章),例如"[object Object]"
。
注意:对象转换为字符串需要经过ToPrimitive
,这会在ToNumber
段细讲,这里跳过。
JSON.stringify(..)
看起来和ToString
相关,但注意,这和类型转换不是一回事。
对大多数基础值来说,JSON.stringify(..)
表现与ToString
一致。
JSON-safe的值可以被JSON.stringify(..)
。但什么是JSON-safe的?即可以被JSON有效表示的。不是JSON-safe的很容易列出:undefined
,function
,symbol
,有循环引用的object
等。
JSON.stringify(..)
会自动忽略这些不合法值,如果这些值在数组中,会被替换为null
。
如果你JSON.stringify(..)
一个对象,这个对象有toJSON
方法,toJSON
会自动先调用(可以在此返回JSON-safe的值)。
ToNumber
Input Type | Result |
---|---|
Undefined | NaN |
Null | +0 |
Boolean | true-->1, false-->+0 |
String | 看下面的阐述 |
Object | 两个步骤:1. 首先调用ToPrimitive 得到primValue; 2. 返回 ToNumber(primValue) |
下面几个小点根据ES5规范添加了内容。
ToNumber
ToNumber
应用到字符串上时,大部分跟数字字面值差不多。如果转换失败,返回NaN
。假设数字字面值为NumericLiteral,要赚换到数字的字符串为StringNumericLiteral,两者的差别是:
ToPrimitive
参数是要转换的值和可选的PreferredType。如果一个对象可以转换成多个基础值,用_PreferredType_来选一个。
Input Type | Result |
---|---|
Undefined | 值不变,不转换 |
Null | 值不变,不转换 |
Boolean | 值不变,不转换 |
String | 值不变,不转换 |
Number | 值不变,不转换 |
Object | 返回对象的默认值。通过调用对象的[[DefaultValue]] 内部方法获取该默认值,传入可选的hint PreferredType |
[[DefaultValue]] (hint)
假设对象O。
如果hint是String,步骤如下:
O.toString
可调用(是函数)吗?是就str=O.toString()
。如果str是基础类型,返回它。O.valueOf
可调用(是函数)吗?是就val=O.valueOf()
。如果val是基础类型,返回它。TypeError
错误。如果hint是Number,步骤如下:
O.valueOf
可调用(是函数)吗?是就val=O.valueOf()
。如果val是基础类型,返回它。O.toString
可调用(是函数)吗?是就str=O.toString()
。如果str是基础类型,返回它。TypeError
错误。没有hint,那么就当作hint是Number。除非O是Date对象(当作hint是String)。
ToBoolean
Input Type | Result |
---|---|
Undefined | false |
Null | false |
Boolean | 值不变,不转换 |
String | 长度为0-->false;其它,true |
Number | +0,-0,NaN-->false;其它,true |
Object | true |
注意:document.all
虽然是对象,但浏览器(尤其IE,因为旧代码用它hack IE
)出于想尽快废弃它的原因,有了!document.all // true
和typeof document.all // undefined
。
String(..)
和Number(..)
函数可以显式转换数字和字符串。注意,没有new
。
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14
String(..)
把任何值转换成字符串,遵从上面的ToString()
规则。Number(..)
把任何值转换成数字,遵从上面的ToNumber()
规则。
.toString()
也是显式转换为字符串。
+value
中+
是一元操作符,可以把操作数转换成数字。那么它足够显式吗?这依赖你的经验和观点。如果你很喜欢+value
这种形式,要注意它的一些令人困惑的地方:
var c = "3.14";
var d = 5+ +c;
d; // 8.14
与+
一元操作符类似的-
一元操作符也把操作数转换成数字,它改变了符号。
+
一元操作符可以把Date对象转换成数字。
+new Date() // 1439391610180
当然,用Date.now()
(以及new Date( .. ).getTime()
)来获取时间戳可能更语义化一点。
~
二进制非~
可能常被忽视。
~
及其它位操作符只支持操作32位操作数,这意味着它们会把操作数转换成32位数字,转换运用的规则是ToInt32
。ToInt32
首先会执行ToNumber
转换。
比如常见的0 | x
就可以把x
转换成32位整数。
那么~
有什么用(除了转换32位数字)?
~x
的值与-(x+1)
一致;-1
比较特殊,常被看作哨兵值(sentinel value)。比如数组/字符串的搜索(indexOf
),-1
代表不存在,>=0
则是下标。if(str.indexOf('x') >= 0){}
,可以用if(~str.indexOf('x')){//找到}
。~
的另一个用处是~~
可以截断数字小数部分。但需注意2点:
Math.floor
不一致。Math.floor(-49.6) === -50
而~~-49.6 === -49
。x | 0
来截断小数?操作符优先级问题。~
优先级更高,所以可以~~1E20 / 10;
而不用(1E20 | 0) / 10
。从字符串解析数字(parseInt
和parseFloat
)和把字符串类型转换为数字可以得到类似结果,但它们有明显的区别。
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
从字符串解析数字可以容忍非数字字符,遇到时它只是停止从左向右解析。而类型转换时直接返NaN
。解析不应该看作类型转换的替代,因为两者目的不同,解析不关心右侧的非数字字符;而类型转换只接受数字字符。
提示:不要忘记parseInt
操作字符串,传入非字符串没有意义。如果传入非字符串,首先按照ToString
转化成字符串。另外,parseInt
接受第二个参数,指定进制。(parseFloat(string)
没有第二个参数!)如果没有指定,如果字符串开头是0x
就会按照16进制解析,而0
开头就8进制。
parseInt( 0.0000008 ); // 8 ("8" from "8e-7")
parseInt( false, 16 ); // 250 ("fa" from "false")
parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2
parseInt( "\n 3" ); // 3
parseInt( "" ); // NaN
parseInt( " x" ); // NaN
parseFloat( " .9"); // 0.9
注意:
parseInt
和parseFloat
对前导的空白字符(包括换行符)也容忍,但如果第一个非空字符不是数字直接返NaN
。parseInt
和parseFloat
对空字符串返NaN
。Boolean(..)
(没有new
)把非布尔值转换成布尔值。但我们更习惯的可能是!
以及!!
。
隐式转换是不是邪恶?很难说。本文的目标是更好的理解隐式转换,减少代码冗余和不必要的实现细节。
在开始具体的罗列分析隐式转换各种情况前,首先列出ES5相关的一些规范:
lval
,rval
。(有步骤合并省略)lprim = ToPrimitive(lval)
。rprim = ToPrimitive(rval)
。lprim
或rprim
是字符串,都转换为字符串然后相加返回。lval
,rval
。(有步骤合并省略)lnum = ToNumber(lval)
。rnum = ToNumber(rval)
。加减很不同!
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
0 + true // 1
有哪些隐式的boolean
转换?
if (..)
for ( .. ; .. ; .. )
第二处值while (..)
和do..while(..)
? :
第一处值||
和&&
左边的值以上所使用的值如果不是布尔值,会被隐式转换成布尔值,遵循ToBoolean
的规则。
||
and &&
逻辑或,逻辑与都是短路的,并且它们都不会把值转换成布尔值。更精确地说,两个操作符是从两个操作数中选一个。
到现在为止,显式和隐式转换间没有可见的结果不同,除了代码的可读性。
但ES6的symbol不同:显式转换symbol
到string
是允许的,但隐式则报错。
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
symbol
完全无法转换到number
,但奇怪的是symbol
可以显式与隐式转换到boolean
(总是true
)。
JS中有(宽松)相等(==
)和严格相等(===
),两者的区别是==
允许类型转换,===
不允许。
尽管==
比===
可能要慢一点(微秒级),但不要纠结这个。
x == y
遵从The Abstract Equality Comparison Algorithm:
true
。true
。false
。false
。true
。true
。true
。false
。true
,否则返回false
。true
或者false
,返回true
,否则返回false
。true
,否则返回false
。true
。true
。false
。x === y
遵从The Strict Equality Comparison Algorithm
false
。true
。true
。false
。false
。true
。true
。true
。false
。true
,否则返回false
。true
或false
,返回true
,否则返回false
。true
,否则返回false
。The Abstract Relational Comparison Algorithm
对x < y
,规则比较复杂,简单概括下:
ToPrimitive
,也就是说,最终比较的都是基本类型。这里假设转换后左右分别是px
和py
(px < py
)。px
和py
都是String,
py
是px
的前缀,返回false
。px
是py
的前缀,返回true
。px
和py
的字符不同。px[k]
字符的编码。py[k]
字符的编码。m < n
, 返true
。否则返回false
。ToNumber
转化为数字nx
和ny
。nx
是NaN
,返回undefined
。ny
是NaN
,返回undefined
。nx
和ny
是同一数字,返回false
。nx
是-0
,ny
是+0
,返回false
。nx
是+0
,ny
是-0
,返回false
。nx
是Infinity
,返回false
。ny
是Infinity
,返回true
。ny
是-Infinity
,返回false
。nx
是-Infinity
,返回true
。nx
小于ny
,且nx
和ny
都有限,不都为0,返回true
。否则,返回false
。步骤很多,但总结下:首先都转换为基础类型,如果都是字符串,按字符串比较;否则都转数字比较。
请问toPrimitive是将object类型先通过valueOf(),如果结果不是基本类型在通过toString转换成基本类型的意思么?hint有些不懂
@Huahua-Chen 这是规范里面定义的一个抽象操作。
ToPrimitive ( input [, PreferredType] )
,对 object 执行该操作时,有两种情况:
PreferredType
是string
,则按"toString", "valueOf"
的顺序去拿到第一个非 object 的值作为 primitive。PreferredType
是number|default
,则按"valueOf", "toString"
的顺序去拿到第一个非 object 的值作为 primitive。比如对 object 的 ToNumber
操作就是执行:
1. Let primValue be ToPrimitive(argument, hint Number).
2. Return ToNumber(primValue).
下面是一个详细例子,比如我们知道 ==
比较时,如果一个是数字而另一个是对象,那么会对对象执行ToPrimitive
操作:
我想这个例子足够讲清楚了,更多信息直接看ES6 Spec: ToPrimitive。
@creeperyang 有点不懂,这个操作是内部运行的?怎么可以调用它,也就是设置preferredType。
谢谢博主的耐心解答,上楼我是懂得。是我没有表达清楚,我只是在纠结PreferredType
这个可选参数在内部是怎么设置的,就比如上面博主举的例子,就是默认的情况,也就是先执行valueOf
再执行toString
,我不知道什么情况下才是先执行toString
,再执行valueOf
(不知道我的问题是不是比较蠢O(∩_∩)O哈哈~)。
像下面
var a = {
toString: function () {
console.log('toString');
return '1'
},
valueOf: function () {
console.log('valueOf');
return {x: 1}
}
// valueOf: function () {
// console.log('valueOf');
// return 1
// }
}
'1' == a//都是先执行valueOf,再执行toString
哦哦哦,我知道了。
var a = {
toString: function () {
console.log('toString');
return {}
},
valueOf: function () {
console.log('valueOf');
return '1'
}
}
parseInt(a) // toString valueOf 1
在这里时就是先执行toString()
,在执行valueOf()
@Huahua-Chen 这是规范层面的描述,实际实现(JS引擎)可能并没有类似的方法。即使有,也是内部运行的,JavaScript层也不会有类似的接口。
@creeperyang 谢谢。所以有些不懂preferredType有什么作用。那请问if(express)
和if(!!express)
两者的区别在那里?是后者的性能好,还是前者可能会造成异常?express
不是会强制类型转换吗,为什么使用!!
呢?
在用 ES6 的话就不要再写
Array.apply(null, { length: 3 });
这样的代码了,因为有更优雅的实现方式,比如
Array(3).fill(void 0);
Array.from({ length: 3 });
typeof null === 'object' 并不是浏览器bug
@eachmawzw 设置成typeof null === 'object'
肯定是有原因的,不过如果说是一个bug,也不过分。
You Don't Know JS: Types & Grammar
JavaScript的类型和语法
第一章:类型(Types)
很多开发者认为动态语言没有类型。但ES5规范定义:
内置类型有:
null
,undefined
,boolean
,number
,string
,object
,symbol
(ES6新加)。除了
object
都是基础类型(primitives)。typeof
操作符检查给定操作数的类型。类型是undefined
,boolean
,number
,string
,object
,symbol
,function
七种中的一个。为什么没有
null
?typeof null; // 'object'
,这是个浏览器的bug,null
不是对象。为什么有
function
?typeof function a(){ /* .. */ } === "function"; // true
,function
是JS内置的顶级类型之一,也是对象(的子类型),可以调用的对象。值类型(Values as Types)
在JS中,变量(variables)没有类型——值有类型。变量可以在任何时候有任何值。
换一种方法理解JS类型:JS没有强制类型,引擎不要求变量总是存储与初始化时相同类型的值。
undefined
vs "undeclared"当前没有值的变量,其实是当前值为
undefined
。两者区别是:undefined
的变量在当前可访问作用域里已经声明了,只是当前没有值;typeof
undeclared对未声明的变量执行
typeof
得到"undefined"
,这可能会造成一点混淆。但这是安全的检测未声明变量的方法。第二章:值(Values)
数组(Arrays)
数组就是数值索引的任何类型值的集合。
数组不需要你提前定义长度。
delete
会删除对应位置的值,但即使你delete
了所有值,数组的长度不会变化。这样的数组是稀疏数组("sparse"array
),即留下或创建了空槽。注意,稀疏数组看起来是索引对应的值为
undefined
,但这和显示设置arr[index] = undefined
不同。数组是数值索引的,但同时它是对象,所以可以有字符串键值对。一般,你设置字符串属性时,不会影响
length
,但如果这个key可以转换成十进制数字时,会假设你想使用数值索引:类数组
类数组可以通过
Array.prototype.slice.call
或Array.from
(ES6)来转换成数组。字符串(Strings)
认为字符串就是字符数组的想法很常见。但不管字符串的底层实现是否使用数组,字符串与数组有很多不同,相似只是表面的。
尽管字符串和数组有
indexOf
,length
等等相似属性,但注意:JS字符串是不可变的(immutable),而数组是可变的。更进一步,字符串的不可变性:没有一个字符串方法可以就地改变字符串的内容,相反,这些方法都创建并返回一个新字符串。而数组的许多方法可以改变数组本身的内容。
数字(Numbers)
JS只有一个数值类型:
number
。这个类型包括"整数"和小数。"整数"之所以有引号是因为JS并不像其它语言有真的整数。所以,在JS中,"整数"就是没有小数部分的数字:
42.0
和42
一样是"整数"。像大多数现代语言,包括实际上所有脚本语言,JS的
number
基于IEEE 754标准,常称为"浮点数"。JS尤其使用了标准的双精度(double precision)格式(64位二进制)。数字语法(Numeric Syntax)
JS中数字通常用十进制表示:
很大或很小的数字一般以指数形式输出,等同于
toExponential()
方法的输出:toFixed(..)
可以指定小数部分的输出位数(0-20)。toPrecision(..)
指定显示数字时有效数字的个数(1-21)。注意数字的
.
点操作符。因为点是有效的数字字符,所以它首先被解释为数字的一部分,而不是属性访问。数字可以以指数形式定义,如
1e3
。可以16进制定义,0xf3
。可以8进制定义,0363
。注意,ES6+
strict
模式下,8进制的0363
不在允许。但ES6允许两种新形式:0o363
-8进制,0b11110011
-2进制。小的数字(Small Decimal Values)
使用二进制浮点数(使用IEEE 754的所有语言)的最著名副作用是:
简单说,
0.1
和0.2
的二进制浮点表示都不是精确的,所以相加后不是0.3
,接近(不等于)0.30000000000000004
。所以,比较数字时,应该有个宽容值。ES6中这个宽容值被预定义了:
Number.EPSILON
。安全的整数范围(Safe Integer Ranges)
由于数字的表示方法,整数肯定有个安全范围,并且肯定小于
Number.MAX_VALUE
。整数的最大安全值是2^53 - 1
,即9007199254740991
,最小安全值是-9007199254740991
,分别被定义在Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
。我们通常会遇到数据库的64位ID值,由于64位数字无法被JS数字表示,所以必须用字符串表示。
测试整数
Number.isInteger(..)
测试是否是整数。Number.isSafeInteger(..)
测试是否安全的整数。32位(有符号)整数
安全的整数可以到53位(二进制),但很多数字操作(如二进制操作符)只支持32位,所以整数的安全范围可能更小。
a | 0
可以把数字强制转换为32位有符号整数,因为|
二进制操作符只对32位整数有效。注意:
NaN
和Infinity
当然不是安全的整数,但二进制操作符要工作的话首先会把它们转换成+0
。Infinity | 0 // => 0
。特殊值(Special Values)
不是值的值(The Non-value Values)
undefined
类型的值有且只有undefined
一个。null
类型的值有且只有null
一个。undefined
和null
通常被用来当作可互换的空值或非值。可以这么区分:null
是空值(empty value);undefined
是无值(missing value)。null
有值但不做任何事;undefined
还没有值。undefined可以做标识符
非严格模式下,可以向全局的
undefined
赋值。严格与非严格模式下,都可以定义叫undefined
的变量。但这么做是会被打的。void
操作符void
操作符可以生成undefined
值,void 42;//undefined
。特殊数字(Special Numbers)
NaN
NaN
--Not a number。NaN
是一个哨兵值,表示数字范围内的一种错误情况。NaN
不等于任何值,包括自己。一般用isNaN
来测试是否是NaN
,但:Infinities
如果一个操作如加法产生太大而难以表示的数字,IEEE 754舍入到最近值("round-to-nearest")的模式指定值。
Zeros
JS中有
0
和-0
。除了-0
的显示写法,-0
一般从特殊算数运算中得来,如0 / -3
或0 * -3
。加减运算不会产生-0
。最近浏览器控制台才输出(揭示)
-0
,但字符串化-0
只会得到0
,根据规范。有趣的是,相反操作(从字符串到数字)不会说谎:
比较操作也说谎,即
0
等于-0
。第三章:Natives
常用的原生对象有:
String()
,Number()
,Boolean()
,Array()
,Object()
,Function()
,RegExp()
,Date()
,Error()
,Symbol()
。可以看出,这些原生对象其实是内置函数。
Internal [[Class]]
typeof
结果为object
的值额外有个[[Class]]
属性来标记(可看做内部分类)。这个属性无法直接访问,可通过Object.prototype.toString(..)
获取。而对基础类型的值来说:
对
null
和undefined
来说,尽管没有Null()
和Undefined()
,但内部[[Class]]
的值暴露了"Null"
和"Undefined"
。对其它基础类型来说,输出的是它对应包装对象的
[[Class]]
。Boxing Wrappers
基础类型没有属性或方法,但JS自动包装基础类型的值,但你尝试访问属性或方法时。
特意手动创建包装对象来访问属性方法是不必要的,看起来JS不用去包装了,但浏览器很久以前就对这些常见情况优化了,手动创建反而会拖慢程序。
包装对象的陷阱(Object Wrapper Gotchas)
拆箱(Unboxing)
使用
valueOf()
来获取包装对象对应的基础类型值。另外拆箱可以隐式发生,如
new String( "abc" ) + ''
。这个(类型转换)会在第四章讲。Natives as Constructors
对于
array
,object
,function
,和正则来说,更常用的是它们的字面值形式。就像上面看到的其它原生对象,这些构造函数形式一般要避免,因为构造函数可能带来陷阱。
Array(..)
Array
可以不加new
。Array(1,2,3)
返回[1, 2, 3]
。Array
的参数是一个数字时,当成数组长度。此时会创建稀疏数组。map
陷阱。a
、c
是稀疏数组,它们一些情况下和b
表现一致,然后其它情况和b
不一样。怎么显式创建填充
undefined
的数组(非手动)?Array.apply( null, { length: 3 } )
。apply
会把第二个参数当作(类)数组,这就是魔法所在。Object(..), Function(..), and RegExp(..)
Object(..)
/Function(..)
/RegExp(..)
构造函数都是可选的,也最好不用。Function(..)
有时很有用,比如你想动态定义采数和函数体。但不要把Function(..)
当做eval
的替代。Date(..) and Error(..)
Date(..)
和Error(..)
很有用,因为没有对应的字面值形式。Symbol(..)
Symbol可以用作属性名。但一般你无法访问或看到symbol的真实值。
ES6预定义了一些symbol,如
Symbol.create
和Symbol.iterator
。Native Prototypes
内置原生对象构造函数都有自己的
.prototype
对象。这些.prototype
对象包含原生对象独特的行为。Prototypes As Defaults
Function.prototype
是空函数。RegExp.prototype
是空正则(不匹配任何字符串)。Array.prototype
是空数组。这些都是很好的默认值。