javascript函数与对象的混乱关系,javascript 的函数和方法区别

首页 > 数码 > 作者:YD1662024-06-30 12:22:33

点击标题下「异步社区」可快速关注

本文包括以下内容:

在本文这一部分讨论JavaScript基础时,也许你会感到惊讶,我们的第一个论点是函数(function)而非对象(object)。当然,第3部分会用大量笔墨解释对象,但归根结底,你要理解一些基本事实,像普通人一样编写代码和像“忍者”一样编写代码的最大差别在于是否把JavaScript作为函数式语言(functional language)来理解。对这一点的认知水平决定了你编写的代码水平。

如果你正在阅读这本文,那么你应该不是一位初学者。对于后续内容,我们假设你已经足够了解面向对象基础(当然,我们会在以后章节详细讨论对象的高级概念),但真正理解JavaScript中的函数才是你能使用的唯一一件重要武器。函数是如此重要,所以本文及接下来两章将带领你彻底理解JavaScript中的函数。

JavaScript中最关键的概念是:函数是第一类对象(first-class objects),或者说它们被称作一等公民(first-class citizens)。函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递。本文一开始会介绍面向函数编程带来的差异,你会发现,在需要调用某函数的位置定义该函数,能让我们编写更紧凑、更易懂的代码。其次,我们还会探索如何把函数用作第一类对象来编写高性能函数。你能学到多种不同的函数定义方式,甚至包括一些新类型,例如箭头(arrow)函数,它能帮你编写更优雅的代码。最后,我们会学习函数形参和函数实参的区别,并重点关注ES6的新增特性,例如剩余参数和默认参数。

让我们通过了解函数式编程的优点来开始学习吧。

你知道吗?

1.1 函数式的不同点到底是什么

函数及函数式概念之所以如此重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,我们编写的所有的脚本代码都将在一个函数内执行。

由于我们的大多数代码会作为函数调用来执行,因此,我们在编写代码时,通用强大的构造器能赋予代码很大的灵活性和控制力。本文的大部分内容解释了如何利用函数作为第一类对象的特性获益。首先浏览一下对象中我们能使用的功能。JavaScript中对象有以下几种常用功能。

1var ninja = {};  ?--- 为变量赋值一个新对象

2ninjaArray.push({});   ?--- 向数组中增加一个新对象

3ninja.data = {};  ?--- 给某个对象的属性赋值为一个新对象

1function hide(ninja){

2  ninja.visibility = false;

3}

4hide({});  ?--- 一个新创建的对象作为参数传递给函数

1function returnNewNinja() {

2 return {};  ?--- 从函数中返回了一个新对象

3}

1var ninja = {};

2ninja.name = "Hanzo";  ?--- 为对象分配一个新属性

其实,不同于很多其他编程语言,在JavaScript中,我们几乎能够用函数来实现同样的事。

1.1.1 函数是第一类对象

JavaScript中函数拥有对象的所有能力,也因此函数可被作为任意其他类型对象来对待。当我们说函数是第一类对象的时候,就是说函数也能够实现以下功能。

1function ninjaFunction() {}

1var ninjaFunction = function() {};  ?--- 为变量赋值一个新函数

2ninjaArray.push(function(){});   ?--- 向数组中增加一个新函数

3ninja.data = function(){};  ?--- 给某个对象的属性赋值为一个新函数

1function call(ninjaFunction){

2 ninjaFunction();

3}

4call(function(){});   ?--- 一个新函数作为参数传递给函数

1function returnNewNinjaFunction() {

2 return function(){};   ?--- 返回一个新函数

3}

1var ninjaFunction = function(){};

2ninjaFunction.ninja = "Hanzo";  ?--- 为函数增加一个新属性

对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

{JavaScript中的函数式编程!} 

  • 把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块化。不过这是一个很大的话题,因此本文仅对这个特性做了肯定。如果你对如何在JavacScript中利用函数式编程感兴趣,推荐阅读Luis Atencio著(由Manning出版社2016年出版)的《JavaScript函数式编程》,购买方式见www.manning.com/ books/functional-programming- in-JavaScript。

第一类对象的特点之一是,它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传入另一个函数,传入函数会在应用程序执行的未来某个时间点才执行。大家所知道的更一般的概念是回调函数(callback function)。下面我们来学习这个重要概念。

1.1.2 回调函数

每当我们建立了一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,我们都是在建立一个回调(callback)。这个术语源自于这样一个事实,即在执行过程中,我们建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”。

有效运用JavaScript的关键在于回调函数,相信你已经在代码中使用了很多回调函数——不论是单击一次按钮、从服务端接收数据,还是UI动画的一部分。

