转行学前端的第 37 天 : 了解 ECMAScript function 声明和调用

507 阅读14分钟

我是小又又,住在武汉,做了两年新媒体,准备用 6 个月时间转行前端。

今日学习目标

昨天基于搜索来仔细学习 Date 实例对象方法。今天主要是基于搜索来基础学习 Function 数据结构,又是适合学习的一天,加油,小又又!!!!


what

JavaScript中,每个函数其实都是一个Function对象。查看Function页面了解其属性和方法。

返回值

如果一个函数中没有使用return语句,则它默认返回undefined

想返回一个特定的值,则函数必须使用return 语句来指定一个要返回的值。(使用new关键字调用一个构造函数除外)。


实参&形参

要调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫作形参

如果实参是一个包含原始值(数字,字符串,布尔值)的变量,则就算函数在内部改变了对应形参的值,返回后,该实参变量的值也不会改变。

如果实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变:


how

定义函数有多种方法

函数声明 (函数语句)

有一个特殊的语法来声明函数(查看函数语句了解详情):

function name([param[, param[, ... param]]]) { statements }

name

函数名.

param

传递给函数的参数的名称,一个函数最多可以有255个参数。

statements

组成函数体的声明语句。


函数表达式 (function expression)

函数表达式函数声明非常相似,它们甚至有相同的语法(查看函数表达式了解详情)。

一个函数表达式可能是一个更大的表达式的一部分。可以定义函数“名字”(例如可以在调用堆栈时使用)或者使用“匿名”函数。函数表达式不会提升,所以不能在定义之前调用。

var myFunction = function name([param[, param[, ... param]]]) { statements }

name

函数名,可以省略。当省略函数名的时候,该函数就成为了匿名函数。

param

传递给函数的参数的名称,一个函数最多可以有255个参数.

statements 组成函数体的声明语句。 下面是匿名函数的一个例子(函数没有名字):

var myFunction = function() {
    // statements
}

也可以在定义时为函数命名:

var myFunction = function namedFunction(){
    // statements
}

命名函数表达式的好处是当我们遇到错误时,堆栈跟踪会显示函数名,容易寻找错误。

可以看到,上面的两个例子都不以function开头。不以function开头的函数语句就是函数表达式定义。

当函数只使用一次时,通常使用IIFE (Immediately Invokable Function Expressions)。 当函数只使用一次时,通常使用IIFE (Immediately Invokable Function Expressions)。

(function() {
    statements
})();

IIFE是在函数声明后立即调用的函数表达式。


函数生成器声明 (function* 语句)

函数声明有一种特殊的语法 (详情请查阅function* statement ):

function* name([param[, param[, ...param]]]) { statements }

name

函数名称.

param

传递给函数的参数的名称,一个函数最多可以有255个参数。

statements

组成函数体的声明语句。


函数生成器表达式 (function*表达式)

构造函数表达式和函数声明类似,并且有着相同的语法 (详情请查阅function* statement ):

function* [name]([param] [, param] [..., param]) { statements }

name

函数名称。函数名可以被省略,在这种情况下该函数将变成匿名函数。

param

传递给函数的参数的名称。一个函数可以有多达255个参数

statements

组成函数体的声明语句。


箭头函数表达式 (=>)

箭头函数表达式有着更短的语法,并在词汇方面结合这个值 (详情请查阅 arrow functions ):

([param] [, param]) => { statements } param => expression

param

参数名称. 零参数需要用()表示. 只有一个参数时不需要括号. (例如 foo => 1)

statements or expression

多个声明statements需要用大括号括起来,而单个表达式时则不需要。表达式expression也是该函数的隐式返回值。


Function构造函数

注意: 不推荐使用Function构造函数创建函数,因为它需要的函数体作为字符串可能会阻止一些JS引擎优化,也会引起其他问题。

所有其他对象, Function 对象可以用new操作符创建:

new Function (arg1, arg2, ... argN, functionBody)

arg1, arg2, ... argN

函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的JavaScript标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。

functionBody

一个构成的函数定义的,包含JavaScript声明语句的字符串。

Function的构造函数当作函数一样调用(不使用new操作符)的效果与作为Function的构造函数调用一样。


生成器函数的构造函数

注意: GeneratorFunction 不是一个全局对象,但可以从构造函数实例取得。(详情请查阅生成器函数).

注意: 不推荐使用构造器函数的构造函数 (GeneratorFunction constructor)创建函数,因为它需要的函数体作为字符串可能会阻止一些JS引擎优化,也会引起其他问题。

所有其他对象, GeneratorFunction 对象可以用 new 操作符创建:

new GeneratorFunction (arg1, arg2, ... argN, functionBody)

arg1, arg2, ... argN

函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的JavaScript标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。

functionBody

一个构成的函数定义的,包含JavaScript声明语句的字符串。

GeneratorFunction的构造函数当作函数一样调用(不使用new操作符)的效果与作为GeneratorFunction的构造函数调用一样。


