应用程序生命周期和开发模型- 了解什么是应用程序生命周期管理

学习目标

完成本单元后,您将能够:

  • 确定可以直接在生产组织中安全进行的自定义。
  • 定义应用程序生命周期管理。
  • 解释为什么使用应用程序生命周期管理流程可以帮助团队更快地开发应用程序。

介绍

Salesforce提供了各种开发工具和流程来满足客户的需求。本模块介绍了应用程序生命周期管理(ALM)流程和三种开发模型。

  • 变更集开发
  • 组织发展
  • 包装开发

在较高级别上,所有三个开发模型都遵循相同的ALM流程。但是,这些模型的不同之处在于,它们允许您管理组织的更改。控制变更在软件开发中很重要,如果您了解自己的选择,则可以选择最适合您情况的开发模型。

让我们跟随Salesforce专业人员及其公司的发展历程,因为他们的发展挑战会随着时间而变化。在整个故事过程中,您将学习每种开发模型如何满足不同情况的需求。本教程的其他模块中涵盖了有关如何使用开发模型的详细信息。

注意

在类似的情况下,您和您的团队可能做出的选择与该模块中虚构的公司所做的选择不同。重要的是要了解您的选择。

与Zephyrus Relocation Services,Inc.的Salesforce管理员Calvin会面。

卡尔文·格林(Calvin Green)为弗吉尼亚州费尔法克斯的人才流动公司Zephyrus Relocation Services担任许多技术职务。卡尔文(Calvin)的职责之一是为公司规模虽小但不断壮大的销售团队定制Salesforce。使用生产组织中的Setup界面,他淘汰了各种令人印象深刻的新仪表板和报告。

卡尔文(Calvin)通过Vetforce开发了他作为Salesforce管理员的技能,该软件是针对军事服务人员,退伍军人和配偶的Salesforce工作培训和职业加速器计划。

加尔文在Zephyrus站着办公桌,拿着他的Vetforce咖啡杯。

什么是安全的生产变更?

您可以在生产组织中安全地开发某些新功能。在生产组织中可以安全地创建不影响数据的自定义设置,例如开发新的仪表板,报告和电子邮件模板。但是,某些直接在生产中进行的自定义设置可能会因删除数据而造成混乱,甚至更糟。

如果在将变更投入生产之前不测试变更,会发生什么?

  • 工作流规则意外地创建了无限的处理循环。
  • 字段类型的更改会以您无法撤消的方式修改数据。
  • 验证规则中的逻辑错误会阻止您保存记录。
  • 页面布局的更改会使人们感到困惑,而不是改善他们的体验。

定制组织的最安全方法是使用专用的开发环境进行和测试更改。实际上,必须在开发环境中进行一些更改。例如,您不能直接在生产组织中编写Apex代码。

移动到变更集以进行更安全的自定义

随着Zephyrus的不断增长,对Salesforce定制的需求也随之增长。该公司增加了另一名员工来提供帮助。越来越多的定制请求包括新的工作流程规则和页面布局,而Calvin明智地拒绝直接在生产组织中进行这些更改。

为了满足不断变化的业务需求,Zephyrus决定升级到专业版。在专业版中,Zephyrus可以在单独的开发环境中使用声明式(单击)开发工具来创建和测试其需求。

在变更集开发模型中,Calvin和他的同事Ella可以使用声明性工具来管理其应用程序。他们不需要使用命令行界面或版本控制系统来满足他们所支持的销售团队不断增长的定制需求。

借助声明性工具,Calvin和Ella可以创建许多精美的东西来提高销售团队的生产力。例如,加尔文使用Lightning App Builder创建一个过滤器,当金额至少为100万美元时,该过滤器会在机会页面上显示富文本格式的组件。

使用Lightning App Builder创建过滤器。

对混沌施加一点命令

现在,自定义是由多个人在多个环境中进行的,Calvin考虑了如何管理即使是很小的更改对下游的影响。

例如,“联系人”标准对象没有“联系人类型”字段。卡尔文(Calvin)可以轻松添加该自定义字段,并立即向所有用户发布新的联系人类型字段。但是他应该吗?

  • 新的“联系人类型”字段是否与其他人的自定义冲突?
  • 销售团队是否知道如何使用新领域或需要培训?
  • 如果需要该字段,是否需要更新任何集成或导入过程?
  • 该字段出现在哪里?在所有页面布局上?哪些列表视图?它会显示在任何报告或仪表板中吗?
  • 该字段也应该位于Lead对象上吗?如果是这样,潜在客户转换流程会改变吗?
  • 与其他系统集成是否需要该字段?如果是这样,您可能需要更改中间件,字段映射,端点等。

