Item 13:使用IIFE创建本地作用域
看来这个for循环的作用域的问题真的是很经典,在DS的博客,KS的YDtKJS系列里都有讨论过。代码如下:
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];
f(); // ?
再次强调一下,一个变量如果被若干个函数的闭包访问,那是通过引用的方式共享同一份拷贝,所以任何函数对它修改都是其他函数看得到的,并不是说每个函数都有一份自己的新拷贝,切记。
作者的方案和Dmitry的有点不一样,作者把赋值操作整个放进IIFE里了,D则是只把右边的部分放进去。
function wrapElements(a) {
var result = [];
for (var i = 0, n = a.length; i < n; i++) {
(function(j) {
result[i] = function() {
return a[j];
};
})(i);
}
return result;
}
另外有几点:
- 用IIFE的话,里面不能包含break或者continue这样的语句跳出外面的循环;
- 如果要用this或者arguments,注意它们的值或许会不同。
Item 14:命名函数表达式的作用域不可移植
这个话题也在DS的博客里被详细讨论。在ECMAScript的规格里,函数表达式可以命名也可以匿名,命名的好处里函数内部可以通过表达式命调用自身实现递归,可是实现方式上,ES3的做法是在函数的作用域链里插入一个带有函数名作为属性的对象,这个做法带来的问题是这个对象是普通对象,它继承Object.prototype上面的东西。所以在ES5里它被修改了。
而由于这个实现规格自身的缺陷,不同浏览器对它的实现也是各不相同,导致了更多的兼容性问题。演示这个问题的代码如下:
var constructor = function() {
return null;
};
var f = function f() {
return constructor();
};
f(); // {} (in ES3 environments)
总的来讲,使用命名的好处在于调试和分析性能时,它们的名字会被用来显示该函数,而不是anonymous function,并没有对性能有直接影响。作者的态度是能避免的话还是尽量避免,不能避免的话,要针对目标运行环境的具体实现做一些对策。
Item 15:在代码块内所声明的函数的作用域不可移植
这个问题非常依赖于所使用的浏览器,而且它也随着时间变化而改变。
在Chrome 58.0.3029.81 (64-bit)和Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)测试下面的代码,结果都是["local", "local"]和["local"]。
function f() {
return "global";
}
function test(x) {
function f() {
return "local";
}
var result = [];
if (x) {
result.push(f());
}
result.push(f());
return result;
}
console.log(test(true)); // ["local", "local"]
console.log(test(false)); // ["local"]
但是如果将内函数f的声明移到if代码块内会怎样呢?
function f() {
return "global";
}
function test(x) {
var result = [];
if (x) {
function f() {
return "local";
} // block-local
result.push(f());
}
result.push(f());
return result;
}
console.log(test(true)); // ?
console.log(test(false)); // ?
这段代码在Chrome 58.0.3029.81 (64-bit)和Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)的执行结果是["local", "local"]和TypeError: f is not a function。
先看看作者怎么说:
作者说,先看看ECMA规范怎么说:
ES3没有描述这种在代码块内声明函数的情况应该怎么处理。ES5则是建议将这种情况列为语法错误。
回到作者的看法:这样做可以澄清很多不必要的误解,为了更为清晰的设计铺好路,他说很多浏览器遵循了这个建议,在严谨模式下将会抛出异常。
现在咱们来看看上面的代码加上‘use strict’之后是不是真的会被抛出语法错误:
'use strict';
function f() {
return "global";
}
function test(x) {
var result = [];
if (x) {
function f() {
return "local";
} // block-local
result.push(f());
}
result.push(f());
return result;
}
console.log(test(true)); // ?
console.log(test(false)); // ?
在上面提到的浏览器版本里,不但不会抛出异常,输出的内容是:[ "local", "global" ]和[ "global" ]。也就是说在严谨模式下,Chrome和Firefox居然反而支持了条件性地,在代码块作用域里的函数声明,违背了ES3里变量提升的规则,违反了ES5的提议。实现的效果和代码语义上的含义终于是吻合的。与作者所说的也相反。
最后,作者认为,如果要写移植性好的代码,最好是避免这种函数声明。如果实在需要条件性地修改函数逻辑,可以将函数保存在一个变量里,修改变量,这样不会由于语义与行为上的不一致而导致误解:
function f() {
return "global";
}
function test(x) {
var g = f,
result = [];
if (x) {
g = function () {
return "local";
}
result.push(g());
}
result.push(g());
return result;
}
Item 18:函数式调用,方法式调用与构造函数式调用之间的不同
三者本质上讲都是通过函数实现的,只是调用方式不同导致了行为不同。作者主要想强调的还是this值的绑定的问题,这些话题在前面的笔记已经聊得很透彻了,这次就只补充强调下,ES5标准里,严谨模式下使用函数式调用函数,函数内的this将会是null,所以如果有this.prop这样的语句就会抛出异常了,这样的设计是为了能及早发现潜在bug。
Item 19:适应高阶函数(Higher-Order Function)
高阶函数就是指接收另外一个函数作为参数,或者返回另外一个函数作为返回值的函数,这个术语来自函数式编程,至于作为参数被传递的那个函数,它往往被称为回调函数(callback function)。JS內建的方法里最典型的高阶函数就是array的一些方法,像sort(),map()。
适用于高阶函数的情景是你在代码里重复同一个模式,比如用for循环构建一个字符串:
var aIndex = "a".charCodeAt(0); // 97
var alphabet = "";
for (var i = 0; i < 26; i++) {
alphabet += String.fromCharCode(aIndex + i);
}
alphabet; // "abcdefghijklmnopqrstuvwxyz"
var digits = "";
for (var i = 0; i < 10; i++) {
digits += i;
}
digits; // "0123456789"
var random = "";
for (var i = 0; i < 8; i++) {
random += String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
}
random; // "bdwvfrtp" (different result each time)
不如改写成:
function buildString(n, callback) {
var result = "";
for (var i = 0; i < n; i++) {
result += callback(i);
}
return result;
}
var alphabet = buildString(26, function (i) {
return String.fromCharCode(aIndex + i);
});
alphabet; // "abcdefghijklmnopqrstuvwxyz"
var digits = buildString(10, function (i) {
return i;
});
digits; // "0123456789"
var random = buildString(8, function () {
return String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
});
random; // "ltvisfjr" (different result each time)
Item 20:用call定制函数接收者
接收者这个术语指的是函数执行时绑定到this的那个值。
作者对于call这个方法出现的缘由的解释很有意思,如果要定制一个函数的接收者,可以把这个函数赋值给接收者对象的一个属性,这样就成了该对象上面的一个方法了,但是作者说这个做法有弊端,你就为了定制接收者就在接收者上面随意添加了一个属性,这个属性名之前并不存在还好,万一存在怎么办,你不就重写了吗。
所以就需要一个更优化的方案,于是就出现了call这样的方法,它背后的核心思维是消除一个被调用的函数与其接收者之间的直接关联,两者不必要发生直接的关系,只在调用的时候通过call来建立关联,所以它们相互的依赖从语义来讲是很松散的。这样重用函数也变得更方便。
为了解释这种松散关系的设计思路,作者还举了个例子:
var hasOwnProperty = {}.hasOwnProperty;
dict.foo = 1;
delete dict.hasOwnProperty;
hasOwnProperty.call(dict, "foo"); // true
hasOwnProperty.call(dict, "hasOwnProperty"); // false
这个例子更多细节依赖于item45的内容,先略过。
最后演示call的例子:
var table = {
entries: [],
addEntry: function (key, value) {
this.entries.push({
key: key,
value: value
});
},
forEach: function (f, thisArg) {
var entries = this.entries;
for (var i = 0, n = entries.length; i < n; i++) {
var entry = entries[i];
f.call(thisArg, entry.key, entry.value, i);
}
}
};
于是你可以这样使用:
table1.forEach(table2.addEntry, table2);
Item 21:用apply调用可变参数函数
也就是说函数参数的数量是任意可变的。这个其实有点误解,不是说只有apply做得到,上面讲到的call也可以,只不过要一个一个传,这就意味着在写代码的时候需要预先知道有几个参数,所以不用eval这种动态生成代码的方法的话,call就不能做到动态决定参数数量。所以用apply,它之所以能解决call的问题是因为它把参数放进一个数组里,再把数组作为参数传给函数。
var buffer = {
state: [],
append: function () {
for (var i = 0, n = arguments.length; i < n; i++) {
this.state.push(arguments[i]);
}
}
};
buffer.append("Hello, ");
buffer.append(firstName, " ", lastName, "!");
buffer.append(newline);
buffer.append.apply(buffer, getInputStrings());
Item 22:用arguments创建可变参数函数(没看懂,暂时略过)
Item 23:永远不要修改arguments对象
- arguments其实并不是严谨的数组类型,所以它的行为和数组并不同,不能指望在它身上调用数组的方法就能得到期待的效果;
- 函数的参数名实际上是arguments的元素的化名,这个Dmitry也提到过,他称为共享,而且还提到一些关于共享的规则,第一个参数是永远指向arguments[0]的,两者共享同一个值,所以修改arguments[0]的值会导致第一个参数的值也被修改;
- 在ES5里的严禁模式下,参数名与arguments元素的这种关联并不存在。
前两条解释了为什么下面的代码不能运行:
function callMethod(obj, method) {
var shift = [].shift;
shift.call(arguments);
shift.call(arguments);
return obj[method].apply(obj, arguments);
}
var obj = {
add: function (x, y) {
return x + y;
}
};
callMethod(obj, "add", 17, 25);
// error: cannot read property "apply" of undefined
所以,永远不要修改arguments的内容。要修改也是将其内容拷贝出来再修改:
function callMethod(obj, method) {
var args = [].slice.call(arguments, 2);
return obj[method].apply(obj, args);
}
var obj = {
add: function (x, y) {
return x + y;
}
};
callMethod(obj, "add", 17, 25); // 42
另外,下面代码可以验证前面的第三点:
function strict(x) {
"use strict";
arguments[0] = "modified";
return x === arguments[0];
}
function nonstrict(x) {
arguments[0] = "modified";
return x === arguments[0];
}
strict("unmodified"); // false
nonstrict("unmodified"); // true
最后,注意数组的一个方法slice()的妙用,[].slice(),不传参数的情况下,它会复制一个数组出来,参见MDN。
Item 24:用一个变量保存arguments
直接上代码好了:
function values() {
var i = 0,
n = arguments.length;
return {
hasNext: function () {
return i < n;
},
next: function () {
if (i >= n) {
throw new Error("end of iteration");
}
return arguments[i++]; // wrong arguments
}
};
}
var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // 1
it.next(); // 4
it.next(); // 1
运行输出的都是undefined,为什么呢?
因为next里面的arguments已经不同于values()的了,每个函数都有自己的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("end of iteration");
}
return a[i++];
}
};
}
var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // 1
it.next(); // 4
it.next(); // 1
Item 25:用bind给方法绑定接受者
- 接受者指的是一个函数里this的值;
- 对象上会有方法,在通过对象调用方法时,接受者会指向这个对象;
- 可是在其他调用情况下,接受者就不再是定义方法的对象了;
- bind()返回的是一个新函数,它在任何情况下被调用都是安全的,甚至被通过原型链继承后。
var buffer = {
entries: [],
add: function (s) {
this.entries.push(s);
},
concat: function () {
return this.entries.join("");
}
};
var source = ["867", "-", "5309"];
source.forEach(buffer.add); // error: entries is undefined
在上面的情况里,this指向的是全局对象。
顺便说一句,foreach是ES5的新方法。
解决办法是:
var source = ["867", "-", "5309"];
source.forEach(buffer.add, buffer);
buffer.join(); // "867-5309"
可是并非所有的函数都支持定制接受者。所以更通用的办法是:
var source = ["867", "-", "5309"];
source.forEach(function (s) {
buffer.add(s);
});
buffer.join(); // "867-5309"
这里的重点是在这个新增加的匿名函数里不要使用this,这个函数作为一个缓冲,确保了add()是在buffer上被调用的。
ES5的做法是:
var source = ["867", "-", "5309"];
source.forEach(buffer.add.bind(buffer));
buffer.join(); // "867-5309"
Item 26:用bind柯里化者
首先要说下柯里化(currying)这个操作。它的维基主页是https://en.wikipedia.org/wiki/Currying。简单讲它讲述的是一种操作,将一个函数拆解为一系列函数,原函数返回一个子函数,子函数再返回一个下级子函数,其中每个函数只接受原函数的一部分参数,这个链接也解释得很清楚。
bind()函数有一个妙用,暂时我用文字解释不清楚,看下面代码吧。假设我们有如下代码:
function simpleURL(protocol, domain, path) {
return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function (path) {
return simpleURL("http", siteDomain, path);
});
后面的做法可以替换成:
var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
这样的做法,bind()会返回一个函数,这个函数又被传递给map(),而map()会在执行这个函数时传递给它path,于是它的执行就等同于执行下面的代码:
simpleURL("http", siteDomain, path);
Item 30:prototype,getPrototypeOf,和__proto__
这三个东西都与原型有关。
见代码:
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", "0ef33ae791068ec64b502d6cb0191387");
User()被设计成通过new调用,新生成的对象的原型属性将指向User.prototype。访问属性首先会在u对象自身上查找,比如name,如果没有找到,就在它的原型上查找,比如对toString()的访问。
ES5提出一个新方法Object.getPrototypeOf(),它可以用来返回一个对象的原型属性:
Object.getPrototypeOf(u) === User.prototype; // true
而__proto__只是一些浏览器提供的非标准支持。
u.__proto__ === User.prototype; // true
上面的User常常被一些程序员理解为类,然而其实它的实例所共有的方法是通过User.prototype来共享的。
Item 31:有Object.getPrototypeOf的时候,就不要用__proto__
Object.getPrototypeOf是从ES5开始引进的,而在这之前,很多浏览器支持__proto__已经有一段时间了。但是由于__proto__不是ECMAScript标准里的东西,不同浏览器对它的实现各有不同。
比如下面的代码:
var empty = Object.create(null);
"__proto__" in empty;
在不同浏览器后面那句输出的结果可能会不同,有些会是true,有些会是false,总之尽量不要使用这个属性。但是在Object.getPrototypeOf没有被支持的情况下,可以用如下代码通过__proto__给它一个补丁:
if (typeof Object.getPrototypeOf === "undefined") {
Object.getPrototypeOf = function (obj) {
var t = typeof obj;
if (!obj || (t !== "object" && t !== "function")) {
throw new TypeError("not an object");
}
return obj.__proto__;
};
}
注:^(* ̄(oo) ̄)^__proto__据说污染了所有对象,具体细节不详,参见Item45。
Item 32:永远不要修改__proto__
能通过__proto__直接修改原型貌似是一件很强大的功能,但是作者建议不要这么做,原因有三:
- 不能移植到不支持它的环境里;
- 影响了引擎对于效率的优化,使这些优化无效;
- 从维护的角度讲,修改原型等于修改了整个继承关系,这样做不利于维护。
Item 33:让构造函数对于关键字new无差别化
这个问题是,当你写一个构造函数时,如果你忘记了用new来实例化对象,那么结果会是你完全意料不到的,所以最稳妥的做法是让你的构造函数在没有使用new关键字调用的情况下返回的值和使用new关键字时是一样的。
先看个基本的构造函数的声明:
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
没有通过new调用的时候如下:
var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
u; // undefined
this.name; // "baravelli"
this.passwordHash; // "d8b74df393528d51cd19980ae0aa028e"
实际上发生的是什么呢?咱们来捋一捋。首先User()的接受者会是全局对象,于是全局对象上会被创建两个属性name和passwordHash。然后这个函数不返回任何值,所以u会变成undefined。
在严禁模式下,调用User函数会直接导致一个错误:
function User(name, passwordHash) {
"use strict";
this.name = name;
this.passwordHash = passwordHash;
}
var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
因为在严禁模式下,函数的接受者会是undefined。
如何调整下User()的定义来解决这个问题呢?
function User(name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}
var x = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
var y = new User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
x instanceof User; // true
y instanceof User; // true
或者借用ES5的Object.create():
function User(name, passwordHash) {
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.passwordHash = passwordHash;
return self;
}
在不支持ES5的环境下可以给Object.create()打补丁:
if (typeof Object.create === "undefined") {
Object.create = function (prototype) {
function C() {}
C.prototype = prototype;
return new C();
};
}
- 在没有使用new调用函数时,其接受者是全局对象,在严禁模式下,接受者是undefined;
- 在构造函数内,可以显式地返回其它对象,而新创建的对象将会被抛弃;
Item 34:通过原型共享方法
简单讲,下面的代码:
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 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;
};
因为方法没有没重复定义。
Item 35:用闭包隐藏数据
参考如下代码:
function User(name, passwordHash) {
this.toString = function () {
return "[User " + name + "]";
};
this.checkPassword = function (password) {
return hash(password) === passwordHash;
};
}
这种情况下,通过User的实例将访问不到name和passwordHash,而只有User的方法可以访问这两个数据,所以它们达到了私有数据的要求。
Item 36:与实例的状态相关的数据要保存在实例上面,而不是保存在原型上被共享
Item 37:识别this的隐式绑定(看不懂)
Item 38:在子类的构造函数里呼叫父类的构造函数
为了解释这个问题,作者使用的例子过于复杂了,理解这个例子都要花费些时间,我就不粘贴代码了,我只是直接总结下作者的观点好了。
首先就是,有些人受到一些过时做法的影响,可能会以下面的代码来建立继承:
Subclass.prototype = new Superclass();
这个做法有问题,就是假如Superclass要通过构造函数给一些属性初始化值,在建立继承的时候这些值都还并不确定。这个问题Dmitry在他的博客里提到过。
- 所以,作者的意思是不要在建立继承关系的时候调用父类构造函数,而是在子类的构造函数里调用:
function Subclass(p1, p2, p3) { Superclass.call(this, p1, p2, p3); this.4 = 0; }
- 另外,通过Object.create()来建立继承关系:
Subclass.prototype = Object.create(Superclass.prototype);
Item 39:永远不要在子类上重复使用父类的属性名
作者又继续使用了个复杂的案例来解释这个其实非常简单的事情。
有些时候,父类上面的一些属性由于命名规范或者其他从设计层面做的手脚会让人觉得它们是私有的,其实在JS里没有私有变量一说,所以当你设计子类的时候,要确保没有使用任何父类上的属性名。
Item 40:不要扩展JavaScript的标准类
JS里有一些內建的类,比如Array,Function什么的。在JS里扩展这些类并不会达到预期的效果。作者以Array为例子解释了这个问题:
function Dir(path, entries) {
this.path = path;
for (var i = 0, n = entries.length; i < n; i++) {
this[i] = entries[i];
}
}
Dir.prototype = Object.create(Array.prototype); // extends Array
var dir = new Dir("/tmp/mysite", ["index.html", "script.js", "style.css"]);
dir.length; // 0
这段代码并不会向预期那样工作,原因很简单,JS的对象都有个內部属性叫[[class]],通过Array构造函数或者[]创建的对象的[[class]]的值会是array,它的行为由这个属性来决定,而上面代码里创建的Dir对象的[[class]]只是object。
这个属性值标注的是一个对象被创建的构造函数,包括:Array,Boolean,Date, Error, Function, JSON, Math, Number, Object, RegExp, String。
Object.prototype.toString.call(dir); // [Object object]
Item 41:把原型理解为实现的细节
- 对象是接口,而原型上面保存着实现的细节;
- 避免过度研究原型链上的实现细节,如果不是你创建的原型上的实现。
Item 42:避免草率的猴子补丁
- 修改內建对象的原型来给其增加功能的做法被称为猴子补丁,这个做法不太好,可能导致不同实现之间冲突;
- 可是可以通过猴子补丁来给未支持的标准函数打补丁。
Item 43:用简单的对象实现轻量级的字典
重点如下:
- 别去继承内建类比如Array;
- 尽量让对象的原型为空,如果做不到,那就尽量别改OBject.prototype。
- 因为,for..in会返回的属性包括对象自身和原型链上的所有属性。
Item 44:使用空的原型属性来避免原型污染
var o = new C();
Object.getPrototypeOf(o) === null; // false
Object.getPrototypeOf(o) === Object.prototype; // true
var x = Object.create(null);
Object.getPrototypeOf(o) === null; // true
var x = { __proto__: null };
x instanceof Object;
Item 45:使用hasOwnProperty来避免原型污染