定义/创建函数的方式

一、创建函数的4种方式:函数声明、函数表达式、Function构造器、箭头函数

函数声明 / 函数语句

一、一个函数定义(也成为函数声明,或函数语句)由一系列的function关键字组成,依次为
1、函数名
2、函数参数列表,包围在括号中并由逗号分隔。
3、定义函数的JavaScript语句(即函数体),用大括号{}括起来。
【示例1】

  1. function name(parameters) {
  2. ...body...
  3. }

【示例2】声明square函数

函数表达式

一、虽然函数声明在语法上是一个语句,但函数也可以由函数表达式创建。这样的函数可以是匿名的
【实例1】创建一个匿名函数

  1. const square = function(number) { return number * number; };
  2. var x = square(4); // x gets the value 16

【实例2】函数表达式也可以提供函数名,并且可以用于在函数内部指代其本身,或者再调试器堆栈跟踪中识别该函数

const factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};

console.log(factorial(3));

二、在JavaScript中,函数是一个值,我们可以把它当成值对待。
【示例1】

function sayHi() {
  alert( "Hello" );
}

alert( sayHi ); // 显示函数代码

1、在某种意义上说一个函数是一个特殊值,我们可以像sayHi()这样调用它。
(1)但它依然是一个值,所有我们可以像使用其他类型的值意义使用它
【示例1】复制函数到其他变量

function sayHi() {   // 声明创建了函数,并把它放入到变量sayHi
  alert( "Hello" );
}

let func = sayHi;    // 将sayHi复制到了变量func。注意,sayHi后面没有括号。如果有括号,func=sayHi()会把sayHi()的调用结果写进func,而不是sayHi函数本身

func(); // Hello     // 调用方式1:运行复制的值(正常运行)
sayHi(); // Hello    // 调用方式2:这里也能运行

(2)也可以在第一行使用函数表达式来声明sayHi
【示例2】

let sayHi = function() {
  alert( "Hello" );
};

let func = sayHi;
// ...

三、一个函数可以指向并调用自身。有3种方法:
1、函数名
2、arguments.callee // es5禁止在严格模式下使用此属性
3、作用域下的一个指向该函数的变量名
【实例1】

var foo = function bar() {
    // statements go here
}

在这个函数体内,以下的语句是等价的:
bar()
arguments.callee() // es5禁止在严格模式下使用此属性
foo()
四、在JavaScript中,可以根据条件来定义一个函数。
【实例1】当num等于0的时候才会定义myFunc

var myFunc;
if (num == 0){
  myFunc = function(theObject) {
    theObject.make = "Toyota"
  }
}

分号

一、为什么函数表达式结尾有一个分号;,而函数声明没有

function sayHi() {
  // ...
}

let sayHi = function() {
  // ...
};

1、在代码块的结尾不需要加分号;,像if { … }, for{ }, function f { }等语法结构后面都不用加。
2、函数表达式是在语句内部的:let sayHi = ...;,作为一个值,它不是代码块而是一个赋值语句。不管值是什么,都建议在语句末尾添加分号;,
(1)所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句。

【示例】下列哪几项可以创建函数?
A. function=myFunction(){……..}
B. function myFunction(){……}
C. myfunction = function(){…….}
D. myFunction(){….}
答案:BC
解析:D这个语法定义的是方法(method)。因为这种写法只能在class里生效,所以D这个function始终是跟一个object有关联的,也就是说,D准确地说应该是一个method,而不是function(函数)

Function构造器

一、运行时用Function构造器由一个字符串来创建一个函数,很像eval()函数。
二、这种创建函数的方法很少被使用,但有些时候只能选择它。

语法

一、创建函数的语法:

let func = new Function ([arg1, arg2, ...argN], functionBody);

1、该函数是通过使用参数arg1…argN和给定的functionBody创建的。
2、由于历史原因,参数也可以按逗号分隔符的形式给出。

| 【示例】以下三种声明的含义相同```javascript new Function(‘a’, ‘b’, ‘return a + b’); // 基础语法 new Function(‘a,b’, ‘return a + b’); // 逗号分隔 new Function(‘a , b’, ‘return a + b’); // 逗号和空格分隔

 |
| --- |

| 【示例】这是一个带有两个参数的函数:```javascript
let sum = new Function('a', 'b', 'return a + b');

alert( sum(1, 2) ); // 3

| | —- |

