blog
blog copied to clipboard
You-Dont-Know-JS笔记之类型和语法
You Don't Know JS: Types & Grammar
JavaScript的类型和语法
第一章:类型(Types)
很多开发者认为动态语言没有类型。但ES5规范定义:
此规范内的算法在处理每个值时都有一个关联的类型。可能的值类型都定义在这个条款中。类型可以进一步分为 ECMAScript 语言类型和规范类型。 ECMAScript 语言类型和使用ECMAScript语言的程序员处理的值相符。ECMAScript语言类型有:Undefined, Null, Boolean, String, Number, 和 Object。
内置类型
内置类型有: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
的变量在当前可访问作用域里已经声明了,只是当前没有值; - "undeclared"的变量在当前可访问作用域没有正式声明。
typeof
undeclared
对未声明的变量执行typeof
得到"undefined"
,这可能会造成一点混淆。但这是安全的检测未声明变量的方法。
var declared;
typeof declared; // "undefined"
typeof undeclared; // "undefined"
if(undeclared){} // Uncaught ReferenceError: undeclared is not defined
第二章:值(Values)
数组(Arrays)
数组就是数值索引的任何类型值的集合。
数组不需要你提前定义长度。delete
会删除对应位置的值,但即使你delete
了所有值,数组的长度不会变化。这样的数组是稀疏数组("sparse" array
),即留下或创建了空槽。
注意,稀疏数组看起来是索引对应的值为undefined
,但这和显示设置arr[index] = undefined
不同。
数组是数值索引的,但同时它是对象,所以可以有字符串键值对。一般,你设置字符串属性时,不会影响length
,但如果这个key可以转换成十进制数字时,会假设你想使用数值索引:
var a = [ ];
a['key'] = 'value';
a.length; // 0
a['13'] = 42;
a.length; // 14, a: [undefined × 13, 42]
类数组
类数组可以通过Array.prototype.slice.call
或Array.from
(ES6)来转换成数组。
Array.prototype.slice.call({length: 2}) // [undefined × 2]
字符串(Strings)
认为字符串就是字符数组的想法很常见。但不管字符串的底层实现是否使用数组,字符串与数组有很多不同,相似只是表面的。
尽管字符串和数组有indexOf
,length
等等相似属性,但注意:JS字符串是不可变的(immutable),而数组是可变的。
更进一步,字符串的不可变性:没有一个字符串方法可以就地改变字符串的内容,相反,这些方法都创建并返回一个新字符串。而数组的许多方法可以改变数组本身的内容。
数字(Numbers)
JS只有一个数值类型:number
。这个类型包括"整数"和小数。"整数"之所以有引号是因为JS并不像其它语言有真的整数。
所以,在JS中,"整数"就是没有小数部分的数字:42.0
和42
一样是"整数"。
像大多数现代语言,包括实际上所有脚本语言,JS的number
基于IEEE 754标准,常称为"浮点数"。JS尤其使用了标准的双精度(double precision)格式(64位二进制)。
数字语法(Numeric Syntax)
JS中数字通常用十进制表示:
var b = 42.3;
b = 0.42;
b = .42; // 十进制开始部分如果是0可以省略
b = 42.0;
b = 42. ;// 十进制结束部分如果是0可以省略
b = 42.300; b; // 42.3 尾部多余的0通常被移除
b = 42.0; b; // 42
很大或很小的数字一般以指数形式输出,等同于toExponential()
方法的输出:
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
toFixed(..)
可以指定小数部分的输出位数(0-20)。toPrecision(..)
指定显示数字时有效数字的个数(1-21)。
var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
注意数字的**.
**点操作符。因为点是有效的数字字符,所以它首先被解释为数字的一部分,而不是属性访问。
// invalid syntax:
42.toFixed( 3 ); // SyntaxError
// these are all valid:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000"
数字可以以指数形式定义,如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; // false
简单说,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
的变量。但这么做是会被打的。
function foo() {
undefined = 2; // really bad idea!
}
foo();
function bar() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
bar();
void
操作符
void
操作符可以生成undefined
值,void 42;//undefined
。
特殊数字(Special Numbers)
NaN
NaN
--Not a number。NaN
是一个哨兵值,表示数字范围内的一种错误情况。
NaN
不等于任何值,包括自己。一般用isNaN
来测试是否是NaN
,但:
window.isNaN(2 / "foo"); // true
window.isNaN("foo"); // true -- ouch!
Infinities
1 /0; // Infinity
-1 / 0; // -Infinity (1 / -0)
Infinity / Infinity; // NaN (Infinity / -Infinity)
如果一个操作如加法产生太大而难以表示的数字,IEEE 754舍入到最近值("round-to-nearest")的模式指定值。
var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity Number.MAX_VALUE + Math.pow( 2, 970 )与Infinity更近
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308 Number.MAX_VALUE + Math.pow( 2, 969 )与Number.MAX_VALUE更近
Zeros
JS中有0
和-0
。除了-0
的显示写法,-0
一般从特殊算数运算中得来,如0 / -3
或0 * -3
。加减运算不会产生-0
。
最近浏览器控制台才输出(揭示)-0
,但字符串化-0
只会得到0
,根据规范。
var a = 0 / -3;
// (some browser) consoles at least get it right
a; // -0
// but the spec insists on lying to you!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// strangely, even JSON gets in on the deception
JSON.stringify( a ); // "0"
有趣的是,相反操作(从字符串到数字)不会说谎:
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
比较操作也说谎,即0
等于-0
。
第三章:Natives
常用的原生对象有:String()
,Number()
,Boolean()
,Array()
,Object()
,Function()
,RegExp()
,Date()
,Error()
,Symbol()
。
可以看出,这些原生对象其实是内置函数。
Internal [[Class]]
typeof
结果为object
的值额外有个[[Class]]
属性来标记(可看做内部分类)。这个属性无法直接访问,可通过Object.prototype.toString(..)
获取。
而对基础类型的值来说:
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]"
Object.prototype.toString.call( true ); // "[object Boolean]"
对null
和undefined
来说,尽管没有Null()
和Undefined()
,但内部[[Class]]
的值暴露了"Null"
和"Undefined"
。
对其它基础类型来说,输出的是它对应包装对象的[[Class]]
。
Boxing Wrappers
基础类型没有属性或方法,但JS自动包装基础类型的值,但你尝试访问属性或方法时。
特意手动创建包装对象来访问属性方法是不必要的,看起来JS不用去包装了,但浏览器很久以前就对这些常见情况优化了,手动创建反而会拖慢程序。
包装对象的陷阱(Object Wrapper Gotchas)
!new Boolean( false ); // false
typeof new String('a'); // object 注意,String前需要new
Object('a') instanceof String; // true 注意,Object前的new可以省略
拆箱(Unboxing)
使用valueOf()
来获取包装对象对应的基础类型值。
new String( "abc" ).valueOf() // "abc"
另外拆箱可以隐式发生,如new String( "abc" ) + ''
。这个(类型转换)会在第四章讲。
Natives as Constructors
对于array
,object
,function
,和正则来说,更常用的是它们的字面值形式。
就像上面看到的其它原生对象,这些构造函数形式一般要避免,因为构造函数可能带来陷阱。
Array(..)
-
Array
可以不加new
。Array(1,2,3)
返回[1, 2, 3]
。 -
Array
的参数是一个数字时,当成数组长度。此时会创建稀疏数组。 - 稀疏数组的
map
陷阱。
var a = new Array( 3 ); // [undefined × 3]
var b = [ undefined, undefined, undefined ]; // [undefined, undefined, undefined]
var c = [];
c.length = 3; // [undefined × 3]
a
、c
是稀疏数组,它们一些情况下和b
表现一致,然后其它情况和b
不一样。
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v,i){ return i; }); // [ undefined x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
怎么显式创建填充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
是空数组。
这些都是很好的默认值。
第四章:Coercion
值的类型转换(Converting Values)
把值从一个类型转为另一个类型,通常称为类型转换("type casting"),可以是显式的,也可以是隐式的(由值怎么使用的规则强制)。
**注意:虽然不明显,但类型转换的结果总是生成基础类型的值。**包装不是严格意义的类型转换。
抽象值操作(Abstract Value Operations)
在分辨显式隐式转换前,首先了解控制转换的基本规则。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 Stringification
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_,两者的差别是:
- StringNumericLiteral前后可以有空白符(包括换行符);
- StringNumericLiteral前面可以有任意位0,且不会被解释成8进制;
- StringNumericLiteral前面可以有'+/-'来表示符号;
- StringNumericLiteral如果为空字符串或者只有空白符(包括换行符)会被转为0。
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
。
显式转换(Explicit Coercion)
显式转换Strings <--> Numbers
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 To number
+
一元操作符可以把Date对象转换成数字。
+new Date() // 1439391610180
当然,用Date.now()
(以及new Date( .. ).getTime()
)来获取时间戳可能更语义化一点。
The Curious Case of the ~
二进制非~
可能常被忽视。
~
及其它位操作符只支持操作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
。
显式:Parsing Numeric Strings
从字符串解析数字(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
Boolean(..)
(没有new
)把非布尔值转换成布尔值。但我们更习惯的可能是!
以及!!
。
隐式转换(Implicit Coercion)
隐式转换是不是邪恶?很难说。本文的目标是更好的理解隐式转换,减少代码冗余和不必要的实现细节。
在开始具体的罗列分析隐式转换各种情况前,首先列出ES5相关的一些规范:
- 分别计算左右操作数,得到
lval
,rval
。(有步骤合并省略) -
lprim = ToPrimitive(lval)
。 -
rprim = ToPrimitive(rval)
。 - 如果
lprim
或rprim
是字符串,都转换为字符串然后相加返回。 - 都转换为数字相加后返回。
- 分别计算左右操作数,得到
lval
,rval
。(有步骤合并省略) -
lnum = ToNumber(lval)
。 -
rnum = ToNumber(rval)
。 - 相减后返回。
加减很不同!
隐式:Strings <--> Numbers
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
隐式:Booleans --> Numbers
0 + true // 1
隐式:* --> Boolean
有哪些隐式的boolean
转换?
-
if (..)
-
for ( .. ; .. ; .. )
第二处值 -
while (..)
和do..while(..)
-
? :
第一处值 -
||
和&&
左边的值
以上所使用的值如果不是布尔值,会被隐式转换成布尔值,遵循ToBoolean
的规则。
Operators ||
and &&
逻辑或,逻辑与都是短路的,并且它们都不会把值转换成布尔值。更精确地说,两个操作符是从两个操作数中选一个。
Symbol Coercion
到现在为止,显式和隐式转换间没有可见的结果不同,除了代码的可读性。
但ES6的symbol不同:显式转换symbol
到string
是允许的,但隐式则报错。
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
symbol
完全无法转换到number
,但奇怪的是symbol
可以显式与隐式转换到boolean
(总是true
)。
Loose Equals vs. Strict Equals
JS中有(宽松)相等(==
)和严格相等(===
),两者的区别是==
允许类型转换,===
不允许。
性能(Equality Performance)
尽管==
比===
可能要慢一点(微秒级),但不要纠结这个。
- 如果你比较的两个值类型相同,那么两种相等采用同一算法,做的工作相同。
- 如果比较的类型不同,那么重点是你希望有类型转换吗?
Abstract Equality
x == y
遵从The Abstract Equality Comparison Algorithm:
- x与y类型相同:
- x的类型是 Undefined,返回
true
。 - x的类型是 Null,返回
true
。 - x的类型是 Number:
- x是NaN,返回
false
。 - y是NaN,返回
false
。 - x与y是同一数字,返回
true
。 - x是+0,y是-0,返回
true
。 - x是-0,y是+0,返回
true
。 - 返回
false
。
- x是NaN,返回
- x的类型是 String,如果x和y是完全相同的字符序列,返回
true
,否则返回false
。 - x的类型是 Boolean,如果x和y同是
true
或者false
,返回true
,否则返回false
。 - 如果x和y指向同一个对象,返回
true
,否则返回false
。
- x的类型是 Undefined,返回
- 如果x是null,y是undefined,返回
true
。 - 如果x是undefined,y是null,返回
true
。 - 如果x的类型是 Number,y的类型是 String,返回 x == ToNumber(y)。
- 如果x的类型是 String,y的类型是 Number,返回 ToNumber(x) == y。
- 如果x的类型是 Boolean,返回 ToNumber(x) == y。
- 如果y的类型是 Boolean,返回 x == ToNumber(y)。
- 如果x的类型是 String 或 Number,y的类型是 Object,返回 x == ToPrimitive(y)。
- 如果y的类型是 String 或 Number,x的类型是 Object,返回 ToPrimitive(x) == y。
- 返回
false
。
Strict Equality Comparison Algorithm
x === y
遵从The Strict Equality Comparison Algorithm
- x与y类型不同,返回
false
。 - 如果x的类型是 Undefined,返回
true
。 - 如果x的类型是 Null,返回
true
。 - 如果x的类型是 Number:
- 如果x是 NaN, 返回
false
。 - 如果y是 NaN, 返回
false
。 - x和y是相同的数字,返回
true
。 - x是+0, y是-0,返回
true
。 - x是-0, y是+0,返回
true
。 - 返回
false
。
- 如果x是 NaN, 返回
- 如果x的类型是 String,如果x和y是完全相同的字符序列,返回
true
,否则返回false
。 - 如果x的类型是 Boolean,如果x和y同是
true
或false
,返回true
,否则返回false
。 - 如果x和y指向同一个对象,返回
true
,否则返回false
。
Abstract Relational Comparison
The Abstract Relational Comparison Algorithm
对x < y
,规则比较复杂,简单概括下:
- 首先会对两个操作数执行
ToPrimitive
,也就是说,最终比较的都是基本类型。这里假设转换后左右分别是px
和py
(px < py
)。 - 如果
px
和py
都是String,- 如果
py
是px
的前缀,返回false
。 - 如果
px
是py
的前缀,返回true
。 - 让 k 是最小的非负整数,在位置 k,
px
和py
的字符不同。 - 让 m 是
px[k]
字符的编码。 - 让 n 是
py[k]
字符的编码。 - 如果
m < n
, 返true
。否则返回false
。
- 如果
- 如果不都是String:
- 用
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,也不过分。