起因
阅读学习《Effective JavaScript》,以自身阅读和理解,着重记录内容精华部分以及对内容进行排版,便于日后自身回顾学习以及大家交流学习。
因内容居多,分为每个章节来进行编写文章,每章节的准条多少不一,故每篇学习笔记的文章以章节为准。
适合碎片化阅读,精简阅读的小友们。争取让小友们看完系列 === 看整本书的 85+%。
前言
内容总览
- 第一章让初学者快速熟悉 JavaScript,了解 JavaScript 中的原始类型、隐式强制转换、编码类型等几本概念;
- 第二章着重讲解了有关 JavaScript 的变量作用域的建议,不仅介绍了怎么做,还介绍了操作背后的原因,帮助读者加深理解;
- 第三章和第四章的主题涵盖函数、对象及原型三大方面,这可是 JavaScript 区别于其他语言的核心;
- 第五章阐述了数组和字典这两种容易混淆的常用类型及具体使用时的建议,避免陷入一些陷阱;
- 第六章讲述了库和 API 设计;
- 第七章讲述了并行编程,这是晋升为 JavaScript 专家的必经之路
第 2 章「变量作用域」
JavaScript 核心的u 作用域规则很简单,其作用域规则被精心设计,且强大得令人难以置信,但也有一些例外情况,故有效地使用 JavaScript 需要掌握变量作用域的一些基本概念,并了解一些可能导致难以捉摸的、令人讨厌问题的极端情况。
第 8 条:尽量少用全局对象
全局变量
JavaScript 很容易在全局命名空间中创建全局变量,因为它不需要任何形式的声明,而且能被整个程序的所有代码自动地访问。
然而我们应该避免使用全局变量,原因如下:
- 定义全局变量会污染共享的公共命名空间,从而导致意外的命名冲突。
- 全局变量不利于模块化,因为它会导致程序中独立的组件间不必要耦合。
由于全局命名空间是 JavaScript 程序中独立的组件进行交互的唯一途径,在组件或程序库不得不定义一些全局变量,以便程序中的其他部分使用。
否则,最好尽量使用局部变量。
如下例子:
let i, n, sum; // globals
function averageScore(players) {
sum = 0
for (i = 0, n = players.length; i < n; i++) {
sum += score(players[i])
}
return sum / n
}
function score(player) {
sum = 0
for (i = 0, n = players.length; i < n; i++) {
sum += player.levels[i].score
}
return sum
}
复制代码
因为定义了三个全局变量,在 score
函数处于自身的目的使用了同名的全局变量,averageScore
函数内部的调用将出现问题。
我们应保持这些变量为局部变量,仅将其作为需要使用它们的代码的一部分。
function averageScore(players) {
let i, n, sum
sum = 0
for (i = 0, n = players.length; i < n; i++) {
sum += score(players[i])
}
return sum / n
}
function score(player) {
let i, n, sum
sum = 0
for (i = 0, n = players.length; i < n; i++) {
sum += player.levels[i].score
}
return sum
}
复制代码
全局对象 this
JavaScipt 的全局命名空间也被暴露为在程序全局作用域中可以访问的全局对象,该对象作用 this
关键字的初始值。因此添加或修改全局变量会自动更新全局对象。
this.foo // undefined
foo = "global foo"
this.foo // "global foo"
复制代码
在 web 浏览器中,全局对象被绑定到全局的 window 变量
这意味着有两种方法可以创建一个全局变量。
- 在全局作用域内声明变量
- 通过初始化 this 来声明变量
而宿主环境提供的内置实现几乎总是更合适的,因为它们按照一定的标准对正确性和一致性进行严格检查,普遍比第三方实现提供了更好性能,因此我们可以使用全局对象来调用其内置方法,并且避免在全局对象上进行操作属性,导致重写了内置的方法和属性。
特性检测
全局对象提供了一个不可或缺的特别用途:特性检测。由于全局对象提供了全局环境的动态反应机制,所以可以使用它查询一个运行环境,检测在这个平台下哪些特性是可用的。
特性检测技术在 Web 浏览器中特别重要,因为在各种各样的浏览器和浏览器版本中可能会执行同样的代码。特性检测技术也适用于其他地方,例如此技术使得在浏览器和 JavaScript 服务器环境中共享程序库成为可能。
总结
- 避免声明全局变量,尽量声明局部变量。
- 避免对全局对象添加属性,导致重写内置方法和属性
- 使用全局对象来做平台特性检测。
第 9 条:始终声明局部变量
JavaScript 的变量赋值规则导致太容易地意外创建全局变量。
function swap(a, i, j) {
temp = a[i] // global
a[i] = a[j]
a[j] = temp
}
复制代码
尽管程序没有声明 temp 变量,但根据 JavaScript 变量赋值规则,执行是不会出错的,只是会导致意外地创建一个全局变量,我们应使用 const
或 let
来声明局部变量。
function swap(a, i, j) {
let temp = a[i]
a[i] = a[j]
a[j] = temp
}
复制代码
所以意外地创建全局变量将是彻头彻尾的灾难,而原生 JavaScript 却不会执行出错。因此我们需要使用 lint 工具检查程序源代码中的不好风格和潜在的错误。
通常情况下,lint 工具使用用户提供的一套已知的全局变量检查未声明的变量,然后报告出所有既没有在列表中提供的又没有在升序中声明的引用或赋值变量,让我们准备提前定位到问题,避免灾难发生。
所以我们花一些时间去探索什么样的 lint 检查工具对 JavaScript 可用是值得的,将自动检查一些常见的错误(如,意外的全局变量)整合到开发过程中可能会成为救命稻草。
总结
- 始终声音新的局部变量。
- 考虑使用 lint 工具帮助检查未绑定的变量。
第 10 条:避免使用 with
with
因为 with
的特性,虽然给予少有的便利,却让程序变的不可靠和低效率。我们使用 with
语句的动机有下列情况。
-
当程序经常需要对单个对象依次调用一系列方法,使用
with
语句可以方便地避免对对象的重复引用。也就是希望引用with
对象的属性变量,如 setBackground、setForeground。function status(info) { const widget = new Widget(); with (widget) { setBackground("blue"); setForeground("white"); setText("Status" + info); // amiguous reference show(); } } 复制代码
-
使用
with
语句从模块对象中 “导入”变量。也就是希望引用外部绑定的变量,如 info、x 和 y。function f(x, y) { with(Math) { return min(round(x), sqrt(y)); // ambiguous references } } 复制代码
with 作用域链
事实上在语法上并没有区分这两种类型的变量,JavaScript 对待所有的变量都是相同的,从最内层的作用域开始向外查找变量。
with 语句对待一个对象代表一个变量作用域,因此在 with 代码块的内部,变量查找从搜索给定的变量作用域中开始。如果没有找到再继续在外部作用域中搜索。
以
status
函数的作用域在 JavaScript 引擎中的内部表示图为例。

- 作用域链中最内层作用域由
widget
对象提供 - 接下来作用域用来绑定该函数的局部变量
info
和widget
- 接下来一层绑定到
status
函数
在一个正常的作用域中,会有与局部作用域中的变量同样多的作用域绑定存储在与之对应的环境层级中。但是对于 with 作用域,绑定集合依赖于碰巧在给定时间点时的对象。
冲突缺陷
变量作用域和对象命名空间之间的冲突使得 with 代码块异常脆弱。例如上述 status
函数中,with 对象获得了名为 info
的冲突属性,那么 status
函数将使用后边属性,而不是一开始传入的 info 参数。
更糟糕的是,当一些意外情况给 Widget 的原型对象在运行时加入 info 属性,那么将导致 status 函数变得不可预测。(因为在 with 作用域中会优先查询指定对象的原型)
查找缺陷
JavaScript 作用域在通常情况可被表示为高校的内部数据结构,变量查找会非常快速。但由于 with 代码块需要搜索对象的原型链来查找 with 代码块里的所有变量,因此,其运行速度还远远低于一般的代码块。
替代方法
在 JavaScript 中没有单个特性能更好地直接代替 with 语句。因此在某些情况下,最简单有效的替代方法是将对象绑定到一个简短的变量名上,以便更好地调用。
function status(info) {
let w = new Widget();
w.setBackground("blue");
w.setForeground("white");
w.setText("Status" + info); // amiguous reference
w.show();
}
复制代码
这样会让行为更具可预测性,并且即时一些代码修改了 Widget 的原型对象,status 函数的行为依旧与预期一致。
在一般情况,最好的方法是将局部变量显示地绑定到相关的属性上,而非利用 with 语句隐式地绑定它们。
function f(x, y) {
const min = Math.min, round = Math.round, sqrt = Math.sqrt
return min(round(x), sqrt(y));
}
复制代码
总结
- 避免使用 with 语句。(具有致命缺陷)
- 使用简短的变量名代替重复访问的对象。
- 显示地绑定局部变量到对象属性上,而不要使用 with 语句隐式地绑定它们。
第 11 条:熟练掌握闭包
闭包 是 JavaScript 的一大特点,当我们努力掌握闭包将会给你带来超值的回报,我们理解闭包只需要学会三个基本事实。而形成闭包的原理是因为函数变量作用域链的规则
和垃圾回收机制的引用计数规则
,将其内存缓存而不被释放。
基本事实一
JavaScript 允许你引用在当前函数以外定义的变量。
function makeSandwich() {
let magic = "peanut butter";
function make(filling) {
return magic + " and " + filling;
}
return make("jelly")
}
makeSandwich(); // "peanut butter and jelly"
复制代码
make
函数可以引用定义在外部 makeSandwich
函数内的 magic
变量。
基本事实二
即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量。
function makeSandwich() {
let magic = "peanut butter";
function make(filling) {
return magic + " and " + filling;
}
return make;
}
const f = makeSandwich();
f("jelly"); // "peanut butter and jelly"
f("bananas"); // "peanut butter and bananas"
复制代码
这里例子中不是在外部的 asndwichMaker
函数中立即调用 make(“jelly”),而是在返回函数在外部进行调用。但即使 makeSandwich
函数已经返回,make
函数仍能记住 magic
变量的值。原因如下:
- JavaScript 的函数值包含了比调用它们时执行所需要的代码还要多的信息。
- JavaScript 函数值在内部存储它们可能会引用的定义和其封闭作用域的变量。
- 哪些在其所涵盖的作用域内跟踪变量的函数被称为闭包。
make
函数就是一个闭包,其代码引用了两个外部变量 magic
和 filling
,并且该闭包存储了着两个变量。因此我们可以优化上述代码:
function makeSandwich(magic) {
function make(filling) {
return magic + " and " + filling;
}
return make;
}
const ham = makeSandwich("ham");
f("jelly"); // "ham and jelly"
f("bananas"); // "ham and bananas"
const lin = makeSandwich("lin");
f("jelly"); // "lin and jelly"
复制代码
事实三
闭包可以更新外部变量的值。
闭包存储的是外部变量的引用,而不是它们值的副本,因此对于任何具有访问这些外部变量的闭包,都可以进行更新。如下例子:
function box() {
let val = undefined;
return {
set: function (newVal) { val = newVal; }
get: function () { return val; }
type: function (newVal) { return typeof val; }
}
}
const b = box
b.type(); // "undefined"
b.set(98.6);
b.get(); // 98.6
b.type(); // "number"
复制代码
这个例子产生了一个包含三个闭包 set
、get
和 type
的属性对象。共享访问 val
变量,利用 set
闭包更新 val
值,随后调用 get
和 type
查看更新的结果。
总结
闭包是 JavaScipt 最优雅、最有表现力的特性之一,也是许多惯用法的核心。
- 闭包函数可以引用定义在其外部作用域的变量。
- 闭包比创建它们的函数有更长的生命周期。
- 闭包在内部存储其外部变量的引用,并能读写这些变量。
第 12 条:理解变量声明提升
Var 变量声明
JavaScript 支持语法作用域,对变量的引用会被绑定到声明变量最近的作用域中。JavaScript 的 var
变量声明不支持块级作用域,即变量定义的作用域并不是离其最近的封闭语句或代码块,而是包含他们的函数。
function isWinner(player, others) {
var highest = 0;
for (var i = 0, n = others.length; i < n; i++) {
var player = others[i];
if (player.score > highest) {
highest = player.score;
}
}
return player.score > highest;
}
复制代码
该程序在 for
循环体内声明一个局部变量 player
,因为 var
是函数级作用域,因此在内部声明的 player
变量只是简单地重声明了一个已经存在于作用域的变量(参数 player),因此 return 语句会将 player 看作 others 的最后一个元素。
变量声明行为
JavaScript 变量声明行为可以看作两部分组成 声明
和 赋值
。JavaScript 隐式地提升声明部分到封闭函数的顶部,而将赋值留在原地。换句话说,var
声明的变量的作用域是整个函数,但仅在 var
语句出现的位置进行赋值,如图:

因此 var
变量声明提升也可能导致变量重声明的混淆,如下:
function trimSections(header, body, footer) {
for (var i = 0, n = header.length; i < n; i++) {
header[i] = header[i].trim();
}
for (var i = 0, n = body.length; i < n; i++) {
body[i] = body[i].trim();
}
for (var i = 0, n = footer.length; i < n; i++) {
footer[i] = header[i].trim();
}
}
复制代码
trimSections
函数理想声明 6 个局部变量,但经过变量声明提升后,其实只声明了 2 个。
异常处理
JavaScript 异常处理 try...catch
语句将捕获的异常绑定到一个变量,该变量的作用域只是 catch
语句块(也就是块级作用域)。
function test() {
var x = "var", result = [];
result.push(x)
try {
throw "exception";
} catch (x) {
x = "catch";
}
result.push(x);
return result;
}
test(); // ["var", "var"]
try {
throw new Error()
} catch (err) {
var test = "Hello"
const test2 = "Hi"
console.log(err); //Error
}
console.log(test); //Hello
console.log(test2); //not defind 块级作用域
console.log(err); //not defind 不是全局作用域;不是函数作用域 类似函数作用域
复制代码
总结
- 在代码块中的变量声明会被隐式地提升到封闭函数的顶部。
- 重声明
var
变量被视为单个变量。 - 应该使用
let
和const
声明变量,而不是var
。
第 13 条:使用立即调用的函数表达式创建局部作用域
绑定与赋值
function wrapElements(a) {
var result = [], i, n;
for(i = 0, n = a.length; i < n; i++) {
result[i] = function() { return a[i] };
}
return result;
}
const wrapped = wrapElements([10, 20, 30, 40, 50]);
const f = wrapped[0];
f(); // undefined
复制代码
我们希望这段程序输出 10,但实际上输出是 undefined
值,因为我们需理解绑定与赋值的区别。在运行进入一个运用于,JavaScript 会为每一个绑定到该作用域的变量在内存中分配一个 “槽”(slot)。因此在 for 循环的每次迭代中,循环体都会为嵌套函数分配一个闭包。
而 i
存储的是外部变量 i
的引用,每次函数创建后变量 i
的值都发生了变化,因此内部函数最终看到的是变量 i
最后的值,故闭包存储的是其外部变量的引用而不是值。
因此由于 wrapElements
函数创建的闭包都引用在循环之前创建变量 i
的同一个共享 “槽”。这时候当我们调用其中任何一个闭包时,它都会找到数组的索引 5
并返回 undefined
值(因为 a[5] 为 undefined)
用上述例子来说,wrapElements
函数绑定了三个局部变量,而最终调用闭包中的引用,赋值时的值均为变量的最终结果。 这是闭包常出现的错误。
即便将 var 声明置于 for 循环头部
for(var i = 0, n = a.length; i < n; i++)
,根据函数作用域的原因,造成结果也一样。
立即调用的函数表达式
通过创建一个嵌套函数并立即调用它来强制创建一个块级作用域。这种方法称之为立即调用的函数表达式,是一种模拟解决 JavaScript 块级作用域的方法。
function wrapElements(a) {
var result = [], i, n;
for(i = 0, n = a.length; i < n; i++) {
(
function(j) {
return a[j]
};
)(i)
}
return result;
}
复制代码
缺陷
但在使用这个解决方法适,需要注意以下几点。
- 代码块中不能包含任何跳出块的
break
语句和continue
语句,因为立即调用的函数表达式中,将立即释放作用域。因此在函数外使用break
语句和continue
语句是不合法的。 - 代码块中引用
this
或特别的arguments
变量,含义将不符合我们的预期。
总结
- 理解绑定与赋值的区别。
- 闭包通过引用而不是值捕获它们的外部变量。
- 使用立即调用的函数表达式来创建局部作用域。
- 当心在立即调用的函数表达式中包裹代码块可能改变其行为的情形。
- 任何跳出块的
break
语句和continue
语句 this
或特别的arguments
变量
- 任何跳出块的
第 14 条:当心命名函数表达式笨拙的作用域
函数表达式
命名函数表达式:定义一个函数并绑定到当前作用域的一个变量,例如:
const f = function double(x) {return x * 2};
复制代码
也可以使用匿名的函数表达式形式:
const f = function(x) {return x * 2};
复制代码
匿名和命名函数表达式的官方区分在于后者会绑定到与其函数名相同的变量上,该变量将作为该函数内的一个局部变量。我们常用其来写递归函数表达式。
cosnt f = function find(tree, key) {
if (!tree) {
return null;
}
if (tree.key === key) {
return tree.value;
}
return find(tree.left, key) || find(tree.right, key)
}
const myTree = {}
find(myTree, "foo") // error: find is not defined
复制代码
命名函数表达式绑定的相同函数名变量,作用域只在其自身函数中。但使用外部作用域的函数名也可达到同样的效果(因作用域链的原理):
cosnt f = function find(tree, key) {
if (!tree) {
return null;
}
if (tree.key === key) {
return tree.value;
}
return f(tree.left, key) || f(tree.right, key)
}
复制代码
调试
因此命名函数表达式真正的用处是进行调试。大多数现代的 JavaScript 环境都提供对 Error 对象的栈跟踪功能,在栈跟踪中,函数表达式的名称通常作为其入口使用,用于检查栈的设备调试器对命名函数表达式有类似使用。
缺陷
作用域
但 JavaScript 引擎被要求将命名函数表达式的作用域表示为一个对象,这有点像有问题的 with
结果。造成该作用域对象只有一个属性,是将函数名和函数自身绑定起来。但该作用域对象也继承了 Object.prototype
的属性,这意味着该对象上原型所有属性将引入到该作用域当中:
var constructor = function() { return null; };
var f = function f() {
return constructor();
};
f(); // {} (在 ES3 环境中)
复制代码
在ES3 环境下上述结果应期望产生 null
,但其实会产生一个新对象。因为命名函数表达式在其作用域内继承 Object.prototype.constructor
,而受到了构造函数的更改,如 with
语句一样,程序的一部分可能对 Object.prototype
上的属性进行了更改,导致结果。
因此在系统中避免对象污染函数表达式作用域的最好方式是避免任何时候在
Object.prototype
中添加属性,以及避免使用任何与标准Object.prototype
属性同名的局部变量。
声明提升
JavaScript 引擎中另一缺陷是对命名函数表达式的声明进行提升,这是不符合标准行为的如:
const f = function g() { return 17 }
g();
复制代码
而一些 JavaScript 环境还会将 f
和 g
这两个函数作为不同对象(实际上是声明和赋值的关系),而导致不必要的内存分配,因此我们可以手动释放命名函数的内存。
const f = function g() { return 17 }
const g = null; // 手动释放内存,确保可被垃圾回收
复制代码
总结
综上所述,命名函数表达式由于会导致很多问题,所以并不值得使用。而通常来说在开发阶段使用命名函数表达式作用调试,在发布前通过预处理程序将所有的函数表达式转为匿名的。
- 在 Error 对象和调试器中使用命名函数表达式进行栈跟踪。
- 在 ES3 和有问题的 JavaScript 环境中谨记函数表达式作用域会被
Object.prototype
污染。 - 谨记在错误百出的 JavaScript 环境中会提升命名函数表达式声明,并导致命名函数表达式的重复存储。
- 考虑避免使用命名函数表达式或在发布前删除函数名。
- 如果你将代码发布到正确实现的 ES5 或更高的环境下,那么也没有什么好担心的。
第 16 条:避免使用 eval 创建局部变量
作用域
JavaScript 的 eval
函数是一个令人难以置信的强大、灵活的工具,但却容易被滥用。调用 eval
函数会将其参数作为 JavaScript 程序进行解释,但嵌入到程序的全局变量会被创建为调用程序的局部变量。
const y = "global";
function test(x) {
if (x) {
eval("const y = 'local';");
}
return y;
}
test(false); // "global"
test(true); // "local"
复制代码
上述例子明显表明,只有当 eval
函数被调用时 const
声明语句才会被调用,放置在 eval
函数才会将其变量加入到作用域中。
当源代码将未在局部作用域内定义的变量传递给 eval 函数时,程序将变得特别棘手。
const y = "global";
function test(src) {
eval(src);
return y;
}
test("const y = 'local';"); // "local"
test("const z = 'local';"); // "global"
复制代码
这段代码因 eval 而十分脆弱不安全,它赋予了外部外部调用者能改变 test 函数内部作用域的能力。
避免
ES5 严格模式将 eval
函数运行在一个嵌套的作用域中以防止这种污染,也为了不影响外部作用域,简单方法是用一个明确的嵌套作用域中运行它。
const y = "global";
function test(x) {
if (x) {
(function() {eval("const y = 'local';"); })();
}
return y;
}
test(false); // "global"
test(true); // "global"
复制代码
总结
- 避免使用 eval 函数创建的变量污染调用者的作用域。
- 如果 eval 函数代码可能创建全局变量,将此调用封装到嵌套的函数中以防止作用域污染。
第 17 条:间接调用 eval 函数优于直接调用
大多数函数只能访问定义它们所在的作用域,eval
函数具有访问调用它那时的整个作用域的能力,一旦被调用,那么每个函数调用都需要确保在运行时整个作用域对 eval
函数是可访问的。
作为折中的解决方案,语言标准演化出了辨别两种不同调用 eval 函数的方法。直接调用和间接调用。
直接调用
函数调用涉及 eval
标识符,被认为是一种“直接”调用 eval 函数的方法。
const x = "global";
function test() {
const x = "local";
return eval("x"); // direct eval
}
test(); // "local"
复制代码
在实践中,唯一能够产生直接调用 eval 函数的语法是可能被(许多)括号包裹的名称为 eval 的变量。
缺陷
- 利于被网络攻击:对一个来自网络的源字符串进行求值,可能会暴露其内部细节给一些未受信者。
- 直接调用 eval 函数将会污染作用域。
- 直接调用 eval 函数性能上的损耗将十分高昂。
间接调用
其他调用 eval 函数的方式被认为是“间接”的,这些方式在全局作用域内对 eval 函数的参数求值。
const x = "global";
function test() {
const x = "local";
const f = eval
return f("x"); // indirect eval
}
test(); // "global"
复制代码
间接调用 eval 函数的一种简洁方法是使用表达式序列运算符(,)和一个明显毫无意义的数字字面量 (0, eval)(src)
。该方法工作的逻辑大抵如下:
- 数字字面量 0 被求值但其值被忽略掉
- 括号表示的序列表达式产生结果是 eval 函数
- 间接调用 eval 函数
总结
- 将 eval 函数同一个毫无意义的字面量包裹在序列表达式中以达到强制使用间接调用 eval 函数的目的。
- 尽可能间接调用 eval 函数,而不要直接调用 eval 函数。
后言
以上为 第一章内容 学习了 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 条准则「六十一至六十八条」
若无链接,则是正在学习当中…