本节我们会看一些实际使用回调函数的典型例子,例如处理事件、简单的排序集合。这部分内容会有点复杂,所以在深入学习之前,先透彻、完整地理解回调函数的概念,用最简单的形式来展现它。下面我们用一个简单例子来阐明这个概念,此例中的函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数:

1function useless(ninjaCallback) {

2 return ninjaCallback();

3}

这个函数可能没什么用,但它反映了函数的一种能力,即将函数作为另一个函数的参数,随后通过参数来调用该函数。

我们可以在清单1.1中测试一下这个名为useless的函数。

清单1.1 简单的回调函数例子

1var text = "Domo arigato!";

2report("Before defining functions");

3function useless(ninjaCallback) {

4  report("In useless function");

5  return ninjaCallback();

6}  ?--- 函数定义,参数为一个回调函数,其函数体内会立即调用该回调函数

7function getText() {

8 report("In getText function");

9 return text;

10}  ?--- 简单的函数定义,仅返回一个全局变量

11report("Before making all the calls");

12assert(useless(getText) === text,

13     "The useless function works! " text);   ?--- 把gerText作为回调函数传入上面的useless函数

14report("After the calls have been made");

在这个代码清单中,我们使用自定义函数report(在本文附录B中定义)来输出代码执行过程中的信息,这样一来我们就能通过这些信息来跟踪程序的执行过程。我们还使用了第1章中的断言函数assert。该函数通常使用两个参数。第一个参数是用于断言的表达式。本例中,我们需要确定使用参数getText调用useless函数返回的值与变量text是否相等(useless(getText) === text)。若第一个参数的执行结果为true,断言通过;反之,断言失败。第二个参数是与断言相关联的信息,通常会根据通过/失败来输出到日志上。(附录B中概括地探讨了测试,以及我们对assert函数和report函数的简单实现)。

这段代码执行完毕后,执行结果如图1.1所示。可以看到,使用getText参数调用useless回调函数后,得到了期望的返回值。

javascript函数与对象的混乱关系,javascript 的函数和方法区别(1)

图1.1 清单1.1中代码的执行结果

我们还可以看看这个简单的回调函数具体是如何执行的。如图1.2所示,getText函数作为参数传入了useless函数。从该图中可以看到,在useless函数体内,通过callback参数可以取得getText函数的引用。随后,回调函数callback()的调用让getText函数得到执行,而我们作为参数传入的getText函数则通过useless函数被回调。

javascript函数与对象的混乱关系,javascript 的函数和方法区别(2)

图1.2 执行useless(getText)调用后的执行流。getText作为参数传入useless函数并调用。useless函数体内对传入函数进行调用,本例中触发了getText函数的执行(即我们对getText函数进行回调)。

完成这个过程是很容易的,原因就在于JavaScript的函数式本质让我们能把函数作为第一类对象。更进一步说,我们的代码可以写成如下形式:

1<pre class="代码无行号"><code>var text = 'Domo arigato!';

2function useless(ninjaCallback) {

3 return ninjaCallback();

4}

5assert(useless(<strong>function () { return text;}</strong>) === text,  ?--- 直接以参数形式定义回调函数

6    "The useless function works! " text); </code></pre>

JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间。

在回调函数的前述例子中,我们调用的是我们自己的回调。除此之外浏览器也会调用回调函数,回想一下第2章中的下述例子:

1document.body.addEventListener("mousemove", function() {

2

3 var second = document.getElementById("second")

4;

5 addMessage(second, "Event: mousemove"

6);

7});

上例同样是一个回调函数,作为mousemove事件的事件处理器,当事件发生时,会被浏览器调用。

{注意 } 

本小节介绍的回调函数是其他代码会在随后的某个合适时间点“回过来调用”的函数。你已经学习了我们自己的代码调用回调(useless函数例子),也看到了当某事件发生时浏览器发起调用(mousemove例子)。注意这些很重要,不同于我们的例子,一些人认为回调会被异步调用,因此第一个例子不是一个真正的回调。这里之所以提到这些是以防万一你偶尔会遇见这类激烈的争论。

现在让我们看一个回调函数的用法,它能极大地简化集合的排序。

使用比较器排序

一般情况下只要我们拿到了一组数据集,就很可能需要对它进行排序。假如有一组随机序列的数字数组:0, 3, 2, 5, 7, 4, 8, 1。也许这个顺序没什么问题,但很可能早晚需要重新排列它。

通常来说,实现排序算法并不是编程任务中最微不足道的;我们需要为手中的工作选择最佳算法,实现它以适应当前的需要(使这些选项是按照特定顺序排列),并且需要小心仔细不能引入故障。除此之外,唯一特定于应用程序的任务是排列顺序。幸运的是,所有的JavaScript数组都能用sort方法。利用该方法可以只定义一个比较算法,比较算法用于指示按什么顺序排列。

