第二章 语法
1.少用块注释
块注释中,像 ‘/‘ 和 ‘/‘ 这样的字符对也可能出现在正则表达式字面量中,所以对于被注释的代码来说不安全。
2.作者提倡用字母开头的标识符,提倡标识符只使用字母,数字和下划线。
其实有待商议吧,因为jQuery的美元符运用得很成功,也比较简便。美元符和下划线其实是可以用在开头的。还有些工具库是用下划线来作为标识符的。
3.字符串相等
包含相同字符且字符顺序相同的字符串对象被认为是相等的, ‘a’ + ‘b’ === ‘ab’
4.代码块
Js的代码块不会创建新作用域,因此要把变量定义放在函数头部,而不是代码块中。
5.空字符串为false,但仅包含空格的字符串为true。
6.使用for…in枚举对象属性名时,应该使用object.hasOwnProperty(variable)来确定一个属性是该对象的成员,还是来自于原型链。
7.break语句可以指定一个标签来退出相应的语句。
第三章 对象
对象通过引用来传递,同一个对象不会被复制。
1.js具有原型链特性,允许对象继承另一个对象的属性,正确使用可以减少对象初始化的时间和内存。
2.对象属性
对象的属性名可以用一个符合命名规则的标识符来命名,也可以使用字符串。如果使用字符串来命名一个对象的属性,则不要求这个字符串要符合命名规则,但是在使用这个属性时不能使用’.’运算符,只能使用obj[‘属性名字符串’]的方法来使用。
3.使用’||’来设置默认值。使用’&&’来避免因从未定义的成员属性中取值而产生的TypeError异常,这时整个表达式的值将会是想要的值或者 undefined。
4.所有通过对象字面量创建的对象都连接到 Object.prototype。
5.根据对象原型创建对象。
可以给Object原型添加一个create方法,可以依照参数中的对象,生成另一个对象。
对新对象的更新不会影响原对象(当然应该如此…),原型只有在检索值的时候才会被用到,比如检索一个新对象中没有的属性时,会顺着原型链寻找这个属性,直到返回某个原型中的该属性值或者undefined。
原型关系是动态的,添加新属性到原型中会立刻对基于该原型创建的对象可见。
给Object增加一个新方法,它使用一个对象作为原型,创建新对象。
if (typeof Object.create!== 'function') {
Object.create= function(o) {
var f = function() {};
f.prototype = o;
return new f();
};
}
在es5中,新增了Object.create()用于原型式继承。
6.反射(检查对象属性)
原型链中所有属性都会产生返回值,所以在检索一个对象中自身具有的属性时,有两个方法:
(1)抛弃类型为”function”的属性。
(2)用hasOwnProperty检查该属性。这个方法不会检查原型链。
delete可以用来删除对象属性。
7.减少全局变量污染
只创建一个唯一的全局变量var MYAPP = {};
然后通过
MYAPP.val1 = {…}
MYAPP.val2 = {…}
这样的方式来定义变量。
这不是最好的方法, 后面还有更详尽的方法。
第四章 函数
1.函数对象
对象字面量产生的对象连接到 Object.prototype;
函数对象连接到Function.prototype(该原型对象本身也连接到Object.prototype)
2.闭包
通过函数字面量(Function literal)创建的函数对象包含一个连接到外部上下文的连接,这就被称为闭包
3.附加参数
每个函数除了形参,还有this和arguments两个参数。this的值根据调用模式而定。
4.this的值
(1)方法调用模式
函数作为对象的一个属性,用’.’调用时,就是方法调用模式,这时this就相当于所属对象,可以对所属对象进行取值或修改。
(2)函数调用模式
函数不是一个对象的属性时,就是函数调用模式,此时this被绑定到全局对象。
比如:
var obj = {
val: 1,
inci: function () {
console.log('inci this: ' + this);
function helper() {
console.log('helper this: ' + this);
}
helper();
}
}
obj.inci();
输出:
mrldeMBP:frontend mrl$ node testfunction.js
inci this: [object Object]
helper this: [object global]
调用helper时,就是函数调用模式,绑定的是全局对象。
解决:
var obj = {
val: 1,
inci: function () {
console.log('inci this: ' + this);
var that = this;
function helper() {
console.log('helper this: ' + this);
console.log('helper that: ' + that);
}
helper();
}
}
obj.inci();
输出:
mrldeMBP:frontend mrl$ node testfunction.js
inci this: [object Object]
helper this: [object global]
helper that: [object Object]
(3)构造器调用模式
new 语句调用一个函数,将会以该函数为原型创建一个对象,此时,this会绑定到这个新创建的对象。
以此为目的创建的函数称为构造器函数,保存它的变量的名称应该采用大写格式,这样有利于区分构造器函数,因为如果调用构造器函数时不加上new可能会发生一些意外。
构造器函数有更好的风格可以采用,后面会说。
(4)apply调用模式
某函数.apply(另一个对象, [参数数组])
apply前一个参数用于指定this所绑定的对象,第二个参数是要调用的函数所接收的参数值数组。
其实,call方法也许原理也类似?不过,call方法第二个往后的参数不是参数值数组,而是分开写的参数。
apply方法可以直接把当前函数的arguments传过去?
5.参数
arguments 可以获取所有传递到当前函数的参数值,哪怕比定义的形参多出来的参数。arguments 并不是真正的数组对象,它不具有数组的方法。
6.异常捕获
抛出异常对象时,可以指定异常对象的属性,通常包括name和message,也可以包括其他属性。
throw {
name: ’TypeError’,
message: ‘add needs numbers’
}
7.扩充类型的功能
可以用这种方法给原型扩充一个添加方法的方法:
Function.prototype.method = function (name, func) {
// 类库混用时,要确定没有该方法的时候才添加
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
示例:
(1)为数字扩充一个取整方法
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
Number.method('integer', function () {
return Math[this < 0 ? 'ceil' : 'floor'](this);
});
var f1 = -1.22;
var f2 = 1.33;
console.log(f1.integer());
console.log(f2.integer());
输出:
mrldeMBP:frontend mrl$ node testfunction
-1
1
(2)为字符串对象扩充一个去除首尾字符串的trim方法。
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
String.method('trim', function () {
return this.replace(/^\s+|\s+$/g, '');
});
var s1 = ' A string. ';
console.log('Original s1: ' + '"' + s1 + '"');
console.log('Trimed s1: ' + '"' + s1.trim() + '"');
输出:
mrldeMBP:frontend mrl$ node testfunction
Original s1: " A string. "
Trimed s1: "A string."
mrldeMBP:frontend mrl$
8.递归
js没有尾递归优化,所以在某些深度递归可能会因为堆栈溢出而运行失败。
但可以自己创建带有尾递归优化的函数。
尾递归,在递归函数结束时返回自身调用的结果。
9.闭包
(1)更好的构造器
创建一个构造器,它返回的对象中只带有getXXX、setXXX等方法,避免将变量暴露在外,这样才更有意义。
(2)循环中的作用域
参考For循环中创建使用外部变量i的函数的糟糕例子。
正确的方法是避免在for循环中创建函数,在for以外创建函数,然后在循环中调用那个外部函数。
1)糟糕的例子:
var testFor = function (nodes) {
for (var i = 0; i < 3; i++) {
nodes[i].h = function () {
console.log(i);
}
}
}
nodes = [{}, {}, {}];
testFor(nodes);
for (var i = 0; i < 3; i++) {
nodes[i].h();
}
输出:
mrldeMBP:frontend mrl$ node testfunction
3
3
3
2)改良的例子:
var testFor = function (nodes) {
var helper = function (i) {
return function () {
console.log(i);
}
}
var i;
for (i = 0; i < 3; i++) {
nodes[i].h = helper(i);
}
}
nodes = [{}, {}, {}];
testFor(nodes);
for (var i = 0; i < 3; i++) {
nodes[i].h();
}
输出:
mrldeMBP:frontend mrl$ node testfunction
0
1
2
3)使用es6的let语法
let 使变量的作用域只在自己所在块之内,所以不存在1)中取循环外变量值的问题
var testFor = function (nodes) {
for (let i = 0; i < 3; i++) {
nodes[i].h = function () {
console.log(i);
}
}
}
nodes = [{}, {}, {}];
testFor(nodes);
for (var i = 0; i < 3; i++) {
nodes[i].h();
}
输出:
mrldeMBP:frontend mrl$ node testfunction
0
1
2
10.模块
利用作用域和闭包的特点,创建提供接口但隐藏状态及实现的函数或对象。
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
String.method('deentityify', function () {
var entity = {
quot: '"',
lt: '<',
gt: '>'
};
return function () {
return this.replace(/&([^&;]+);/g, function (a, b) {
var r = entity[b];
return typeof r === 'string' ? r : a;
});
};
}());
console.log('haha < haha'.deentityify());
字符串replace方法中,第二个参数可以是一个返回替换字符串的函数,这个函数第一个参数是匹配模式的字符串,后面是子表达式匹配的字符串。
但是,以上这种方法为什么比以下这种方法要好?
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
String.method('deentityify', function () {
var entity = {
quot: '"',
lt: '<',
gt: '>'
};
return this.replace(/&([^&;]+);/g, function (a, b) {
console.log('a: "' + a + '", b: "' + b + '"');
var r = entity[b];
return typeof r === 'string' ? r : a;
});
});
console.log('haha < haha'.deentityify());
首先要注意到,第一种方法中,为String添加deentityify方法时,使用了自运行的函数,该函数里定义了一个entity对象集合,然后是返回了一个函数,在该返回函数中才对字符串进行替换处理。
不同之处正是这里。后面的方法虽然也能达到同样的效果,但是由于每次都会对entity进行赋值初始化等操作,将会多耗费一点时间。而第一种方法,只初始化一次entity,而由于其后的返回函数中要用到这个变量,所以这个变量初始化一次后,将会一直存在,其后使用deentityify方法时,不会再对entity对象执行赋值操作,提高了一点效率。如果是一些比较复杂的赋值初始化操作,较少无意义的重复操作应该能节省不少时间。
模块模式有利于程序封装和构造单例对象(根据字面量创建,声明周期内不发生变化),还可以构造安全的对象。
11.级联
对于设置或修改对象状态却不返回值的方法,可以返回this而不是undefined,便于在一条语句中依次调用同一个对象的多个方法。(类似jQuery的链式调用)
12.柯里化
13.记忆
利用作用域和自运行函数,创建具有记忆功能的函数,减少递归调用的运算量。
典例,斐波那契数列的计算。
可推广的带记忆功能的函数的构造,只需提供需要记忆的数组初值和带有递归计算式的函数即可构造带有记忆功能的函数。
var memorizer = function (memo, formula) {
var recur = function (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = formula(recur, n);
memo[n] = result;
}
return result;
};
return recur;
};
var fibonacci = memorizer([0, 1], function (recur, n) {
return recur(n - 1) + recur(n - 2);
});
for (let i = 0; i < 10; i++) {
console.log(fibonacci(i));
}
[0 1]自传入memorizer构造器后便一直存在,可重复利用。
第五章 继承
1.伪类
js的原型机制和类有些许复杂。
一个函数对象被创建时,新的函数对象有一个prototype属性,这个属性的值是一个包含constructor属性的对象。通过prototype可以扩充一个构造器的原型,比如给一个构造器函数增加一个方法。
通过prototype还可以指定构造器函数的原型, 如
var Cat = function(){…}
Cat.prototype = new AnothCostrc();
但这不是十分理想的继承方式,可以通过给Function原型添加inherits方法,用以确定继承的父类。
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
Function.method('inherits', function (Parent) {
this.prototype = new Parent();
return this;
})
两个方法都返回this,可以用级联的方式编程。
构造器首字母大写的习惯应该被记住。
非常值得注意的地方是new语句和对象属性的使用。没有new,将导致this的绑定错误,但这是没有错误提示的。这样创建的对象属性是公开的,也没办法访问父类的方法。所以,js的类构造器函数仅仅是提供了一种类似于基于类的语言的使用方法,但是并不彻底,也稍显杂乱。
2.对象说明符
为了便于构造新对象,可以利用对象说明符,因为这样就不必记住繁杂的参数的顺序:
var myObject = maker({
first: f,
middle: m,
last: l,
})
3.原型(式继承)
利用对象字面量构造出一个基本对象parent,然后可以用第三章提到的create方法,构造出更多基于parent的对象。由于原型链的作用,这样构造出来的对象是可以完全利用parent对象的资源的。但是也要注意,这样构造出来的对象只是空对象,如果在未对它进行重新初始化前,修改某个引用对象值,那么由于原型链的作用,parent中的属性将会被改变。
比如:
var Parent = {
name: "Jim",
sex: "male",
hobby: ["sports"],
children:{son:"Jack",daughter:"Lily"},
getName : function(){
return this.name;
}
}
var c = clone(Parent);
c.hobby.push("game");
c.children.son = "James";
console.log(Parent.hobby); //["sports","game"]
console.log(Parent.children.son); //"James"
Parent被修改了。所以,当要利用的属性是一个对象或者其他类型的对象时,应当注意不要修改了原型。
‘new’ 关键字类似于类式继承。而Object.create则更像是原型式继承。
4.函数化构造器
伪代码模板:
// 参数是可选的, my 应该是一个从它那里继承属性的对象
var constructor = function (spec, my) {
var that = {},
priVal1; // 其他的私有实例变量
my = my || {};
// 添加公有变量
my.attr1 = spec && spec.attr1;
my.attr2 = spec && spec.attr2;
// 根据my对象利用适当的方法创建一个新对象,
// 如字面量构造、new前缀调用构造器函数、
// 前面第三章介绍的Object.create和现在这种函数化构建对象等等
that = `一个新对象`;
// 设置私有变量
...
// 添加获取私有变量的getter方法
that.getPriVal1 = function () {
return priVal1;
}
// 设置其他添加给that的特权方法
…
var func1 = function () {…}
that.func1 = func1; // 分两步定义一个对象方法,这个构建函数内部的方法可以调用func1,可以最大限度减少因为that.func1的破坏而导致的潜在影响
// 返回新对象
return that;
}
可以如下添加一个访问父类方法的对象原型方法,用that.superior(‘父类对象方法名’)来调用父类的方法。
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}
Object.method('superior', function (name) {
var that = this,
method = that[name];
return function () {
return method.apply(that, arguments);
}
})
var parObj = function () {
var that = {},
name = 'par';
var getName = function () {
return name;
}
that.getName = getName;
return that;
}
var par = parObj();
console.log(par.getName());
var childObj = function () {
var that = parObj();
var superGetName = that.superior('getName');
var getName = function () {
return superGetName() + "'s child."
};
that.getName = getName;
return that;
}
var child = childObj();
console.log(child.getName());
如果对象的所有状态都是私有的,那么该对象就成为一个防伪(tamper-proof)对象,对象的完整性不会受到破坏。
持久性对象,是使用函数化方法创建的对象,且该对象所有方法都不使用this或that。
5.部件
从一套部件中把对象组装出来。例子,构造一个给任何对象添加简单事件处理特性的函数,它有fire、on和事件注册表对象。
var eventuality = function (that) {
var registry = {};
that.fire = function (event) {
// 在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串
// 或者是一个拥有包含事件名称的 type 属性的对象。
// 通过'on'方法注册的事件处理程序中匹配事件名称的函数将被调用。
var array,
func,
handler,
i,
type = typeof event === 'string' ? event : event.type;
if (registry.hasOwnProperty(type)) {
array = registry[type];
for (i = 0; i < array.length; i += 1) {
handler = array[i];
// 每个处理程序包含一个方法和一组可选的参数。
// 如果该方法是一个字符串形式的名字,那么寻找到该函数。
func = handler.method;
if (typeof func === 'string') {
func = this[func];
}
// 调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象。
func.apply(this, handler.parameters || [event]);
}
}
return this;
};
that.on = function (type, method, parameters) {
// 注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中,
// 如果这种类型的事件还不存在,就构造一个。
var handler = {
method: method,
parameters: parameters
};
if (registry.hasOwnProperty(type)) {
registry[type].push(handler);
} else {
registry[type] = [handler];
}
return this;
};
return that;
};
第六章 数组
真正的数组是一种性能优良的数据结构,但是js中的数组只是拥有类数组特性的对象,它比较慢,但是使用起来更加方便。它只不过多了一个可以用整数作为属性名的特性。
1.长度
长度与元素个数不一定相等。数组不会下标越界,var a = [], a[10] = 1,那么a的长度是11,但只有一个元素。
方括号后置运算符将方括号内的表达式转换为字符串,这个字符串被用作属性名。如 var a = [],a[‘att’] = ‘v’。
可直接设置length,设置更大的length不会分配更多空间给数组,但设置更小的length将使第length位之后的元素被删去。
length只计算属性名为整数的元素个数。
2.删除
delete可以删去数组元素,但会留下一个占位空洞,其后的元素没有前移。
用splice方法可以更好地删除一个数组元素,anArr.splice(2, 1)删除anArr[2]起的一个元素。但对于大型数组来说效率可能不高。
3.容易混淆的地方
typeof 不能区分数组和对象,都是object。
弥补缺陷:
var isArray = function (value) {
return Object.prototype.toString.apply(value) === "[object Array]”;
};
鉴别 value 是否为数组,且可以鉴别来自不同 window或frame 中构造的数组对象。
4.指定初始值
为数组对象增加指定初始值的方法,以免因数组元素不存在而出现undefined错误。
Array.dim = function (dimension, initial) {
var a = [];
for (var I = 0; i < dimension; i++ ) {
a[I] = initial;
}
return a;
};
可以类似地构造构造一个二维数组,但是在初始化内部每一个一维数组时不能用Array.dim(n, []),这样每一个元素都将引用同一个一维数组。
可以设置两个循环来初始化一个二维数组。
第七章 正则表达式
1.一个例子
捕获型和非捕获型分组(有?:前缀)。前者会有性能上的损失。
2.结构
字面量创建、构造器函数创建。
三种标识。
正则对象的几个属性。
同样字面量构建的正则表达式共享一个单例? 貌似在chrome上测试并不是同一个单例。
3.元素
已被匹配的子串不会再被下一个子匹配式匹配。
4.正则表达式的转义
\1表示指向分组1的捕获文本的引用
5.量词
量词后加上后缀 ?
表示非贪婪匹配。
第八章 方法
这章讲各种对象的一些原生方法。
(一)数组部分
1.anArr.join()与 + 运算符连接字符串问题。
作者写书的时候,IE6/7比较流行,那时候join方法性能比较好。现在经过优化后,+ 运算符性能已经比join要好了。
2.anArr.shift()方法移除数组的第一个元素并将它返回。
3.anArr.slice(a,b)方法中,会复制数组第a个元素开始,复制到 anArr[b-1] 作为数组返回。参数如果是负数,那么将会取它与数组长度之和,如
var a = [1,2,3]
a.slice(0,2)和a.slice(-3,2) 和a.slice(0,-1)都返回[1, 2]。
B可选,没有它将返回a到最后一个元素的复制的数组。
4.anArr.sort方法
将数组中的元素进行排序。可以自定义比较函数,比较函数fn接受两个参数,若第一个参数等于第二个,返回0,若第一个排在第二个前面,返回负数,若第二个排在第一个前,返回正数。
arr.sort(fn)。
5.anArr.splice()方法
6.anArr.unshift()方法
将元素添加到数组开始部分,返回新长度。
(二)函数部分
apply、call
(三)数字部分
1.toExponential(x),把数字转换为指数形式的字符串,x是小数点位数,范围是0-20,可选
2.toFixed(x), 将数字转换为十进制数字,x是小数点位数,范围是0-20,可选
3.toPrecision(x),将number转换为十进制形式的字符串,x控制数字精度,范围0-21,可选
4.toString(x),将数字转换为字符串,x控制进制,默认是10,范围2-36,可选
(四)对象
hasOwnProperty()
(五)RegExp
exec比test慢。
lastIndex的运用。
(六)字符串
charAt、charCodeAt、concat、indexOf、lastIndexOf、localeCompare、match,
match中正则式带g则返回匹配子串数组,不带g则与reg.exec(str)结果相同
replace, 普通的替代字符串和正则式只替换匹配到的第一个字符串,带g修饰符的正则式可以替换全部匹配的字符串。$的意义。
Search。
fromCharCode()方法根据一串字符编码返回一个字符串。
第九章 代码风格
作者的风格、坚持{}括住代码块、if后加一个空格。
第十章 优美的特性
作者设想的精简的js的精华。
附录A 毒瘤
1.全局变量
所有全局变量都存放在全局对象window中
var foo = 1;
window.foo == 1;
2.作用域
函数开头声明变量。
3.自动插入分号
js的自动修复机制可能会干坏事:
function testR() {
return
{
status: 1
}
}
console.log(testR());
看起来会输出1,但是因为js在return后自动加入了分号’;’,所以输出了 undefined
。
将返回对象的花括号放在上一行return后面可以解决这个问题。
4.保留字
括号表示法可以使用保留字作为对象的属性名,但不是推荐的做法。
5.Unicode
一对字符在js和Unicode中的含义不同。
6.typeof
null是对象
7.parseInt
最好在parseInt时加上表示进制的参数。
大概是因为某些浏览器对于字符串中数字的进制判断有问题,可能会采用10进制以外的进制进行转换。
经过测试,chrome68和node v8.9.3都没有书中提到的’08’和’09’转换为0的问题。
8.浮点数
浮点数运算有精度问题,可以通过*100之后运算最后/100等操作来提高精度。
9.NaN
检查是否为数字:
var isNumber = function isNumber(value) {
return typeof value === ‘number’ && isFinite(value);
}
10.伪数组
数组与对象都是对象,最好检查原型toString之后的字符串来确定是不是数组。
11.假值(指逻辑上的假)
有时候,==的强制类型转换可能会使假值满足判断条件。
var value;
value == null // true
value === null // false
12.hasOwnProperty
这是个方法,不是运算符,所以可能被替换,导致判断出错。
13.对象
注意原型链上可能存在着一个对象本不期待出现的属性。
附录B 糟粕
有问题的特性
1.==
强制类型转换导致出现了一些传递性问题。
2.with
用以快捷访问对象属性,但运行效果难以确定。
3.eval
降低性能、减弱安全性。Function构造器也如此。还有定时器要尽量避免使用字符串参数。
4.continue
降低性能。
5.switch穿越
从一个case条件跳到下一个case条件。容易出错。
6.缺少块的语句
要严格规范
7.++—
8.位运算符
性能问题
9.function语句对比function表达式
function语句解析提升,放宽了先声明后使用的要求。
坚持 var foo = function foo() {}。
(function() {
var hidden_variable;
// 尽管可能对环境有影响,但不引入全局变量
} ());
10.类型的包装对象
少用new Object、new Array这样的操作,可用{}、[]代替。
11.new
少用new。
12.void
接受运算数并返回undefined,容易造成困惑。
附录C JSLint
1.未定义的变量和函数
避免因隐式的全局变量声明,减少错误。JSLint可以读取/*global va1,… /这样的注释,辨别拼写错误和被遗忘的全局变量,还可以模拟浏览器环境。
2.成员属性
。。。
附录D 语法图
附录E JSON
1.安全问题
JSON.parse取代eval。
不要直接把从服务器获取的文本放到节点的innerHTML中,以免把恶意脚本插入到文档里面,恶意脚本可以读取全部全局变量,接触到用户能看到的所有信息。