在Salesforce Trailblazer社区中,其他成员鼓励Calvin研究Salesforce建议的应用程序生命周期管理步骤,以开发新的和自定义的应用程序。

在Salesforce开发人员网站上,卡尔文了解到ALM定义了从设计到最终发布的管理应用程序开发的过程。ALM还建立了一个框架,用于随着时间的推移进行应用程序错误修复和功能增强。

等等,这不会增加流程速度吗?

在与IT部门主管ErnestoRondán的一次会议上,Calvin主张转向应用程序生命周期管理。ALM为他们提供了流程和策略,可帮助他们平稳且因此更快构建应用程序,而不会造成麻烦。应用程序和工具可能会有所不同,但是ALM周期中的步骤适用于任何Salesforce开发项目。

ALM周期:计划发布,开发,测试,构建版本,测试发布,发布
  • 步骤1:计划发布从计划开始定制或开发项目。收集需求并进行分析。让您的产品经理(或等效人员)创建设计规范,并与开发团队共享以执行。确定项目在ALM周期中进行时团队需要的各种开发和测试环境。
  • 步骤2:开发按照设计规范完成工作。在包含生产组织的元数据副本但没有生产数据的环境中执行工作。使用说明性工具(Process Builder,Custom Object向导以及UI中的其他工具)和编程工具(开发人员控制台,源代码编辑器或Visual Studio Code)的适当组合在Lightning Platform上进行开发。
  • 步骤3:测试在将其与其他人的工作集成之前,请练习您所做的更改以检查它们是否按预期工作。在与开发步骤中使用的环境类型相同的环境中进行测试,但将开发环境与集成测试环境分开。在这一点上,着重于自己测试更改,而不是了解更改如何影响发行版或整个应用程序的其他部分。
  • 步骤4:内部版本将您在开发阶段创建或修改的所有资产聚合到一个发布工件中:一个逻辑定制包,您将其部署到生产中。从这一点开始,专注于您要发布的内容,而不是个人的贡献。
  • 步骤5:测试发布测试您实际要部署的内容,但在尽可能模拟生产的临时环境中进行安全测试。使用大量实际的代表性生产数据。将您的测试环境与模拟生产系统集成点所需的所有外部系统连接起来。在此步骤中运行完整的回归和最终性能测试。与一小组提供反馈的有经验的人一起测试发行版(一种称为用户接受测试的技术)。
  • 步骤6:放行完成测试并达到质量基准后,可以将定制部署到生产中。培训您的员工和合作伙伴,使他们了解变化。如果发行版对用户有重大影响,请创建一个包含实际数据的单独环境以培训用户。

Salesforce开发人员的JavaScript技能-编写异步JavaScript

学习目标

完成本单元后,您将能够:

  • 识别JavaScript中重要的异步功能。
  • 使用setTimeout异步调用函数。
  • 编写和调用回调函数。
  • 编写和调用基于promise的函数。
  • 描述Aura组件中的异步功能。

回想起我们最初引入JavaScript引擎时的方式。引擎有一个单线程,它可以正常工作,完成工作,然后填充新的工作以重新开始。

当然,至关重要的是不要阻塞线程。 

让我们来看一个例子。

<html>
  <script>
    alert("Does JavaScript show first?");
  </script>
  <body>
    <p>
      Does HTML show first?
    </p>
  </body>
</html>

如果在浏览器中加载该HTML页面,则会发现先弹出警报,然后阻止HTML显示。这是因为该alert()函数会中止JavaScript线程的执行,直到用户将其关闭为止。简而言之,当JavaScript阻止您的浏览器时,就永远不会带来良好的用户体验。 

好消息是,除了一些alert()上面的功能仍然存在的遗留特性外,JavaScript是一种异步语言。 

异步JavaScript无处不在

为了开始异步之旅,让我们重新回顾事件和功能。以前我们看过这样的HTML和JavaScript。

<!-- HTML -->
<button id="clicker">
//JavaScript
let button = document.getElementById("clicker");
button.addEventListener("click", handleClick);

