Javascript面向对象编程指南(四)——函数也是数据

函数也是数据

在Javascript中,函数实际上也是数据。这概念对于我们日后的学习至关重要。也就是说,我们可以把一个函数赋值给一个变量。

var f = function () {
  return 1;
}

上面这种定义方式通常被叫做函数标识记法

function (){return 1;}是一个函数表达式。表达式可以被命名,成为命名函数表达式所以以下这种情况也是合法的。虽然我们不常常用到(在这里,myFunc是函数的名字,而不是变量)

var f = function myFunc(){
  return 1;
}

这样看起来,似乎命名函数表达式和函数声明没有什么区别。但他们其实是不同的。两者的差别表现于它们的上下文。(以后会举例来阐明这些概念)

如果我们对函数变量调用typeof,操作符返回的字符串将会是"function"

所以,Javascript中的函数也是一种数据,只不过这种特殊的数据类型有两个重要的特性:

  • 它们所包含的是代码
  • 它们是可以执行的(或者说是可以调用的)

由于函数也是赋值给变量的一种数据,所以函数的命名规则与一般变量相同——即函数名不能以数字开头,并且可以由任意的字母、数字、下划线和$组合而成。

匿名函数

我们可以这样定义一个函数;

var f = function (a){
return a;
}

通过这种方式定义的函数常被称为匿名函数(即没有名字的函数),特别是当它不被赋值给变量单独使用的时候。在这种情况下,此类函数有两种优雅的用法:

  • 你可以将匿名函数作为参数传递给其他函数,这样接收方函数就能利用我们所传递的函数来完成某些事情。
  • 你可以定义某个匿名函数来执行某些一次性任务。

接下来,我们来看两个具体的应用示例。通过其中的细节来进一步了解匿名函数。

回调函数

既然函数可以与任何可以被赋值给变量的数据是相同的,那么它当然可以像其他数据那样被定义、删除、拷贝,以及当成参数传递给其他函数。

在下面的示例中,我们定义了一个函数,这个函数有两个函数类型的参数,然后它们会分别执行这两个参数所指向的函数,并返回它们的返回值之和。

function invokeAdd(a,b) {
  return a() + b();
}

下面我们来简单定义一下这两个参与加法运算的函数(使用函数声明模式),它们只是单纯地返回一个固定值:

function one() {
  return 1;
}

function two() {
  return 2;
}

现在,我们只需将这两个函数传递给目标函数invokeAdd(),就可以得到执行的结果了。

invokeAdd(one, two);  // 3

事实上,我们也可以直接用匿名函数(即函数表达式)来代替one()和two(),以作为目标函数的参数,例如:

invokeAdd(
  function () { return 1; },
  function () { return 2; }
);

当我们将函数A传递给函数B,并又B来执行A时,A就成了一个回调函数(callback function),如果这时A还是一个无名函数,我们就称它为匿名回调函数。

那么,应该什么时候使用回调函数呢?下面我们将通过几个应用实例来示范下回调函数的优势,包括:

  • 它可以让我们在不做命名的情况下传递函数(这意味着可以节省变量的使用)
  • 我们可以将一个函数调用错做委托给另一个函数(这以为着可以节省一些代码编写工作)
  • 它们也有助于提升性能

回调示例

在编程过程中,我们通常需要将一个函数的返回值传递给另一个函数。在下面的例子中,我们定义了两个函数;第一个是multiplyByTwo(),该函数会通过一个循环将其所接受的三个参数分别乘以2,并一数组的形式返回结果;第二个函数addOne()只接受一个值,然后将它加1并返回。

function multiplyByTwo(a,b,c){
  var i, arr = [];
  for(i = 0; i < 3; i++){
    arr[i] = arguments[i] * 2;
  }
  retuen arr;
}

function (a) {
  return a + 1;
}

现在我们来测试下这两个函数,结果如下:

multiplyByTwo(1,2,3)  // [2,4,6]
addOne(100)  // 101

接下来,假设我们又三个元素,我们要实现这三个元素在两个函数之间的传递。这需要定义另一个数组,用于存储来自第一步的结果。我们先从multiplyByTwo()的调用开始。

var myarr = [];
myarr = multiplyByTwo(10,20,30);

然后,用循环来遍历每个元素,并将它们分别传递给addOne().

for(var i = 0; i < 3; i++){
  myarr[i] = addOne(myarr[i]);
}
myarr;  // [21,41,61]

如你所见,这段代码可以工作,但是显然还有一定的改善空间。特别是我们这里使用了两个循环,如果数据量很大或循环操作很复杂的话,开销一定不小。因此,我们需要将他们合二为一。这就需要对multiplyByTwo()函数做一些改动,使其接受一个回调函数,并在每次迭代操作中调用它。具体如下:

function multipslyByTwo(a,b,c,callback){
  var i, arr = [];
  for(i = 0; i < 3; i++){
    arr[i] = callback(arguments[i] * 2);
  }
  return arr;
}

函数修改完成之后,之前的工作只需要一次函数调用就够了,我们只需要像下面这样同时将初始值和会调函数传递给它:

myarr = multipslyByTwo(1,2,3,addOne);
// [3,5,7]

同样,我们还可以用匿名函数来代替addOne(),这样做可以节省一个额外的全局变量。

