在以前某次尤大大作直播的讲演中,回答了哪些前端书籍是值得被阅读的,其中一本即是《Effective JavaScript》,因而开始阅读学习,以自身阅读和理解,着重记录内容精华部分以及对内容进行排版,便于往后自身回顾学习以及你们交流学习。前端
因内容居多,分为每一个章节来进行编写文章,每章节的准条多少不一,故每篇学习笔记的文章以章节为准。程序员
适合碎片化阅读,精简阅读的小友们。争取让小友们看完系列 === 看整本书的 85+%。正则表达式
澄清一下 JavaScript 和 ECMAScript 的术语。总所周知,当人们提起 ECMAScript时,一般是指由 Ecma 国际标准化组织制定的 “理想语言”。算法
而 JavaScript 这个名字意味着来自语言自己的全部事物,例如某个供应商特定的 JavaScript 引擎、DOM、BOM 等。编程
为了保持清晰度和一致性,在本书中,我将只使用 ECMAScript 来谈论官方标准,其余状况,将使用 JavaScript 指代语言。数组
避开 Web 来谈 JavaScript 是很难的,不过本书是关于 JavaScript 而非 Web 的编程,故本书重点是 JavaScript 语言的语法、语义和语用,而不是 Web 平台的 API 和技术。浏览器
JavaScript 一个新奇的方面是在并发环境中其行为是彻底不明朗的。所以,只是从技术角度介绍一些非官方的 JavaScript 特性,但实际上,全部主流的 JavaScript 引擎都有一个共同的并发模型。安全
将来版本的 ECMAScript 标准可能会正式标准化这些 JavaScript 并发模型的共享方面markdown
JavaScript 语言提供为数很少的核心概念,所以显得如此的平易近人,可是精通这门语言须要更多的时间,须要更深刻地理解它的语义、特性以及最有效的习惯用法。网络
本书每一个章节都涵盖了高效 JavaScript 编程的不一样主题。第 1 章主要讲述一些最基本的主题
因为 JavaScript 历史悠久且实现多样化,所以咱们很难肯定哪些特性在哪些平台上是可用的。而 Web 浏览器,它并不支持让程序员指定某个 JavaScript 的版原本执行代码,最终用户可能使用不一样 Web 浏览器的不一样版本。
好比应用程序在本身的计算机活着测试环境上运行良好,但部署到不一样的产品环境中时却没法运行。例如 const 关键字在支持非标准特性的 JavaScript 引擎上测试时运行良好,但将部署到不识别 const 关键字的 Web 浏览器就会出现语法错误等等。
ES5 引入另外一种版本控制的考量 —— 严格模式。此特性容许选择在受限制的 JavaScript 版本中禁止使用一些 JavaScript 语言中问题较多或易于出错的特性。在程序中启用严格模式的方式是在程序的最开始增长一个特定的字符串字面量 "use strict"
"use strict"
指令只有在脚本或函数的顶部才能生效,若在开发中使用多个独立的文件,而一个文件是严格模式下,另外一个是非严格模式下,部署到产品环境时却须要链接成一个单一文件。
// file1.js
"use strict"
function f() {
// ...
}
// file2.js
function g() {
var grauments = []
}
复制代码
若是以 file1.js 文件开始,那么链接后的代码运行于严格模式下
// file1.js
"use strict"
function f() {
//...
}
// file2.js
function g() {
var arguments = [] // error: redefinition of arguments
}
复制代码
若是以 file2.js 文件开始,那么链接后的代码运行于非严格模式下
// file2.js
function g() {
var arguments = []
}
// file1.js
"use strict"
function f() {
// ...
}
复制代码
在本身的项目中能够坚持只使用 “严格模式” 或只使用 “非严格模式” 的策略,但若是你要编写健壮的代码应对各类各样的代码连接,如下有两个可选方案。
第一个解决方案是不要将进行严格模式检查的文件和不进行严格模式检查的文件链接起来。
第二个解决方案是经过将其自身包裹在当即调用的函数表达式中的方式链接多个文件。(第 13 条将对当即调用的函数表达式进行深刻的讲解)
将每一个文件的内容包裹在一个当即调用的函数中,即便在不一样的模式下,它们都将被独立地解决实行,例子以下:
// 当即调用的函数表达式中
(function () {
// file1.js
"use strict";
function f() {
// ...
}
// ...
})()
(function () {
// file2.js
function g() {
var arguments = []
}
})()
复制代码
因为每一个文件的内容被放置在一个单独的做用域中,因此用不用严格模式指令只影响本文件的内容。
但这种方式会致使这些文件的内容不会在全局做用域内解释。
所以若是为了达到更为广泛的兼容性,为将来新版本的 Javascript 更好地作铺垫,以及消除代码运行的一些不安全之处,保证代码运行的安全等等。建议在必要时刻使用严格模式下编写代码
JavaScript 只有一种数字类型 Number
。
typeof 17; // "number"
typeof 98.6; // "number"
typeof -2.1; // "number"
复制代码
事实上,JavaScript 中全部的数字都是双精度浮点数,它能完美地表示高达 53 位精度的整数(JavaScript 正是如此隐式转换为整数)。所以,尽管 JavaScript 中缺乏明显的整数类型,可是彻底能够进行整数运算。
0.1 * 1.9 // 0.19
-99 + 100 // 1
21 - 12.3 // 8.7
2.5 / 5 // 0.5
21 % 8 // 5
复制代码
在进行位算术运算符时,JavaScript 不会直接将操做数做为浮点数进行运算,而是会将其隐式转换为 32 位整数后进行运算。
8 | 1 // 9
复制代码
以上表达式进行的实际步骤为
(8).toString(2) // "1000"
方法进行查看JavaScript 中数字是以浮点数存储的,必须将其转换为整数,而后再转换回浮点数。然而某些状况下,算术表达式甚至变量只能使用整数参与运算,优化编译器有时候能够推断出这些情形而在内部将数字以整数的鹅方式存储以免多余的转换。
双精度浮点数也只能表示一组有限的数字,当执行一系列的运算,随着舍入偏差的积累,运算结果会愈来愈不精确。
例如,实数知足结合律,这意味着,对于任意的实数 x, y, z,老是知足(x+y)+z = x+(y+z)
。然而对于浮点数来讲,却不老是这样:
(0.1 + 0.2) + 0.3 // 0.6000000 000000001
0.1 + (0.2 + 0.3) // 0.6
复制代码
-2^53 ~ 2^53
我的的解决方案
(num1 * baseNum + num2 * baseNum) / baseNum;)
JavaScript 对类型错误出奇宽容,在静态类型语言中,含有不一样类型运算的表达式 3 + true;
是不会被容许运行。然而 JavaScript 却会顺利地产生结果 4.
在 JavaScript 中也有极少数的状况,提供错误的类型会产生一个即时错误。
// 调用一个非函数对象
"hello"(1) // error: not a function
// 试图选择 null 属性
null.x; // error: cannot rend property 'x' of null
复制代码
算术运算符 -
、*
、/
和 %
在计算以前都会尝试将其参数转换为数字。如运算符 +
既重载了数字相加、又重载了字符串链接操做。
2 + 3; // 5
"hello" + "world"; // "hello world"
复制代码
因为加法运算是自左结合(即左结合律),所以有以下等式。
1 + 2 + "3" // "33"
// 等于
(1 + 2) + "3" // "33"
1 + "2" + 3 // "123"
// 等于
(1 + "2") + 3 // "123"
复制代码
位运算符不只会将操做数转换为数字,并且还会将操做数转换为 32 位整数。
~
、&
、^
和 |
<<
、>>
、>>>
和 <<<
这些强制转换十分方便。例如,来自用户输入、文本文件或者网络流的字符串都将被自动转换。
"17" * 3; // 51
"8" | "1"; // 9
复制代码
强制转换也会隐藏错误。结果为 null
的变量在算术运算中不会致使失败,而是被隐式地转换为 0.
一个未定义的变量将被转换为特殊的浮点数值 NaN
,这些强制转换不是当即抛出一个异常,而是继续运算,每每致使一些不可预测的结果。而测试 NaN 值也是异常困难,由于两个缘由
JavaScript 遵循了 IEEE 浮点数标准使人头疼的要求 - NaN 不等于其自己
const x = NaN
x === NaN // false
复制代码
标准库函数 isNaN
也不是很可靠。
isNaN(NaN) // true
isNaN("foo") // true
isNaN(undefined) // true
isNaN({}) // true
isNaN({ valueOd: "foo" }) // true
复制代码
幸运的是有一个既简单又可靠的习惯用法来测试 NaN
function isReallyNaN(x) {
return x !== x
}
复制代码
对象经过隐式地调用其自身的 toString
方法转换为字符串。
Match.toString() // "[object Math]"
JSON.toString() // "[object JSON]"
复制代码
相似地,对象也能够经过其 valueOf
方法转换为数字。经过其方法能够来控制对象的类型转换。
"J" + { toString: function() { return "S" } } // JS
2 * { valueOf: function() { return 3 } } // 6
复制代码
当一个对象同时包含 toString
和 valueOf
方法时,运算符 +
应该调用哪一个方法并不明显。所以,JavaScript经过盲目地选择 valueOf
方法而不是 toString
方法来解决这种含糊地状况。
const obj = {
toString: function() {
return "[object MyObject]"
},
valueOf: function() {
return 17
}
}
"object:" + obj // "object: 17"
复制代码
所以,无论是对象的的链接仍是对象的相加,重载的运算符 +
老是一致的行为 - 相同数字的字符串或数值表示。
通常状况下,字符串的强制转换远比数字的强制转换更常见、更有用。最好避免使用 valueOf
方法,除非对象的确是一个数字的抽象,而且 obj.toString() 能产生一个 obj.valueOf() 的字符串表示。
真值运算 if
、||
和 &&
等运算符逻辑上须要布尔值做为操做参数,但之际上能够接受任何值。
JavaScript 中有 7 个假值:false
、0
、-0
、NaN
、""
、null
和 undefined
,其余全部的值都为真值。
检查参数是否为 undefined
更为严格的方式是使用 typeof
function point(x, y) {
if (typeof x === "undefined") {
x = 320
}
if (typeof y === "undefined") {
y = 240
}
return {x: x, y: y}
}
// 此方法能够判断比较 0 和 undefind
point() // {x: 320, y: 240}
point(0, 0) // {x: 0, y: 0}
复制代码
另外一种方法是与 undefined 直接比较 if (x === undefined) {...}
+
是进行加法运算仍是字符串链接操做取决于其参数类型。valueOf
方法的对象应该实现 toString
方法,返回一个 valueOf
方法产生的数字的字符串表示。typeof
或者与 undefined
进行比价而不是使用真实运算。JavaScript 标准库中提供了构造函数来封装原始值的类型。如能够建立一个 String 对象,该对象封装了一个字符串值。
const s = new String("hello")
// 也能够将其与另外一个值链接建立字符串
s + "world" // "hello world"
复制代码
可是不一样于原始的字符串,String 对象是一个真正的对象
typeof "hello" // "string"
typeof s // "object"
复制代码
这意味着不能使用内置的操做符来比较两个大相径庭的 String 对象的内容,每一个 String 对象都是一个单独的对象,不论内容是否一致,其老是只等于自身。
const s1 = new String("hello")
const s2 = new String("hello")
s1 == s2 // false
s1 === s2 // false
复制代码
由于原始值不是一个对象,因此不能对原始值设置属性,但能对封装对象设置属性。
const sObj = new String("hello")
const s = "hello"
sObj.prop = "world"
s.prop = "world"
sObj.prop // "world"
s.prop // undefined
复制代码
封装对象存在的理由,也就是它们的做用是构造函数上的实用方法。
而当咱们对原始值提取属性或进行方法调用时,JavaScript 会内置隐式转换为对应的对象类型封装。例如,String 的原型对象有一个 toUpperCase
方法,能够将字符串转换为大写,那么能够对原始字符串调用这个方法。
"hello".toUpperCase() // "HELLO"
复制代码
每次隐式封装都会产生一个新的 String 对象,更新第一个封装对象并不会形成持久的影响。
这也常常形成错误给一个原始值设置属性,而程序默认行为,致使一些难以发现的错误并难以诊断。
先看一个例子,你认为返回的结果是什么?
"1.0e0" == { valueOf: function() { return true } }
复制代码
像第 3 条描述的隐式强制转换同样,在比较以前它们都会被转换为数字。最终结果与 1 == 1
是等价的
所以,咱们很容易使用这些强制转换完成一些工做。例如,从一个 Web 表单读取一个字段并与一个数字进行比较
const today = new Date()
if (form.month.value == (today.getMonth() + 1) && form.day.value == today.getDate()) {
// ...
}
// 是与下列隐式转换为数字等价的
const today = new Date()
if (+form.month.value == (today.getMonth() + 1) && +form.day.value == today.getDate()) {
// ...
}
复制代码
当两个属性属于同一类型时,==
和 ===
运算符的行为是没有区别的。但最好使用严格相等运算符,来准确比较数据的内容和类型,而非仅仅看数据的内容。
== 运算符强制转换的规则并不明显,但这些规则具备对称性。
转换规则一般都试图产生数字,但它们处理对象时会变得难以捉摸。会将对象试图转换为原始值来进行判断,能够经过调用对象的 valueOf
和 toString
方法而实现。而使人值得注意的是,Date
对象以相反的顺序尝试调用这两个方法。
咱们在第 3 条提到了,JavaScript 默认先调用 valueOf 再调用 toString 来转换为原始值
参数类型1 | 参数类型2 | 强制转换 |
---|---|---|
null | undefined | 不转换,老是返回 true |
null 或 undefined | 其余任何非 null 或 undefined 的类型 | 不转换,老是返回 false |
原始类型:string、number、boolean 或 Symbol | Date 对象 | 将原始类型转换为数字;将 Date 对象转换为原始类型(优先尝试 toString 方法,再尝试 toString 方法) |
原始类型:string、number、boolean 或 Symbol | 非 Date 对象 | 将原始类型转换为数字;将非 Date 对象转换为原始类型(优先尝试 valueOf 方法,再尝试 toString 方法) |
原始类型:string、number、boolean 或 Symbol | 原始类型:string、number 或 boolean | 将原始类型转换为数字 |
JavaScript 的自动分号插入技术是一种程序解析技术。能推断出某些上下文中省略的分号,而后有效地自动地将分号“插入”到程序中,ECMAScript 标准也指定了分号机制,所以可选分号能够在不一样的 JavaScript 引擎之间移植。
分号插入在解析时有其陷阱,JavaScript 语法对其也有额外的限制。所以咱们需了解学会分号插入的三条规则,便能从删除没必要要的分号痛苦中解脱出来。
分号仅在 } 标记以前、一个或多个换行以后和程序输入的结尾被插入。
也就是说,只能在一个代码块、一行或一段程序结束的地方省略分号,不能在连续的语句中省略分号。
function area(r) { r = +r; return Math.PI * r * r }
复制代码
function area(r) { r = +r return Match.PI * r * r } // error
复制代码
分号仅在随后的输入标记不能解析时插入。
也就是说,分号插入是一种错误矫正机制。咱们老是要注意下一条语句的开始,从而发现可否合法地省略分号。
有 5 个明确有问题的字符须要密切注意:(
、[
、+
、-
和 /
。这些依赖于具体上下文,且都能做为一个表达运算符或上一条语句的前缀。以下例子:
()
a = b
(f());
// 等价于
a = b(f());
// 将被解析为两条独立语句
a = b
f()
复制代码
[]
a = b
["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
// 等价于
a = b["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
// 将被解析为两条独立语句,一条赋值,一条数组 forEach 方法
a = b
["r", "g", "b"].forEach(function (key) {
background[key] = foreground[key] / 2;
})
复制代码
+
和 -
a = b
+c;
// 等价于
a = b + c;
// 将被解析为两条独立语句,一条赋值,一条转为正整数。
a = b
+c
// - 如上
复制代码
/
:有特殊意义于正则表达式标记的开始字符
a = b
/Error/ i.test(str) && fail();
// 等价于
a = b / Error / i.test(str) && fail(); // '/' 将会被解析为除法运算符
// 将被解析为两条独立语句
a = b
/Error/ i.test(str) && fail()
复制代码
省略分号可能致使脚本链接问题,若每一个文件可能由大量的函数调用表达式组成。当每一个文件做为一个单独的程序加载时,分号能自动地插入到末尾,将函数调用转变为一条语句。
// file1.js
(function() {
// ...
})()
// file2.js
(function() {
// ...
})()
复制代码
但当咱们使用多个文件做为程序加载文件时,若咱们省略了分号,结果将被解析为一条单独的语句。
(function() {
// ...
})()(function() {
// ...
})()
复制代码
咱们能够防护性地在每一个文件前缀一个额外的分号以保护脚本免受粗心链接的影响。也就是说,若是文件最开始的语句以上述全部 5 个字符问题开头,则需作出如下解决方法。
// file1.js
;(function() {
// ...
})()
// file2.js
;(function() {
// ...
})()
复制代码
总的以上来讲,省略语句的分号不只须要小心当前文件的下一个标记(字符问题),并且还须要小心脚本链接后可能出现语句以后的任一标记。
JavaScript 语法限制产生式**不容许在两个字符之间出现换行,所以会强制地插入分号。**以下例子:
return
{ };
// 将被解析为 3 条单独的语句。
return;
{ };
;
复制代码
换句话说,return
关键字后的换行会强制自动地插入分号,该代码例子被解析为不带参数的 return 语句,后接一个空的代码块和一个空语句。
除了
return
的语法限制生成式,还有如下其余的 JavaScript 语句限制生产式。
throw
语句- 带有显示标签的
break
或continue
语句- 后置自增或自减运算符
分号不会做为分隔符在
for
循环空语句的头部或空循环体的while
循环中被自动插入。
意味着你须要在 for
循环头部显示地包含分号。
// 在 for 循环头部中,以换行代替分号,将致使解析错误。
for (let i = 0, total = 1 // parse error
i < n
i++) {
total *= 1
}
复制代码
在空循环体的 while
循环一样也须要显示分号。
function infiniteLoop() { while (true) } // parse error
function infiniteLoop() { while (true); } // 正确
复制代码
(
、[
、+
、-
或 /
字符开头的语句前毫不能省略分号。return
、throw
、break
、continue
、++
或 --
地参数以前觉不能换行。Unicode 概念是为世界上全部的文字系统的每一个字符单位分配了一个惟一的整数,该整数介于 0 和 1114 111 之间,在 Unicode 术语中称为代码点
。
Unicode 与其余字符编码几乎没有任何不一样(例如,ASCII)。但不一样点是,ASCII 将每一个索引映射为惟一的二进制表示,Unicode 容许多个不一样二进制编码的代码点。不一样的编码在存储的字符串数量和操做速度之间进行权衡(也就是时间与空间的权衡)。目前由多种 Unicode 的编码标准,最流行的几个是:UTF-8
、UTF-16
和 UTF-32
。
Unicode 的设计师根据历史的数据,错误估算了代码点的容量范围。起初产生了 UCS-2
其为 16 位代码的原始标准,也就是 Unicode 具备 2^16 个代码点。因为每一个代码点能够容纳一个 16 位的数字,当代码点与其编码元素一对一地映射起来,这称为一个代码单元。
其结果是当时许多平台都采用 16 位编码的字符串。如 Java,而 JavaScript 也紧随其后,因此 JavaScript 字符串的每一个元素都是一个 16 位的值。
现在 Unicode 也扩大其最初的范围,标准从当时的 2^16 扩展到了超过 2^20 的代码点,新增长的范围被组织为 17 个大小为 2^16 代码点的字范围。第一个子范围称为基本多文种平面,包含最初的 2^16 个代码点,余下的 16 个范围称为辅助平面。

因代码点的范围扩展,UCS-2 就变的过期,所以UTF-16
采用代理对表示附加的代码点,一对 16 位的代码单元共同编码一个等于或大于 2^16 的代码点。
例如分配给高音谱号的音乐符号 𝄞
的代码点为 U+1D11E(代码点数 119 070 的 Unicode 的惯用 16 进制写法),UTF-16 经过合并两个代码单元 0xd834
和 0xddle
选择的位来对这个代码点进行解码。
"𝄞".charCodeAt(0); // 56606(0xd834)
"𝄞".charCodeAt(1); // 56606(0xdd1e)
'\ud834\udd1e' // "𝄞"
复制代码
JavaScript 已经采用了 16 位的字符串元素,字符串属性和方法(如 length、charAt 和 charCodeAt)都是基于代码单元层级,而不是代码点层级。因此简单来讲,一个 JavaScript 字符串的元素是一个 16 位的代码单元。

JavaScript 引擎能够在内部优化字符串内容的存储,但考虑到字符串的属性和方法,字符串表现得像 UTF-16 的代码单元序列。
也就是说虽然事实上
𝄞
只有一个代码点,但由于是基于代码单元层级,故.length
显示为代码单元的个数 2。
"𝄞".length // 2
"a".length // 1
复制代码
提取该字符串的某个字符的方法 `` 获得的是代码单元,而不是代码点。
"𝄞 ".charCodeAt(0); // 56606(0xd834)
"𝄞 ".charCodeAt(1); // 56606(0xdd1e)
"𝄞 ".charAt(1) === " " // false,表示第二个代码单元不是空字符
"𝄞 ".charAt(2) === " " // true
'\ud834\udd1e' // "𝄞"
复制代码
正则表达式也工做于代码单元层级,因单字符模式 .
匹配一个单一的代码单元。
/^.$/.test("𝄞"); // false
/^..$/.test("𝄞") // true
复制代码
这意味着若是需操做代码点,应用程序不能信赖字符串方法、长度值、索引查找或者许多正则表达模式。若是但愿使用除 BMP 以外的代码点,那么求助于一些支持代码点的库是个好主意。
虽然 JavaScript 内置的字符串数据类型工做于代码单元层级,但这并不能阻止一些 API 意识到代码点和代理对。例如 URI 操做函数:sendcodeURI
、decodeURI
、encodeURIComponent
和 decodeURIComponent
。
故每当一个 JavaScript 环境提供一个库操做字符串(例如操做一个 Web 页面的内容或者执行关于字符串的 I/O 操做),你都须要查阅这些库文档,看它们如何处理 Unicode 代码点的整个范围。
字符串元素计数
,length
、charAt
、charCodeAt
方法以及正则表达式模式(例如 .
)受到了影响。以上为 第一章内容 学习了 1~7 条规则 着重于熟悉 JavaScript,了解 JavaScript 中的原始类型、隐式强制转换、编码类型等几本概念;
系列以下:
- 《Effective JS》的 68 条准则「一至七条」
- 《Effective JS》的 68 条准则「八至十七条」
- 《Effective JS》的 68 条准则「十八至二十九条」
- 《Effective JS》的 68 条准则「三十至四十二条」
- 《Effective JS》的 68 条准则「四十三至五十二条」
- 《Effective JS》的 68 条准则「五十三至六十条」
- 《Effective JS》的 68 条准则「六十一至六十八条」
若无连接,则是正在学习当中...