在此示例中,我们将handleClick事件控件添加到按钮发出的click事件中。 

在那里!我们已经编写了一些异步JavaScript。 

事件触发时,所有发生的事情是将新消息添加到队列中。没有事件可以接管线程。触发的每个事件都必须进入队列并等待轮流运行。 

一种说明方法是使用setTimeout函数。在此 示例 invoking中setTimeout,我们以毫秒为单位传递了事件处理程序和计时器。计时器到时,它将触发,将事件处理程序添加到队列中。 

setTimeout(function(){
  console.log("This comes first");
}, 0);
console.log("This comes second");
//output in console
// "This comes second"
// "This comes first"

在这里,我们将计时器设置为零。但这并不意味着“立即致电”。这仅表示“立即将其放入队列中”。但是,代码块本身的执行需要完成,从而清除了调用堆栈。只有这样,函数才能setTimeout 轮流使用。 

另一个常见的错误是认为计时器是事件处理程序何时触发的准确预测器,但不一定。事件处理程序仍然必须等待轮到队列。通过测量时间,我们可以看到这个 在行动。 

const timer  = function(){
  let start = Date.now();
  setTimeout(function(){
    let end = Date.now();
    console.log( "Duration: " + (end - start) )
  },1000);
};
timer();
// Console output when invoked several times:
// "Duration: 1007"
// "Duration: 1000"
// "Duration: 1002"
// "Duration: 1004"

时间设置为一秒,并且它接近于该时间。但是很明显,将函数添加到队列然后在每次调用时运行的速度有所不同。 

现在,我们已经看到了一些异步调用的示例,我们可以看看一些常见的异步模式和构造。 

回调模式

回调只是一个传递给另一个函数的函数,该函数会在将来的某个时刻调用它。 

因此,实际上,我们已经看到了很多回调。 

setTimeout(callback, timer)
Element.addEventListener(event, callback)
Array.map(function(item){...})

让我们将其应用于自行车 用例,以了解如何实现回调。当您换挡时,大多数情况下它都起作用。但是失败的可能性仍然很小。这是异步JavaScript的理想方案。让我们看一下回调的外观,该回调接收有关齿轮如何移动的数据,然后在完成时调用传入的函数。 

Bike.prototype.changeGearAsync = function(shiftObject, callback){
  let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
  if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
    callback(new Error("There is a problem"), null);
  } else {
    callback(null, newIndex);
  }
};

该参数callback实际上是一个函数。如果有错误,我们将调用它并为第一个参数设置要发送回的任何错误数据。成功后,我们将错误参数设为空并传回正确的数据。然后,我们可以看到如何调用新的换档功能。 

Bike.prototype.changeGear = function(frontOrRear, changeBy) {
  const shiftIndexName = frontOrRear + "GearIndex"
  const that = this;
  //contains state change for making the shift
  let shiftObject = {
    currentIndex: this[shiftIndexName],
    maxIndex: this.transmission[frontOrRear + "GearTeeth"].length,
    changeBy: changeBy
  }
  // invoke async function with anonymous callback
  this.changeGearAsync(shiftObject, function(err, newIndex){
    if (err) {
      console.log("No Change");
    } else {
      that[shiftIndexName] = newIndex;
    }
  });
};

回调模式已被广泛接受并广泛使用,但是它也有一些缺点。首先,当多个回调链接在一起时,它们被嵌套在另一个中。这会造成不必要的复杂性,可读性问题,并且在阅读别人的代码时很难推理。此缺陷称为回调地狱。回调也没有隐式错误状态(像try/catch这样)。由开发人员编写回调以明确查找带有if条件的错误,这取决于开发人员。这些障碍导致了诺言的创造。 

箭头功能

在前面的示例中,您可能已经注意到了这一行。

const that = this;

这是旧版JavaScript的遗物。我们仅介绍一种新的函数语法:箭头函数。回想一下调用函数时会发生什么。具体来说,它将绑定到新this上下文。与匿名函数关闭范围内的其他变量不同,JavaScript this实际上在实际上我们需要this包含函数时重新绑定。 

一个长期的解决方法是分配this一个新变量(按照惯例通常称为selfor that),然后将上下文引用保留在闭包中。

箭头函数通过不重新绑定来消除编码技巧的这一点this。箭头函数语法如下所示:

(arg1, arg2) => {...function body...}

使用箭头功能,我们可以删除该that = this位并将其调用更改changeGearsAsync为以下内容。

  // the anonymous function is now an arrow function
this.changeGearAsync(shiftObject, (err, newIndex)=>{
  if (err) {
    console.log("No Change");
  } else {
    // we reference this instead of that
    this[shiftIndexName] = newIndex;
  }
});

有前途的东西

开发为处理异步代码的库的承诺,使您可以更轻松地推断代码成功或失败的时间。它们还包含内置机制,可将一个呼叫链接到另一个呼叫。最终,竞争库在浏览器中被标准化为Promise对象。让我们变身bike一个多 的时间。 

Bike.prototype.changeGearAsync = function(shiftObject){
  return new Promise(
    (resolve, reject) => {
      let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
      if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
        reject("New Index is Invalid: " + newIndex);
      } else {
        resolve(newIndex);
      }
    }
  );
};

首先,更新后的changeGearAsync函数接收我们传递的数据,并返回一个新的Promise对象。我们传入一个参数:一个回调函数,它本身有两个函数传递给它,resolvereject。 

在实现promise时,您可以执行回调函数中所需的任何计算,请求等。完成后,如果一切顺利,您将resolve使用要传递回的数据进行调用。如果遇到问题,您可以通过调用reject任何相关错误作为参数来向函数调用者发出信号。 

让我们看看我们现在如何使用它。 

// invoke async function that returns a promise
this.changeGearAsync(shiftObject)
  .then(
    (newIndex) => {
      this[shiftIndexName] = newIndex;
      console.log(this.calculateGearRatio());
    }
  )
  .catch(
    (err) => {console.log("Error: " + err);}
  );

现在,我们有了一些更容易推理的东西。如果changeGearAsync可行,则将then函数与传递给其参数的函数一起调用。如果没有,catch则被调用。 

如果回调函数本身返回的实例Promise,那么事情就会变得令人兴奋。您可以简单地将这两个Promise函数链接在一起。举例来说,如果我们想改变 这两个前后齿轮。 

Bike.prototype.changeBothGears = function(frontChange, rearChange) {
  let shiftFront = {
    currentIndex: this.frontGearIndex,
    maxIndex: this.transmission.frontGearTeeth.length - 1,
    changeBy: frontChange
  };
  let shiftRear = {
    currentIndex: this.rearGearIndex,
    maxIndex: this.transmission.rearGearTeeth.length - 1,
    changeBy: rearChange
  };
  this.changeGearAsync(shiftFront)
    .then(
      (newIndex) => {
        this.frontGearIndex = newIndex;
        console.log(this.calculateGearRatio());
        return this.changeGearAsync(shiftRear);
      }
    )
    .then(
      (newIndex) => {
        this.rearGearIndex = newIndex;
        console.log(this.calculateGearRatio());
      }
    )
    .catch(
      (err) => {console.log("Error: " + err);}
    );
  };

changeBothGears上面的函数向我们显示了对的两个调用的链接changeGearsAsync,每个调用都与对应于前齿轮或后齿轮的对象相关。第一次调用之后,我们在第一次调用结束时再次调用它thenthen可以在上面加上另一个。从根本上讲,每当一个then承诺返回时,都可以跟随另一个承诺,then直到我们用尽了所有链式动作。  

异步/等待

在签字之前,值得一提的是异步武器库中的一些新功能:asyncawait运算符。这些基于promise,使它们的使用方式与同步JavaScript非常相似。

闪电Web组件和异步JavaScript

利用Lightning Web Components,开发人员可以同时使用基于承诺的异步功能和异步/等待功能。现在唯一的建议是,为开发人员在Internet Explorer 11上为用户创建功能,asyncawait 在该浏览器中未实现。不过,请放心,您的代码可以使用。但是,当在IE11中使用async / await运行任何内容时,LWC会自动使用polyfill,以便语法正确运行。因此,如果经常使用,IE11的性能可能会有所下降。 

与Salesforce互动

在Lightning Web Components中实现了使用异步JavaScript的若干功能。其中大多数围绕与服务器的交互。一个示例是可以在Lightning Web组件中强制调用Apex方法的方法。 

考虑以下Apex类和方法: 