对象方法定义

从ECMAScript 6开始, 你可以用更短的语法定义自己的方法,类似于getterssetters。详情请查阅 method definitions .

语法

const obj = {
  get property() {},
  set property(value) {},
  property( parameters… ) {},
  *generator( parameters… ) {},
  async property( parameters… ) {},
  async* generator( parameters… ) {},

  //  with computed keys
  get [property]() {},
  set [property](value) {},
  [property]( parameters… ) {},
  *[generator]( parameters… ) {},
  async [property]( parameters… ) {},
  async* [generator]( parameters… ) {},
};

案例

该简写语法与ECMAScript 2015的gettersetter语法类似。

如下代码:

var obj = {
  foo: function() {
    /* code */
  },
  bar: function() {
    /* code */
  }
};

现可被简写为:

var obj = {
  foo() {
    /* code */
  },
  bar() {
    /* code */
  }
};

注意:简写语法使用命名函数而不是匿名函数(如…foo: function() {}…)。命名函数可以从函数体调用(这对匿名函数是不可能的,因为没有标识符可以引用)。


声明方式对比

基础对比

对比下面的例子:

一个用Function构造函数定义的函数,被赋值给变量multiply

var multiply = new Function('x', 'y', 'return x * y');

一个名为multiply的函数声明:

function multiply(x, y) {
   return x * y;
} // 没有分号

一个匿名函数的函数表达式,被赋值给变量multiply

 var multiply = function(x, y) {
   return x * y;
 };
 

一个命名为func_named的函数的函数表达式,被赋值给变量multiply

var multiply = function func_name(x, y) {
   return x * y;
};


差别

虽然有一些细微的差别,但所起的作用都差不多

  • 函数名和函数的变量存在着差别。
  • 函数名不能被改变,但函数的变量却能够被再分配。
  • 函数名只能在函数体内使用。
  • 倘若在函数体外使用函数名将会导致错误(如果函数之前是通过一个var语句声明的则是undefined)。

例如:

var y = function x() {};
alert(x); // throws an error

当函数是通过 Function's toString method被序列化时,函数名同样也会出现。


另一方面,被函数赋值的变量仅仅受限于它的作用域,该作用域确保包含着该函数被声明时的作用域。

正如第四个例子所展示的那样,函数名与被函数赋值的变量是不相同的. 彼此之间没有关系。函数声明同时也创建了一个和函数名相同的变量。

因此,与函数表达式定义不同,以函数声明定义的函数能够在它们被定义的作用域内通过函数名而被访问到:

使用用 new Function定义的函数没有函数名。 然而,在SpiderMonkey JavaScript引擎中,其函数的序列化形式表现的好像它拥有一个名叫"anonymous"的名称一样。

比如,使用 alert(new Function()) 输出:

function anonymous() {
}

而实际上其函数并没有名称,anonymous 不是一个可以在函数内被访问到的变量。

例如,下面的例子将会导致错误:

var foo = new Function("alert(anonymous);"); 
foo();

和通过函数表达式定义或者通过Function构造函数定义的函数不同,函数声明定义的函数可以在它被声明之前使用。举个例子:

foo(); // alerts FOO!
function foo() {
   alert('FOO!');
}

函数表达式定义的函数继承了当前的作用域。换言之,函数构成了闭包。


对象属性

属性名 描述 使用方法
arguments 不推荐使用,属性代表传入函数的实参,它是一个类数组对象。已经被废弃很多年了,现在推荐的做法是使用函数内部可用的 arguments 对象来访问函数的实参。在函数递归调用的时候(在某一刻同一个函数运行了多次,也就是有多套实参),那么 arguments 属性的值是最近一次该函数调用时传入的实参。如果函数不在执行期间,那么该函数的 arguments 属性的值是 null functionName.arguments
arity 已废弃,返回一个函数的形参数量,是一个古老的已经没有浏览器支持的属性,你应该使用length属性来代替.。 functionName.arity
caller 不推荐使用,如果一个函数functionName是在全局作用域内被调用的,则functionName.callernull,相反,如果一个函数是在另外一个函数作用域内被调用的,则functionName.caller指向调用它的那个函数,该属性的常用形式arguments.callee.caller替代了被废弃的arguments.caller functionName.caller
callee 不推荐使用,callee放回正在执行的函数本身的引用,它是arguments的一个属性。 arguments.callee
displayName 不推荐使用,获取函数的显示名称。 functionName.displayName
length 指明函数的形参个数,length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。与之对比的是,arguments.length 是函数被调用时实际传参的个数。 functionName.length
name 返回一个函数声明的名称,使用new Function(...)语法创建的函数或只是 Function(...) create Function对象及其名称为anonymous functionName.name
prototype 函数对象具有属性__proto__,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型。 functionName.prototype

注意事项

