30天学习计划 js忍者秘籍 前四章

第一部分 准入训练

第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()。

推荐阅读更多精彩内容