public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> findContacts(String searchKey) {
        if (String.isBlank(searchKey)) {
            return new List<Contact>();
        }
    String key = '%' + searchKey + '%';
    return [SELECT Id, Name, Title, Phone, Email, Picture__c FROM Contact WHERE Name LIKE :key AND Picture__c != null LIMIT 10];
    }
...

Lightning Web Components使用基于Promise的API来展示此方法。您可以这样调用它: 

import { LightningElement, track } from 'lwc';
import findContacts from '@salesforce/apex/ContactController.findContacts';
export default class ApexImperativeMethodWithParams extends LightningElement {
    @track searchKey = '';
    @track contacts;
    @track error;
    handleSearch() {
        findContacts({ searchKey: this.searchKey })
        .then(result => {
            this.contacts = result;
            this.error = undefined;
        })
        .catch(error => {
            this.error = error;
            this.contacts = undefined;
        });
    }
}
注意

注意

这里有一些超出此模块范围的内容,最值得注意的是ES6中JavaScript模块的使用。要了解这些功能,这是考虑进入本教程“现代JavaScript开发”中的下一个模块的绝佳时机。但是在您这样做之前,请先了解一下此代码。 

当我们称其import findContacts…为标准模块语法时,该组件中将包含另一个模块的功能。我们在findContacts这里将Apex方法展示为同名的JS函数。 

当我们在handleSearch()函数中调用它时,Apex方法的参数作为文字对象传递,然后我们看到了then和catch函数的基于promise的语法。

Salesforce开发人员的JavaScript技能-了解上下文,范围和闭包

学习目标

完成本单元后,您将能够:

  • 确定变量在JavaScript中的范围。
  • 描述如何this根据调用函数的位置进行更改。
  • 使用闭包捕获对函数中变量的引用。

理解任何编程语言的关键在于理解变量的可用性,如何维护状态以及如何访问该状态。 

在JavaScript中,变量的可用性和可见性称为范围。范围由声明变量的位置确定。 

上下文是当前代码执行的状态。通过this指针访问它。 

可变范围

在JavaScript变量使用申报varletconst关键字。在何处调用关键字指示要创建的变量的范围。 

了解这三者之间的区别归结为两个因素:分配可变性和支持非功能块作用域。我们在本模块的第一个单元中介绍了分配可变性。现在该讨论范围了。 

范围的乐趣

声明变量或参数的代码块确定其范围。但var不能识别非功能代码块。这意味着调用var在一个if块或一个环块将变量分配给最近的封闭功能的范围。此功能称为提升。 

使用let或时const,参数或变量的范围始终是在其中声明参数的实际块。有一个经典的思维 练习可以证明这一点。

function countToThree() {
  // i is in the scope of the countToThree function
  for (var i = 0; i < 3; i++){
    console.log(i); // iteration 1: 0
    // iteration 2: 1
    // iteration 3: 2
  }
  console.log(i); // What is this?
}

所述console.log内部输出for回路不足为奇,输出的值i对于每次迭代。可能更令人惊讶的是最终console.log声明,它输出3。您可能已经预料到了错误,因为i在您认为是for循环范围之内声明了该错误。但是随着吊装,i实际上属于countToThree的范围。 

虽然不一定很糟糕,但是如果在代码块中重新声明了变量,则吊装通常会被误解,并可能导致变量泄漏或导致意外覆盖。为了解决这些误解let,将const其添加到语言中以创建具有块级作用域的变量。让我们重新思考一下。

for (let j = 0; j < 3; j++){
  console.log(j); // 0
  // 1
  // 2
}
console.log(j); // error

通过替换letvar,我们现在有了一个仅在for循环上下文中存在的变量。在循环关闭后尝试访问它会给我们一个错误。 

上下文和这个

正如我们探索的那样,JavaScript围绕对象。对象是跟踪状态的地方。调用一个函数时,该函数周围始终有一个对象容器。该对象容器是其上下文,this关键字指向该上下文。因此,在声明函数时不会设置上下文,而是在调用函数时设置上下文。 

因为功能可以在对象之间传递,所以this指向的内容可以更改。 

例如说这个JavaScript 对象。

var obj = {
  aValue: 0,
  increment: function(incrementBy) {
    this.aValue = this.aValue + incrementBy;
  }
}

如果然后访问增量功能,它将按预期工作。

obj.increment(2);
console.log(obj.aValue); // 2