这才是回调函数所要介入的!不同于让排序算法来决定哪个值在前哪个值在后,我们将会提供一个函数来执行比较。我们会让排序算法能够获取这个比较函数作为回调,使算法在其需要比较的时候,每次都能够调用回调。该回调函数的期望返回值为:如果传入值的顺序需要被调换,返回正数;不需要调换,返回负数;两个值相等,返回0。对于排序上述数组,我们对比较值做减法就能得到我们所需要的值。

1<pre class="代码无行号"><code>var values = [0, 3, 2, 5, 7, 4, 8, 1];

2values.sort<strong>(function(value1, value2) {</strong>

3 return value1 - value2;

4<strong>}</strong>);</code></pre>

没有必要思考排序算法的底层细节(甚至是选择了什么算法)。JavaScript引擎每次需要比较两个值的时候都会调用我们提供的回调函数。

函数式方式让我们能把函数作为一个单独实体来创建,正像我们对待其他类型一样,创建它、作为参数传入一个方法并将它作为一个参数来接收。函数就这样显示了它一等公民的地位。

1.2 函数作为对象的乐趣

本节我们会考察函数和其他对象类型的相似之处。也许让你感到惊讶的相似之处在于我们可以给函数添加属性:

1var ninja = {};

2ninja.name = "hitsuke";  ?--- 创建新对象并为其分配一个新属性

3var wieldSword = function(){};

4wieldSword.swordType = "katana";  ?--- 创建新函数并为其分配一个新属性

我们再来看看这种特性所能做的更有趣的事:

让我们行动起来吧。

1.2.1 存储函数

某些例子中(例如,我们需要管理某个事件发生后需要调用的回调函数集合),我们会存储元素唯一的函数集合。当我们向这样的集合中添加函数时,会面临两个问题:哪个函数对于这个集合来说是一个新函数,从而需要被加入到该集合中?又是哪个函数已经存在于集合中,从而不需要再次加入到集合中?一般来说,管理回调函数集合时,我们并不希望存在重复函数,否则一个事件会导致同一个回调函数被多次调用。

一种显著有效的简单方法是把所有函数存入一个数组,通过循环该数组来检查重复函数。令人遗憾的是,这种方法的性能较差,尤其作为一个“忍者”要把事情干得漂亮而不仅是做到能用。我们可以使用函数的属性,用适当的复杂度来实现它,如清单1.2所示。

清单1.2 存储唯一函数集合

1var store = {

2 nextId: 1,  ?--- 跟踪下一个要被复制的函数

3 cache: {},  ?--- 使用一个对象作为缓存,我们可以在其中存储函数

4 add: function(fn) { 

5   if (!fn.id) {

6    fn.id = this.nextId ;

7    this.cache[fn.id] = fn;

8    return true;

9  }

10 }  ?--- 仅当函数唯一时,将该函数加入缓存

11};

12function ninja(){}

13assert(store.add(ninja), 

14     "Function was safely added.");

15assert(!store.add(ninja),

16     "But it was only added once.");   ?--- 测试上面代码按预期工作

在这个清单中,我们创建了一个对象赋值给变量store,这个变量中存储的是唯一的函数集合。这个对象有两个数据属性:其一是下一个可用的id,另外一个缓存着已经保存的函数。函数通过add()方法添加到缓存中。

