effective-js
Table of Contents
- Chapter 1: Accustoming Yourself to JavaScript
- Item 01: Know Which JavaScript You are Using
- Item 02: Understand JavaScript's Floating-Point Numbers
- Item 03: Beware of Implicit Coercions
- Item 04: Prefer Primitives to Object Wrappers
- Item 05: Avoid using == with Mixed Types
- Item 06: Learn the Limits of Semicolon Insertion
- Item 07: Think of Strings As Sequences of 16-Bit Code Units
- Chapter 2: Variable Scope
- Item 08: Minimize Use of the Global Object
- Item 09: Always Declare Local Variable
- Item 10: Avoid with
- Item 11: Get Comfortable with Closures
- Item 12: Understand Variable Hoisting
- Item 13: Use Immediately Invoked Function Expressions to Create Local Scopes
- Item 14: Beware of Unportable Scoping of Named Function Expresions
- Item 15: Beware of Unportable Scoping of Block-Local Function Declarations
- Item 16: Avaoid Creating Local Variables with eval
- Item 17: Prefer Indirect eval to Direct eval
- Chapter 3: Working with Functions
- Item 18: Understand the Difference between Function, Method, and COnstructor Cells
- Item 19: Get Comfortable Using Higher-Order Functions
- Item 20: Use call to Call Methods with a Custom Receiver
- Item 21: Use apply to Call Functions with Different Numbers of Arguments
- Item 22: Use arguments to Create Variadic Functions
- Item 23: Never Modify the arguments Object
- Item 24: Use a Variable to Save a Reference to arguments
- Item 25: Use bind to Extract Methods with a Fixed Receiver
- Item 26: Use bind to Curry Functions
- Item 27: Prefer Closures to Strings for Encapsulating Code
- Item 28: Avoid Relyingon the toString Method of Functions
- Item 29: Avoid Nonstandard StackInspection Properties
- Chapter 4: Objects and Prototypes
- Item 30: Understand the difference between prototype, getPrototypeOf, and __proto__
- Item 31: Prefer Object.getPrototypeOf to __proto__
- Item 32: Never Modify __proto__
- Item 33: Make Your Constructors new-Agnostic
- Item 34: Store Methods on Prototypes
- Item 35: Use Closure to Store Private Data
- Item 36: Store Instance State only on Instance Objects
- Item 37: Recognize the Implicit Binding of this
- Item 38: Call Superclass Constructors from Subclass Constructors
Chapter 1: Accustoming Yourself to JavaScript
- js设计的初衷是让人看起来熟悉,它有很多script语言以及java的影子
Item 01: Know Which JavaScript You are Using
- js最开始是作为java在web编程领域的补充而开发的
- js的流行造成了第一次的标准化行动1997年,诞生了ECMAScript
- 第三个版本的ECMAScript(也就是ES3,完成于1999年)是当今世界最为流行的js标准化 的版本
- 2005年引入的ES5也是非常的重要,它引入了很多新feature,同时把一些广泛使用的非 标准化的feature给标准化了
- 在js的世界里面,除了标准化,还有很多的非标准化,比如:
- 有些浏览器支持const
const PI = 3.1415926; PI = "modified"; PI; //3.1415926
- 有些浏览器直接把const当做var
const PI = 3.1415926; PI = "modified"; PI; // "modified"
- 有些浏览器支持const
- ES5引入了一个新的feature叫做strict mode,这个feature可以让你选择使用"严格版 本"的js:也就是去成那些有问题的,容易出错的feature版本的js
- 而没有实施strict mode check的浏览器也是可以执行这些代码的,因为并没有使用新
的feature,只不过是严格使用"固定的feature",使用方法是:
- 在程序最开始使用字符串来声明
"use strict";
- 或者在function body里面声明(在这个function内部使用strict mode)
function f(x) { "use strict"; // ... }
- 在程序最开始使用字符串来声明
- 使用一个字符串来作为strict mode的标识符是为了向前兼容,因为ES3不知道strict mode的存在,所以它看到这个字符串evaluate它的值一下,然后马上就丢弃了,不会产生 任何负面的影响
- 当然了,你的strict mode一定要在支持ES5的浏览器里面测试一下,只在ES3的浏览器下 怎么写都是对的–因为ES3的浏览器就压根不会去测试strict mode啊
- strict mode的一个坑源自于它的要求:
- 要么在文件最开始
- 要么在function最开始
- 但是,js有可能是开发的时候各自为战,部署的时候,合成一个大的js文件,这个时候:
- 原本是文件第一行的strict mode,可能已经变成文件中部的一句字符串了.
- 原本第一行不是strict mode,但是因为"跟了一个strict mode"的文件,也变成strict mode了.
- 所以我们开发的时候,要尽量避免strict和nonstrict文件合在一起,最极端的情况下,
如果我们必须合并文件(为了减少文件大小),我们可最终合并成两个文件:
- 一个strict mode
- 一个nonstrict mode
- 当然还有比较屌的方法:就是把一个文件所有的内容放入到一个function里面去,这就
避免了合并文件时候的问题
(function() { //file1.js "use strict"; function f() { // ... } // ... })(); (function() { //file2.js // no strict-mode directive function f() { var arguments = []; //... } })();
Item 02: Understand JavaScript's Floating-Point Numbers
- 很多编程语言都有多种的numer type,但是js只有一种,这个可以通过type各种js里面
的数字得出结论.
console.log(typeof 17); console.log(typeof 98.6); console.log(typeof -2.1); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // number // // number // // number // ////////////////////////////////////////////////////
- 实际上,所有的js number都是64位精度的浮点数
- 一般的操作符都是正常的
console.log(0.1 * 1.9); console.log(-99 + 100); console.log(21 - 2.3); console.log(2.5 / 5); console.log(21 % 8); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 0.19 // // 1 // // 18.7 // // 0.5 // // 5 // ////////////////////////////////////////////////////
- 但是对于bitwise操作符,js却会把浮点数转换成32-bit的整数
console.log(8|1); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 9 // ////////////////////////////////////////////////////
- 十进制数字和二进制"字符串"之间的转换如下:
- 十进制到二进制
(8).toString(2); // "1000"
- 二进制字符串到十进制
parseInt("1001", 2); // 9
- 十进制到二进制
- 浮点数的计算,有时候是不准确的
console.log(0.1+0.2); // 0.30000000000000004
- 浮点数的不准确性还在于,它不满足"结合性(associative property)",所谓结合性就
是总符合
(x + y) + z = x + (y + z)
- 但是js里面的浮点数肯定就不符合啦
console.log((0.1 + 0.2) + 0.3); console.log(0.1 + (0.2 + 0.3)); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 0.6000000000000001 // // 0.6 // ////////////////////////////////////////////////////
- 如果js里面需要计算,那么把浮点数转换成整数去计算是非常好的一个workaround,因
为整数不会有rounding errors.
(10 + 20) + 30; //60 10 + (20 + 30); //60
- 在js里面,integer只是double的一个subset,而不是另外的一个datatype,js里面所有 的数据类型都是number
Item 03: Beware of Implicit Coercions
- 对于type error, js的容忍性极强,很多别的语言编译器会抱怨的,js都能忍,比如
3 + true; // 4
- 对于静态语言,上面肯定是报错的.对于大部分的静态语言,上面也是至少会throw一个 exception
- 当然js的忍耐是有限度的,如下两个过于明显的type错误它还是会抱怨的
"hello"(1); null.x;
- 其他情况下,js之所以没有抱怨(或者说错的不离谱),其原因是js会coerce某些类型到
另外一种类型,使得错的不离谱的js能够运行下去, coerce的规则是:
- 减号,乘号,除号,取余(出了加号以外):都是把argument转换成number
- 加号的话,又分两种情况:
- 如果两边都不是string,那么转换成number:
2 + 3 ; //5
- 两边有一个是string,那么就转换为string:
1 + 2 + "3"; // "33" 1 + "2" + 3; // "123"
- 如果两边都不是string,那么转换成number:
- bitwise还是会统一转换成nubmer来处理,只不过,还是老样子,double变成integer
"8.2" | "1" //9
- coercion并不是总是能够hide error,有时候会把事情搞得更糟糕:
- 一个null的变量会被coerce成0
- 一个undefined的变量会被coerce成一个特殊的浮点数NaN
- 更加悲惨的是NaN是一个特别特殊的数,非常难以对付,甚至是测试一变量是不是NaN是
非常麻烦的,因为:
- NaN和自己不相等
var x = NaN; console.log(x === NaN); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // false // ////////////////////////////////////////////////////
- isNaN(num)函数也不可靠,因为它只有在num是number类型的时候才有用,所以如果num
不是number的情况下,它总是认为这是一个NaN
isNaN("foo") // true isNaN(undefined) // true isNaN({}) // true isNaN({ valueOf: "foo" }) // true
- 当然了NaN也有弱点,它的弱点就是自己不和自己相等,所以可以使用如下函数来进行
判断NaN
function isReallyNaN(x) { return x !== x; }
- NaN和自己不相等
- object也可以转换成primitive类型, 一般用在object变化成string, 注意是因为加号
左边是字符串,所以右边也要转换成字符串. 内部其实是调用的toString函数
"The Math object:" + Math; // "the Math object: [object Math]" "The Json object:" + Json; // "the Json object: [object Json]"
- object要转换成number,那么就是要调用valueOf函数
2 * { valueOf: function() { return 3; } }; // 6
- 如果你认为"+"号左右有string就会调用toString(),那就大错特错了,因为toValue()
返回了number还可以在"+"的时候转换成string.而且结果也是大家没有想到的
在toString()和valueOf()都存在的情况下,优先调用valueOf()
- 知道了上面的情景设定,下面的代码就不难理解了
var obj = { toString: function() { return "[object MyObject]"; }, valueOf: function() { return 17; } }; console.log("object: " + obj); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // object: 17 // ////////////////////////////////////////////////////
- 可见valueOf其实是为number类型的object准备的,大部分情况下,我们不要为自己的 object准备valueOf函数,toString函数是大多数object需要的.
- 最后一种coercion就是boolean类型的转换.如果变量和if, ||, &&等一起合作的话,它
会自动从任意类型转换成布尔类型,转换的规则很简单:
- 如下几个value转换成false:
false, 0, -0, "", NaN, null, undefined
- 剩下的value都转换成true
- 如下几个value转换成false:
- 所以0和""都会被认为是false,因此,如下的代码是错误的,因为没有考虑到0为false的
情况
function point(x, y) { if (!x) { x = 320; } if (!y) { y = 240; } return { x: x, y: y }; } console.log(point(0,0)); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // { x: 320, y: 240 } // ////////////////////////////////////////////////////
- 正确的方法是使用typeof
function point(x, y) { if (typeof x === "undefined") { x = 320; } if (typeof y === "undefined") { y = 240; } return { x: x, y: y }; } console.log(point()); console.log(point(0, 0)); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // { x: 320, y: 240 } // // { x: 0, y: 0 } // ////////////////////////////////////////////////////
Item 04: Prefer Primitives to Object Wrappers
- js中object是一个"复杂"的类型, 而除了object以外,js还有五种primitive类型:
- boolean
- number
- string
- null: 非常令人费解的是typeof null的结果是object
console.log(typeof null); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // object // ////////////////////////////////////////////////////
- undefined
- 就像Java里面的Boxing一样,js也有Object类型的String, Boolean, Number,以String
为例, object类型的String和string类型有不少类型的地方
var s = new String("hello"); console.log(s + " world"); console.log(s[4]); console.log(typeof s); console.log(typeof "hello"); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // hello world // // o // // object // // string // ////////////////////////////////////////////////////
- 但就是typeof不相同这一样,就会导致一些和primitive string不一样的行为,比如虽然
两个String object都是"hello",但并不相等(object只和自己相等)
var s1 = new String("hello"); var s2 = new String("hello"); s1 === s2; // false s1 == s2; // false
- 正是因为不相等这个现象,所以String object其实没啥用,之所以创建及出来,是因为
primitive类型不能有自己的函数,而String object因为是object,那肯定可以有自己
的函数.
- primitive不能有自己的函数, 但是set和get property的时候会创建临时的boxing
类型,这里就是String object.因为下面两个语句其实是两个不同的Object,所以第二
句不会有结果
"hello".somProperty = 17; "hello".somProperty; // undefined
- primitive可以调用toUpperCase()其实是先生成一个String Object, 然后调用String
object的函数toUpperCase()
"hello".toUpperCase(); // "HELLO"
- primitive不能有自己的函数, 但是set和get property的时候会创建临时的boxing
类型,这里就是String object.因为下面两个语句其实是两个不同的Object,所以第二
句不会有结果
- 所以primitive不能有函数,但是因为相应的"Box"类型可能为他提供了某些函数,所以 primivite.func()也是不会报错的.这同时也隐藏了错误,如果你使用了var.func(),你 以为var是object,其实是primitive,那么其实var.func()是不会报错的.这种错误非常 隐蔽,要小心
Item 05: Avoid using == with Mixed Types
- js中有两个equal:
- strict equal (===) 如果比较的两个数连类型都不一样,就直接返回false
- nonstrict equal(==)允许比较的两者类型不一样,会都转换成number类型,然后再比
较,所以如下的两个看起来不相干的值,都会转换成1,一比较,还真相等
"1.0e0" == { valueOf: function() { return true; } }; // true
- 既然==能够比较两个类型不一样的对象,那么很多人就会马上想到"应用场景",而且对
此非常满意(feature总算用上了)
var today = new Date(); if (form.month.value == (today.getMonth() + 1 && form.day.value == today.getDate()) { // happy birthday! }
- 但是这却并不是推荐的做法,使用==给读者传递的信号是,==两边的类型是不一样的,他 们需要依靠js的coerce功能进行转换,这就给了js各种奇怪的语法以空隙,所以更加好 的practice是把两者转化成同一种类型(number),然后使用===来比较
- ===给读者传递的语义就是===左右两者类型是是一样的,减少了读者的焦虑,而unary+
operator可以把value转换成number类型,所以更加清晰的写法如下
var today = new Date(); if (+form.month.value === (today.getMonth() + 1) && +form.day.value === today.getDate()) { // happy birthday! }
- 最后我们来总结下===和==的规则(总体上只需要记住===要求类型一样,==不要求就可
以了):
- 对于===:
- 如果类型不同,就[不相等]
- 如果两个都是number类型,并且值一样,那么[相等],例外是NaN和NaN不相等
- 如果两个都是string类型,并且每个位置都一样,那么[相等]
- 如果两个都是boolean类型,并且都是true或者false,那么[相等]
- 如果两个都是null类型,那么[相等]
- 如果两个都是undefined类型,那么[相等]
- 如果两个都是指向同一个object或者function,那么[相等]
- 对于==:
- 如果两个类型相同,进行===比较
- 如果一个是null类型,另一个是undefined类型,那么[相等]
- 如果一个是string类型,一个是number类型,那么字符串先valueOf(),然后===比较
- 如果任意一个是boolean类型,那么先也是valueOf(),true为1, false为0
- 如果任意一个是object类型,那么先调用valueOf(), 不成功再调用toString()转 换后再进行比较,例外是Date,它先调用toString(),然后调用valueOf().
- 对于===:
Item 06: Learn the Limits of Semicolon Insertion
- Js的一个方便的地方在于,你可以不在行尾添加分号,系统可以自动添加,自动添加的机
制叫做automatic semicolon insertion
function Point(x, y) { this.x = x || 0 this.y = y || 0 }
- ECMAScript标准明确表示了分号插入的机制,所以少些分号是不影响代码的portablity 的.
- 但是就像前面的coercion一样,semicolon insertion有自己的陷阱在里面,所以我们要 非常了解semicolon insertion的机制,才能正常工作.
- semicolon insertion的第一个机制如下:
Semicolons are only ever inserted before a } token, after one or more newlines, or at the end of the program input. 分号通常在}之前加入分号,或者换行符之后,或者在program input之后
- 所以如下的代码是错误的,因为"+r"后面既没有},也没有换行符,也不是程序的结束之前
function area(r) { r = +r return Math.PI * r * r}
- semicolon insertion的第二个机制如下:
Semicolons are only ever inserted when the next input token cannot be parsed 分号,只有再下一个input无法解析的时候,才会自动插入分号
- 比如如下的这个例子
a = b (f());
- 上述这个例子不会在b后面自动插入分号,因为如下的代码是合法的
a = b(f());
- 所以,当下一行是如下的几种符号的时候,我们要特别的小心,因为可能会被和当前行解
析在一起
(, [, +, -, /
- 看到这里,读者可能会想,如果我从来不省略分号,是不是最后就不会有问题?答案是否 定的:因为在某些情况下js会强制的插入分号,即便能够正确的分析下一个input,这种 情况叫做restricted production
- 在restricted production中, 在两个token之间是不允许出现newline的:
- 正常代码中如下代码是可行的,因为js不会主动在return后面,{前面加分号,因为js
能够正常parse这个语句
return {};
- 在restricted production中,只要出现newline就会加入分号(来替代newline,因为
不能存在newline嘛)
return; {} ;
- 正常代码中如下代码是可行的,因为js不会主动在return后面,{前面加分号,因为js
能够正常parse这个语句
- 其他restricted production还有:
- A throw statement
- break or continue statement
- postfix ++ or – operator
- semicolon insertion的第三个机制是:
Semicolons are never inserted as separators in the head of a for loop or as empty statements for循环里面的分号从来都不能省略, 有empty body的loop要有明确的分号
Item 07: Think of Strings As Sequences of 16-Bit Code Units
- Unicode的本质非常容易理解:每一个世界上存在的单个字符都被赋予了一integer值, 这个值的range是0~1,114,111这也被称之为Unicode的code piont.换句话说,其实 Unicode就是一个超大字符集的ASCII
- 说Unicode是一个超大字符集的ASCII是有先决条件的,那就是要使用UTF-32的encoding 格式.所谓UTF-32,意思就是每个字符都使用32个bit来存储,Unicode有1百多万么,32-bit 就是40个亿,所以可以轻松存储Unicode
- 后来人们发现用UTF-32太浪费了,那就使用UTF-16, 而UTF-16只有6万多个表示方式,所 以就有一个映射,比如如果小于6万的Unicode就用一个UTF-16表示,大于的,就用两个UTF-15 字符表示
- 更后来大家发现ASCII占大多数,那么我们可以使用UTF-8, 常用的ASCII就使用一个UTF-8 其他的可以使用两个,三个,或者四个UTF-8字符.
- 那很明显最省劲的办法是使用UTF-32,一劳永逸,每个字符都有唯一对应的integer值,但
是坑爹的是在js诞生的那个时候,Unicode还是16bit的,所以UTF-16自然是一劳永逸的.
所以和JavaScript和Java一样选择了UTF-16.所以
An element of a JavaScript string is a 16-bit code unit
- 也就是说,如果有Unicode integer超过65535的字符的话,那么在JS里面就会使用两个
16-bit code unit来进行存储
console.log("𝄞loveyou".length); console.log("Iloveyou".length); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 9 // // 8 // ////////////////////////////////////////////////////
- 也就是说,如果js要应对full range of Unicdoe,那么它的很多内置函数比如length, index, regular expression pattern都不能使用了.
Chapter 2: Variable Scope
Item 08: Minimize Use of the Global Object
- 全局变量有着很特殊的一面,它可以接触整个程序里面的任意代码.但这种便利只对新手 有诱惑,对于有经验的程序员来说,他们会尽量避免global variable
- 定义全局变量会污染common namespace,会引入可能的name collision
- 全局变量还违反了模块化原则,他们的存在可能会造成两个本来模块化的代码之间的联 系.
- 也正是因为global namespace是js中不同component之间联系的唯一纽带,所以很多时 候global namespace也是不得不使用的.比如一些js的库,必须定义global的name,所以 其他部分的js才能调用它.
- 一个常见的错误就是在全局定义temporary变量, 如下例,如果我买的score函数也使用
了i,n,sum等变量的话,那么程序就会出错
var i, n, sum; function averageScore(players) { sum = 0; for (i = 0, n = players.length; i < n; i++) { sum += score(players[i]); } return sum /n; }
- 解决办法当然是让临时变量的scope尽可能的小
function averageScore(players) { var i, n, sum; sum = 0; for (i = 0, n = players.length; i < n; i++) { sum += score(players[i]); } return sum / n; }
- JS的global namespace也会被导出成global object,在程序开始的时候,会被作为this
的初始化值.这个是非常特殊的用法,需要特别注意
var foo = "global foo"; this.foo = "changed"; foo; // "changed"
- 而且在浏览器里面global object还和window variable进行了绑定,也就是说,在浏览器 的global域里面,this就等于window
- 我们虽然可以使用this(或者window).variable = xxx的方式来定义全局变量,但是使用 var是更加清晰的做法
- 当然了this(window)的存在还是很有意义的,因为一个library要想发挥作用,就要在global
里面定义一个变量,所以我们可以通过查询this(window)里面时候有某个变量来判读某
个feature是否已经应用,比如我们测试浏览器是否提供了ES5的JSON功能
if (!this.JSON) { this.JSON = { //parse: .. }; }
Item 09: Always Declare Local Variable
- 如果有比global variable更麻烦的事情的话,那就当属unintential global variable 了.但是由于JS的自身特点,就很容易"一不小心创建了global variable"
- 比如下面的例子,由于忘了给temp前面加var,所以程序自动创建了一个全局变量temp,
并给他赋了值
function swap(a, i, j) { temp = a[i]; a[i] = a[j]; a[j] = temp; } swap([2, 3], 0, 1); console.log(temp); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 2 // ////////////////////////////////////////////////////
- 正确的做法是使用var
function swap(a, i, j) { var temp = a[i]; a[i] = a[j]; a[j] = temp; } swap([2, 3], 0, 1); console.log(typeof temp); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // undefined // ////////////////////////////////////////////////////
- 有很多lint工具可以检查这种错误,推荐使用lint工具进行检查.
Item 10: Avoid with
- with是为了让大家能够少写一点代码,自动绑定某个object,但是事实证明,这个feature
由于歧义太大,完全不推荐使用!
function status(info) { var widget = new Widget(); with(widget) { setBackground("blue"); setForeground("white"); setText("Status: " + info); // ambiguous reference because of info show(); } }
Item 11: Get Comfortable with Closures
- Closure是js自己带的一种feature,其他语言里面并没有.理解closure需要理解如下的
三个重要的fact:
- JS允许function引用那些定义在当前function之外scope的variable, 比如下例中
的make函数,它调用了不在它scope里面的magicIngredient
function makeSandwich() { var magicIngredient = "peanut butter"; function make(filling) { return magicIngredient + " and " + filling; } return make("jelly"); } console.log(makeSandwich()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // peanut butter and jelly // ////////////////////////////////////////////////////
- JS允许function引用那些定义在"outer function"里面的variable,即便这个outer
function已经returned了!这种情况描述起来比较晦涩,需要看看代码,比如下例中,
inner function就是make, outer function就是sandwichMaker, inner function
引用了outer function中定义的变量magicIngredient, 但是即便outer function
已经返回,我们还是可以引用到它里面的变量
function sandwichMaker() { var magicIngredient = "peanut butter"; function make(filling) { return magicIngredient + " and " + filling; } return make; } var f = sandwichMaker(); console.log(f("jelly")); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // peanut butter and jelly // ////////////////////////////////////////////////////
- 上述两种情况可以实现的原因就是closure,它的原理就是function一旦引用了某个
变量.那么它就会一直保持着对这个变量的reference.这种机制叫做closure.我们
第三个fact就是closure保持的reference是真正的ref(而不是copy),所以一旦函数
对其ref的变量进行了更改,那么所有包括这个ref的closure都会看到
function box() { var val = undefined; return { set: function(newVal) { val = newVal; }, get: function() {return val;}, type:function() {return typeof val;} }; }; var b = box(); console.log(b.type()); b.set(98.6); console.log(b.get()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // undefined // // 98.6 // ////////////////////////////////////////////////////
- JS允许function引用那些定义在当前function之外scope的variable, 比如下例中
的make函数,它调用了不在它scope里面的magicIngredient
- closure存储了其能够访问的变量的ref,那么一个function能够访问的变量肯定是在它
的scope里面.这里又涉及到一个问题,js里面的scope是多大,答案如下.注意js里面的
scope是和外部function紧密联系的,而不是`{}`
parameters and variables of outer functions 外部函数里面定义的参数和变量
Item 12: Understand Variable Hoisting
- JS里面的scope不是以block为界限的,也就是说variable difinition不是限定在最近 的`{}`,而是限定在包含它(这个variable)的function里面
- 如果不明白上述这个原理,就会犯如下的错误:以为自己在for里面定义player,就会有
一个local的variable player,但是其实这只是重复定义了函数参数里面的player
function isWinner(player, otehrs) { var highest = 0; for (var i = 0, n = others.length; i < n; i++) { var player = otehrs[i]; if (player.score > highest) { highest = player.score; } } return player.score > highest; }
- 这个看似奇怪的"重复定义"其实是没了解js的variable declaration规则,js的变量定
义规则其实包括两个部分:
- declaration: js暗中把所有的出现的变量都declaration都放在function closure 的最上面
- assignment: var出现的时候,其实不是声明,而是assignment,每次var出现就赋值一 次.既然var只不过是"赋值",那么var两次一样的变量是完全没有问题的
- 唯一一个不遵守js的约定,使用block scoping的是try…catch里面是catch scope
function test() { var x = "var", result = []; result.push(x); try { throw "exception"; } catch (x) { x = "catch"; } result.push(x); return result; } console.log(test()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // [ 'var', 'var' ] // ////////////////////////////////////////////////////
Item 13: Use Immediately Invoked Function Expressions to Create Local Scopes
- closure存储outer function的reference,而不是value copy,所以下面的代码没有能
够得到10,而是得到了undefined.
function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i];}; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; console.log(f()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // undefined // ////////////////////////////////////////////////////
- return a[i]的这个i,只能使用a.length这个值, 因为result的每个成员都是a[i], 而
i最后都是5,了,所以肯定都返回undefined,如果我们最后强制的改变i的值为1的话,那
么所有的值最后都是20了
function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i];}; } i = 1; return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; console.log(f()); var f2 = wrapped[1]; console.log(f2()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 20 // // 20 // ////////////////////////////////////////////////////
- 解决办法是创建一个nested function,并且快速调用它,这个技巧叫做immediately
invoked function expression (IIFE)
function wrapElements(a) { var result = []; for (var i = 0, n = a.length; i < n; i++) { (function() { var j = i; result[i] = function() { return a[j];}; })(); } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; console.log(f()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 10 // ////////////////////////////////////////////////////
Item 14: Beware of Unportable Scoping of Named Function Expresions
- TODO
- named function 问题太多,不推荐使用
Item 15: Beware of Unportable Scoping of Block-Local Function Declarations
- TODO
Item 16: Avaoid Creating Local Variables with eval
- TODO
Item 17: Prefer Indirect eval to Direct eval
- TODO
Chapter 3: Working with Functions
Item 18: Understand the Difference between Function, Method, and COnstructor Cells
- 从oo语言过来的人,肯定会把function, method, class当做三个不同的概念,但是在
js里面,这个三者都是对function的不同应用而已:
- 最简单的当然就是function call了
function hello(username) { return "hello, " + username; } hello("Keyser Soze"); // "hello, Keyser Soze"
- 在js里面,如果一个object的某个property是个function的话,那么object.property
的使用,就是一种method啦. 和function不同的是,method需要一个reciver,会去这个
receiver的property里面寻找.
var obj = { hello: function() { return "hello, " + this.username; }, username: "Hans Gruber" }; obj.hello(); // "hello, Hans Gruber"
- js里面new后面跟的不是class name,而是一个function(也就是ctor function一
般function name是大写的)
function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } var u = new User("sfalken", "0ef33a79dawbaweadw"); u.name; // "sfalken"
- 最简单的当然就是function call了
Item 19: Get Comfortable Using Higher-Order Functions
- functional 语言用的非常多的两点是:
- 把function当做函数的参数
- 把function当做函数的返回值
- 这两种方法能让我们的代码变得非常简洁容易理解,比如排序,我们可以把规则做成一
个函数,然后传入到sort()函数里面
function compareNumbers(x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; } [3, 1, 4, 1, 5, 9].sort(compareNumbers); // [1, 1, 3, 4, 5, 9]
- ES5还引入了map函数,可以对一个数组里面所有的成员实施某个'函数',而我们的函数
其实也没必要使用named
var names = ["Fred", "Wilma", "Pebbles"]; var upper = names.map(function(name) { return name.toUpperCase(); }); upper; // ["FRED", "WILMA", "PEBBLES"]
Item 20: Use call to Call Methods with a Custom Receiver
- 一般来说,function或者method的调用者是固定的,比如method的caller就是method前 面的object
- 但是有些时候,我们需要使用特定的receiver来调用函数,当然了,最简单的办法是把 function作为'特定receiver'的caller
- 但是这种做法有很大风险,并不推荐使用(因为object里面可能已经有同名函数)
- JS提供了内置的call函数来做这件事情,如下两个语句是等价的
f.call(obj, arg1, arg2, arg3); f(arg1, arg2, arg3);
- 这个build-in功能很实用,比如hasOwnProperty这个函数可以使用任何类型来调用它的
作用是能够查找某个property是"自定义的"而不是"原型链"上继承来的.
Object.prototype.bar = 1; var foo = { goo: undefined}; console.log(foo.bar); console.log('bar' in foo); console.log(foo.hasOwnProperty('bar')); console.log(foo.hasOwnProperty('goo')); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 1 // // true // // false // // true // ////////////////////////////////////////////////////
- call还可以用来调用已经被删除,或者覆盖的函数,比如下面例子中的hasOwenProperty
var dict = {}; var hasOwnProperty = {}.hasOwnProperty; dict.foo = 1; delete dict.hasOwnProperty; console.log(hasOwnProperty.call(dict, "foo")); console.log(hasOwnProperty.call(dict, "hasOwnProperty")); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // true // // false // ////////////////////////////////////////////////////
Item 21: Use apply to Call Functions with Different Numbers of Arguments
- apply是call函数的一个变体,就是把call的第二个参数开始到最后一个函数,合成一个
数组参数也就是说,如下两个函数是等价的
func.call(obj, arg1, arg2, arg3) func.apply(obj, [arg1, arg2, arg3])
Item 22: Use arguments to Create Variadic Functions
- variadic function是非常好用的函数形式,因为它可以使用"任意长度"的参数,由于js
里面arguments的存在,所以自己写一个variadic function是非常容易的
function average() { for (var i = 0, sum = 0, n = arguments.length; i < n; i++) { sum += arguments[i]; } return sum / n; }
Item 23: Never Modify the arguments Object
- arguments只是看起来像Array,但是却并不是真的Array,所以不要更改arguments的内 容
- 如果想使用arguments的内容,那么我们得把arguments的内容先存放到一个array里面
console.log([0, 1, 2, 3, 4, 5].slice(2)); function callMethod() { var args = [].slice.call(arguments, 2); console.log(args); } callMethod(0, 1, 2, 3, 4, 5); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // [ 2, 3, 4, 5 ] // // [ 2, 3, 4, 5 ] // ////////////////////////////////////////////////////
Item 24: Use a Variable to Save a Reference to arguments
- iterator 是一个常见的设计模式,在js里面的的实现的话,不知道js的细节的话,我们
可以实现一个如下的版本
function values() { var i = 0, n = arguments.length; return { hasNext: function() { return i < n; }, next: function() { if (i >= n) { throw new Error("endo of iteration"); } return arguments[i++]; } }; } var i = values(0, 1, 2, 3, 4, 5, 6); console.log(i.next()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // undefined // ////////////////////////////////////////////////////
- 这个版本的结果有点出乎人们的意料,原因是arguments是每个函数都有的,所以我们调
用next的时候,其实是调用的next自己的arguments,而不是values()的arguments,所以
我们要使用如下的更改
function values() { var i = 0, n = arguments.length, a = arguments; return { hasNext: function() { return i < n; }, next: function() { if (i >= n) { throw new Error("endo of iteration"); } return a[i++]; } }; } var i = values(0, 1, 2, 3, 4, 5, 6); console.log(i.next()); console.log(i.next()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 0 // // 1 // ////////////////////////////////////////////////////
Item 25: Use bind to Extract Methods with a Fixed Receiver
- object的property可以是function,但是这个function在"调用那一刻"的this是绑定的 不同的object,所以直接把object里面的function property提取出来,然后在其他的地 方"当做method使用(也就是object不一定)"的时候,可能会出现错误,因为"提取"method 的过程,并不能一并"提取"object
- 下面的例子会出现错误,因为forEach在调用buffer.add这个method的时候,并没有能力
知道buffer.add的this是谁,它只好用了global object作为了default receiver.而
global object并没有entries,所以push就会被认为是使用在了undefined object上面
var sources = ["867", "-", "5309"]; var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(""); } }; sources.forEach(buffer.add); console.log(buffer.concat()); ///////////////////////////////////////////////////////// // <===================OUTPUT===================> // // TypeError: Cannot read property 'push' of undefined // /////////////////////////////////////////////////////////
- 幸运的是forEach意识到它可能会调用一些method,所以forEach可以添加第二个参数来
设置default receiver
source.forEach(buffer.add, buffer); buffer.concat(); // "867-5309"
- 但是并不是所有的function都如此的有礼貌,会提供一个第二参数让我们来指定default
receiver.所以需要一个更通用的方案,ES5给了我们答案就是bind,bind是所有的
object.property都拥有的一个函数
var sources = ["867", "-", "5309"]; var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(""); } }; sources.forEach(buffer.add.bind(buffer)); console.log(buffer.concat()); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // 867-5309 // ////////////////////////////////////////////////////
- 需要注意的是,bind会产生一个新的函数(只不过receiver不同),这是一种更安全的处 理方式,因为这样bind产生的函数可以放心大胆的被share
- 如果不支持ES5可以使用如下的临时方案
source.forEach(function(s) { buffer.add(s); });
Item 26: Use bind to Curry Functions
- bind函数并不是只能用来指定receiver,有时候receiver就是global object(或者无所 谓是谁)的时候,我们依然可以使用bind函数来达到简化代码的目的
- 比如curry function就是这样一种情况,这种情况的特点是:
- receiver 无所谓,所以我们一般设置为null或者undefined,也就是bind的一个参数 是null或者undefined
- bind从第二个参数开始到最后一个参数,都是原来调用需要的参数(如果是map或者 forEach,那么最后一个参数不用提供)
- 看一个不使用curry的例子
function simpleURL(protocol, domain, path) { return protocol + "://" + domain + "/" + path; } var urls = paths.map(function(path) { return simpleURL("http", siteDomain, path); });
- 使用了curry以后,显然更加简洁
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
Item 27: Prefer Closures to Strings for Encapsulating Code
- function是比string更好的一种把code存储成data structure的方式
Item 28: Avoid Relyingon the toString Method of Functions
- 不要依赖JS里面函数的toString()结果,因为有些函数是使用c++书写的
var s1 = (function(x) { return x + 1; }).toString(); var s2 = (function(x) { return x + 1; }).bind(16).toString(); console.log(s1); console.log(s2); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // node.exe c:/tmp/hh.js // // function (x) { // // return x + 1; // // } // // function () { [native code] } // ////////////////////////////////////////////////////
Item 29: Avoid Nonstandard StackInspection Properties
- 不要使用标准并不支持的argument.caller和argument.callee
Chapter 4: Objects and Prototypes
Item 30: Understand the difference between prototype, getPrototypeOf, and __proto__
- 如题目所示,js里面存在着三个特别像又有点联系的概念:
- C.prototype的作用是"建立和完善"被new()函数创建的object的prototype的.注意 这个C.prototype并不是指向prototype object, 它只能用来设置prototype object 的property,只有下面的方法可以取得prototype object
- Object.getPrototype(object)是ES5才引入的机制,用来取得一个object的prototype object
- obj.__proto__是在ES5之前的一种非官方的取得object的prototype object的方法
- 第一种用法的例子如下,我们要创建一个js的datatype,那么就要有一个ctor(js里面没
有class), 下面的User就是这样一个ctor,除了user和hash两个attribute以外,还为这
两个attribute各自提供了一个setter,其代码如下
function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } User.prototype.toString = function() { return "[User " + this.name + "]"; }; User.prototype.checkPassword = function(password) { return hash(password) === this.passwordHash; }; var u = new User("sfalken", "0ef33awefswe");
- 这里的机制比较复杂,我们要详细的说一下:
- User本质上是一个function,functionn作为一个first class object,其也是可以拥 有property的,而每个function都有一个default的property叫做prototype(当然这个 property如果function不作为ctor的话,没什么作用)
- 这个default的property,其实是一个object,所以一个object是可以有property的,所 以这里我们赋予了这个property object两个函数
- 当我们运行new来生成"object u"的时候,"object u"的prototype就指向了刚才我们 生成的"function User"的prototype object
- 上面所述的复杂的关系列图于如下
Figure 1: effective_js_prototype_all.png
- 我们需要注意的是"object u"从某种意义上来说,是"继承"自"function User"的
prototype object:
当使用u.methodname调用的时候,methodname寻找的顺序是,首先查找object u 自己的property,如果没有就查找object u的property的perperty
- 而ES5新近引入的Object.getPrototypeOf(obj)正是取得一个函数的prototype
function User() { this.password = "ello"; } var u = new User(); console.log(Object.getPrototypeOf(u) === User.prototype); //////////////////////////////////////////////////// // <===================OUTPUT===================> // // true // ////////////////////////////////////////////////////
- __proto__就很好说了,这就是一个非standard的取得prototype的方法
u.__proto__ === User.prototype // true
- 抽象一下其他语言里面class的概念其实就是提供一个ctor(ctor里面共享变量),另外 提供一系列共享的函数(一般在class体内,和ctor并列)
- 而js其实是把其他语言里面的class的概念一分为二:
- ctor就是function User,内部可以初始化变量
- 其他"类和自己的instance"共享的函数放在了User.prototype里面
- 我们使用下图来表示一下这个概念
Figure 2: effective_js_prototype_class.png
Item 31: Prefer Object.getPrototypeOf to __proto__
- 很显然,一个是标准的一个是非标准的肯定是使用标准的getPrototypeOf有更好的可移 植性啊
Item 32: Never Modify __proto__
- 本来就不推荐使用,那更不要去修改这个值啦.
Item 33: Make Your Constructors new-Agnostic
- JS的问题在于其new后面跟的是一个function,人家毕竟是function,所以没有new的情
况下,也是可以调用的.而且竟然在global域里面产生了两个全局变量name和passwordHash
function User(name, passwordHash) { this.name =name; this.passwordHash = passwordHash; } var u = User("abaravelli", "d8laiabwes"); u; // undefined this.name; // "abaravelli" this.passwordHash // "d8laiabwes"
- 上述代码在strict mode下面不可用
function User(name, passwordHash) { "use strict"; this.name =name; this.passwordHash = passwordHash; } var u = User("abaravelli", "d8laiabwes"); //////////////////////////////////////////////////////// // <===================OUTPUT===================> // // TypeError: Cannot set property 'name' of undefined // ////////////////////////////////////////////////////////
- 所以,我们最后提供一个机制(其实是为js填坑),让本来作为ctor用的function一旦被
调用的时候忘了加new,我们能智能的帮助其进行补救,这里用到了我们ES5的新特性Object.create
其参数是一个prototype object,并且返回一个新的继承自这个prototype object的新
的object.运用在这里刚好
function User(name, passwordHash) { var self = this instanceof User ? this : Object.create(User.prototype); self.name = name; self.passwordHash = passwordHash; }
Item 34: Store Methods on Prototypes
- 上面讲到的prototype写起来非常的麻烦(很不像其他语言书写class的感觉),我们其实
可以如下,把method直接写到ctor里面
function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; this.toString = function() { return "[User " + this.name + "]"; }; this.checkPassword = function(password) { return hash(password) === this.passwordHash; }; }
- 这看起来还好像是"function作为first-class object"的一个佐证.但是这样做的话,
在多个instance都从这个ctor诞生的情况下,会有麻烦,创建多个instance的代码如下
var u1 = new User(/* ... */); var u2 = new User(/* ... */); var u3 = new User(/* ... */);
- 麻烦就是如下所示,我们会存储三份一样的function代码
User.prototype +----+----------+----+ | | +--------------------+ / | \ / | \ prototype/ |prototype \prototype +---------------/ +------+--------+ \---------------+ | .toString | | .toString | | .toString | +---------------+ +---------------+ +---------------+ |.checkPassword | |.checkPassword | |.checkPassword | +---------------+ +---------------+ +---------------+ | .name | | .name | | .name | +---------------+ +---------------+ +---------------+ | .passwordHash | | .passwordHash | | .passwordHash | +---------------+ +---------------+ +---------------+ | | | | | | +---------------+ +---------------+ +---------------+
- 而如果我们把toString和checkPassword放到ctor的prototype里面去的话,结果是如下
的样子.不比不知道,function还是放到ctor的prototype里面好, 因为更节省资源
User.prototype +--------------------+ | .toString | +--------------------+ | .checkPassword | +--------------------+ | | +--------------------+ / | \ / | \ prototype/ |prototype \prototype +---------------/ +------+--------+ \---------------+ | .name | | .name | | .name | +---------------+ +---------------+ +---------------+ | .passwordHash | | .passwordHash | | .passwordHash | +---------------+ +---------------+ +---------------+ | | | | | | +---------------+ +---------------+ +---------------+
Item 35: Use Closure to Store Private Data
- JS的object系统从来也就没考虑过数据封装,一个object里面其所有的数据都可以使用
多种方法取出
var obj = { name: 'hfeng', age: 30 }; console.log(Object.keys(obj)); console.log(Object.getOwnPropertyNames(obj)); for (var one in obj) { console.log(one); } //////////////////////////////////////////////////// // <===================OUTPUT===================> // // [ 'name', 'age' ] // // [ 'name', 'age' ] // // name // // age // ////////////////////////////////////////////////////
- 在JS的世界中,命名规范会起到"一定的"数据封装的作用,比如是使用'_'开头的变量被 认为是private变量
- 但是"命名规范"的数据封装方式,只能防君子,而没法对付小人,JS里面对付小人的办法 就是closure
- closure在js里面值得大书特书,但是我们这里只讲closure和object对比起来的一个完
全"相反"的特性:能够访问closure的function才有机会访问closure的数据
The properties of an object are automatically exposed, whereas the variables in a closure are automatically hidden
- 下面就是能够"真正"的数据封装的ctor(虽然这个ctor还是犯了在不同instance会重复
存储function的毛病,但是显然数据封装更重要)
function User(name, passwordHash) { this.toString = function() { return "[User " + name + "]"; }; this.checkPassword = function(password) { return hash(password) === passwordHash; }; }
Item 36: Store Instance State only on Instance Objects
- 理解prototype object和instances之间的one-to-many的关系至关重要, prototype里 面存储的应该是多个instance"可以共享"的数据,比如函数(因为函数是stateless的).
- 但是代表了instance的内部state的数据,比如下例中一个树ctor里面的children列表,
就应该是不同instance拥有各自的列表,而不是共享一个.
function Tree(x) { this.value = x; } Tree.prototype = { children: [], addChild: function(x) { this.children.push(x); } };
- 正确的实现方式如下,因为this保证在运行的时候是绑定调用函数的instance的,所以
this.children也是可以正确使用的
function Tree(x) { this.value = x; this.children = []; } Tree.prototype = { addChild: function(x) { this.children.push(x) } };
Item 37: Recognize the Implicit Binding of this
- this在js里面有着非常丰富的内涵,因为js的动态属性,只有在调用的时候,才知道真的
this是什么,所以会出现很多的问题,比如下面这个例子用来处理CSV文件,因为CSV文件
的分隔符也可能不止是',',所以我们设置了一个参数来设置分隔符
function CSVReader(separators) { this.separators = separators || [","]; this.regexp = new RegExp(this.separators.map(function(sep) { return "\\" + sep[0]; // sep[0] means strings[0] char }).join("|")); }
- 而分割cvs的过程简单来说有两步:第一把文件按行分成多个,第二每一行都分成多个字
符而组成的数组,这个过程可以简化成一个函数(放在ctor的prototype里面)
CSVReader.prototype.read = function(str) { var lines = str.trim().split(/\n/); return lines.map(function(line) { return line.split(this.regexp); // wrong here!, this point to `lines` }); };
- 上面出错的原因是因为this取决于最近的enclosing函数(nearest enclosing function) 所以这里的this指的是lines
- 幸运的是map这个函数和forEach一样,经常出现这种问题,所以可以加一个第二参数来
制定函数内部用哪个this
CSVReader.prototype.read = function(str) { var lines = str.trim().split(/\n/); return lines.map(function(line) { return line.split(this.regexp); }, this); };
- 如果map没提供这种支持(或者其他不提供这种支持的函数),那么我们需要使用self(或
者其他名字)在调用前绑定this
CSVReader.prototype.read = function(str) { var lines = str.trim().split(/\n/); var self = this; return lines.map(function(line) { return line.split(self.regexp); }); };
Item 38: Call Superclass Constructors from Subclass Constructors
- 在游戏当中,有一种模式叫做Scene,就是包含所有actors的信息而包括所有的关于actors
的图片等信息的,叫做context:
- 先看Scene:
function Scene(context, width, height, images) { this.context = context; this.width = width; this.height = height; this.images = images; this.actors = []; } Scene.prototype.register = function(actor) { this.actors.push(actor); } Scene.prototype.unregister = function(actor) { var i = this.actors.indexOf(actor); if (i >= 0) { this.actors.splice(i, 1); } }; Scene.prototype.draw = function() { this.context.clearRect(0, 0, this.width, this.height); for (var a = this.actors, i = 0, n = a.length; i < n; i++) { a[i].draw(); } };
- 再看看Actor
function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; scene.register(this); } Actor.prototype.moveTo = function(x, y) { this.x = x; this.y = y; this.scene.draw(); }; Actor.prototype.exit = function() { this.scene.unregister(this); this.scene.draw(); }; Actor.prototype.draw = function() { var image = this.scene.images[this.type]; this.scene.context.drawImage(image, this.x, this.y); }; Actor.prototype.width = function() { return this.scene.images[this.type].width; }; Actor.prototype.height = function() { return this.scene.images[this.type].height; };
- 先看Scene:
- TODO