You-Dont-Know-JS 笔记之类型和语法
第一章:类型(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
是空数组。
这些都是很好的默认值。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 高性能网页动画实践
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
设置成
typeof null === 'object'
肯定是有原因的,不过如果说是一个 bug,也不过分。typeof null === 'object' 并不是浏览器bug
在用 ES6 的话就不要再写
这样的代码了,因为有更优雅的实现方式,比如
@creeperyang 谢谢。所以有些不懂preferredType有什么作用。那请问
if(express)
和if(!!express)
两者的区别在那里?是后者的性能好,还是前者可能会造成异常?express
不是会强制类型转换吗,为什么使用!!
呢?@Huahua-Chen 这是规范层面的描述,实际实现(JS引擎)可能并没有类似的方法。即使有,也是内部运行的,JavaScript层也不会有类似的接口。
@creeperyang 有点不懂,这个操作是内部运行的?怎么可以调用它,也就是设置preferredType。
谢谢博主的耐心解答,上楼我是懂得。是我没有表达清楚,我只是在纠结
PreferredType
这个可选参数在内部是怎么设置的,就比如上面博主举的例子,就是默认的情况,也就是先执行valueOf
再执行toString
,我不知道什么情况下才是先执行toString
,再执行valueOf
(不知道我的问题是不是比较蠢O(∩_∩)O哈哈~)。像下面
哦哦哦,我知道了。
在这里时就是先执行
toString()
,在执行valueOf()
@Huahua-Chen 这是规范里面定义的一个抽象操作。
ToPrimitive ( input [, PreferredType] )
,对 object 执行该操作时,有两种情况:PreferredType
是string
,则按"toString", "valueOf"
的顺序去拿到第一个非 object 的值作为 primitive。PreferredType
是number|default
,则按"valueOf", "toString"
的顺序去拿到第一个非 object 的值作为 primitive。比如对 object 的
ToNumber
操作就是执行:下面是一个详细例子,比如我们知道
==
比较时,如果一个是数字而另一个是对象,那么会对对象执行ToPrimitive
操作:我想这个例子足够讲清楚了,更多信息直接看ES6 Spec: ToPrimitive。
请问toPrimitive是将object类型先通过valueOf(),如果结果不是基本类型在通过toString转换成基本类型的意思么?hint有些不懂
第四章: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