5.1 黑盒
从概念上讲,函数接受输入,进行计算,然后产生输出。下图是一个函数黑盒示意图,它计算一个账户在t
年之后的余额,其初始余额为p
,年利率为r
,每年取n
次复利。
要使用这个函数,只需向函数发送四个数值,并在其回应信息中获取计算所得的余额。函数的用户看不到其“内在工作”,所以我们把函数想象成黑盒子。
这里书本给出一个关于什么是抽象的说明:在日常生活中处处可以看到类似的情况。我们开车,但并不了解内燃机或者氢燃料电池;我们用微波炉加热事食物,却不明白深层的物理学知识;我们发送即使消息、推文、打电话,却对文字、声音的编码与传输方式一无所知。我们把这种只看事物的主体部分而不关心细节的理念叫做抽象。函数则是对计算的抽象。
5.2 定义和调用函数
在JavaScript中,函数类型值包含一个可执行的代码块,成为函数体,以及零个或多个输入,成为形参(parameter)。下面这个函数只有一个参数,它会计算此参数的三次方。
function (x) {return x*x*x;}
函数也是值,和数字、真值、字符串、数组及普通对象一样。因此,可以把函数类型值赋给变量。
var cube = function (x) {return x*x*x;}
要运行一个函数(或者说调用一个函数),可以向它传递一个放在小括号中的列表,其中是零个或者多个实参(argument
)。在被调用事,函数首先把每个实参值赋给对应的形参,然后执行函数体。如果存在return
语句,它会将计算结果传回给调用者。
下面的脚本展示一个函数定义以及对它的三次调用。
// 定义函数 —— 这时不会运行函数体
var cube = function (x) {
return x*x*x;
};
// 进三次调用,将函数体运行三次
alert(cube(-2));
alert(cube(10));
alert(("在一个魔方中有" + (cube(3) - 1) + "个立方体 "))
在第一次调用中,我们向函数cube
传递了-2,cube
会把-2赋值给x
,然后计算-2-2-2,并把结果值(-8)返回给调用处。这个值随后又被传给alert
函数的调用。
函数也可以有名字。函数有了名字,在调用时,就不一定要将它赋值给变量了。
function cube (x) {
return x*x*x;
};
alert(cube(-2)); // -8
在JavaScript中,这种定义方式成为函数声明,类似于(但又不完全等同于)把函数赋值给一个同名变量。尽管很多程序员喜欢函数声明的方式,但我们更喜欢使用变量声明方式。我们会在章尾讨论。
var diceRoll = function () {
return 1+Math.floor(6*Math.random());
};
要运行这个函数,必须写成diceRoll()
,而不能写成diceRoll
。前一个表达式会调用函数,而后一个就是函数自身。
var diceRoll = function () {
return 1+Math.floor(6*Math.random());
};
alert(
diceRoll()
);
alert(
diceRoll
);
这里都没什么问题,我想了想试了下如下函数:
function test() {
return "fn-test";
};
alert(test);
可见,对函数名进行调用,返回的是函数定义语句,而我在对函数声明语句进行调用时与匿名函数赋值变量方式调用情况相同,是否可证明函数声明语句实际上隐式声明了一个变量(变量名就是函数名),并且将函数引用赋值给函数名(变量名)。
如果一个函数完成了某主体的执行,却没有执行任何return
语句,它会返回undefined
值。这个undefined
值真的只是一个技术术语,因为在调用一个没有return
语句的函数时,主要是为了它产生的效果,而不是为了它产生的任何值。
我对这就句话理解:如果函数定义时没有要求返回最终值,则默认返回undefined
。调用一个没有返回值的函数后面还不是太理解...效果?何种效果?
var echo = function (message) {
alert(message + ".");
alert("I said: "+message+"!");
};
echo("Sanibonani"); // 调用这个函数最自然的方式
var x =echo("Hello"); // 为x赋值undefined,但在实际中不会发生
console.log(x); // undefined
如果没有为函数传递足够了实参值,则额外的形参变量会被初始化为undefined。
var show = function (x,y) {
alert(x+" "+y);
};
show(1); // "1 undefined"
5.3 示例
5.3.1 简单的一行函数
// 返回半径为r的圆的面积
var circleArea = function (r) {
return Math.PI*r*r;
};
// 返回y能否被x整除
var divides = function (x,y) {
return y % x === 0;
};
我们可以利用自己编写的函数来构建其他函数。
...都是基本的例子略过。
需要注意的是:函数语句内隐式类型转换和优先级与结合性问题!
5.3.2 验证实参
略.......
5.3.3 将对象引用作为参数传送
看一下向函数传递对象的情况
// 返回一个数组中所有元素之和
var sum = function (a) {
var result = 0;
for (var i=0;i<a.length;i+=1) {
result += a[i];
}
return result;
};
alert(sum([]));
alert(sum([10,-3,8]));
再看另一个例子,它使用了一种完全不同的风格。
// 把一个数组中所有字符串都转换成大写
var uppercaseAll = function (a) {
for (var i=0;i<a.length;i+=1) {
a[i]=a[i].toUpperCase();
}
};
区别在于,函数sum返回一个值,而uppercaseAll根本没有包含return语句!相反,它修改了传递给它
// 把一个数组中所有字符串都转换成大写
var uppercaseAll = function (a) {
for (var i=0;i<a.length;i+=1) {
a[i]=a[i].toUpperCase();
}
};
var result = uppercaseAll(["a","b","c"]);
alert(result); // undefined
alert(uppercaseAll(["a","b","c"])); // undefined
自我理解:调用函数后,传参,计算,因为没有return
返回值,所以只是计算而已,则uppercaseAll(["a","b","c"])
就会像章开头说的那样,默认返回undefined
,然后赋值给变量result
。(最后alert
调用函数证明这一点)。
var uppercaseAll = function (a) {
for (var i=0;i<a.length;i++) {
a[i]=a[i].toUpperCase();
}
};
var dogs = ["spike","spot","rex"];
alert(uppercaseAll(dogs)) // undefined,此值并不代表函数没有执行,而是执行了未指定返回值,则返回默认值。
alert(dogs); // ["SPIKE","SPOT","REX"],修改了传入对象的属性
无返回值函数,按照文中描述,它修改的是传递给它的实参对象的属性。就是修改了实参的属性
可以看到,直接向函数传参,然后被
alert
调用,结果是undefined
。因为调用函数并没有返回结果,只会返回undefined
(虽然函数内部的确执行了大写转换操作,但是没有返回值然并卵
),所以最后显示undefined
。但是
dogs
引用的数组对象已经被修改,即之前说的,它修改了传递给它的对象的属性。因为没有设置返回值,默认返回的undefined
被上一个alert
函数调用。而dogs
引用的数组被调用结束后由于没有返回值,避免了成为新数组被返回出去。所以大写字母保留下来。
另一个:
var uppercaseAll = function (a) {
var result = [];
for (var i=0;i<a.length;i+=1) {
result.push(a[i].toUpperCase())
}
return result; // 返回的是一个新数组!
};
var dogs = ["spike","spot","rex"];
alert(uppercaseAll(dogs)); // ["SPIKE","SPOT","REX"],这里alert调用的对象,是函数返回的新数组!不是dogs
alert(dogs); // ["spike","spot","rex"]
有返回值函数,直接调用、传参,因为有返回值,这个值被
alert
函数接收,显示处理结果:["SPIKE","SPOT","REX"]
为什么
dogs
引用的数组还是小写呢?因为调用函数返回的最终值没有重新赋值给dogs
。换句话说,alert
函数调用的dogs
数组,和uppercaseAll
函数没有关系。uppercaseAll
执行结束已经返回了一个新数组
。
也就是说alert(uppercaseAll(dogs));
这段语句的结果,是大写字母
还是undefined
,取决于是否对函数设置返回值!!!
确保你理解了最后这两个函数的区别,第一个函数修改了其实参的属性,第二个函数没有改动实参,而是
返回一个新的数组
。
5.3.4 先决条件
// 返回数组中的最大元素
var max = function (a) {
var largest = a[0];
for (var i=0;i<a.lengt;ai++) {
if (a[i]>largest) {
largest = a[i];
}
}
return largest;
};
max([7,19,-22,0]);
max(["dog","rat","cat"]);
它能正常工作吗?
这个函数依靠>
操作符一次比较数组中的连续值,跟踪当前找到的最大值(从第一个元素a[0]
开始)。现在>
知道如何比较数字与数字、字符串与字符串,但奇怪的是,除非>两边的值都是字符串,否则JavaScript会把这两个值都看作数字(隐式转换),然后进行相应比较。有时,这种做法是没问题的。
但如果有一个值被转换成NaN
,那么情况就不妙了。如果x
或y
为NaN
,表达式x>y
会得出false
。3>NaN
是false
,NaN>3
也是false
!这就表示:
alert(max([3,"dog"])); // 3
alert(max(["dog",3])); // "dog"
3
和"dog"
其实是不可比较的,所以计算这种数组的最大值基本上没有什么意义。那在这种情况下难道不应当抛出一个异常吗?很多语言都会这么做。其他语言甚至会拒绝运行包含这种比较的程序!然后,JavaScript很愉快地运行了这种比较,然后给出了没什么意义的结果,如果愿意的话,可以尝试在代码里探测这些问题。
/*返回数组中的最大元素。如果数组包含了不可比较的元素,函数会返回一个不确定的任意值*/
函数在这个注释中承诺:只要调用者仅传递有意义的参数,那它就返回最大值;否则契约失效。函数对实参提出了这些约数条件称为先决条件
。函数自身不会检查先决条件,没有满足先决条件只是会导致未指明的行为。先决条件是编程圈子非常熟悉而且深刻理解的一个术语,所以我们将为引入先决条件的注释采用一种约定。
// 返回数组中的最大元素。先决条件:数组中的所有元素必须是可以互相比较的
5.3.5 关注点分离
下面要举的例子几乎会出现在所有介绍编程的书中 ———— 一个质数判断函数。该函数接受一个输入值,然后返回它是否为质数。4.5.1节给出了一份完整的质数脚本,但是现在要学习的内容要多得多了,所以下面将对该脚本进行重构
。重构
:重构就是对代码做结构性的调整,让其变得更好,一般(但不一定)是将大而混乱的代码分解成较小的组成部分。在这个案例中我们要将用户交互与主要计算区分开
来,将主要计算部分包装成一个漂亮的函数。
// 返回n是否为质数。先决条件:n是一个大于或等于2的整数,在JavaScript可表示的整数范围之内。
var isPrime = function (n) {
for (var k=2,last=Math.sqrt(n);k<=last;k+=1) {
if (n%k===0) {
return false;
}
}
return true;
};
请务必注意:这个函数只会返回它的实参是不是质数,并不会弹出一条说明判断结果的消息!其余脚本负责提示输入、检查错误、报告结果。
var SMALLEST = 2,BIGGEST = 9E15;
var n = prompt("输入一个数组,我会检查它是不是质数");
if (isNaN(n)) {
alert("这不是个数字");
} else if (n<SMALLEST) {
alert("我不能检测这么小的数字");
} else if (n>BIGGEST) {
alert("这个数字对我来说太大了,无法检测");
} else if (n%1!==0) {
alert("我只能测试整数");
} else {
alert(n+"是"+(isPrime(n)? "质数" : "合数"));
// 注意这里如果去掉三目运算符的括号,则会先计算字符串连接符,永远弹出:"质数"
}
重构后的代码体现了关注点的分离,这是一种很优秀的编程做法,它主要有两点好处。
分离关注点可以让复杂系统变得容易理解。
对于像航天飞机或金融服务系统这样额大型系统,要理解或诊断其中的某个问题,必须能够确定一些具有明确行为的子系统。如果只是把一个大型系统看成一系列语句的集合,那就永远无法真正理解它。将质数计算放到它自己的函数中,就能生成一段可以重复使用的代码,可以将它放到我们将来编写的任意脚本中。我们已经体验过
函数的复用性
了:我们已经调用过alert
和Math.sqrt
,却不需要自己去编写其中的细节。
但我们这个质数函数的复用性到底如何呢?调用这个函数的脚本做了很多错误检查。如果真的希望这个函数只需编写一次,却能被数百个、数千个脚本调用,那期待这些“调用者”来做同样的错误检查是否公平呢?当然不公平了。我们可以在函数中检查错误。
// 返回实参是否为2到9e15之间的质数。
// 如果实参不是整数或者超出2到9e15的范围,则会抛出异常。
var isPrime = function (n) {
if (n%1!==0 || n<2 || n>9e15) {
throw "这个数组不是整数或者超出范围";
};
for (var k=2,last=Math.sqrt(n);k<last;k++) {
if (n%k===0) {
return true;
}
}
return false;
};
注意,这个函数在遇到问题时会抛出异常,而不是弹出错误提示!这是很关键的。要使函数真正实现可复用,它永远都不应接管用户交流的责任。(我理解为错误不与交互模块混用
)
函数的不同用户对错误报告可能会有不同的要求。有些人会把错误写到网页的某个位置,有些人可能会把错误收集到一个数组中,有些人可能想用别的某种语言博报告错误,预测用户可能使用的每种语言不是这个函数的任务。
当编写为调用者计算数值的函数时,应当通过抛出异常来指示错误。
5.3.6 斐波那契数列
本节最后一个例子是一个生成斐波那契数列的函数。斐波那契数列是一个非常值得注意的数列,在自然、音和金融市场中都会出现它的属性。这个数列的开头如下:
0,1,1,2,3,5,8,13,21,34,55,89,144,...
数列中每个值(前两个值除外)都是前两个值之和。
f(n)=f(n-1)+f(n-2)
我们的函数会构造一个数组f,从[0,1]开始,然后不停地把最后一个元素(f[f.length-1])和倒数第二个元素(f[f.length-2])相加。因为函数只能处理整数,所以我们必须确保结果只不会超过JavaScript可以连续表达的整数范围,大约是9e15。就目前来说,我们先做个弊,只生成其中的前75的数字,因为我知道这些数字是安全的。
// 返回一个数组,其中包含斐波那契数列的前75个数字。即f.length = 75
var fibonacciSequence = function () {
var f = [0,1];
for (var i=0;i<=75;i++) {
f.push(f[f.length-1]+f[f.length-2]);
}
alert(f);
};
fibonacciSequence();
练习
改写这个斐波那契数列,使其接受一个实参,表明要产生多少个斐波那契数。如果传入的参数不是介于0到75之间(包含)的整数,则抛出一个异常。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script type="text/javascript">
/*计算*/
function fbnqFn(n) {
var fbnqArr = [0,1];
for (var i=0;i<n;i++) {
fbnqArr.push(fbnqArr[fbnqArr.length-2]+fbnqArr[fbnqArr.length-1]);
}
fbnqArr.length = n;
return fbnqArr;
};
/*交互*/
function client() {
var Max = prompt("需要生成多少个斐波那契数?(不能超过150)");
if (isNaN(Max)===true || Max%1!==0 || Max<0 || Max>150) {
throw "输入数字不能是非数字、整数、负数且不能超过150";
}
var test = fbnqFn(Max);
console.log(test+" | "+test.length);
};
// start
client();
</script>
</body>
</html>
5.4 作用域
利用函数,可以将其任意的计算进行打包,在调用者看来,就是一条单独地简单命令。请看以下计算阶乘的函数:
// 返回n的阶乘。先决条件:n是一个介于0到21之间的整数(包含0和21)。超过21,返回近似值
var factorial = function (n) {
var result = 1;
for (var i=1;i<=n;i++) {
result *= i;
}
return result;
};
这个函数声明了一个形参n,以及它自己的两个变量:i和result。在函数内部声明的变量成为局部变量,和形参一样,属于函数自己,与脚本其他位置的同名变量完全无关。这点非常好,请看:
var result = 100;
alert(factorial(5)); // 120
alert(result); // 100
我们不会希全局变量result
仅仅因为我们计算了一次阶乘就发生改变。脚本的不同部分往往是由不同人编写的。编写函数调用部分的作者完全不知道在函数中会用到哪些变量。如果你调用了alert
函数,而它改变了你的某些变量,你肯定会不高兴。
在JavaScript中,在函数内部声明的变量以及函数的形参均拥有函数作用域,而在函数之外声明的变量则拥有全局作用域,成为全局变量。拥有函数作用域的变量只在声明它们的函数中可见,与外部世界隔离,就像我们前面看到的那样。下面这段非常简短的脚本更清地表明了这一点。
var message = "冥王星只是一个矮行星";
var warn = function () {
var message = "你马上要看到一些争议性的东西";
alert(message);
};
warn(); // "你马上要看到一些争议性的东西"
alert(message); // "冥王星只是一个矮行星"
这里有两个恰巧同名的不同变量。全局变量的作用域开始于它的声明位置,一直延伸到脚本结束,而局部变量的作用域则是声明它的函数体内部。在这种情况下,局部变量和全局变量的名字相同(message),其作用域重叠。在重叠区域中,最内层的声明优先。
局部变量对外部是隐藏的,无法从外部引用,而全局变量则能在函数中看到,除非你特地隐藏它们。
var warning = "不要双击提交按钮";
var warn = function () {
alert(warning); // 这里可以看到全局变量
};
warn(); // "不要双击提交按钮"
alert(warning); // "不要双击提交按钮"
能在函数访问全局变量并没有什么令人惊讶的。实际上,我们已经用过了很多全局变量:alert
、prompt
、isNaN
、Math
等等。如果不允许在函数中用它们,要完成任何事情都会面临巨大的阻碍。但是,这也意味着一个潜在的问题。
var message = "新游戏的时间";
var play = function () {
message = "正在玩"; // 没有声明
alert(message);
};
alert(message);
play();
alert(message);
play();
上面脚本定义了一个message变量,它的值由函数更新。在函数中修改全局变量几乎总被认为是非常差的编程实践:脚本中的函数进行"相互交流"的正确做法是通过函数实参和返回值,而不是通过全局变量。程序应当尽量少的使用全局变量:
尽量减少全局变量的使用。具体来说,函数应该通过参数和返回值进行"交流",而不是通过更新全局变量。
JavaScript中局部变量的作用域包含了声明它们的整个函数体,这一事实又会导致另一种可能情况:全局变量是在声明之后才会出现,而局部变量则是在其函数开始执行时就马上存在的,即便变量是在函数体中间声明的。考虑以下代码:
var x = 1;
// 在此处,全局变量x已经存在,而全局变量y则尚未存在
// 在此处使用y则会抛出一个ReferenceError引用错误
var y = 2;
// 此时全局变量y已经存在
var f = function () {
alert(z); // 没有错误,显示undefined
var z = 3;
alert(y+3); // 5
};
f();
其实上面例子有一个变量提升
的问题,根据变量提升机制,var
会提升到当前作用域的顶端,z
的作用域是f
所包含的区块,所以你的代码等价于
var x = 1;
var y = 2;
var f = function () {
var z;
// 会把 var 声明提升到最高的位置 这种特性叫做 变量提升 此时声明了 z 但是为定义值 所以z的值是 undefined
alert(z);
z = 3;
alert(y+3);
};
f();
当调用函数时,JavaScript引擎会在该处创建一个对象,用以保存函数的形参和局部变量。形参会被立即初始化,获得调用时所传实参值的副本,所有局部变量会被立刻初始化为undefined
(这里不是先初始化再赋值的?)。上面例子里,在z声明前就引用了它,但并没有抛出ReferenceError
,其原因就在于此。
但是,尽管你知道局部变量在声明之前即可调用,但这并不意味着就应该使用处于未定义状态的局部变量。事实上,故意在定义变量之前就使用它们,几乎可以让所有阅读你代码的人产生混淆,所以这被认为是非常差的风格。很多JavaScript风格指南甚至直接认定这是一种错误;JSLint甚至包含了一项设置,专门用于检查这一情况。
练习(包含变量声明提升和函数声明提升问题)
-
请定义术语作用域
函数内部定义的变量只对函数内部可见,对外部不可见
或,函数内部定义的变量、对象。使其能被外部发现,使用的范围。
下面的脚本中,弹出什么提示内容?
var x = 1;
var f = function (y) {
alert(x+y);
};
f(2); // 3
那么按照变量提升,实际上是:
var x = 1;
var f = function (y) {
var y; // 声明提升
y = 2; // 获得调用函数传入的实参
alert(x+y); // 这里的y引用的是全局变量y
};
f(2);
如果把形参y
改名为x
,脚本会提示什么?
var x = 1;
var f = function (x) {
alert(x+y);
};
f(2);
实际上会报错,因为变量y
没有定义
var x = 1;
var f = function (x) {
var x;
x = 2;
alert(x+y);
};
关于变量提升和块级作用域问题可以看几位大神的文章:
5.5 作为对象的函数
JavaScript是每一个值,只要它不是undefined、null、布尔值、数字和字符串,那它就是一个对象。因此,函数值也是对象,而且跟所有对象一样,也可以有属性。它们还可以像其他值一样,其本身是其他对象的属性。
5.5.1 函数的属性
知道函数是对象之后,自然会问,函数有那些属性?
函数属性的其他用途包括:计算生成特定结果的次数、记住函数在给定实参下的返回值,以及定义与特定对象集合相关的数据。
当创建了函数对象之后,JavaScript会其初始化两个属性。第一个是length
,初始值为函数的形参
个数。
var average = function (x,y) {
return (x+y)/2;
};
alert(average.length); // 2,一个用于x,一个用于y
第二个预定义属性是prototype
,之后在讨论
5.5.2 作为属性的函数
由于函数也是值,所以可以作为对象的属性。把函数放在对象内部有两个主要理由,第一个理由是把许多相关函数放在一组。例如:
var geometry = {
circleArea:function (radius) {
return Math.PI*radius*radius;
},
circleCircumference:function (radius) {
return 2*Math.PI*radius;
},
sphereSurfaceArea:function (radius) {
return 4*Math.PI*radius*radius;
},
boxVolume:function (length,width,depth) {
return length*width*depth;
}
};
把许多函数组合到单个对象中,有助于组织和理解大型程序。人类不希望去尝试理解一个拥有数百个甚至数千个函数的系统,如果一个系统只有数十个软件组成部分,那我们理解起来会容易很多。例如,在一个游戏程序中,我们会很自然地为玩家、地貌、物理属性、消息传递、装备、图像等分别创建出子系统,每个都是一个很大的对象。
将函数作为属性的第二个理由是让程序从面向过程
转向面向对象
。例如,我们不一定要将函数看作对形状执行操作,将函数存储为形状的属性。将函数放在对象的内部,可以让人们专注于这些函数,让函数扮演对象行为的角色。
var circle = {
radius:5,
area:function () {
return Math.PI*this.radius*this.radius;
},
circumference:function () {
return 2*Math.PI*this.radius;
}
};
alert(circle.area()); // 78.53981633974483
circle.radius = 1.5;
alert(circle.circumference()); // 9.42477796076938
这个例子引入了JavaScript的this
表达式,这是一个相当强大的表达式,可以根据上下文表达出不同含义。当一个调用中引入了包含函数的对象时(就如上面的circle.area()
),this
指的就是这个包含函数的对象。
使用
this
表达式的函数属性称为方法
。因此,我们说circle
有一个area
方法和一个circumference
方法。
练习
-
将函数值用作对象属性的两个主要理由是什么?
谈谈自己理解,有大神有别的建议欢迎评论
更好的运用面向对象编程思维
对象在创建时自带操作函数(属性),存储在对象内部,作为对象的一部分存在。
下面的脚本会提示什么?为什么?
var x = 2;
var p = {
x:1,
y:1,
z:function () {
return x + this.x; // x引用的是全局变量,this.x指向的是p.x
},
};
alert(p.z()); // 3
5.5.3 构造器
在上一节,我们仅定义了一个circle
圆对象。但如果需要很多个圆,怎么办?
// 错误的示范
var Circle = function (r) {
return {
radius:r,
area:function () {
return Math.PI*this.radius*this.radius;
},
circumference:function () {
return 2*Math.PI*this.radius;
}
};
};
var c1 = Circle(2); // 创建一个半径为2的圆
var c2 = Circle(10); // 创建一个半径为10的圆
alert(c1.area()) // "314.1592653589793"
这段代码表面上看没问题,但有一个缺陷。每次创建一个圆,也另行创建了额外的面积和周长方法。
在创建多个圆时,会浪费大量的内存来保存面积和周长函数的冗余副本——这是很糟糕的事情,因为内存资源是有限的。当脚本耗尽内存就会崩溃。型号,JavaScript的原型prototype
提供了一种解决方案。
// 一个圆的原型,其设计目的是作为下面用Circle函数创建的所有圆的原型
var protoCircle = {
radius:1,
area:function () {return Math.PI*this.radius*this.radius;},
circumference:function () {return 2*Math.PI*this.radius;}
};
// 创建具有给定半径的圆
var Circle = function (r) {
var c= Object.create(protoCircle); // 将protoCircle原型创建到变量c中
c.radius = r; // c的_proto_指向protoCircle对象
return c;
};
每个通过调用Circle
创建的圆都有自己的radius
属性和一个隐藏链接,指向一个唯一的共享原型
,其中包含了area
和circumference
函数(分别只有一个)。这是极好的,不过还只是有小小缺陷。我们使用了两个全局变量Circle
和protoCircle
。如果只有一个就更好了,这样可以让我们的原型圆作为Circle
函数的一个属性。我们现在就有了一模式,用于很方便的定义一系列同种"类型"
的对象。
/* 一个圆数据类型。概要:
*
* var c = Circle(5);
* c.radius => 5
* c.area() => 25pi
* c.circumference() => 10pi
*/
var Circle = function (r) {
var circle = Object.create(Circle.prototype);
circle.radius = r;
return circle;
};
Circle.prototype = {
area:function () {return Math.PI*this.radius*this.radius},
circumference:function () {return 2*Math.PI*this.radius},
};
我们可以应用这一模式,生成一个用于创建矩形的函数。
/* 矩形数据类型。概要:
*
* var r = Rectangle(5,4);
* r.width => 5
* r.height => 4
* r.area() => 20
* r.perimeter() => 18
*/
var Rectangle = function (w,h) {
var rectangle = Object.create(Rectangle.prototype);
rectangle.width = w;
rectangle.height = h;
return rectangle;
};
Rectangle.prototype = {
area:function () {return this.width*this.height};
perimeter:function () {return 2*(this.width+this.height)}
};
全新方式:JavaScript中的每个函数对象都自动包含一个prototype
属性,prototype
是函数两个预定义属性中的第二个,第一个length
。只要函数一经定义,它的prototype
属性就会被初始化为一个全新对象。(这个全新对象有自己的一个属性,叫做constructor
)。
下图展示了一个新鲜出炉的函数,用于算两个值的平均值。
其次在使用函数创建对象时,只要是用来魔法操作符new,就无需明确连接原型,也无需返回新创建对象。当你在函数调用之前加上了new时,会发生三件事情。
JavaScript会创建一个全新的空对象,然后使用引用这个新对象的表达式this来调用此函数。
该构造对象的原型被设定为函数的prototype属性。
该函数会自动返回新的对象(除非你明确要求函数返回其他东西)
这些规则看上去很复杂,但看一个例子就清楚了。
产生一个圆的函数,如何使用new操作符来调用该函数,创建的圆的实例
/*一个圆数据类型。概要:
* var c = new Circle(5);
* c.radius => 5
* c.area() => 25pi
* c.circumference() => 10pi
*/
var Circle = function (r) {
this.radius = r;
};
Circle.prototype.area = function () {
return Math.PI*this.radius*this.radius;
};
Circle.prototype.circumference = function () {
return 2*Math.PI*this.radius;
};
var c1 = new Circle(2); // 创建半径为2的圆
var c2 = new Circle(10); // 创建半径为10的圆
alert(c2.area()); // "314.1592653589793"
此脚本先创建一个函数对象,我们将用变量Circle
引用它。和所有函数一样,创建它时,拥有一个第二对象,这个对象被prototye
属性引用。随后,我们向这个原型对象添加area
和circumference
函数。接下来我们调用new Circle
创建一对圆对象。操作符new
创建新的对象,这个对象其原型为Circle.prototype
。
根据设计,诸如Circle
这样的函数就是要用new
调用的,这种函数称为构造器。根据约定,我们用大写首字母命名,并省略return
语句,优先使用JavaScript的自动功能返回新创建的对象。之所以要约定使用大写首字母,原因在下一节给出。
没有return
语句的构造器调用将返回对象,而不是返回通常的undefined
,新创建对象的原型将被神奇地指定给一个从来不会显式创建的对象。
这种方法不够直接,这可能是JavaScript语言中要添加Object.create
的原因之一。一些JavaScript程序员建议对于新脚本仅使用Object.create
,因为这样可以让对象与其原型之间的链接更为明确。明确的代码更易读易懂易于处理。坚持使用Object.create
的另一个原因可能是出于哲学考虑:我们可以直接用对象来考虑问题,而不用另行引用“类型”的概念。
但是,我们不能放弃构造器和操作符new
。JavaScript从一开始就在使用它们,数以千计的现有脚本中都使用了它们,JavaScript的许多内置对象都是通过这些方式构建的,所以我们需要真正理解它们。通过一些练习可以熟悉它们,对目前来说,请复习以下步骤。
要使用操作符new
创建和使用一种自定义数据类型,比如圆:
编写一个构造器函数,通过体会
this.radius = r
这样的赋值语句,为每个圆初始化一个独有的属性;将所有圆共享的方法指定给
Circle.prototype
;通过调用
new Circle()
来创建特定圆。对于如此创建的每个圆,其原型将自动变为Circle.prototype
5.6 上下文(apply
和call
)
前面的在JavaScript——this、全局变量和局部变量混谈中已经给出前两种规则(全局作用域和函数作用域下的this引用),接下来要说一个注意点。
规则3
:当用一个以new
操作符调用的函数中时,this
引用指的是新创建的对象。
var Point = function (x,y ) {
this.x = x;
this.y = y;
};
var p = new Point(4,-5); // 新的实例
var q = Point(3,8); // 这里修改了全局变量x和y!
上面的最后一行表明,我们一定要非常注意,总是以new
来调用构造器,以免修改了已有的全局变量,导致脚本运行失控。为减少发生这种意外的可能性,JavaScript程序员用大写字母书写构造器的名字。可以使用一些工具(JSLint)来扫描代码,不要调用函数而不使用new前缀,很危险!
规则4
:利用函数方法apply
和call
,可以专门定义一个希望用作this
值的对象。
var f = function (a,b,c) {
this.x += a+b+c;
};
var a = {x:1,y:2};
f.apply(a,[10,20,5]); // 调用f(10,20,5),以"a"为this
f.call(a,3,4,15); // 调用f(3,4,15),以"a"为this
alert(a.x); // 58
var Point = function (x,y) {
this.x = x;
this.y = y;
};
var p = {z:3};
Point.apply(p,[2,9]); // 现在p为{x:2,y:9,z:3}
Point.call(p,10,4); // 现在p为{x:10,y:4,z:3}
这些方法允许借用(或劫持)现有的方法和构造器,将它们用于一些本来没打算为其使用的对象。这些方法稍有不同:call
会传送其实参,而apply
会将实参打包放在一个数组中。
练习:
this
引用有哪四种应用?
分别对应不同的作用域和上下文中,全局作用域、被当做方法调用、new
构造函数调用、apply
与call
。
以下脚本会输出什么?
var p = {
x:1,
f:function (y) {
this.x += y;
return this.x;
}
};
var q = {x:5};
alert(p.f(1));
alert(p.f.call(q,3));
先分析第一个alert
:既然是p.f
,接收方对象是p
,则this
引用指向p
。且原本的p
对象中,局部变量x
值为1,执行函数传参后,1 += 1;所以最后p.x
的值为2。
第二个alert
同样是p.f
,只是这次用了call
方法,这个方法可以借用现有的方法和构造器,也就是说,p.f
这个方法被借用了,给谁呢?对了括号内的q
对象,并传参3,此时this
引用指向了q
对象(注意当调用的一瞬间,this
已经指向了q
对象),且q
对象已经有q.x=5
,传参相加,最终结果q.x
值为8。
5.7高阶函数
考虑下面两个函数:
var squareAll = function (a) {
var result = [];
for (var i=0;i<a.length;i+=1) {
result[i]=a[i]*a[i];
}
return result;
};
var capitalizeAll = function (a) {
var result = [];
for (var i=0;i<a.length;i+=1) {
result[i]=a[i].toUpperCase();
}
return result;
};
这两个函数只有很小的一点不同,他们都是向一个数组中的每个元素应用一个函数,并收集结果;但是,第一个函数是计算这些元素的平方,而第二个函数则是将这些元素变为大写。我们能不能仅为共同结构编写一次代码,然后用参数来实现它们之间的小小区别?
var collect = function (a,f) {
var result = [];
for (var i=0;i<a.length;i+=1) {
result[i]=f(a[i]);
}
return result;
};
对每个数组元素实际执行的函数(比如求平方或转换为大写)现在作为实参传送。
var square = function (x) {return x*x};
var capitalize = function (x) {return x.toUpperCase();};
var squareAll = function (a) {return collect(a,square);};
var capitalizeAll = function (a) {return collect(a,capitalize)};
对于这些小小的square和capitalize函数,我们甚至可以不为其声明变量。
var squareAll = function (a) {
return collect(a,function (x) {return x*x};)
};
var capitalizeAll = function (a) {
return collect(a,function (x) {return x.toUpperCase();});
};
好,来看看它们如何工作的。
var arr1 = [-2,5,0];
var arr2 = ["hi","ho"];
alert(squareAll(arr1));
alert(capitalizeAll(arr2));
函数f接受一个另一个函数g作为其实参(并在自己体内调用g),这种函数f称为高阶函数。函数collect称为高阶函数,内置的sort也是如此。我们可以向sort传送一个比较函数,使它采用不同的排序方式。比比较函数就是我们自己编写的一个两实参函数,当第一个实参小于第二个时返回一个负值,当两个实参相等时返回0,当第一个实参较大时则返回一个正值。
var a = [3,6,10,1,40,25,8,73];
alert(a.sort()); // 按字母排序
alert(a.sort(function (x,y) {return x-y;})); // 按数值递增排序
alert(a.sort(function (x,y) {return y-x;})); // 按数值递减排序
因为我们可以告诉sort
函数,按照我们喜欢的任意方式来比较元素,所以可以编写一些代码,用几种不同方式对一组对象进行排序。
在web页上设置定时器、与用户操作进行交流时,经常会传送函数。它也是人工智能编程中最为重要的程序设计范例之一。且有助于构建非常大的分布式应用程序。
高阶函数一词不仅适用于以函数为实参的函数,还适用于返回函数的函数。
var withParentheses = function (s) {return "("+s+")";};
var withBrackets = function (s) {return "["+s+"]";};
var withBraces = function (s) {return "{"+s+"}";};
这三个函数非常类似。可以怎样进行重构呢?这三个函数中的每一个都可以由另一函数构造而成,只需告诉构造者要使用那种分隔符即可。
var delimitWith = function (prefix,suffix) {
return function (s) {return prefix+s+suffix;}
};
var withParentheses = delimitWith("("+s+")");
var withBrackets = delimitWith("[","]");
var withBraces = delimitWith("{","}");
withParentheses、withBrackets、withBraces
这三个函数都成为闭包。粗略的说,JavaScript闭包是一种函数,它的函数体使用了来自外围(enclosing)函数的变量。闭包在一些非常高级复杂的java结构中扮演着不可或缺的角色。
5.8函数声明与函数表达式
function circleArea(x) {
return Math.PI*Math.pow(x,2)
};
这种形式的函数官方名称为函数声明,它的工作方式与前者非常相似,但是这两种定义形式是不同的。
函数声明不能出现在代码中的某些地方。
通过函数声明引入的变量遵循不同于普通变量的作用域规则。
具体来说,函数声明只能出现在脚本中的全局位置,或者出现在一个函数体的"顶级",不允许只出现在语句内部。根据官方的EA规范,下面代码出现移一处语法错误:
if (true) {
function successor() {return x+1;} // 不允许
}
这里的戒律是:即便浏览器允许,也绝对不要将函数声明放在一条语句内。
无论是否选择使用函数声明,它们的存在都会影响我们编写特定表达式的方式,因为函数声明一以单词function开头,所以JavaScript的设计者决定任何语句都不能以这个单词开头,以免读者混淆。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。