1add: function(fn) {

2  if (!fn.id) {

3   fn.id = this.nextId ;

4   this.cache[fn.id] = fn;

5   return true;

6  }

7...

在add函数内,我们首先检查该函数是否已经存在id属性。如果当前的函数已经有id属性,我们则假设该函数已经被处理过了,从而忽略该函数,否则为该函数分配一个id(同时增加nextId)属性,并将该函数作为一个属性增加到cache上,id作为属性名。紧接着该函数的返回值为true,从而可得知调用了add()后,函数是什么时候被添加到存储中的。

在浏览器中运行该程序后,页面显示:测试程序尝试两次添加ninja()函数,而该函数只被添加一次到存储中,如图1.3所示。第9章展示了用于操作合集的更好技术,它利用了ES6的新的对象类型集合(Set)。

javascript函数与对象的混乱关系,javascript 的函数和方法区别(3)

图1.3 给函数附加一个属性后,我们就能够引用该属性。本例通过这种方式可以确保该ninja函数仅被添加到函数中一次

另外一种有用的技巧是当使用函数属性时,可以通过该属性修改函数自身。这个技术可以用于记忆前一个计算得到的值,为之后计算节省时间。

1.2.2 自记忆函数

如同前面所提到的,记忆化(memoization)是一种构建函数的处理过程,能够记住上次计算结果。在这个果壳里,当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍。像这样避免既重复又复杂的计算可以显著地提高性能。对于动画中的计算、搜索不经常变化的数据或任何耗时的数学计算来说,记忆化这种方式是十分有用的。

看看下面的这个例子,它使用了一个简单的(也的确是效率不高的)算法来计算素数。尽管这是一个复杂计算的简单例子,但它经常被应用到大计算量的场景中(例如可以引申到通过字符串生成MD5算法),这里不便展示。

从外表来说,这个函数和任何普通函数一样,但在内部我们会构建一个结果缓存,它会保存函数每次计算得到的结果,如清单1.3所示。

清单1.3 计算先前得到的值

1function isPrime(value) {

2 if (!isPrime.answers) {

3  isPrime.answers = {};

4 }  ?--- 创建缓存

5 if (isPrime.answers[value] !== undefined) {

6  return isPrime.answers[value];

7 }  ?--- 检查缓存的值

8 var prime = value !== 0 && value !== 1; // 1 is not a prime

9 for (var i = 2; i < value; i ) {

10   if (value % i === 0) {

11    prime = false;

12    break;

13   }

14 }

15 return isPrime.answers[value] = prime;   ?--- 存储计算的值

16}

17assert(isPrime(5), "5 is prime!");

18assert(isPrime.answers[5], "The answer was cached!");   ?--- 测试该函数是否正常工作

在isPrime函数中,首先通过检查它的answers属性来确认是否已经创建了一个缓存,如果没有创建,则新建一个:

1if (!isPrime.answers) {

2  isPrime.answers = {};

3}

只有第一次函数调用才会创建这个初始空对象,之后这个缓存就已经存在了。然后我们会检查参数中传的值是否已经存储到缓存中:

1if (isPrime.answers[value] !== undefined) {

2 return isPrime.answers[value];

3}

这个缓存会针对参数中的值value来存储该值是否为素数(true或false)。如果我们在缓存中找到该值,函数会直接返回。

1return isPrime.answers[value] = prime;

这个缓存是函数自身的一个属性,所以只要该函数还存在,缓存也就存在。

最后的测试结果可以看到记忆函数生效了。

1assert(isPrime(5), "5 is prime!");

2assert(isPrime.answers[5], "The answer was cached!");

这个方法具有两个优点。

当然这种方法并不是像玫瑰和提琴一样完美,还是要权衡利弊。

现在你看到了函数作为第一类公民的一些实例,接下来看看不同的函数定义的方式。

1.3 函数定义

JavaScript函数通常由函数字面量(function literal)来创建函数值,就像数字字面量创建一个数字值一样。要记住这一点,作为第一类对象,函数是可以用在编程语言中的值,就像例句字符串或数字的值。无论你是否意识到了这一点,你一直都是这样做的。

JavaScript提供了几种定义函数的方式,可以分为4类。

理解这几种方式的不同很重要,因为函数创建的方式很大程度地影响了函数可被调用的时间、函数的行为以及函数可以在哪个对象上被调用。

这一节中,我们将会探索函数定义、函数表达式和箭头函数。你将学到它们的语法和它们的工作方式,我们也将会在本文中多次回顾它们的细节。另一方面,生成器函数则有一点独特,它不同于普通函数。在第6章我们会再来学习它们的细节。

剩下的JavaScript特性——函数构造函数我们将全部跳过。尽管它具有某些有趣的应用场景,尤其是在动态创建和执行代码时,但我们依然认为它是JavaScript语言的边缘功能。如果你想知道更多关于函数构造函数的信息,请访问http://mng.bz/ZN8e。

让我们先用最简单、最传统的方式定义函数吧:函数声明和函数表达式。

1.3.1 函数声明和函数表达式

JavaScript中定义函数最常用的方式是函数声明和函数表达式。这两种技术非常相似,有时甚至难以区分,但在后续章节中你将看到,它们之间还是存在着微妙的差别。

函数声明

JavaScript定义函数的最基本方式是函数声明(见图1.4)。正如你所见,每个函数声明以强制性的function开头,其后紧接着强制性的函数名,以及括号和括号内一列以逗号分隔的可选参数名。函数体是一列可以为空的表达式,这些表达式必须包含在花括号内。除了这种形式以外,每个函数声明还必须包含一个条件:作为一个单独的JavaScript语句,函数声明必须独立(但也能够被包含在其他函数或代码块中,在下一小节中你将会准确理解其含义)。

javascript函数与对象的混乱关系,javascript 的函数和方法区别(4)

首页 12下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.