前言
毕业到入职腾讯已经差不多一年的时光了,接触了很多项目,也积累了很多实践经验,在处理问题的方式方法上有很大的提升。随着时间的增加,愈加发现基础知识的重要性,很多开发过程中遇到的问题都是由最基础的知识点遗忘造成,基础不牢,地动山摇。所以,就再次回归基础知识,重新学习JavaScript相关内容,加深对JavaScript语言本质的理解。日知其所亡,身为有追求的程序员,理应不断学习,不断拓展自己的知识边界。本系列文章是在此阶段产生的积累,以记录下以往没有关注的核心知识点,供后续查阅之用。
2017
03/13
ECMAScript 语言类型包 括 Undefined、Null、Boolean、String、Number 和 Object,符号(symbol,ES6 中新增)
“类型”:对语言引擎和开发人员来说,类型是值 的内部特征,它定义了值的行为,以使其区别于其他值。
检测null :
var a = null; (!a && typeof a === "object"); // true
JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类 型,因为 JavaScript 中的变量没有类型。
已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明 过的变量,是 undeclared 的。
03/14
对于 undeclared(或者 not defined)变量,typeof 照样返回 "undefined",这是因为 typeof 有一个特殊的安全防范 机制。
如何在程序中检查全局变量 DEBUG 才不会出现 ReferenceError 错误。这时 typeof 的 安全防范机制就成了我们的好帮手;还有一种不用通过 typeof 的安全防范机制的方法,就是检查所有全局变量是否是全局对象 的属性,浏览器中的全局对象是 window。
使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数 组的 length 属性并不会发生变化。数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性 (但这些并不计算在数组长度内): 如果字符串键值能够被强制类型转换为十进制数字的话,它 就会被当作数字索引来处理。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符 串。而数组的成员函数都是在其原始值上进行操作。
字符串和数组的确很相似,它们都是类数组,都有 length 属性以及 indexOf(..)(从 ES5 开始数组支持此方法)和 concat(..) 方法。
特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。 toPrecision(..) 方法用来指定有效数位的显示位数。
不过对于 . 运算符需要给予特别注 意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属 性访问运算符。
// 无效语法: 42.toFixed( 3 ); // SyntaxError // 下面的语法都有效: (42).toFixed( 3 ); // "42.000" 0.42.toFixed( 3 ); // "0.420" 42..toFixed( 3 ); // "42.000"
a | 0 可以将变量 a 中的数值转换为 32 位有符号整数,因为数位运算符 | 只适用于 32 位 整数(它只关心 32 位以内的值,其他的数位将被忽略)。因此与 0 进行操作即可截取 a 中 的 32 位数位。
- null 指空值(empty value)
- undefined 指没有值(missing value)
null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。
03/15
void 并不改变表达式的结果, 只是让表达式不返回值。void 0、void 1 和 undefined 之间并没有实质上的区别。
NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误 情况,即“执行数学运算没有成功,这是失败后返回的结果”。
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不 成立)的值。而 NaN != NaN 为 true,isNaN(..) 有一个严重的缺陷,它的检查方式过于死板,就 是“检查参数是否不是 NaN,也不是数字”。
要区分 -0 和 0,不能仅仅依赖开发调试窗口的显示结果,还需要做一些特殊处理: function isNegZero(n) { n = Number( n ); return (n === 0) && (1 / n === -Infinity); }
- 简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
- 复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递。 由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
function foo(x) { x.push(4); x; // [1,2,3,4] x = [4, 5, 6]; x.push(7); x; // [4,5,6,7] } var a = [1, 2, 3]; foo(a); a; // 是[1,2,3,4],不是[4,5,6,7]
我们不能通过引用 x 来更改引用 a 的指向,只能更改 a 和 x 共同指向的值。
slice(..) 不带参数会返回当前数组的一个浅复本(shallow copy)。
03/16
常用的原生函数有:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()——ES6 中新加入的!
所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]](我们可 以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问, 一般通过 Object.prototype.toString(..) 来查看。
虽然 Null() 和 Undefined() 这样的原生构造函数并不存在,但是内部 [[Class]] 属性值仍 然是 "Null" 和 "Undefined"。
一般情况下,我们不需要直接使用封装对象。最好的办法是让 JavaScript 引擎自己决定什 么时候应该使用封装对象。
如果想要自行封装基本类型值,可以使用 Object(..) 函数(不带 new 关键字)。
如果想要得到封装对象中的基本类型值,可以使用 valueOf() 函数。在需要用到封装对象中的基本类型值的地方会发生隐式拆封。
强烈建议使用常量形式(如 /^a*b+/g)来定义正则表达式,这样不仅语法简单,执行效率 也更高,因为 JavaScript 引擎在代码执行前会对它们进行预编译和缓存。与前面的构造函 数不同,RegExp(..) 有时还是很有用的,比如动态定义正则表达式时。
创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分 JavaScript 引擎 通过只读属性 .stack 来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号, 以便于调试(debug)。
符号并非对象,而是一种简单标量基本类型。
03/17
将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况隐 式的情况称为强制类型转换(coercion)。
JavaScript 中的强制类型转换总是返回标量基本类型值,如字 符串、数字和布尔值,不会返回对象和函数。
类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。 在 JavaScript 中通常将它们统称为强制类型转换。
基本类型值的字符串化规则为:null 转换为 "null",undefined 转换为 "undefined",true 转换为 "true"。数字的字符串化则遵循通用规则,不过那些极小和极大的 数字使用指数形式。
对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回内部属性 [[Class]] 的值,如 "[object Object]"。
数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起 来, toString() 可以被显式调用,或者在需要字符串化时自动调用。
所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。 为了简单起见,我们来看看什么是不安全的 JSON 值。undefined、function、symbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。 JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在 数组中则会返回 null(以保证单元位置不变)。 对包含循环引用的对象执行 JSON.stringify(..) 会出错。
如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回 值来进行序列化。 如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就 需要定义 toJSON() 方法来返回一个安全的 JSON 值。
(1) 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
(2) 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符 串化前调用,以便将对象转换为安全的 JSON 值。
var a = { b: 42, c: "42", d: [1, 2, 3] }; JSON.stringify(a, null, 3); /* "{ "b": 42, "c": "42", "d": [ 1, 2, 3 ] }" */ var a = { b: 42, c: "42", d: [1, 2, 3] }; JSON.stringify(a, ["b", "c"]); // "{"b":42,"c":"42"}" JSON.stringify(a, function(k, v) { if (k !== "c") return v; }); // "{"b":42,"d":[1,2,3]}"
03/18
ToNumber
其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型 值,则再遵循以上规则将其强制转换为数字。 为了将值转换为相应的基本类型值,抽象操作 ToPrimitive(参见 ES5 规范 9.1 节)会首先 (通过内部操作 DefaultValue,参见 ES5 规范 8.12.8 节)检查该值是否有 valueOf() 方法。 如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
从 ES5 开始,使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没 有 valueOf() 和 toString() 方法,因此无法进行强制类型转换。
03/19
ToBoolean
1. 假值(falsy value)
(1) 可以被强制类型转换为 false 的值 (2) 其他(被强制类型转换为 true 的值)
JavaScript 规范具体定义了一小撮可以被强制类型转换为 false 的值。
以下这些是假值:
- undefined
- null
- false
- +0、-0 和 NaN
- ""
从逻辑上说,假值列表以外的都应该是真值(truthy)。但 JavaScript 规范对此并没有明确 定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表以 外的值都是真值。
假值对象(falsy object)
浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来(exotic) 值,这些就是“假值对象”。 假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔 值时结果为 false。
最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由 DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。它以前曾是一个真正意义上 的对象,布尔强制类型转换结果为 true,不过现在它是一个假值对象。
03/20
1. 日期显式转换为数字
一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为 Unix 时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间)。
ES5 中新加入的静态方法 Date.now()。
2. 奇特的 ~ 运算符
字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位 格式。ToInt32 首先执行 ToNumber 强制类型转换,比如 "123" 会先被转换为 123,然后再执行 ToInt32。
Math.floor( -49.6 ); // -50 ~~-49.6; // -49
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
var a = "42"; var b = "42px"; Number( a ); // 42 parseInt( a ); // 42 Number( b ); // NaN parseInt( b ); // 42
parseInt(..) 针对的是字符串值,非字符串参数会首先被强制类型转换为字符串。 如果没有第二个参数来指定转换的 基数(又称为 radix),parseInt(..) 会根据字符串的第一个字符来自行决定基数。 如果第一个字符是 x 或 X,则转换为十六进制数字。如果是 0,则转换为八进制数字。 从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10。
03/24
parseInt( 1/0, 19 ); // 18
一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将 真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方 法是 !!,因为第二个 ! 会将结果反转回原值。 建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换。
03/25
简单来说就是,如果 + 的其中一个操作数是字符串, 则执行字符串拼接;否则执行数字加法。 a + ""(隐式)和前面的 String(a)(显式)之间有一个细微的差别需要注意。根据 ToPrimitive 抽象操作规则,a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象 操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。
“操作数选择器” :
|| 和 && 首先会对第一个操作数(a 和 c)执行条件判断,如果其不是布尔值(如上例)就 先进行 ToBoolean 强制类型转换,然后再执行条件判断。 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为 false 就返回第二个操作数(b)的值。 && 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返 回第一个操作数(a 和 c)的值。 || 和 && 返回它们其中一个操作数的值,而非条件判断的结果(其中可能涉及强制类型转 换)。c && b 中 c 为 null,是一个假值,因此 && 表达式的结果是 null(即 c 的值),而非 条件判断的结果 false。
但 ES6 中引入了符号类型,它的强制类型转换有一个坑,在这里有必要提一下。ES6 允许 从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换 为布尔值(显式和隐式结果都是 true)。
03/29
== 允许在相等比较中进行强制类型转换,而 === 不允许。== 和 === 都会检查操作数的类型。区别在于操作数类型不同时它们的处理方 式不同。
03/30
== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之 一或两者都转换为相同的类型后再进行比较。
(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
null 和 undefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 规定:
(1) 如果 x 为 null,y 为 undefined,则结果为 true。
(2) 如果 x 为 undefined,y 为 null,则结果为 true。
var a = null; var b; a == b; // true a == null; // true b == null; // true a == false; // false b == false; // false a == ""; // false b == ""; // false a == 0; // false b == 0; // false
null 和 undefined 之间的强制类型转换是安全可靠的,上例中除 null 和 undefined 以外的 其他值均无法得到假阳(false positive)结果。个人认为通过这种方式将 null 和 undefined 作为等价值来处理比较好。
(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。
之前介绍过的 ToPromitive 抽象操作的所有特性(如 toString()、valueOf()) 在这里都适用。如果我们需要自定义 valueOf() 以便从复杂的数据结构返回 一个简单值进行相等比较,这些特性会很有帮助。
03/31
"0" == null; // false "0" == undefined; // false "0" == false; // true -- 晕! "0" == NaN; // false "0" == 0; // true "0" == ""; // false false == null; // false false == undefined; // false false == NaN; // false false == 0; // true -- 晕! false == ""; // true -- 晕! false == []; // true -- 晕! false == {}; // false "" == null; // false "" == undefined; // false "" == NaN; // false "" == 0; // true -- 晕! "" == []; // true -- 晕! "" == {}; // false 0 == null; // false 0 == undefined; // false 0 == NaN; // false 0 == []; // true -- 晕! 0 == {}; // false
04/05
""、"\n"(或者 " " 等其他空格组合)等空字符串被 ToNumber 强制类型转换 为 0。
我们要对 == 两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
- 如果两边的值中有 true 或者 false,千万不要使用 ==。
- 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。 这时最好用 === 来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制 类型转换的坑。
a < b :
比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强 制类型转换为数字来进行比较。 如果比较双方都是字符串,则按字母顺序来进行比较。
var a = { b: 42 }; var b = { b: 43 }; a < b; // false a == b; // false a > b; // false a <= b; // true a >= b; // true
因为根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所 以 a <= b 的结果是 true。
<= 应该是“小于或者等于",实际上 JavaScript 中 <= 是 “不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a。
04/06
然而 JavaScript 通过标签跳转能够实现 goto 的部分功能。continue 和 break 语句都可以带 一个标签,因此能够像 goto 那样进行跳转。
// 标签为foo的循环 foo: for (var i = 0; i < 4; i++) { for (var j = 0; j < 4; j++) { // 如果j和i相等,继续外层循环 if (j == i) { // 跳转到foo的下一个循环 continue foo; } // 跳过奇数结果 if ((j * i) % 2 == 1) { // 继续内层循环(没有标签的) continue; } console.log(i, j); } }
contine foo 并不是指“跳转到标签 foo 所在位置继续执行”,而是“执行 foo 循环的下一轮循环”。所以这里的 foo 并非 goto。
带标签的循环跳转一个更大的用处在于,和 break __ 一起使用可以实现从内层循环跳转到 外层循环。没有它们的话实现起来有时会非常麻烦:
// 标签为foo的循环 foo: for (var i = 0; i < 4; i++) { for (var j = 0; j < 4; j++) { if ((i * j) >= 3) { console.log("stopping!", i, j); break foo; } console.log(i, j); } }
break foo 不是指“跳转到标签 foo 所在位置继续执行”,而是“跳出标签 foo 所在的循环 / 代码块,继续执行后面的代码”。因此它并非传统意义上的 goto。
标签也能用于非循环代码块,但只有 break 才可以。我们可以对带标签的代码块使用 break ___,但是不能对带标签的非循环代码块使用 continue ___,也不能对不带标签的代 码块使用 break。
JSON 的确是 JavaScript 语法的一个子集,但是 JSON 本身并不是合法的 JavaScript 语法。 JSON-P(将 JSON 数据封装为函数调用, 比如 foo({"a":42}))通过将 JSON 数据传递给函数来实现对其的访问。
事实上 JavaScript 没有 else if,但 if 和 else 只包含单条语句的时候可以省略代码块的 { }。下面的代码你一定不会陌生:
if (a) doSomething(a);
else 也是如此,所以我们经常用到的 else if 实际上是这样的:
if (a) { // .. } else { if (b) { // .. } else { // .. } }
if (b) { .. } else { .. } 实际上是跟在 else 后面的一个单独的语句,所以带不带 { } 都 可以。换句话说,else if 不符合前面介绍的编码规范,else 中是一个单独的 if 语句。 else if 极为常见,能省掉一层代码缩进,所以很受青睐。但这只是我们自己发明的用法, 切勿想当然地认为这些都属于 JavaScript 语法的范畴。
04/07
有时 JavaScript 会自动为代码行补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion,ASI)。 请注意,ASI 只在换行符处起作用,而不会在代码行的中间插入分号。 如果 JavaScript 解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分 号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会 这样做。 语法规定 do..while 循环后面必须带 ;,而 while 和 for 循环后则不需要。大多数开发人员 都不记得这一点,此时 ASI 就会自动补上分号。 ASI 是一个语法纠错机制。若将换行符当作有意义的字符来对待,就会遇到很多 问题。
向函数传递参数时,arguments 数组中的对应单元会和命名参数建立关联(linkage)以得 到相同的值。相反,不传递参数就不会建立关联。
var a = "42"; switch (true) { case a == 10: console.log("10 or '10'"); break; case a == 42; console.log("42 or '42'"); break; default: // 永远执行不到这里 } // 42 or '42'
04/09
还有一个不太为人所知的事实是:由于浏览器演进的历史遗留问题,在创建带有 id 属性 的 DOM 元素时也会创建同名的全局变量。例如: <div id="foo"></div> 以及: if (typeof foo == "undefined") { foo = 42; // 永远也不会运行 } console.log( foo ); // HTML元素 你可能认为只有 JavaScript 代码才能创建全局变量,并且习惯使用 typeof 或 .. in window 来检测全局变量。但是如上例所示,HTML 页面中的内容也会产生全局变量,并且稍不注 意就很容易让全局变量检查错误百出。
JavaScript 规范对于函数中参数的个数,以及字符串常量的长度等并没有限制;但是由于 JavaScript 引擎实现各异,规范在某些地方有一些限制。
- 字符串常量中允许的最大字符数(并非只是针对字符串值);
- 可以作为参数传递到函数中的数据大小(也称为栈大小,以字节为单位);
- 函数声明中的参数个数;
- 未经优化的调用栈(例如递归)的最大层数,即函数调用链的最大长度;
- JavaScript 程序以阻塞方式在浏览器中运行的最长时间(秒);
- 变量名的最大长度。