第三章 函数是根基
函数的独特之处
函数是第一型(first-class)对象
对象在 javascript 中有如下功能:
- 可以通过字面量进行创建。
- 可以赋值给变量、数组或其他对象的属性。
- 可以作为参数传递给函数。
- 可以作为函数的返回值进行返回。
- 可以拥有动态创建并赋值的属性。
在 javascript 中,函数拥有全部这些功能。除了可以像其它对象类型一样使用外,函数还可以被调用。这些调用通常以异步方式进行。
浏览器的事件轮询
桌面应用程序(GUI)大多采用如下方式:
- 创建用户界面。
- 进入轮询,等待事件触发。
- 调用事件的处理程序(监听器[listener])。
浏览器编程唯一的不同就是:代码不负责事件轮询和事件派发,而是浏览器处理。
我们的职责是为浏览器中发生的各种时间建立事件的处理程序(handler)。这些事件在触发时被放置在一个事件队列(先进先出列表[FIFO])中,然后浏览器将调用已经为这些事件建立好的处理程序(handler)。因为这些事件发生的时间和顺序都是不可预知的,所以事件处理函数的调用也是异步的。
浏览器的事件轮询是单线程的。每个事件都是按照在队列中所放置的顺序来处理的。这就是所谓的FIFO(先进先出)列表,或者一个使用古老定时器的筒仓(silo)。每个事件都在自己的生命周期内进行处理,所有其他事件必须等到这个事件处理结束后才能继续处理。执行过程如图所示:
浏览器把事件放到队列上的机制是在事件轮询模型之外。确定事件何时发生并把它们放到事件队列上的过程所处的线程,并不参与事件本身的处理。
回调的概念
定义一个函数,以便其它一些代码在适当的时候调用它。
函数声明
声明一个函数时,该名称在整个函数声明范围内时有效的。此外,如果一个命名函数声明在顶层,window 对象上的同名属性则会引用到该函数。
所有的函数都有一个 name
属性,该属性保存的是该函数名称的字符串。没有名称的函数也仍然有 name
属性,只是该属性值为空字符串。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>证明函数声明相关内容</title>
<style>
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<ul id="results"></ul>
<script>
// 定义assert方法
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上添加一个同名属性
function isNimble(){return true;}
// 判断window属性是确定的
assert(typeof window.isNimble === "function", "isNimble() defined");
// 判断函数的 name 属性
assert(isNimble.name === "isNimble", "isNimble() has a name");
// 创建一个匿名函数,并赋值给canFly变量
var canFly = function(){return true;};
assert(typeof window.canFly === "function", "canFly() defined");
assert(canFly.name === "", "canFly() has no name");
// 创建一个匿名函数并引用到window的一个属性上
window.isDeadly = function(){return true;};
assert(typeof window.isDeadly === "function", "isDeadly() defined");
// 在outer函数内定义一个inner函数,测试该inner()在其定义之前和之后都可以访问到,并且没有创建全局的inner()
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()可以在全局作用域内访问到,而inner()则不可以
outer();
assert(window.inner === undefined, "inner() still not in global scope");
// 这里声明的函数名无效,真正起到控制作用的是变量名
window.wieldsSword = function swingsSword(){return true;};
assert(window.wieldsSword.name === 'swingsSword', "wieldsSword's real name is swingsSword");
</script>
</body>
</html>
作用域和函数
在 JavaScript 中,作用域是由 function 进行声明的,而不是代码块。声明的作用域创建于代码块,但不是终结于代码块。
- 变量声明的作用域开始于声明的地方,结束于所在函数的结尾,于代码嵌套无关。
- 命名函数的作用域是指声明该函数的整个函数范围,于代码嵌套无关。(机制提升)
- 对于作用域声明,全局上下文就像一个包含页面所有代码的超大型函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>作用域断言测试</title>
<style>
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<ul id="results"></ul>
<script>
// 定义assert方法
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 OUTER ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
function outer(){
assert(true,"|------ INSIDE OUTER,BEFORE a ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
var a = 1;
assert(true,"|------ INSIDE OUTER,AFTER a ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
function inner(){/******/}
var b = 2;
assert(true,"|------ INSIDE OUTER,AFTER inner() AND b ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
if(a == 1){
assert(true,"|------ INSIDE OUTER,INSIDE if AND INSIDE if ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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"); // 这里c 被初始化为undefined,所有断言会失败
var c = 3;
// 不能再let const 声明之前调用 assert 会报错
let d=4; // 添加 ES6 的let
const e=5; // 添加 ES6 的const
assert(true,"|------ INSIDE OUTER,INSIDE if AND AFTER let const ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
}
assert(true,"|------ INSIDE OUTER,OUTSIDE if ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
}
outer();
assert(true,"|------ INSIDE OUTER,OUTSIDE if ------|")
assert(typeof outer == 'function', "outer() is in scope");
assert(typeof inner === 'function', "inner() is 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(typeof d === 'number', "d is not in scope");
assert(typeof e === 'number', "e is not in scope");
</script>
</body>
</html>
函数调用
有四种不同的方式进行函数调用,每种方式都有细微的差别(主要区别在于如何定义每种调用类型的this
):
- 作为一个函数进行调用,是最简单的形式。
- 作为一个方法进行调用,在对象上进行调用,支持面向对象编程。
- 作为构造器进行调用,创建一个新对象。
- 通过
apply()
或call()
方法进行调用。
从参数到函数形参
- 如果实际传递的参数数量大于函数声明的形参数量,超出的参数不会配给形参。
- 如果声明的形参数量大于实际传递的参数数量,没有对应参数的形参会赋值为undefined。
所有的函数调用都会传递两个隐式参数:arguments 和 this。
-
arguments
:是传递给函数的所有参数的一个集合,该集合有一个length
属性。arguments 不是JS的数组,不能使用数组方法。 -
this
:this
参数引用了与该函数调用进行隐式关联的一个对象,被称为函数上下文。它依赖于函数的调用方式,因此,this
又称为调用上下文。
下面的代码中展示了作为函数调用和作为方法调用的区别:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函数调用和方法调用的区别</title>
<style>
/*定义结果样式*/
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<!--显示测试结果-->
<ul id="results"></ul>
<script>
// 定义assert方法
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; // 创建变量引用creep
assert(sneak() === window, "Sneaking in the window");
var ninja1 = {
skulk: creep // 创建属性引用creep
};
assert(ninja1.skulk() === ninja1, "The 1st ninja is skulking");
var ninja2 = {
skulk: creep
};
assert(ninja2.skulk() === ninja2, "The 2nd ninja is skulking");
</script>
</body>
</html>
将函数作为构造器(constructor)进行调用,要在函数调用前使用 new
关键字。构造器调用时,会发生如下特殊行为:
- 创建一个新的空对象。
- 传递给构造器的对象时
this
参数,从而成为构造器的函数上下文。 - 如果没有显示的返回值,新创建的对象则作为构造器的返回值进行返回。
构造器的目的是创建一个新对象并对其进行设置,然后将其作为构造器的返回值进行返回。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用构造器设置通用对象</title>
<style>
/*定义结果样式*/
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<!--显示测试结果-->
<ul id="results"></ul>
<script>
// 定义assert方法
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? "pass" : "fail";
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
// 声明一个构造器,在函数上下文对象上创建一个skulk属性。该属性方法又返回了上下文自身
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, "The 1st ninja is skulking");
</script>
</body>
</html>
函数和方法的命名通常以动词开头,来描述它们所做的事情,并以小写字母开头。而构造器的命名通常是由一个描述所构造对象的名词来命名,并以大写字母开头。
JavaScript 的每个函数都有 apply()
和call()
方法,使用任何一个方法,都可以显式指定任何一个对象作为其函数上下文。
函数作为第一型对象,可以向其它任何类型的对象一样,拥有属性和方法。
-
apply()
:接收两个参数,一个是作为函数上下文的对象,另一个是作为函数参数所组成的数组。例如:f.apply(o,[1,2,3]);
-
call()
:接收的参数包括作为函数上下文的对象和一个参数列表而不是单个数组。例如:f.call(o,1,2,3);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用apply()和call()指定函数上下文</title>
<style>
/*定义结果样式*/
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<!--显示测试结果-->
<ul id="results"></ul>
<script>
// 定义assert方法
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<arguments.length; 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);
assert(ninja1.result === 10, "juggled via apply");
assert(ninja2.result === 26, "juggled via call");
</script>
</body>
</html>
函数式编程和命令式编程的区别在于思维层面:函数式程序的构建块而不是命令式语句。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>构建for-each函数演示函数上下文功能</title>
<style>
/*定义结果样式*/
li.pass{color: green;}
li.fail{color: red;}
</style>
</head>
<body>
<!--显示测试结果-->
<ul id="results"></ul>
<script>
// 定义assert方法
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<list.length; n++){
callback.call(list[n],n);
}
}
// 创建测试对象
var weapons = ['shuriken','katana','nunchuncks'];
forEach(weapons,function(index){
assert(this == weapons[index], "Got the expected value of " + weapons[index]);
});
</script>
</body>
</html>