callee

  • 这个属性只有在函数执行时才有效
  • 它有一个length属性,可以用来获得形参的个数,因此可以用来比较形参和实参个数是否一致,即比较arguments.length是否等于arguments.callee.length
  • 它可以用来递归匿名函数。
  var a = function() {
      console.log("arguments.callee"); //  arguments.callee
      console.log(arguments.callee);
      //在浏览器控制台 打印效果
      // ƒ () {
      // console.log("arguments.callee");
      // console.log(arguments.callee);
      // console.log("arguments.callee.length");
      // console.log(arguments.callee.length);
      // }

      //在js文件以node的形式运行  打印效果
      //[Function: a]

      console.log("arguments.callee.length------a"); //arguments.callee.length
      console.log(arguments.callee.length);   // 0
  }
  var b = function(n,m) {
      a();
      console.log("arguments.callee.length------b"); //arguments.callee.length
      console.log(arguments.callee.length);   // 2
  }
  b();

length

  • 请注意这个指的是形参的个数,如果参数在传入的时候是以已经定义的情况下,这个时候是不会被计算的
    console.log(Function.length); /* 1 */

    console.log((function()        {}).length); /* 0 */
    console.log((function(a)       {}).length); /* 1 */
    console.log((function(a, b)    {}).length); /* 2 etc. */

    console.log((function(...args) {}).length);
    // 0, rest parameter is not counted


    console.log((function(a, b = 1, c) {}).length);
    // 1, only parameters before the first one with
    // a default value is counted

    console.log((function(a = 1, b, c) {}).length) // 0
    console.log((function(b, a = 1, c) {}).length) // 1
    console.log((function(b, c, a = 1) {}).length) // 2

Function 作用域

有一点应该要注意的,在通过解析Function构造函数字符串产生的函数里,内嵌的函数表达式函数声明不会被重复解析。例如:

var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
foo(); // 函数体字符串"function() {\n\talert(bar);\n}"的这一部分不会被重复解析。

函数声明非常容易(经常是意外地)转换为函数表达式。当它不再是一个函数声明:

  • 成为表达式的一部分
  • 不再是函数或者脚本自身的“源元素” (source element)。“源元素”是脚本或函数体中的非嵌套语句。
var x = 0;               // source element
if (x === 0) {           // source element
   x = 10;               // 非source element
   function boo() {}     // 非 source element
}
function foo() {         // source element
   var y = 20;           // source element
   function bar() {}     // source element
   while (y === 10) {    // source element
      function blah() {} // 非 source element
      y++;               //非source element
   }
}

这一段看不太懂~~~~~


Function构造函数作用域继承

Function构造函数定义的函数,不继承任何全局作用域以外的作用域(那些所有函数都继承的)。

通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而Function构造函数定义的函数却不同。也就是说,每次构造函数被调用,传递给Function构造函数的函数体字符串都要被解析一次 。

虽然函数表达式每次都创建了一个闭包,但函数体不会被重复解析,因此函数表达式仍然要快于"new Function(...)"。 所以Function构造函数应尽可能地避免使用。


调用函数

基础说明

定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。

例如,一旦你定义了函数square,你可以如下这样调用它:

square(5);

上述语句通过提供参数5 来调用函数。函数执行完它的语句会返回值25

函数一定要处于调用它们的域中,但是函数的声明可以被提升(出现在调用语句之后),如下例:

console.log(square(5));
/* ... */
function square(n) { return n*n } 

函数域是指函数声明时的所在的地方,或者函数在顶层被声明时指整个程序。


调用注意事项

注意只有使用如上的语法形式(即 function funcName(){})才可以。而下面的代码是无效的。

就是说,函数提升仅适用于函数声明,而不适用于函数表达式

console.log(square); // square is hoisted with an initial value undefined.
console.log(square(5)); // TypeError: square is not a function
var square = function (n) { 
  return n * n; 
}

函数的参数并不局限于字符串或数字。你也可以将整个对象传递给函数。函数show_props(其定义参见 用对象编程)就是一个将对象作为参数的例子。

函数可以被递归,就是说函数可以调用其本身。例如,下面这个函数就是用递归计算阶乘:

function factorial(n){
  if ((n == 0) || (n == 1))
    return 1;
  else
    return (n * factorial(n - 1));
}

你可以计算1-5的阶乘如下:

var a, b, c, d, e;

a = factorial(1); // 1赋值给a
b = factorial(2); // 2赋值给b
c = factorial(3); // 6赋值给c
d = factorial(4); // 24赋值给d
e = factorial(5); // 120赋值给e

还有其它的方式来调用函数。常见的一些情形是某些地方需要动态调用函数,或者函数的实参数量是变化的,或者调用函数的上下文需要指定为在运行时确定的特定对象。

显然,函数本身就是对象,因此这些对象也有方法(参考Function )。作为此中情形之一,apply()方法可以实现这些目的。


今日学习总结


今日心情

今日主要是基于搜索来基础学习 function 对象如何声明和基础调用,希望明天准备详细学习一下 function 作用域 闭包之类 和 箭头函数 ~~~~

本文使用 mdnice 排版