multipslyByTwo(1,2,3,function (a){
  return a + 1;
})
// [3,5,7]

而且,使用匿名函数也更抑郁随时根据需求来调整代码。

即时函数

目前我们已经讨论了匿名函数在回调方面的应用。接下来,我们来看匿名函数的另一个应用示例——这种函数可以在顶以后立即调用。比如:

(
  function () {
    alert('Bob')
  }
) ();

这种语法看上去有点吓人,但其实很简单——我们只需要将匿名函数的定义放进一对括号中,然后再紧跟一堆括号即可。其中,第二队括号起到的“立即调用”的作用,同时它也是我们向匿名函数传递参数的地方。

(
  function (name) {
    alert('Hello' + name + ' ! ');
  }
) ('dude');

另外,你也可以将第一对括号闭合于第二对括号之后。这两种做法都有效。

(
  function () {
    // dosomething
  } () )

//vs

(function () {
  //dosomething
}) ()

使用即时(自调)匿名函数的好处是不会产生任何全局变量,当然,缺点在于这样的函数是无法重复执行的(除非你将它放在某个循环或者其他函数中),这样也使得即时函数非常适合执行一些一次性的或初始化的任务。

如果需要的话,即时函数也可以有返回值,虽然并不常见。

内部(私有)函数

函数与其他类型的值本质上是一样的,因此,没有什么理由可以阻止我们在一个函数内部定义另一个函数。

function outer(param) {
  function inner(theinput) {
    return theinput * 2;
  }
  return 'The result is' + inner(param)
}

当我们调用全局函数outer()时,本地函数inner()也会在其内部调用。由于inner()是本地函数,它在outer()以外的地方是不可见的,所以我们也能将它称为私有函数。

outer(2)  // The result is 4
inner(2)  // inner is not defined

使用私有函数的好处有以下几点:

  • 有助于我们确保全局名字空间的纯净性(这意味着命名冲突的机会很小)
  • 确保私有性——这使我们可以选择只讲一些必要的函数暴露给“外部世界”,而保留属于自己的函数,使他们不为该应用程序的其他部分所用。

返回函数的函数

正如之前所提到的,函数始终都会有一个返回值,即便不是显示返回,它也会隐式返回一个undefined。既然函数能返回一个唯一值,那么这个值也有可能是另一个函数。例如:

function a() {
  alert('A!');
  return funtion () {
    alert('B!');
  }
}

在这个例子中,函数a()会在执行它工作(弹出A!)之后返回另一个函数。而所返回的函数又会去执行另外一些事情(弹出B!)。我们只需将返回值赋值给某个变量,然后就可以像使用一般函数那样调用它了。

var newFunc = a();
newFunc();

上面第一行执行的是alert('A!'),第二行才是alert('B!')。

如果你像让返回的函数立即执行,也可以不用将它赋值给变量,直接在该调用后面再加一对括号,效果是一样的:

a() ();

能重写自己的函数

由于一个函数可以返回另一个函数,一次我们可以用新的函数来覆盖旧的。例如在之前的例子中,我们也可以通过a()的返回值来重写a()函数自己:

a = a();

当然这句话依然只执行alert('A!'),但如果我们再次调用a(),他就会执行alert('B!')了。这对于要执行某些一次性初始化工作的函数来说会非常有用。这样一来,该函数可以在第一次被调用后重写在即,从而避免了每一次调用时重复一些不必要的操作。

在上的例子中,我们实在外面来重定义该函数的——即我们将函数返回值赋值给函数本身。但是们也可以让函数从内部重写自己。例如:

function a(){
  alert("A!");
  a = function () {
    alert("B!")
  }
}

这样一来,当我们第一次调用该函数时,会有如下情况发生:

  • alert('A!')将会被执行(可以视之为一次性的准备操作)。
  • 全局变量a将会被重定义,并赋予新的函数。

而如果该函数被调用的话,被执行就是alert('B!')了。

下面我们来看一个组合型的应用示例,其中有些技术我们会在以后的章节中讨论。

var a = (function () {
  function someSutup () {
    var setup = 'done';
  }
  function actualWork() {
    alert('worky-worky')
  }
  someSetup();
  return actualWork; 
} () );

在这个列子中有如下情况:

  • 我们使用了私有函数——someSetup()和actualWork().
  • 我们也使用了即时函数——函数a()的定义后面有一对括号,因此它会执行自调。
  • 当函数第一次被调用时,它会调用someSetup(),并返回函数变量actualWork的引用。请注意,返回值中是不带括号的,因此该结果仅仅是一个函数的引用,并不会产生函数调用。
  • 由于这里的执行语句是以var a = ...开头的,因而该自调函数所返回的值会重新赋值给a。

好了,我们可以尝试一下以下的问题:上面的代码在以下情景中分别会alert()什么内容?

  • 当它最初被载入时。
  • 之后再次调用a()时。

这项技术对于某些浏览器相关的操作会相当有用。因为在不同的浏览器中,实现相同任务的方法可能不同,我们都知道浏览器的特性不可能行为函数调用而发生任何改变,因此,最好的选择就是让函数根据其当前所在的浏览器来从新定义自己。这就是所谓的“浏览器兼容性探测”技术。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容