| 【示例】这是一个没有参数的函数,只有函数体:```javascript let sayHi = new Function(‘alert(“Hello”)’);

sayHi(); // Hello

 |
| --- |

二、与我们已知的其他方法相比,这种方法最大的不同在于,它实际上是通过运行时通过参数传递过来的字符串创建的。<br />1、以前的所有声明方法都需要我们 —— 程序员,在脚本中编写函数的代码。<br />2、但是new Function允许我们将任意字符串变为函数。

| 【示例】从服务器接收一个新的函数并执行它:```javascript
let str = ... 动态地接收来自服务器的代码 ...

let func = new Function(str);
func();

| | —- |

使用场景

一、使用new Function创建函数的应用场景非常特殊,比如在复杂的 Web 应用程序中,我们需要从服务器获取代码或者动态地从模板编译函数时才会使用。

[[Environment]]

一、JavaScript 中的函数会自动通过隐藏的[[Environment]]属性记录函数自身创建时的环境。它具体指向了函数创建时的词法环境。
二、使用new Function创建的函数,它的[[Environment]]指向全局词法环境,而不是函数所在的外部词法环境。
1、因此,此类函数无法访问外部(outer)变量,只能访问全局变量。

| 【示例】```javascript function getFunc() { let value = “test”; let func = new Function(‘alert(value)’); return func; }

getFunc()(); // error: value is not defined

将其与常规行为进行比较:```javascript
function getFunc() {
  let value = "test";
  let func = function() { alert(value); };
  return func;
}

getFunc()(); // "test",从 getFunc 的词法环境中获取的

| | —- |

优点

一、我们不能在new Function中直接使用外部变量,有助于降低我们代码出错的可能。
二、从代码架构上讲,显式地使用参数传值是一种更好的方法,并且避免了与使用压缩程序而产生冲突的问题。

【示例】需求:我们必须通过一个字符串来创建一个函数。在编写脚本时我们不会知道该函数的代码(这也就是为什么我们不用常规方法创建函数),但在执行过程中会知道了。我们可能会从服务器或其他来源获取它。
我们的新函数需要和主脚本进行交互。
一、如果这个函数能够访问外部(outer)变量会怎么样?
1、问题在于,在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩 —— 一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量。
(1)例如,如果一个函数有 let userName,压缩程序会把它替换为 let a(如果 a 已被占用了,那就使用其他字符),剩余的局部变量也会被进行类似的替换。一般来说这样的替换是安全的,毕竟这些变量是函数内的局部变量,函数外的任何东西都无法访问它。在函数内部,压缩程序会替换所有使用了这些变量的代码。压缩程序很聪明,它会分析代码的结构,而不是呆板地查找然后替换,因此它不会“破坏”你的程序。
(2)但是在这种情况下,如果使 new Function 可以访问自身函数以外的变量,它也很有可能无法找到重命名的 userName,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了。
二、即使我们可以在 new Function 中访问外部词法环境,我们也会受挫于压缩程序。
三、此外,这样的代码在架构上很差并且容易出错。
四、当我们需要向 new Function 创建出的新函数传递数据时,我们必须显式地通过参数进行传递。

箭头函数

【见】箭头函数:https://www.yuque.com/tqpuuk/yrrefz/sdh746

定义函数的方式之间的差异

函数表达式 vs 函数声明

主要区别

语法

一、如何通过代码对它们进行区分。
1、函数声明:在主代码流中声明为单独的语句的函数。

// 函数声明
function sum(a, b) {
  return a + b;
}

2、函数表达式:在一个表达式中或另一个语法结构中创建的函数。
下面这个函数是在赋值表达式=右侧创建的:

// 函数表达式
let sum = function(a, b) {
  return a + b;
};

更细微的差别:JavaScript 引擎会在什么时候创建函数。

一、函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。
1、一旦代码执行到赋值表达式let sum = function…的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。
二、函数声明:在函数声明被定义之前,它就可以被调用。
1、一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。
(1)这是内部算法的原故。当 JavaScript准备运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。
(2)在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。
【示例1】下面的代码会正常工作:

sayHi("John"); // Hello, John

function sayHi(name) { // 函数声明sayHi是在 JavaScript 准备运行脚本时被创建的,在这个脚本的任何位置都可见
  alert( `Hello, ${name}` );
}

【示例2】如果它是一个函数表达式,它就不会工作:

sayHi("John"); // error!

let sayHi = function(name) {  // 函数表达式在代码执行到它时才会被创建。只会发生在这行
  alert( `Hello, ${name}` );
};

块级作用域

一、函数声明的另外一个特殊的功能是它们的块级作用域。
1、严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
【示例1】如果我们需要依赖于在代码运行过程中获得的变量age声明一个函数welcome()。并且我们计划在之后的某个时间使用它。

let age = prompt("What is your age?", 18);

// 有条件地声明一个函数
if (age < 18) {
  function welcome() {
    alert("Hello!");
  }
} else {
  function welcome() {
    alert("Greetings!");
  }
}

