第一部分 准入训练
第1章 进入忍者世界
js开发人员通常使用js库来实现通用和可重用的功能。这些库需要简单易用,产生最少的开销,并能兼容所有浏览器。本书研究创建这些流行js库所使用的技术。
1.1 即将探索的js库
jquery,prototype,yui,base2
一个js库的组成可以分为如下三个方面:
1)js语言的高级使用
2)跨浏览器代码的精心构建
3)当前能够聚众合一的最佳实践应用。
1.2 理解js语言
对象、函数和闭包之间有着很密切的关系。
定时器和正则表达式远未被充分利用。
with语句和eval()方法,两个重要但是具有争议的语言特性。
1.3跨浏览器注意事项
分级浏览器支持,创建一个浏览器支持矩阵,作为记录浏览器和其平台的重要性快照。
由于支持大部分的平台和浏览器是不切实际的,因此我们必须权衡支持各种浏览器的成本和收益。
1)目标受众的期望和需求。
2)浏览器的市场份额。
3)支持该浏览器所需的工作量。
1.4 当前最佳实践
1)测试
将使用的一个主要工具是assert()函数,其目的是断言代码是true还是false。
assert(condition,message)
assert(a==1,'disaster! a is not 1!');
2)性能分析
start = new Date().getTime();
for(var n=0; n
/**/
}
elapsed = new Date().getTime - start;
assert(true,'measured time:'+elapsed);
3)调试技巧
第2章 利用测试和调试武装自己
2.1调试代码
1)日志记录
console.log()方法
代码2.1 一个适用于所有现代浏览器的简单日志记录方法
function log(){
try{//尝试使用最常见的方法记录信息
console.log.apply(console,arguments);
}
catch(e){
try{//尝试使用opera方式记录日志
opera.postError.apply(opera,arguments);
}
catch(e){//如果都不行,则使用alert()函数
alert(Array.prototype.join.call(arguments,' '))
}
}
}
2)断点
它能在特定的代码上暂停脚本的执行,从而暂停浏览器运行。这使得我们可以在该断点处,随意查看任意代码的状态。其中包括所有可访问的变量、上下文以及作用域链。
2.2 测试用例生成
优秀的测试用例具有三个重要特征:
可重用性——测试结果应该是高度可再生的。多次运行应产生相同结果。
简单性——测试应该只专注于测试一件事。
独立性——测试用例应该独立执行。把测试分解成尽可能小的单元。
构建测试的方法:
解构型测试用例——在消弱代码隔离问题时进行创建,以消除任何不恰当的问题。
构建型测试用例——从一个大家熟知的良好精简场景开始,构建用例,直到我们能够重现bug为止。
JS Bin(http://jsbin.com/),它是一个用于构建测试的简单工具,可以生成一个唯一的url地址,可以引用一些受欢迎的js库的副本。
2.3 测试框架
测试框架功能包括:
.显示测试的结果,以便很容易地确定哪些测试是通过的,哪些是失败的
.能够模拟浏览器行为(单击按键等)
.测试的交互式控制(暂停和恢复测试)
.处理异步测试超时问题
.能够过滤哪些会被执行的测试。
1)QUnit
为单元测试提供一个简单的解决方案,提供最小但却易于使用的API。
特点:
简洁的API
支持异步测试
不限于jquery或使用jquery的代码
特别适合于回归测试。
http://qunitjs.com
2)YUI Test
提供了大量的特性和功能,以确保覆盖代码库所需要的任何单元测试用例。
特点:
广泛和全面的单元测试功能
支持异步测试
良好的事情仿真
http://developer.yahoo.com/yui/3/test/
3)JsUnit
比较古老
http://www.jsunit.net
4)新出的单元测试框架
JUnithttp://pivotallabs.com/what/mobile/overview
testswarmhttps://github.com/jquery/testswarm/wiki
2.4 测试套件基础知识
1)断言
单元测试框架的核心是断言方法,通常叫assert()。
示例:js断言的一个简单实现
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
window.onload = function(){
assert(true,'the test suite is running.');
assert(false,'fail!');
}
2)测试组
简单的断言是很有用的,但真正发力,却是在测试上下文中将它们组合在一起形成测试组的时候。
示例:测试分组的实现
test suite
#results .pass{color:green;}
#results .fail{color:red;}
(function(){
var results;console.log(this)
this.assert = function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
results.appendChild(li);
if(!value){
li.parentNode.parentNode.className = 'fail';
}
return li;
};
this.test = function test(name,fn){
results = document.getElementById('results');
results = assert(true,name).appendChild(document.createElement('ul'));
fn();
}
})()
window.onload = function(){
test('A test.',function(){
assert(true,'first assertion completed');
assert(true,'second assertion completed');
assert(true,'third assertion completed');
});
test('Another test.',function(){
assert(true,'first test completed');
assert(false,'second test failed');
assert(true,'third assertion completed');
});
test('A third test.',function(){
assert(null,'fail');
assert(5,'pass')
})
}
3)异步测试
异步测试有一个问题是测试的结果在一段不确定的时间后返回,例如ajax请求和动画。处理该问题的方式通常是过度设计,并且设计的要比实际需要的更复杂,以下步骤:
a 将依赖相同异步操作的断言组合成一个统一的测试组
b 每个测试组需要放在一个队列上,在先前其他的测试组完成运行之后再运行。
每个测试组必须能够异步运行。
示例:简单的异步测试套件
test suite
#results .pass{color:green;}
#results .fail{color:red;}
(function(){
var queue = [],paused = false, results;
this.test = function(name,fn){
queue.push(function(){
results = document.getElementById('results');
results = assert(true,name).appendChild(document.createElement('ul'));
fn();
});
runTest();
};
this.pause = function(){
paused = true;
};
this.resume = function(){
paused = false;
setTimeout(runTest,1);
};
function runTest(){
if(!paused && queue.length){
queue.shift()();
if(!paused){
resume();
}
}
}
this.assert = function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass':'fail';
li.appendChild(document.createTextNode(desc));
results.appendChild(li);
if(!value){
li.parentNode.parentNode.className = 'fail';
}
return li;
}
})()
window.onload = function(){
test('async test #1',function(){
pause();
setTimeout(function(){
assert(true,'first test completed');
resume();
},1000);
});
test('async test #2',function(){
pause();
setTimeout(function(){
assert(true,'second test completed');
resume();
},1000)
});
test('async test #3',function(){
pause();
setTimeout(function(){
assert(false,'fail');
resume();
},1000)
});
};
第二部分 见习训练
第3章 函数是根基
js是一门函数式语言。
在js中,函数是第一型对象,函数可以共处,可以将其视为其他任意类型的js对象。
示例:
values.sort(function(value1,value2){return value2-value1});
3.1函数的独特之处
3.1.1 js的函数式特性为何如此重要
函数是代码执行的主要模块化单元。本书为页面编写的所有代码都将在单独的函数内。
1)函数是第一型对象
对象在js中有如下功能:
.它们可以通过字面量进行创建
.它们可以赋值给变量、数组或其他对象的属性。
.它们可以作为函数的返回值进行返回。
.它们可以拥有动态创建并赋值的属性。
在js中,函数拥有全部这些功能。
函数还有一个特殊的功能,它们可以被调用。
这些调用,通常是以异步方式进行调用。
2)浏览器的事件轮询
桌面应用编程是采用如下方式:
.创建用户界面
.进入轮询,等待事件触发
.调用事件的处理程序(侦听器)
浏览器编程唯一的不同是,代码不负责事件轮询和事件派发,而是浏览器帮我们处理。
我们的职责是为浏览器中发生的各种事件建立事件的处理程序。这些事件在触发时被放置在一个事件队列中,然后浏览器将调用已经为这些事件建立好的处理程序。
因为这些事件发生的时间和顺序都是不可预知的,所以事件处理函数的调用也是异步的。
以下类型的事件都有可能互相穿插发生:
浏览器事件,如当一个页面完成加载或卸载的时候。
网络事件,如响应ajax请求
用户事件,如鼠标单击、鼠标移动或者按键。
计时器事件,如超时或计时器触发。
示例:
function startup(){
/**/
}
window.onload = startup;
非侵入式javascript
将脚本从文档标记中分离出来。
浏览器的事件轮询是单线程的。每个事件都是按照在队列中所放置的顺序来处理的。这就是FIFO(先进先出)列表。每个事件都在自己的生命周期内进行处理,所有其他事件必须等到这个事件处理结束以后才能继续处理。在任何情况下,单线程都不能同时执行两个处理程序。
3)回调概念
我们定义一个函数,以便其他一些代码在适当的时机回头再调用它。
示例:将函数作为参数传递给另一个函数,并随后对该参数进行调用
function useless(callback){return callback()}
3.1.2 使用比较器进行排序
示例:
var values=[213,16,2058,64,10,1965,57,9];
values.sort();
降序排列
value.sort(function(value1,value2){ return value2-value1});
不管函数是如何声明的,它们是可以作为值进行引用的,并且也可以作为基础的构件块在代码库中进行重用。
3.2 函数声明
js函数是使用函数字面量进行声明从而创建函数值的,就像使用数字字面量创建数字值一样。
函数字面量由四个部分组成:
1. function关键字
2. 可选名称,如果指定名称,则必须是一个有效的js标识符
3. 括号内部,一个以逗号分隔的参数列表。各个参数名称必须是有效的标识符,而且参数列表允许为空。即使是空参数列表,圆括号也必须始终存在。
4. 函数体,包含在大括号内的一系列js语句。函数体可以为空,但大括号必须始终存在。
函数名是可选的,例如匿名函数。
命名一个函数时,该名称在整个函数声明范围内是有效的。此外,如果一个命名函数声明在顶层,window对象上的同名属性则会引用到该函数。
所有的函数都有一个name属性,该属性保存的是该函数名称的字符串。没有名称的函数name属性值为空字符串。
示例:3.1 证明函数声明相关内容
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function isNimble(){return 1;}
assert(typeof window.isNimble === 'function','isNimble() defined');
assert(isNimble.name === 'isNimble','isNimble() has a name');
var canFly = function(){return true;}
assert(typeof window.canFly === 'function','canFly() defined');
assert(canFly.name === '','canFly() has no name');
window.isDeadly = function(){return true;}
assert(typeof window.isDeadly === 'function','isDeadly() defined');
function outer(){
assert(typeof inner === 'function','inner() in scope before declaration');
function inner(){}
assert(typeof inner === 'function','inner() in scope after declaration');
assert(window.inner === undefined,'inner() not in global scope');
}
outer();
assert(window.inner === undefined,'inner() still not in global scope');
window.wieldsSword = function swingsSword(){return true;};
assert(window.wieldsSword.name = 'swingsSword','wieldSword\'s real name is swingsSword');
在这个测试页面中,通过三种不同的方式声明了全局作用域函数
.isNimble()被声明为一个命名函数。常见的声明风格
.创建一个匿名函数,并赋值给一个名为canFly的全局变量。该函数可以通过它的引用canFly()进行调用。
.创建另一个匿名函数,并将其赋值给window的isdeadly属性。
作用域和函数
在js中,作用域是由function进行声明的,而不是代码块。声明的作用域创建于代码块,但不是终结于代码块。
.变量声明的作用域开始于声明的地方,结束于所在函数的结尾,与代码嵌套无关。
.命名函数的作用域是指声明该函数的整个函数范围,与代码嵌套无关。
.对于作用域声明,全局上下文就像一个包含页面所有代码的超大型函数。
示例:3.2 监控声明项的作用域行为
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
assert(true,'|---before outter---|');
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
function outer(){
assert(true,'|---inside outer, before a---|');
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
var a=1;
assert(true,'|---inside outter, after a---|')
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
function inner(){}
var b=2;
assert(true,'|---inside outter,after inner() and b---|')
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
if(a==1){
var c=3;
assert(true,'|---inside outter, inside if---|')
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
}
assert(true,'|---inside outter, outside if---|')
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
}
outer();
assert(true,'|---after outter---|')
assert(typeof outer === 'function','outer() is in scope');
assert(typeof inner === 'function','inner() is in scope');
assert(typeof a==='number','a is in scope');
assert(typeof b==='number','b is in scope');
assert(typeof c==='number','c is in scope');
以上测试表明,函数可以在其作用域范围内提前被引用,但变量不行。
每个声明项的作用域不仅取决于它的声明,还取决于它是变量还是函数。
3.3 函数调用
有四个不同的方式可以进行函数调用,每种方式都有自己的细微差别。
.作为一个函数进行调用,是最简单的形式
.作为一个方法进行调用,是在对象上进行调用,支持面向对象编程
.作为构造器进行调用,创建一个新对象
.通过apply()或call()方法进行调用,这种比较复杂,再讨论。
前三种都是 expression(arg1,arg2)
3.3.1 从参数到函数形参
.如果实际传递的参数数量大于函数声明的形参数量,超出的参数则不会配给形参名称。
.如果声明的形参数量大于实际传递的参数数量,则没有对应参数的形参会赋值为undefined。
所有的函数调用都会传递两个隐式参数:arguments和this。
在函数内部,它们可以像其他显式命名的参数一样使用。
1)arguments参数
是传递给函数的所有参数的一个集合。该集合有一个length属性,单个参数可以arguments[2]这样的方式获取。
它是一个类数组结构,只拥有数组的某些特性。
2)this参数
this参数引用了与该函数调用进行隐式关联的一个对象,被称之为函数上下文。
js中的this依赖于函数的调用方式,将this称为调用上下文。
四种调用机制的主要区别在于,如何定义每种调用类型的this。
3.3.2 作为函数进行调用
示例:
function ninja(){}
ninja();
var samurai = function (){}
samurai();
以这种方式调用时,函数的上下文是全局上下文——window对象
3.3.3 作为方法进行调用
当一个函数被赋值给对象的一个属性,并使用引用该函数的这个属性进行调用时,那么函数就是作为该对象的一个方法进行调用的。
示例:
var o={};
o.whatever = function(){}
o.whatever();
将函数作为对象的一个方法进行调用时,该对象就变成了函数上下文,并且在函数内部可以以this参数的形式进行访问。
示例:3.3 函数调用和方法调用之间的区别
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function creep(){return this;}
assert(creep() === window,'creeping in the window');
var sneak = creep;
assert(sneak()===window,'sneaking in the window');
var ninja1 = {skulk:creep}
assert(ninja1.skulk() === ninja1,'the 1st ninja is skulking');
var ninja2 = {skulk:creep}
assert(ninja2.skulk() === ninja2,'the 2nd ninja is skulking');
示例中所有测试断言都通过了。
创建了一个名为creep的函数,该函数的唯一操作是返回函数上下文,这样就可以从函数外部看到函数调用时的函数上下文了。
通过函数名称进行调用时,函数上下文是全局上下文,是window。
创建变量sneak引用到该函数,它只是在同一个函数上创建一个引用
接下来定义一个ninja1对象,并在该对象上再定义一个接收creep()函数引用的skulk属性。这样就在对象上创建了一个名为skulk的方法。
creep()是一个可以以多种方式进行调用的独立函数。
在通过方法引用进行函数调用时,函数上下文是该方法所在的对象。
我们可以在任意方法中,通过this引用该方法所属的对象——面向对象编程的基本概念。
即便在上述所有这些例子中调用的都是相同的函数,其函数上下文也会随着函数调用方式的变化而变化,而不是取决于函数是怎样声明的。
我们不需要创建单独的函数副本,就可以在不同的对象上完成完全相同的处理过程——这是面向对象编程的一个宗旨。
3.3.4 作为构造器进行调用
构造器函数的声明和其他函数声明一样,不同的地方是在于如何调用该函数。
将函数作为构造器进行调用,我们要在函数调用前使用new关键字。
示例:
function creep(){return this}
new creep();
1)构造器的超能力
构造器调用时,如下特殊行为就会发生:
.创建一个新的空对象
.传递给构造器的对象是this参数,从而成为构造器的函数上下文
.如果没有显式的返回值,新创建的对象则作为构造器的返回值进行返回。
构造器的目的是要创建一个新对象并对其进行设置,然后将其作为构造器的返回值进行返回
示例3.4 使用构造器设置通用对象
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function Ninja(){
this.skulk = function(){return this;};
}
var ninja1 = new Ninja();
var ninja2 = new Ninja();
assert(ninja1.skulk()===ninja1,'the 1st ninja is skulking');
assert(ninja2.skulk()===ninja2,'thd 2nd ninja is skulking');
对所构造出对象的方法进行测试,每个方法的返回值都应该是其构造对象本身
在示例中,创建一个名为Ninja()的函数,在使用new关键字进行调用时,将会创建一个空对象实例,并作为this参数传递给该函数。构造器在该空对象上又创建一个名为skulk的属性,该属性被赋值为一个函数,从而将属性作为新创建对象的一个方法进行使用。
该方法执行的操作是返回函数上下文,以便在外部进行测试。
2)构造器编码注意事项
构造器的目的是通过函数调用初始化创建新的对象。
函数和方法的命名通常以动词开头,来描述它们所做的事情,并且是以小写字母开头。而构造器的命名通常是由一个描述所构造对象的名词来描述,并且以大写字母开头。
通过构造器,使用相同的模式就可以更容易地创建多个对象,而无需再一遍又一遍地重复相同的代码。通用代码,作为构造器的构造体,只需编写一次。
3.3.5 使用apply()和call()方法进行调用
函数调用方式之间的主要差异是:作为this参数传递给执行函数的上下文对象之间的区别。作为方法进行调用,该上下文是方法的拥有者;作为全局函数进行调用,其上下文永远是 window(也就是说,该函数是window的一个方法), 作为构造器进行调用,其上下文对象则是创建的对象实例。
1)使用apply()和call()方法
在函数调用的时候,可以显式指定任何一个对象作为其函数上下文。js的每个函数都有apply()方法和call()方法,使用它们可以实现这种功能。
函数可以像其他任何类型的对象一样,拥有属性和方法。
apply(作为函数上下文的对象,作为函数参数所组成的数组)
call(作为函数上下文的对象,参数列表)
示例3.5 使用apply()和call()方法指定函数上下文
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function juggle(){
var result = 0;
for(var n=0; n
result += arguments[n]
}
this.result = result;
}
var ninja1 = {};
var ninja2 = {};
juggle.apply(ninja1,[1,2,3,4]);
juggle.call(ninja2,5,6,7,8);
console.log(ninja1.result)
assert(ninja1.result===10,'juggled via apply');
assert(ninja2.result===26,'juggled via call')
在示例中,创建juggle()函数,在该函数内部,将所有参数值相加,并将结果保存到上下文的result属性上。
然后创建两个即将作为函数上下文的result属性,将第一个对象和数组一起传递给apply()方法,然后再将第二个对象和多个其他参数一起传递给call()方法
示例测试通过,我们可以指定任意对象作为函数上下文进行函数调用。
2)在回调中强制指定函数上下文
示例3.6 构建for-Each函数演示函数上下文功能
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function forEach(list,callback){
for(var n=0; n
callback.call(list[n],n)
}
}
var weapons = ['shuriken','katana','nunchucks'];
forEach(weapons,function(index){
assert(this == weapons[index],'got the expected value of '+ weapons[index]);
})
测试通过,我们可以让任何对象作为callback函数调用的的函数上下文
什么时候用call(),什么时候用apply()呢?
哪个方法可以提高代码的清晰度就用哪个。用最能匹配参数的那个方法。
第4章 挥舞函数
4.1 匿名函数
在函数式语言中,包括js,函数经常在需要的时候才进行定义,然后用完就抛弃。
示例4.1 使用匿名函数的常见示例
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
window.onload = function(){
assert(true,'power!');
}
var ninja = {
shout:function(){
assert(true,'ninja');
}
}
ninja.shout();
setTimeout(function(){
assert(true,'forever!')
},500)
尽管没有名称,匿名函数在不同的时机都是可以调用的
函数式编程专注于:少、通常无副作用、将函数作为程序代码的基础构件块。
4.2 递归
当函数调用自身,或调用另外一个函数,但这个函数的调用树中某个地方又调用了自己时,递归就发生了。
4.2.1 普通命名函数中的递归
示例:使用命名函数发出“啾啾”声
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function chirp(n){
return n>1 ? chirp(n-1)+'-chirp':'chirp';
}
assert(chirp(3)=='chirp-chirp-chirp','calling the named function comes naturally.')
函数递归的两个条件:引用自身,并且有终止条件。
4.2.2 方法中的递归
示例:对象中的方法递归
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = {
chirp:function(n){
return n>1 ?ninja.chirp(n-1)+'-chirp':'chirp';
}
}
assert(ninja.chirp(3) === 'chirp-chirp-chirp','an object property isn\'t too confusing either.');
匿名函数通过ninja.chirp引用自身
4.2.3 引用的丢失问题
示例:4.4 递归中的函数引用丢失
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = {
chirp:function(n){
return n>1 ?this.chirp(n-1)+'-chirp':'chirp';
}
}
var samurai = {chirp:ninja.chirp};
var ninja = {};
try{
assert(samurai.chirp(3) === 'chirp-chirp-chirp','Is this going to work?');
}catch(e){
assert(false,'uh,this is\'n good! where\'d ninja.chirp go?');
}
在匿名函数中不再使用显式的ninja引用,而是使用函数上下文(this)进行引用。
当一个函数作为方法被调用时,函数上下文指的是调用该方法的那个对象。调用ninja.chirp()时,this对象引用的是ninja,而调用samurai.chirp()时,this对象引用的则是samurai.
4.2.4 内联命名函数
示例4.5 使用内联函数进行递归的新方式
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = {
chirp:function signal(n){
return n>1 ?signal(n-1)+'-chirp':'chirp';
}
}
assert(ninja.chirp(3) === 'chirp-chirp-chirp','Works as we would expect it to!');
var samurai = {chirp:ninja.chirp};
var ninja = {};
assert(samurai.chirp(3) === 'chirp-chirp-chirp','The method correctly calls itself.');
示例4.6 验证内联函数的标识
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = function myNinja(){
assert(ninja == myNinja,'this function is named two tings at once!')
}
ninja();
assert(typeof myNinja == 'undefined','But myNinja isn\'t defined outside of thd function.');
尽管可以给内联函数进行命名,但这些名称只能在自身函数内部才是可见的。 内联函数的名称和变量名称有点像,它们的作用域仅限于声明它们的函数。
4.2.5 callee属性
arguments参数的callee属性
在ES5中callee被去除了
示例:使用arguments.callee引用当前调用函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var ninja = {
chirp:function(n){
return n>1?arguments.callee(n-1)+'-chirp':'chirp';
}
}
assert(ninja.chirp(3)=='chirp-chirp-chirp','auguments.call is the function itself');
参数arguments是隐式传递给每一个函数的,arguments有一个callee属性,callee属性引用的是当前所执行的函数。该属性可以作为一个可靠的方法引用函数自身。
4.3 将函数视为对象
js中函数最重要的特性之一是将函数作为第一型对象。
函数可以有属性,也可以有方法,可以分配给变量和属性,也可以享有所有普通对象所拥有的特性,而且还有一个超级特性:它们可以被调用。
var obj = {};
var fn = function(){};
assert(obj&&fn,'both the object and function exist.')
4.3.1 函数存储
示例4.8 存储一组独立的函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var store = {
nextId:1,
cache:{},
add:function(fn){
if(!fn.id){
fn.id = store.nextId++;
return !!(store.cache[fn.id] = fn);
}
}
};
function ninja(){}
assert(store.add(ninja),'function was safely added.');
assert(store.add(ninja),'but it was only added once.')
!!构造是一个可以将任意js表达式转化为其等效布尔值的简单方式。
4.3.2 自记忆函数
缓存记忆是构建函数的过程,这种函数能够记住先前计算的结果。通过避免已经执行过的不必要复杂计算,这种方式可以显著提高性能。
1)缓存记忆昂贵的计算结果
示例4.9 记忆之前计算出的值
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function isPrime(value){
if(!isPrime.answers) isPrime.answers = {};
if(isPrime.answers[value]!=null){
return isPrime.answers[value];
}
var Prime = value != 1;
for(var i=2; i
if(value%i==0){
Prime = false;
break;
}
}
return isPrime.answers[value] = Prime;
}
assert(isPrime(5),'5 is prime!');
assert(isPrime.answers[5],'the answer was cached!')
2)缓存记忆DOM元素
function getElements(name){
if(!getElements.cache) getElements.cache={};
return getElements.cache[name] = getElements.cache[name] || document.getElementsByTagName(name)
}
这个简单的缓存代码会产生5倍的性能提升。
函数的属性特性:我们可以将状态和缓存信息存储在一个封装的独立位置上,不仅在代码组织上有好处,而且外部存储或缓存对象无需污染作用域,就可以获取性能提升。
4.3.3 伪造数组方法
示例4.10 模拟类似数组的方法
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var elems = {
length:0,
add:function(elem){
Array.prototype.push.call(this,elem);
},
gather:function(id){
this.add(document.getElementById(id))
}
};
elems.gather('first');
assert(elems.length == 1 && elems[0].nodeType,'verify that we have an element in our stash');
elems.gather('second');
assert(elems.length == 2 && elems[1].nodeType,'verify the other insertion')
4.4 可变长度的参数列表
4.4.1 使用apply()支持可变参数
示例4.11 数组上的通用min()和max()函数
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function smallest(array){
return Math.min.apply(Math,array);
}
function largest(array){
return Math.max.apply(Math,array);
}
assert(smallest([0,1,2,3]) == 0,'located the smallest value.');
assert(largest([0,1,2,3])==3,'located the largest value.')
4.4.2 函数重载
1)检测并遍历参数
示例4.12 遍历可变长度的参数列表
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function merge(root){
for(var i=1; i
for(var key in arguments[i]){
root[key] = arguments[i][key];
}
}
console.log(root)
return root;
}
var merged = merge({name:'batou'},{city:'niihama'});
assert(merged.name == 'batou','the original name is intact');
assert(merged.city == 'niihama','and the city has been copied over');
在js中没有强制函数声明多少个参数就得传入多少参数。函数是否可以成功处理这些参数完全取决于函数本身的定义。函数在只定义一个参数root时,意味着只能用root这个名称访问所传入参数的第一个。
2)对arguments列表进行切片(slice)和取舍(dice)
示例4.13 对arguments列表进行切片
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function multiMax(multi){
return multi*Math.max.apply(Math,arguments.slice(1))
}
assert(multiMax(3,1,2,3)==9,'first arg, by largest');
这样写,有报错,
Uncaught TypeError: arguments.slice is not a function
因为arguments参数引用的不是真正的数组,它没有基本数组应该有的方法,例如slice()。
我们需要改造下
示例4.14 对arguments列表切片——这次成功了
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function multiMax(multi){
return multi*Math.max.apply(Math,Array.prototype.slice.call(arguments,1))
}
assert(multiMax(3,1,2,3)==9,'first arg, by largest');
3)函数重载方式
在代码处理很简单的情况:定义一个函数重载时,基于传递的参数定义一个有很多不同功能的函数,检查参数列表并使用ifelse子句执行不同的行为,这种方法很好用。
在复杂的情况下,大量的函数使用会导致代码笨拙。我们将探讨一种可以创建多个相同名称函数的技术,但是根据期望参数的不同,每个函数也不同——可以写成独立且独特的匿名函数,而不是作为一个整体的ifelse块。
4)函数的length属性
函数的length属性的值等于该函数声明时所需要传入的形参数量。
不是arguments参数的length。
示例:
function makeNinja(name){}
function makesamurai(name,rank){}
assert(makeNinja.length==1,'only expecting a single argument');
assert(makesamurai.length==2,'two arguments expected');
对于一个函数,在参数方面,可以确定两件事情:
. 通过其length属性,可以知道声明了多少命名参数。
. 通过arguments.length,可以知道在调用时传入了多少参数。
5)利用参数个数进行函数重载
基于传入的参数,有很多种方法可以判断并进行函数重载。
一种通用的方法是,根据传入参数的类型执行不同的操作。
另一种方法是,可以通过某些特定参数是否存在来进行判断。
还有一种方法是通过传入参数的个数进行判断。
示例:4.15 重载函数的方法
test suite
#results .pass{color:green;}
#results .fail{color:red;}
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function addMethod(object,name,fn){
var old = object[name];
object[name] = function(){
if(fn.length == arguments.length) return fn.apply(this,arguments);
else if(typeof old == 'function') return old.apply(this,arguments);
}
}
var ninjas = {
values:['dean edwards','sam stephenson','alex russell']
}
addMethod(ninjas,'find',function(){
return this.values;
})
addMethod(ninjas,'find',function(name){
var ret = [];
for(var i=0; i
if(this.values[i].indexOf(name) == 0){
ret.push(this.values[i])
}
}
return ret;
})
addMethod(ninjas,'find',function(first,last){
var ret = [];
for(var i=0; i
if(this.values[i] == (first + ' ' + last)){
ret.push(this.values[i])
}
}
return ret;
})
assert(ninjas.find().length == 3,'found all ninjas');
assert(ninjas.find('sam').length == 1,'found ninja by first name');
assert(ninjas.find('dean','edwards').length == 1,'found ninja by first and last name');
assert(ninjas.find('alex','russell','jr') == null,'found nothing.')
内部函数在执行的时候,可以访问到old和fn的值。
4.5 函数判断
如何判断一个给定对象是一个函数的实例,并且是可以调用的。
通常使用typeof语句就可以,但是在firefox,id,safari下有兼容问题。
函数有apply()和call()方法,将函数转换为一个字符串,根据其序列化值判断其类型:
function abc(){}
console.log(isFunction(abc))
function isFunction(obj){
return Object.prototype.toString.call(obj)==='[object Function]'
}
通过直接访问Object.prototype的方法,可以确保我们得到的不是覆盖版本的toString()。