JavaScript中定义函数的方法

2,738 阅读6分钟

翻译:道奇
作者:Yash Agrawa
原文:blog.logrocket.com/defining-fu…

一般来说,函数是一串指令或是一段可以被该函数内部或外部代码调用的“子程序”,本质上,函数“封装”了一个特定的任务。函数是JavaScript中一个基础模块,真正理解函数可以帮助理解JavaScript的一些奇怪的点。

JavaScript中的函数

需要注意的是,函数是JavaScript的重要的一类对象,这意味着JavaScript中的函数可以像其他对象一样进行处理,并且像其他变量一样被引用或作为参数传入函数中。

函数甚至有属性和方法,就像其他JavaScript对象一样。函数和其他对象的关键区别在于函数可以调用。

JavaScript中的每个函数是个Function对象,可以进到控制台然后尝试以下脚本:

function typeCheck() {};
typeCheck instanceof Function // Logs True

Function对象有一些内置的方法和属性,如apply, call, bind, isGenerator 等等,而其他对象就没有。

JavaScript中定义函数有好几种方式,不同的定义方式会影响函数最后的行为,下面就挨个看一下。

函数声明

这是最常见的定义函数的方式了,函数声明包括关键字function,其后是一个函数名,接着是一对必需要有的括号,括号内是可选的参数列表。

function sum(param1, param2) {
  return param1 + param2;
}

用这种方式定义函数有两件事需要注意一下:

  • 在当前作用域下会创建一个包含这个函数对象的变量,变量的标识名就是函数的名称,在本例中指的就是sum
  • 变量是会被提升到当前作用域的顶层域的。可以在这里查看更多内容 为了更好的理解提升,看个例子:
console.log(notYetDeclared()); // Logs 'Here!'

function notYetDeclared() {
  return 'Here';
}

我们可以在定义notYetDeclared之前就调用这个函数。

函数表达式

函数表达式在语法上和函数声明很相似,主要的不同在于函数表达式不需要函数名。

let sum = function(param1, param2) {
  return param1 + param2;
};

函数表达式是一条语句的一部分,上面的例子中,函数表达式就是sum变量赋值的一部分, 不同于函数声明的是函数表达式不会进行提升。

console.log(notYetDeclared); // Logs 'undefined'

let notYetDeclared = function() {

  return 'Here';
}

函数表达式的一个有意思的使用场景是创建IIFE或是立即调用函数表达式的能力,很多时候我们想定义一个函数然后立即调用它,但是其他地方不会再调用。

当然,使用函数声明也可以做到,但为了让代码可读性更好,并且让其他程序不会意外访问到它,可以使用IIFE,看下面的代码:

function callImmediately(foo) {
  console.log(foo);
}

callImmediately('foo'); // Logs 'foo'

上面的代码创建了callImediately函数,该函数包含参数foo,并把参数foo打印到控制台,然后立即调用它。下面这样做可以达到同样的结果:

(function(foo) {
  console.log(foo);
})('foo'); // Logs 'foo'

两者关键的不同在于第一个例子中函数声明污染了全局命名空间,并且命名函数callImmediately在使用后很长一段时间依然随处可调用。但IIFE是匿名的,因此以后是不能调用的。

箭头函数

箭头函数是ES6新增的,是函数表达式的语法紧凑版,箭头函数通过使用一对包含参数列表的括号进行定义,跟着一个双箭头=>,然后是由大括号{}包裹函数语句。

let sum = (param1, param2) => {
  return param1 + param2;
};

由于箭头函数背后的主要动机之一是语法紧凑,如果箭头函数中只有return语句,可以去掉大括号和return关键字,像下面这样:

let sum = (param1, param2) => param1 + param2;

同样,如果只有一个参数,大括号也可以去掉:

let double = param1 => param1 * 2;

这种函数定义的形式需要注意的是:

  • 箭头函数没有自己的this,它使用的是封闭词法作用域,可以在这里阅读更多关于this的内容
let foo = {
    id: 10,
    logIdArrow: () => { console.log(this.id) },
    logIdExpression: function() {
      console.log(this.id);
    }
  }
  
  foo.logIdArrow(); // Logs 'undefined'
  foo.logIdExpression(); // Logs '10'

上面的例子中,foo对象中定义了一个箭头函数和一个函数表达式,两者都调用this记录了foo.id

  • 箭头函数没有prototype属性。
let foo = () => {};
console.log(foo.prototype); // Logs 'undefined'
  • arguments对象在箭头函数内是不可用的,可以在这里阅读更多关于arguments对象的内容

Function 构造器

前面提到过,JavaScript中每个函数是Function对象,所以要定义函数,可以直接调用Function对象的构造器。

let sum = new Function('param1', 'param2', 'return param1 + param2');

参数以逗号分隔的字符串'param1', 'param2', 'param3', ..., 'paramN'传入,函数体作为最后一个参数传入。

从性能方面考虑的话,这种定义函数的方式没有函数声明或函数表达式那么高效,使用Function构造器定义函数在每次构造器被调用的时候,构造器都会被解析一次,因为字符串的函数体需要每次都需要重新进行解析,不像其他方式那样,只需要解析其他的代码就可以了。

这种方式定义函数的一种用途是在浏览器中通过Nodewindow对象访问global对象。这些函数经常在全局作用域中创建并且不需要在当前作用域中访问。

Generator函数

Generator函数是ES6新增的。Generator函数是一种特殊的函数类型,就意义上而言它不像传统的函数,Generator函数会在每个请求的基础上生成多个值,并在这些请求之间暂停它们的执行。

function* idMaker() {
  let index = 0;
  while(true)
    yield index++;
}

let gen = idMaker();

console.log(gen.next().value); // Logs 0
console.log(gen.next().value); // Logs 1
console.log(gen.next().value); // Logs 2

function*和yield关键字对于generator函数来说是唯一的。Generator函数通过在function尾部添加一个 * 来定义Generator函数,这让我们在generator函数体中可以用yield在请求上生成多个值。可以在这里查看更多的细节。

总结

选用哪种定义类型取决于实际情况和你想要实现什么。以下是几个可以考虑的点:

  • 如果想利用函数提升,使用函数声明,例如,为了清晰起见顶部仅仅是抽象流,而将函数实现细节放在底部。
  • 箭头函数同样适用于短回调函数,更重要的是使用this时是封闭函数。
  • 尽量避免使用Function构造器来定义函数,如果讨厌的语法不足以让你远离他,那么你需要了解它的速度非常慢的,每次调用时都会解析一次。