// 稍后使用
welcome(); // Error: welcome is not defined,代码无法像预期那样工作

(1)这是因为函数声明只在它所在的代码块中可见。
【示例2】

let age = 16; // 拿 16 作为例子

if (age < 18) {
  welcome();               // \   (运行)
                           //  |
  function welcome() {     //  |
    alert("Hello!");       //  |  函数声明在声明它的代码块内任意位置都可用
  }                        //  |
                           //  |
  welcome();               // /   (运行)

} else {

  function welcome() {
    alert("Greetings!");
  }
}

// 在这里,我们在花括号外部调用函数,我们看不到它们内部的函数声明。


welcome(); // Error: welcome is not defined

2、我们怎么才能让welcome在if外可见呢?
(1)正确的做法是使用函数表达式,并将welcome赋值给在if外声明的变量,并具有正确的可见性。
【示例1】下面的代码可以按照预期运行:

let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {
  welcome = function() {
    alert("Hello!");
  };
} else {
  welcome = function() {
    alert("Greetings!");
  };
}

welcome(); // 现在可以了

(2)或者我们可以使用问号运算符?来进一步对代码进行简化:

let age = prompt("What is your age?", 18);

let welcome = (age < 18) ?
  function() { alert("Hello!"); } :
  function() { alert("Greetings!"); };

welcome(); // 现在可以了

选择

一、什么时候选择函数声明与函数表达式?
1、根据经验,当我们需要声明一个函数时,首先考虑函数声明语法。它能够为组织代码提供更多的灵活性。因为我们可以在声明这些函数之前调用这些函数。
(1)这对代码可读性也更好,因为在代码中查找function f(…) {…}比let f = function(…) {…}更容易。函数声明更“醒目”。
2、如果由于某种原因而导致函数声明不适合我们(我们刚刚看过上面的例子),那么应该使用函数表达式。

函数命名

