一、在 JavaScript 中,函数就是值。
二、JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?
1、在 JavaScript 中,函数就是对象。
三、一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等。

函数的属性

属性 “name”

一、name:函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。
一、函数对象包含一些便于使用的属性。
【示例1】一个函数的名字可以通过属性 “name” 来访问:

  1. function sayHi() {
  2. alert("Hi");
  3. }
  4. alert(sayHi.name); // sayHi

二、更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:

  1. let sayHi = function() {
  2. alert("Hi");
  3. };
  4. alert(sayHi.name); // sayHi(有名字!)

三、当以默认值的方式完成了赋值时,它也有效:

  1. function f(sayHi = function() {}) {
  2. alert(sayHi.name); // sayHi(生效了!)
  3. }
  4. f();

四、规范中把这种特性叫做「上下文命名」。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。
五、对象方法也有名字:

  1. let user = {
  2. sayHi() {
  3. // ...
  4. },
  5. sayBye: function() {
  6. // ...
  7. }
  8. }
  9. alert(user.sayHi.name); // sayHi
  10. alert(user.sayBye.name); // sayBye

六、有时会出现无法推测名字的情况。此时,属性name会是空,像这样:

  1. // 函数是在数组中创建的
  2. let arr = [function() {}];
  3. alert( arr[0].name ); // <空字符串>
  4. // 引擎无法设置正确的名字,所以没有值

1、实际上,大多数函数都是有名字的。

属性 “length”

一、length:函数定义时的入参的个数。Rest 参数不参与计数。
一、还有另一个内置属性 “length”,它返回函数入参的个数,比如:

  1. function f1(a) {}
  2. function f2(a, b) {}
  3. function many(a, b, ...more) {}
  4. alert(f1.length); // 1
  5. alert(f2.length); // 2
  6. alert(many.length); // 2
  1. (()=>{}).length; // 0

1、可以看到,rest 参数不参与计数。
二、属性length有时在操作其它函数的函数中用于做内省/运行时检查(introspection))。
三、比如,下面的代码中函数ask接受一个询问答案的参数question和可能包含任意数量handler的参数…handlers。
1、当用户提供了自己的答案后,函数会调用那些handlers。我们可以传入两种handlers:

  • 一种是无参函数,它仅在用户回答给出积极的答案时被调用。
  • 一种是有参函数,它在两种情况都会被调用,并且返回一个答案。

2、为了正确地调用handler,我们需要检查handler.length属性。
3、我们的想法是,我们用一个简单的无参数的handler语法来处理积极的回答(最常见的变体),但也要能够提供通用的 handler:

  1. function ask(question, ...handlers) {
  2. let isYes = confirm(question);
  3. for(let handler of handlers) {
  4. if (handler.length == 0) {
  5. if (isYes) handler();
  6. } else {
  7. handler(isYes);
  8. }
  9. }
  10. }
  11. // 对于积极的回答,两个 handler 都会被调用
  12. // 对于负面的回答,只有第二个 handler 被调用
  13. ask("Question?", () => alert('You said yes'), result => alert(result));

四、这种特别的情况就是所谓的多态性)—— 根据参数的类型,或者根据在我们的具体情景下的length来做不同的处理。这种思想在 JavaScript 的库里有应用。

自定义属性

一、函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。
它们创建一个“主”函数,然后给它附加很多其它“辅助”函数。例如,jQuery库创建了一个名为$的函数。lodash库创建一个函数,然后为其添加了.add、_.keyBy以及其它属性(想要了解更多内容,参查阅docs)。实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。
一、我们也可以添加我们自己的属性。
二、这里我们添加了counter属性,用来跟踪总的调用次数:

function sayHi() {
  alert("Hi");

  // 计算调用次数
  sayHi.counter++;
}
sayHi.counter = 0; // 初始值

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // Called 2 times

三、属性不是变量
1、被赋值给函数的属性,比如sayHi.counter = 0,不会在函数内定义一个局部变量counter。换句话说,属性counter和变量let counter是毫不相关的两个东西。
2、我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然。它们之间是平行的。
四、函数属性有时会用来替代闭包。例如,我们可以使用函数属性将变量作用域,闭包章节中 counter 函数的例子进行重写:

function makeCounter() {
  // 不需要这个了
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

1、现在count被直接存储在函数里,而不是它外部的词法环境。
2、那么它和闭包谁好谁赖?
3、两者最大的不同就是如果count的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的函数可以修改它。而如果它是绑定到函数的,那么就很容易:

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

4、所以,选择哪种实现方式取决于我们的需求是什么。

命名函数表达式

一、如果函数是通过函数表达式的形式被声明的(不是在主代码流里),并且附带了名字,那么它被称为命名函数表达式(Named Function Expression)。这个名字可以用于在该函数内部进行自调用,例如递归调用等。
一、命名函数表达式(NFE,Named Function Expression),指带有名字的函数表达式的术语。
二、例如,让我们写一个普通的函数表达式:

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};

三、然后给它加一个名字:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

1、我们这里得到了什么吗?为它添加一个”func”名字的目的是什么?
2、首先请注意,它仍然是一个函数表达式。在function后面加一个名字”func”没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。
3、添加这个名字当然也没有打破任何东西。
4、函数依然可以通过sayHi()来调用:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

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

5、关于名字func有两个特殊的地方,这就是添加它的原因:

  1. 它允许函数在内部引用自己。
  2. 它在函数外是不可见的。

6、例如,下面的函数sayHi会在没有入参who时,以”Guest”为入参调用自己:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 使用 func 再次调用函数自身
  }
};

sayHi(); // Hello, Guest

// 但这不工作:
func(); // Error, func is not defined(在函数外不可见)

四、我们为什么使用func呢?为什么不直接使用sayHi进行嵌套调用?
1、当然,在大多数情况下我们可以这样做:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

2、上面这段代码的问题在于sayHi的值可能会被函数外部的代码改变。如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error,嵌套调用 sayHi 不再有效!

3、发生这种情况是因为该函数从它的外部词法环境获取sayHi。没有局部的sayHi了,所以使用外部变量。而当调用时,外部的sayHi是null。
五、我们给函数表达式添加的可选的名字,正是用来解决这类问题的。
1、让我们使用它来修复我们的代码:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 现在一切正常
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest(嵌套调用有效)

六、现在它可以正常运行了,因为名字func是函数局部域的。它不是从外部获取的(而且它对外部也是不可见的)。规范确保它只会引用当前函数。
七、外部代码仍然有该函数的sayHi或welcome变量。而且func是一个“内部函数名”,可用于函数在自身内部进行自调用。
八、函数声明没有这个东西
1、这里所讲的“内部名”特性只针对函数表达式,而不是函数声明。对于函数声明,没有用来添加“内部”名的语法。
2、有时,当我们需要一个可靠的内部名时,这就成为了你把函数声明重写成函数表达式的理由了。

函数对象创建过程

JavaScript代码中定义函数,或者调用Function创建函数时,最终都会以类似这样的形式调用Function函数:var newFun=Function(funArgs, funBody); 。创建函数对象的主要步骤如下:

  1. 创建一个build-in object对象fn
  2. 将fn的内部[[Prototype]]设为Function.prototype
  3. 设置内部的[[Call]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤3
  4. 设置内部的[[Construct]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤1,2,3,4
    1. 创建函数对象时,引擎会将当前执行环境的作用域链传给Function的[[Construct]]方法。[[Construct]]会创建一个新的作用域链,内容与传入的Scope Chain完全一样,并赋给被创建函数的内部[[Scope]]属性。
  5. 设置fn.length为funArgs.length,如果函数没有参数,则将fn.length设置为0
  6. 使用new Object()同样的逻辑创建一个Object对象fnProto
  7. 将fnProto.constructor设为fn
  8. 将fn.prototype设为fnProto
  9. 返回fn

步骤1跟步骤6的区别为,步骤1只是创建内部用来实现Object对象的数据结构(build-in object structure),并完成内部必要的初始化工作,但它的[[Prototype]]、[[Call]]、[[Construct]]等属性应当为null或者内部初始化值,即我们可以理解为不指向任何对象(对[[Prototype]]这样的属性而言),或者不包含任何处理(对[[Call]]、[[Construct]]这样的方法而言)。步骤6则将按照前面描述的对象创建过程创建一个新的对象,它的[[Prototype]]等被设置了。
从上面的处理步骤可以了解,任何时候我们定义一个函数,它的prototype是一个Object实例,这样默认情况下我们创建自定义函数的实例对象时,它们的Prototype链将指向Object.prototype。
另外,Function一个特殊的地方,是它的[[Call]]和[[Construct]]处理逻辑一样。