但是,让我们将该函数分配给另一个变量,看看它是如何工作的。 

//assign function to variable
var newIncrement = obj.increment;
//now invoke through the new pointer
newIncrement(2);
console.log(obj.aValue); // still 2 not 4

通过将变量分配给newIncrement,现在可以在其他上下文中执行该功能。具体而言,在这种情况下,在全局范围内。 

注意

注意

Function.apply()Function.call()Function.bind()函数提供的方式来调用函数,同时明确其绑定到不同的对象上下文。

全局对象

当执行JavaScript而没有以开发人员身份编写的任何包含对象时,它将在全局对象中运行。因此,据称在此调用的函数正在全局上下文中运行,这意味着访问this将指向该全局上下文。 

在浏览器中,全局上下文是window对象。您可以通过在浏览器开发人员工具中运行以下命令来轻松测试该功能。 

this === window; // true

在该increment示例中,将increment函数分配给newIncrement变量会将调用它的上下文移动到全局对象。这很容易证明。

console.log(this.aValue); // NaN
console.log(window.aValue); // NaN
console.log(typeof window.aValue); // number

当我们尝试this.aValue使用新的上下文进行分配时,JavaScript对象的可变性开始发挥作用。新的未初始化aValue属性已添加到中this。对未初始化的变量执行数学运算将失败,因此该NaN值也会失败。但是我们可以看到aValue存在于window,的确是一个数字。 

与对象的上下文

在此increment示例中,只要increment使用obj点符号来调用函数,就this指向obj。或者,通常来说,当调用函数作为object.function() 点左侧的事物时,始终是调用该函数的上下文。 

想想这个Bike例子。的Bike构造限定了与几个属性this参考。它还具有分配给其引用该原型的功能this。 

const Bike = function(frontIndex, rearIndex){
  this.frontGearIndex = frontIndex || 0;
  this.rearGearIndex = rearIndex || 0;
  ...
}
...
Bike.prototype.calculateGearRatio = function(){
  let front = this.transmission.frontGearTeeth[this.frontGearIndex],
  rear = this.transmission.rearGearTeeth[this.rearGearIndex];
  if (front && rear) {
    return (front / rear) ;
  } else {
    return 0;
  }
};

然后Bike,我们使用new关键字进行调用。 

const bike = new Bike(1,2);
console.log(bike.frontGearIndex); // 1
console.log(bike.rearGearIndex); // 2

看起来我们正在Bike全局上下文中调用构造函数。但是,new关键字将上下文(和this指针)移动到分配左侧的新对象。 

当我们调用任何函数时,它们现在是bike对象的成员,因此它们将其用作包含上下文。 

let gearRatio = bike.calculateGearRatio();
console.log(gearRatio); // 3

以错误的方式调用构造函数很容易。在这里事情会崩溃。

const badBike = Bike(1,2);
console.log(badBike.frontGearIndex); // error
console.log(window.frontGearIndex); // 1

当您忘记使用时newBike将像其他任何函数一样调用,并且thiswindow到新创建的对象的关键转换失败。frontGearIndex 引入对象可变性,并添加属性window。 

注意

classJavaScript中的语法会强制您使用new关键字调用构造函数,因此您不会误导上下文。

关闭

声明函数时,它将保留对其中声明的任何变量或参数的引用,以及对其所包含范围内引用的任何变量的引用。它的变量和自变量以及其包含范围中的局部变量和自变量的这种组合称为闭包。 

考虑此 函数及其返回的函数。 

const greetingMaker = function(greeting){
  return function(whoGreeting){
    return greeting + ", " + whoGreeting + "!";
  }
}
const greetingHello = greetingMaker("Hello");
const greetingBonjour = greetingMaker("Bonjour");
const greetingCiao = greetingMaker("Ciao");
console.log(greetingHello("Gemma")); // Hello, Gemma!
console.log(greetingBonjour("Fabien")); // Bonjour, Fabien!
console.log(greetingCiao("Emanuela")); // Ciao, Emanuela!

greetingMaker被调用时,我们通常可以想象它的greeting参数仅在被调用的整个生命周期内持续存在。 

但是返回的函数会greetinggreetingMaker的范围内保留对参数的引用。这样,最后通过greetingHelloBonjour/ 调用它时Ciao,它仍然可以访问。 

掌握闭包也是理解和使用该语言的重要组成部分。