绝大部分语言都有自己专门的面向对象的语法,而 JavaScript 没有:它是通过函数来实现面向对象特性的。
定义和使用函数
什么是函数
所谓函数,本质上是一种代码的分组形式。我们可以通过这种形式赋予某组代码一个名字,以便于之后的调用。
//函数的声明
function sum(a, b) {
var c = a + b;
return c;
}
函数声明通常由以下几部分组成
-
关键词 [function]
-
函数名称,即这里的 sum
-
函数所需的参数,即这里的 a、 b。一个函数通常都具有 0 个或多个参数。参数之间用逗号分隔
-
函数所要执行的代码块,我们称之为函数体
-
return 子句。函数通常都会有返回值,如果某个函数没有显式的返回值,我们就会默认它的返回值为 undefined。
调用函数
调用的方式很简单,只需在函数名后面加一对用以传递参数的括号即可
var result = sum(1, 2);
result;
3
参数
但如果您设定了,而又在调用时忘了传递相关的参数值, JavaScript 引擎就会自动将其设定为 undefined。例如在下面这个调用中,函数返回的是 NaN,因为这里试图将 1 与 undefined 相加。
sum(1);
NaN
-
形式参数 形参是指定义函数时所用的那些参数
-
实际参数 实参则指的是在调用函数时所传递的那些参数
function sum(a, b){
return a + b;
}
sum(1, 2);
对于那些已经传递进来的参数, JavaScript 是来者不拒的。但是,即便我们向 sum()传递再多的参数,多余的那部分也只会被默默地忽略掉
arguments 变量
该变量为内建变量,每个函数中都能调用。它能返回函数所接收的所有参数
function args() {
return arguments;
}
args();
[]
args( 1, 2, 3, 4, true, 'ninja');
[1, 2, 3, 4, true, "ninja"]
arguments 实际上不是一个数组(虽然它有很多数组的特性),而是一 个类似数组的对象。
预定义函数
JavaScript 引擎中有一组可供随时调用的内建函数
- parseInt()
parseInt()会试图将其收到的任何输入值(通常是字符串)转换成整数类型输出。如果转换失败就返回 NaN
该函数还有个可选的第二参数:基数(radix),它负责设定函数所期望的数字类型—十进制、十六进制、二进制等
如果我们在调用 parseInt()时没有指定第二参数,函数就会将其默认为十进制,但有两种情况例外。
-
如果首参数字符串是 0x 开头,第二参数就会被默认指定为 16(也就是默认其为十六进制数)。
-
如果首参数以 0 开头, 第二参数就会被默认指定为 8(也就是默认其为八进制数)。
- parseFloat()
parseFloat()的功能与 parseInt()基本相同,只不过它仅支持将输入值转换为十进制数。因此,该函数只有一个参数。
此外, parseFloat()还可以接受指数形式的数据(这点与 parseInt()不同)
-
isNaN() 通过 isNaN(),我们可以确定某个输入值是否是一个可以参与算术运算的数字
-
isFinite() isFinite()可以用来检查输入是否是一个既非 Infinity 也非 NaN 的数字。
转义特殊字符
-
encodeURI()
-
decodeURI()
分别都有各自对应的反编码函数
-
encodeURIComponent()
-
decodeURIComponent()
- eval() eval()会将其输入的字符串当做 JavaScript 代码来执行
变量的作用域
即在 JavaScript 中,变量的定义并不是以代码块作为作用域的,而是以函数作为作用域
var global = 1;
function f() {
var local = 2;
global++;
return global;
}
> f();
2
> f();
3
> local;
ReferenceError: local is not defined
这里还有一点很重要,如果我们声明一个变量时没有使用 var 语句,该变量就会被默认为全局变量。
在该函数被调用之前,这个变量是不存在的。该变量会在函数首次被调用时创建,并被赋予全局作用域。这使得我们可以在该函数以外的地方访问它。
变量提升
var a = 123;
function f() {
alert(a);
var a = 1;
alert(a);
}
f();
但事实并非如此,第一个 alert()实际上显示的是undefined,这是因为函数域始终优先于全局域,所以局部变量 a 会覆盖掉所有与它同名的全局变量,尽管在 alert()第一次被调用时, a 还没有被正式定义(即该值为undefined),但该变量本身已经存在于本地空间了。这种特殊的现象叫做提升(hoisting)。
函数也是数据
在 JavaScript 中,函数实际上也是一种数据
var f = function() {
return 1;
};
上面这种定义方式通常被叫做函数标识记法(function literal notation)。
function(){ return 1;}是一个函数表达式。函数表达式可以被命名,称为命名函数表达式(named function expression, NFE)。所以以下这种情况也是合法的,虽然我们不常常用到(在这里, myFunc 是函数的名字,而不是变量; IE 会错误地创建 f 和 myFunc两个变量)
var f = function myFunc() {
return 1;
};
这样看起来,似乎命名函数表达式与函数声明没有什么区别。但它们其实是不同的。两者的差别表现于它们所在的上下文。
如果我们对函数变量调用 typeof,操作符返回的字符串将会是"function"。所以, JavaScript 中的函数也是一种数据,只不过这种特殊的数据类型有两个重要的特性
-
它们所包含的是代码
-
它们是可执行的(或者说是可调用的)
将函数拷贝给不同的变量
> var sum = function(a, b) {
return a + b;
};
> var add = sum;
> typeof add;
"function"
> add(1, 2);
3
匿名函数
定义一个函数:
var f = function(a){
return a;
};
通过这种方式定义的函数常被称为匿名函数(即没有名字的函数)
-
您可以将匿名函数作为参数传递给其他函数,这样,接收方函数就能利用我们所传递的函数来完成某些事情
-
您可以定义某个匿名函数来执行某些一次性任务
回调函数
既然函数与任何可以被赋值给变量的数据是相同的,那么它当然可以像其他数据那样 被定义、删除、拷贝,以及当成参数传递给其他函数。
function invokeAdd(a, b){
return a() + b();
}
function one() {
return 1;
}
function two() {
return 2;
}
> invokeAdd(one, two);
3
//另外可以直接使用匿名函数
> invokeAdd(
function () { return 1; },
function () { return 2; }
);
3
什么时候使用回调函数呢?回调函数的优势,包括:
-
它可以让我们在不做命名的情况下传递函数(这意味着可以节省变量名的使用)
-
我们可以将一个函数调用操作委托给另一个函数(这意味着可以节省一些代码编写工作)
-
它们也有助于提升性能
回调示例
//三个参数分别乘以 2
function multiplyByTwo(a, b, c) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = arguments[i] * 2;
}
return ar;
}
//传参+1
function addOne(a) {
return a + 1;
}
> var myarr = [];
> myarr = multiplyByTwo(10, 20, 30);
[20, 40, 60]
//然后,用循环遍历每个元素,并将它们分别传递给 addOne()。
> for (var i = 0; i < 3; i++) {
myarr[i] = addOne(myarr[i]);
}
> myarr;
[21, 41, 61]
//改善方案
function multiplyByTwo(a, b, c, callback) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = callback(arguments[i] * 2);
}
return ar;
}
> myarr = multiplyByTwo(1, 2, 3, addOne);
[3, 5, 7]
//同样, 我们还可以用匿名函数来代替 addOne()
> multiplyByTwo(1, 2, 3, function (a){
return a + 1;
});
[3, 5, 7]
即时函数
这种函数可以在定义后立即调用
//方式一
(
function(){
alert('boo');
}
)();
//方式二
(
function(name){
alert('Hello ' + name + '!');
}
)('dude');
另外,您也可以将第一对括号闭合于第二对括号之后。这两种做法都有效。
(function () {
// …
} () );
// vs.
(functioin () {
// …
})();
使用即时(自调)匿名函数的好处是不会产生任何全局变量。
缺点在于这样的函数是无法重复执行的(除非您将它放在某个循环或其他函数中)。即时函数非常适合于执行一些一次性的或初始化的任务
内部(私有)函数
一个函数内部定义另一个函数
function outer(param) {
function inner(theinput) {
return theinput * 2;
}
return 'The result is ' + inner(param);
}
//我们也可以改用函数标识记法来写这段代码:
var outer = function (param) {
var inner = function (theinput) {
return theinput * 2;
};
return 'The result is ' + inner(param);
};
当我们调用全局函数 outer()时,本地函数 inner()也会在其内部被调用。
使用私有函数的好处主要有以下几点:
-
有助于我们确保全局名字空间的纯净性(这意味着命名冲突的机会很小)。
-
确保私有性—这使我们可以选择只将一些必要的函数暴露给“外部世界”,而保留属于自己的函数,使它们不为该应用程序的其他部分所用
返回函数的函数
函数始终都会有一个返回值,即便不是显式返回,它也会隐式返回一个 undefined。既然函数能返回一个唯一值,那么这个值就也有可能是另一个函数。
function a() {
alert('A!');
return function(){
alert('B!');
};
}
在这个例子中,函数 a()会在执行它的工作(弹出'A!')之后返回另一个函数。而所返回的函数又会去执行另外一些事情(弹出'B!')。我们只需将该返回值赋值给某个变量,然后就可以像使用一般函数那样调用它了。
> var newFunc = a();
> newFunc();
//第一行执行的是 alert('A!'),第二行才是 alert ('B!')
如果您想让返回的函数立即执行,也可以不用将它赋值给变量,直接在该调用后面再加一对括号即可,效果是一样的
> a()();
能重写自己的函数
由于一个函数可以返回另一个函数,因此我们可以用新的函数来覆盖旧的。例如在之前的例子中,我们也可以通过 a()的返回值来重写 a()函数自己:
> a = a();
外面来重定义该函数的—即我们将函数返回值赋值给函数本身。但我们也可以让函数从内部重写自己。例如:
function a() {
alert('A!');
a = function(){
alert('B!');
};
}
这样一来,当我们第一次调用该函数时会有如下情况发生
-
alert ('A!')将会被执行(可以视之为一次性的准备操作)。
-
全局变量 a 将会被重定义,并被赋予新的函数。
而如果该函数再被调用的话,被执行的就将是 alert ('B!')了
var a = (function () {
function someSetup () {
var setup = 'done';
}
function actualWork() {
alert('Worky-worky');
}
someSetup();
return actualWork;
}() );
闭包
作用域链
尽管 JavaScript 中不存在大括号级的作用域,但它有函数作用域,也就是说,在某函数内定义的所有变量在该函数外是不可见的。但如果该变量是在某代码块中被定义的(如在某个 if 或 for 语句中),那它在代码块外是可见的。
> var a = 1;
> function f() {
var b = 1;
return a;
}
> f();
1
> b;
ReferenceError: b is not defined
在这里,变量 a 是属于全局域的,而变量 b 的作用域就在函数 f()内了。所以:
-
在 f()内, a 和 b 都是可见的;
-
在 f()外, a 是可见的, b 则不可见。
利用闭包突破作用域链
var a = "global variable";
var F = function () {
var b = "local variable";
var N = function () {
var c = "inner local";
};
};
究竟是如何突破作用域链的呢?我们只需要将它们升级为全局变量(不使用var 语句)或通过 F 传递(或返回)给全局空间即可
闭包#1
首先,我们先来看一个函数。这个函数与之前所描述的一样,只不过在 F 中多了返回N,而在函数 N 中多了返回变量 b, N 和 b 都可通过作用域链进行访问。
var a = "global variable";
var F = function () {
var b = "local variable";
var N = function () {
var c = "inner local";
return b;
};
return N;
};
//函数 F 中包含了局部变量 b,因此后者在全局空间里是不可见的。
> b;
ReferenceError: b is not defined
函数 N 有自己的私有空间,同时也可以访问 f()的空间和全局空间,所以 b 对它来说是可见的。因为 F()是可以在全局空间中被调用的(它是一个全局函数),所以我们可以将它的返回值赋值给另一个全局变量,从而生成一个可以访问 F()私有空间的新全局函数。
> var inner = F();
> inner();
"local variable"
闭包#2
下面这个例子的最终结果与之前相同,但在实现方法上存在一些细微的不同。在这里 F()不再返回函数了,而是直接在函数体内创建一个新的全局函数 inner()
var inner; // placeholder
var F = function (){
var b = "local variable";
var N = function () {
return b;
};
inner = N;
};
//现在,请读者自行尝试, F()被调用时会发生什么:
> F();
> inner();
"local variable".
我们在 F()中定义了一个新的函数 N(),并且将它赋值给了全局变量 inner。由于N()是在 F()内部定义的,它可以访问 F()的作用域,所以即使该函数后来升级成了全局函数,但它依然可以保留对 F()作用域的访问权。
相关定义与闭包#3
事实上,每个函数都可以被认为是一个闭包。因为每个函数都在其所在域(即该函数的作用域)中维护了某种私有联系。但在大多数时候,该作用域在函数体执行完之后就自行销毁了—除非发生一些有趣的事(比如像上一小节所述的那样),导致作用域被保持。但其实每个函数本身就是一个闭包,因为每个函数至少都有访问全局作用域的权限,而全局作用域是不会被破坏的。
//子函数返回的则是其父函数的参数
function F(param) {
var N = function(){
return param;
};
param++;
return N;
}
//然后我们可以这样调用它:
> var inner = F(123);
> inner();
124
请注意,当我们的返回函数被调用时②, param++已经执行过一次递增操作了。所以inner()返回的是更新后的值。由此我们可以看出,函数所绑定的是作用域本身,而不是在函数定义时该作用域中的变量或变量当前所返回的值。
循环中的闭包
//该新函数会被添加到一个数组中,并最终返回
function F() {
var arr = [], i;
for (i = 0; i < 3; i++) {
arr[i] = function () {
return i;
};
}
return arr;
}
//下面,我们来运行一下函数,并将结果赋值给数组 arr。
> var arr = F();
//按通常的估计,它们应该会依照循环顺序分别输出 0、 1 和 2,下面就让我们来试试:
> arr[0]();
3
> arr[1]();
3
> arr[2]();
3
显然,这并不是我们想要的结果。究竟是怎么回事呢?原来我们在这里创建了三个闭包,而它们都指向了一个共同的局部变量 i。但是,闭包并不会记录它们的值,它们所拥有的只是相关域在创建时的一个连接(即引用)。在这个例子中,变量 i 恰巧存在于定义这三个函数域中。对这三个函数中的任何一个而言,当它要去获取某个变量时,它会从其所在的域开始逐级寻找那个距离最近的 i 值。由于循环结束时 i 的值为 3,所以这三个函数都指向了这一共同值
//那么,应该如何纠正这种行为呢?答案是换一种闭包形式:
function F() {
var arr = [], i;
for(i = 0; i < 3; i++) {
arr[i] = (function (x){
return function () {
return x;
}
}(i));
}
return arr;
}
//这样就能获得我们预期的结果了:
> var arr = F();
> arr[0]();
0
> arr[1]();
1
> arr[2]();
2
在这里,我们不再直接创建一个返回 i 的函数了,而是将 i 传递给了另一个即时函数。在该函数中, i 就被赋值给了局部变量 x,这样一来,每次迭代中的 x 就会拥有各自不同的值了。或者,我们也可以定义一个“正常点的”内部函数(不使用即时函数)来实现相同的 功能。要点是在每次迭代操作中,我们要在中间函数内将 i 的值“本地化
function F() {
function binder(x) {
return function(){
return x;
};
}
var arr = [], i;
for(i = 0; i < 3; i++) {
arr[i] = binder(i);
}
return arr;
}
getter 与 setter
var getValue, setValue;
(function() {
var secret = 0;
getValue = function(){
return secret;
};
setValue = function (v) {
if (typeof v === "number") {
secret = v;
}
};
}());
在这里,所有一切都是通过一个即时函数来实现的,我们在其中定义了全局函数setValue()和 getValue(),并以此来确保局部变量 secret 的不可直接访问性。
> getValue();
0
> setValue(123);
> getValue();
123
> setValue(false);
> getValue();
123
迭代器
将一些“谁是下一个”的复杂逻辑封装成易于使用的 next()函数,然后,我们只需要简单地调用 next()就能实现对于相关的遍历操作了
function setup(x) {
var i = 0;
return function(){
return x[i++];
};
}
> next();
"a"
> next();
"b"
> next();
"c"
本章小结
-
定义和调用函数的基础知识—您既可以使用函数声明语法,也可以使用函数表达式
-
函数的参数及其灵活性
-
内置函数—包括 parseInt()、 parseFloat()、 isNaN()、 isFinite()、 eval() 以及对 URL 执行编码、反编码操作的四个相关函数。
-
JavaScript 变量的作用域—尽管这些变量没有大括号级作用域,但它有函数作用域以及相关的作用域链。
-
函数也是一种数据—即函数可以跟其他数据一样被赋值给一个变量,我们可以据此 实现大量有趣的应用。例如:
-
私有函数和私有变量
-
匿名函数
-
回调函数。
-
即时函数
-
能重写自身的函数。
-
-
闭包
网友评论