一、函数就是行为(action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。
二、一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。
1、以”show”开头的函数通常会显示某些内容。
2、函数以 XX 开始:

  • “get…”—— 返回一个值,
  • “calc…”—— 计算某些内容,
  • “create…”—— 创建某些内容,
  • “check…”—— 检查某些内容并返回 boolean 值,等。

【示例1】这类名字的示例:

showMessage(..)     // 显示信息
getAge(..)          // 返回 age(gets it somehow)
calcSum(..)         // 计算求和并返回结果
createForm(..)      // 创建表格(通常会返回它)
checkPermission(..) // 检查权限并返回 true/false

二、常用的函数有时会有非常短的名字。
【示例1】jQuery框架用$定义一个函数。LoDash库的核心函数用_命名。
1、这些都是例外,一般而言,函数名应简明扼要且具有描述性。

函数 == 注释

一、一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。
1、两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。
【示例1】getAge: 如果它通过alert将 age 显示出来,那就有问题了(只应该是获取)。
【示例2】createForm: 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。
【示例3】checkPermission—— 如果它显示access granted/denied消息,那就有问题了(只应执行检查并返回结果)。
二、一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!
1、比较如下两个函数showPrimes(n)。它们的功能都是输出到n的素数
【示例1】第一个变体使用了一个标签:

function showPrimes(n) {
  nextPrime: for (let i = 2; i < n; i++) {
    for (let j = 2; j < i; j++) {
      if (i % j == 0) continue nextPrime;
    }
    alert( i ); // 一个素数
  }
}

【示例2】第二个变体使用附加函数isPrime(n)来检验素数:

function showPrimes(n) {
  for (let i = 2; i < n; i++) {
    if (!isPrime(i)) continue;
    alert(i);  // 一个素数
  }
}

function isPrime(n) {
  for (let i = 2; i < n; i++) {
    if ( n % i == 0) return false;
  }
  return true;
}

(1)第二个变体更容易理解,不是吗?我们通过函数名(isPrime)就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为自描述。
(2)因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。

变量

局部变量

一、在函数中声明的变量只在该函数内部可见。
【示例1】

function showMessage() {
  let message = "Hello, I'm JavaScript!"; // 局部变量

  alert( message );
}

showMessage(); // Hello, I'm JavaScript!

alert( message ); // <-- 错误!变量是函数的局部变量

二、作为参数传递给函数的值,会被复制到函数的局部变量。
三、为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。

外部变量

一、函数也可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。
【示例1】

let userName = 'John';

function showMessage() {
  let message = 'Hello, ' + userName;
  alert(message);
}

showMessage(); // Hello, John

二、函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
【示例1】

let userName = 'John';

function showMessage() {
  userName = "Bob"; // (1) 改变外部变量

  let message = 'Hello, ' + userName;
  alert(message);
}

alert( userName ); // John 在函数调用之前

showMessage();

alert( userName ); // Bob,值被函数修改了

三、只有在没有局部变量的情况下才会使用外部变量。
四、如果在函数内部声明了同名变量,那么函数会遮蔽外部变量。
【示例1】函数使用局部的userName,而外部变量被忽略:

let userName = 'John';

function showMessage() {
  let userName = "Bob"; // 声明一个局部变量

  let message = 'Hello, ' + userName; // Bob
  alert(message);
}

// 函数会创建并使用它自己的 userName
showMessage();

alert( userName ); // John,未被更改,函数没有访问外部变量。

五、与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。

全局变量

一、任何函数之外声明的变量,例如上述代码中的外部变量userName,都被称为全局变量。
二、全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。
三、减少全局变量的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。

参数

一、我们可以使用参数(也称“函数参数”)来将任意数据传递给函数。
【示例1】下面示例中,函数有两个参数:from和text。

function showMessage(from, text) { // 参数:from 和 text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

1、当函数在()和(*)行中被调用时,给定值被复制到了局部变量from和text。然后函数使用它们进行计算。
【示例2】我们有一个变量from,并将它传递给函数。请注意:函数会修改from,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:

function showMessage(from, text) {
  from = '*' + from + '*'; // 让 "from" 看起来更优雅
  alert( from + ': ' + text );
}

let from = "Ann";

showMessage(from, "Hello"); // *Ann*: Hello

// "from" 值相同,函数修改了一个局部的副本。
alert( from ); // Ann

二、参数传值
1、原始参数(如一个数字)被作为值传递给函数,如果被调用的函数改变了这个参数的值,这样的改变不会影响到全局或调用函数。
2、如果传递一个对象(即非原始值,如Array或用户自定义的对象)作为参数,而函数改变了这个对象的属性,这样的改变对函数外部是可见的。
【示例1】

function myFunc(theObject) {
  theObject.make = "Toyota";
}

var mycar = {make: "Honda", model: "Accord", year: 1998};
var x, y;

x = mycar.make;     // x获取的值为 "Honda"

myFunc(mycar);
y = mycar.make;     // y获取的值为 "Toyota"
                    // (make属性被函数改变了)

参数类型

一、ECMAScript6开始,有两个新的类型的参数:默认参数,剩余参数。

默认参数

一、JavaScript中,函数参数的默认值是undefined。
二、es6之前:设定默认参数的一般策略是在函数体重测试参数值是否为undefined,如果是则赋予这个参数一个默认值。

function multiply(a, b) {
  b = (typeof b !== 'undefined') ?  b : 1;

  return a*b;
}

multiply(5); // 5

es6:

function multiply(a, b = 1) {
  return a*b;
}

multiply(5); // 5

后备的默认参数

一、有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明的时候,也能行得通。
二、为了判断参数是否被省略掉,我们可以拿它跟undefined做比较:

function showMessage(text) {
  if (text === undefined) {
    text = 'empty message';
  }

  alert(text);
}

showMessage(); // empty message

/* 或者我们可以使用||运算符*/
// 如果 "text" 参数被省略或者被传入空字符串,则赋值为 'empty'
function showMessage(text) {
  text = text || 'empty';
  ...
}

三、现代 JavaScript 引擎支持空值合并运算符??,当可能遇到其他假值时它更有优势,如0会被视为正常值不被合并:

// 如果没有传入 "count" 参数,则显示 "unknown"
function showCount(count) {
  alert(count ?? "unknown");
}

showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown

剩余参数

一、剩余参数语法允许将不确定数量的参数表示为数组。
【实例1】剩余剩余参数收集从第二个到最后参数

function multiply(multiplier, ...theArgs) {
  return theArgs.map(x => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

使用arguments对象

一、函数的实际参数会被保存在一个类似数组的arguments对象中。在函数内,你可以按如下方式找出传入的参数

arguments[i]

其中i是参数的序数编号(译注:数组索引),以0开始。
【实例1】用分隔符分隔函数参数

function myConcat(separator) {
   var result = ''; // 把值初始化成一个字符串,这样就可以用来保存字符串了!!
   var i;
   // iterate through arguments
   for (i = 1; i < arguments.length; i++) {
      result += arguments[i] + separator;
   }
   return result;
}

可以给函数传递任意数量的参数,会将各个参数连接成一个字符串“列表”

// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");

// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");

// returns "sage. basil. oregano. pepper. parsley. "
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");

二、arguments变量只是“类数组对象”,并不是一个数组。称其为类数组对象是说它有一个索引编号和length属性。