声明JavaScript函数的六种方法

发布于 大漠

一个函数一次性定义的代码块可以多次调用。在JavaScript中,一个函数有很多元素组成,同时也受很多元素影响:

  • 函数体的代码
  • 函数的参数列表
  • 接受外部变量域的变量
  • 返回值
  • 当函数被调用时,this指上下文
  • 命名和匿名函数
  • 函数对象作为变量声明
  • arguments对象(在ES6中的箭头函数中将丢弃这个)

这些元素都会影响到函数,但具体影响函数的行为还是取决于函数的声明类型。在JavaScript中常见的声明类型有以下几种方法:

函数声明类型对函数代码的影响只是轻微的。重要的是函数如何与外部组件交互功能(比如外部作用域、闭包、对象自身拥有的方法等)和调用方式(普通函数调用、方法调用和构造函数调用等)。

例如,你需要通过this在一个函数调用封闭的下下文(即this从外部函数继承过来)。最好的选择是使用箭头函数,很清楚的提供了必要的下下文。

比如下面示例:

class Names {  
  	constructor (names) {
    	this.names = names;
  	}
  	contains(names) {
    	return names.every((name) => this.names.indexOf(name) !== -1);
  	}
}
var countries = new Names(['UK', 'Italy', 'Germany', 'France']);  
countries.contains(['UK', 'Germany']); // => true  
countries.contains(['USA', 'Italy']);  // => false  

箭头函数传给.every()this(一个替代Names类)其实就是一个contains()方法。使用一个箭头(=>)来声明一个函数是最适当的声明方式,特别是在这个案例中,上下文需要继承来自外部的方法.contains()

如果试图使用一个函数表达式来调用.every(),这将需要更多的手工去配置上下文。有两种方式,第一种就是给 .every(function(){...}, this)第二个参数,来表示上下文。或者在 function(){...}.bind(this)使用.bind()作为回调函数。这是额外的代码,而箭头函数提供的上下文透明度更容易让人理解。

这篇文章介绍了如何在JavaScript中声明一个函数的六种方法。每一种类型都将会通过简短代码来阐述。感偿趣?

函数声明(Function declaration)

函数声明通过关键词function来声明,关键词后面紧跟的是函数的名称,名称后面有一个小括号(()),括号里面放置了函数的参数(para1,...,paramN)和一对大括号{...},函数的代码块就放在这个大括号内。

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

来看一个函数声明的示例:

// function declaration
function isEven (num) {
    return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false

function isEven(num) {...}是一个函数声明,定义了一个isEven函数。用来判断一个数是不是偶数。

函数声明创建了一个变量,在当前作用域,这个变量就是函数的名称,而且是一个函数对象。这个函数变量存在变量生命提升,它会提到当前作用域的顶部,也就是说,在函数声明之前可以调用。

函数声明创建的函数已经被命名,也就是说函数对的name属性就是他声明的名称。在调试或者错误信息阅读的时候,其很有用。

下面的示例,演示了这些属性:

// Hoisted variable
console.log(hello('Aliens')); // => 'Hello Aliens!'
// Named function
console.log(hello.name); // => 'hello'
// Variable holds the function object
console.log(typeof hello); // => 'function'

function hello(name) {
    return `Hello ${name}!`;
}

函数声明function hello(name) {...}创建了一个hello变量,并且提升到当前作用域最顶部。hello变量是一个函数对象,以及hello.name包括了函数的名称hello

一个普通函数

函数声明匹配的情况应该是创建一个普通函数。普通的意思意味着你声明的函数只是一次声明,但在后面可以多次调用它。它下的示例就是最基本的使用场景:

function sum (a, b) {
    return a + b;
}
sum(5, 6); // => 11
([3, 7]).reduce(sum); // => 10

因为函数声明在当前作用域内创建了一个变量,其除了可以当作普通函数调用之外,还常用于递归或分离的事件侦听。函数表达式或箭头函数是无法创建绑定函数名称作为函数变量。

下面的示例演示了一递归的阶乘计算:

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

factorial(4); // => 24

有关于阶乘(Factorial)相关的详细介绍,可以点击这里

factorial()函数做递归计算时调用了开始声明的函数,将函数当作一个变量:factorial(n - 1)。当然也可以使用一个函数表达式,将其赋值给一个普能的变量,比如:var factorial = function (n) {...}。但函数声明function factorial(n)看起来更紧凑(不需要var=)。

函数声明的一个重要属性是它的提升机制。它允许在相同的作用域范围内之前使用声明的函数。提升机制在很多情况下是有用的。例如,当你一个脚本内先看到了被调用的函数,但又没有仔细阅读函数的功能。而函数的功能实现可以位于下面的文件,你甚至都不用滚动代码。

你可以在这里了解函数声明的提升机制。

与函数表达式区别

函数声明函数表达式很容易混淆。他们看起来非常相似,但他们具有不同的属性。

一个容易记住的规则:函数声明总是以function关键词开始,如果不是,那它就是一个函数表达式。

下面就是一个函数声明的示例,声明是以function关键词开始:

// Function declaration: starts with "function"
function isNil(value) {  
  	return value == null;
}

函数表达式不是以function关键词开始(目前都一般出现在代码的中间地方):

// Function expression: starts with "var"
var isTruthy = function(value) {  
  	return !!value;
};

// Function expression: an argument for .filter()
var numbers = ([1, false, 5]).filter(function(item) {  
  	return typeof item === 'number';
});

// Function expression (IIFE): starts with "("
(function messageFunction(message) {
  	return message + ' World!';
})('Hello');

条件中的函数声明

当函数声明出现ifforwhile这样的条件语句块{...}时,在一些JavaScript环境内可能会抛出一个引用错误。让我们来看看在严格模式下,函数声明出现在一个条件语句块中,看看会发生什么。

(function() {
  	'use strict';
  	if (true) {
    	function ok() {
      		return 'true ok';
    	}
  	} else {
    	function ok() {
      		return 'false ok';
    	}
  	}
  	console.log(typeof ok === 'undefined'); // => true
  	console.log(ok()); // Throws "ReferenceError: ok is not defined"
})();

当调用ok()函数时,JavaScript抛出一个异常错误"ReferenceError: ok is not defined",因为函数声明出现在一个条件语句块内。注意,这种情况适用于非严格模式环境下,这让人更感到困惑。

一般来说,在这样的情况之下,当一个函数应该创建在基于某些条件内时,应该使用一个函数表达式,而不应该使用函数声明。比如下面这个示例:

(function() {
  	'use strict';
  	var ok;
  	if (true) {
    	ok = function() {
      		return 'true ok';
    	};
  	} else {
    	ok = function() {
      		return 'false ok';
    	};
  	}
  	console.log(typeof ok === 'function'); // => true
  	console.log(ok()); // => 'true ok'
})();

因为函数是一个普通对象,根据不同的条件,将其分配给一个变量,是一个不错的选择。调用ok()函数也能正常工作,不会抛出任何错误。

函数表达式

函数表达式是由一个function关键词,紧随其后的是一个可选的函数名,一串参数(para1,...,paramN)放在小括号内和代码主体放在大括号内{...}

一些函数表达式的使用方法:

var count = function(array) { // Function expression  
  	return array.length;
}

var methods = {  
  	numbers: [1, 5, 8],
  	sum: function() { // Function expression
    	return this.numbers.reduce(function(acc, num) { // func. expression
      		return acc + num;
    	});
  	}
}

count([5, 7, 8]); // => 3  
methods.sum();    // => 14  

函数表达式创建了一个函数对象,可以用在不同的情况下:

  • 当作一个对象赋值给一个变量count = function(...) {...}
  • 在一个对象上创建一个方法sum: function() {...}
  • 当作一个回调函数.reduce(function(...) {...})

函数表达式在JavaScript中经常使用。大多数的时候,开发人员处理这种类型的函数,喜欢使用箭头函数。

命名函数表达式

当函数没有一个名称(名称属性是一个空字符串)时这个函数是一个匿名函数。

var getType = function(variable) {  
  	return typeof variable;
};
getType.name // => ''  

getType就是一个匿名函数,其getType.name的值为''

当表达式指定了一个名称时,这就是一个命名函数表达式。它和简单的函数表达式相比具有一些额外的属性。

  • 创建一个命名函数,其name属性就是函数名
  • 在函数体中具有和函数对象相同名称的一个变量

我们使用上面的例子,不同的是在函数表达式内指定了一个名称:

var getType = function funName(variable) {  
  	console.log(typeof funName === 'function'); // => true
  	return typeof variable;
}
console.log(getType(3));                    // => 'number'  
console.log(getType.name);                  // => 'funName'  
console.log(typeof funName === 'function'); // => false

function funName(variable) {...}是一个命名函数表达式。在函数作用范围内存一个funName变量。函数对象的name属性就是函数的名称funName

支持命名函数表达式

当变量赋值时使用一个函数表达式var fun = function() {},很多引擎可以推断这个变量的函数名。回调时常常给其传递的是一个匿名函数表达式,并没有存储到变量中,所以引擎不能确定它的名字。

在很多情况之下,使用命名函数和避免匿名函数似乎是很在理的。而且这也会带来一系列的好处:

  • 在调试时,错误信息和调用堆栈时使用函数名能显示更详细的信息
  • 调试时更舒服,可以减少anonoymous堆栈的名字出现的次数
  • 函数名有助于快速理解其功能
  • 在函数递归调用的范围内或事件监听时可以按名称来访问函数

方法定义

方法定义可以在object literals和ES6 class时定义。可以使用一个函数的名称,并紧随其后跟一对小括号放置参数列表(para1,...,paramN)和函数主体代码放在一个大括内{...}

下面的示例是基于object literals上使用方法定义函数。

var collection = {  
  	items: [],
  	add(...items) {
    	this.items.push(...items);
  	},
  	get(index) {
    	return this.items[index];
  	}
};
collection.add('C', 'Java', 'PHP');  
collection.get(1) // => 'Java'

add()get()方法在collection对象使用方法定义。这些方法可以像这样调用collection.add(...)collection.get(...)

方法定义和传统的属性定义有点类似,通一个冒号:把名称和函数表达式连接在一起,比如add:function(...) {...}

  • 更短的语法更易读和写
  • 方法定义创建命名函数,和函数表达式刚好相反。有利于用于调试

注意,使用class语法需要短形式方法来声明:

class Star {  
  	constructor(name) {
    	this.name = name;
  	}
  	getMessage(message) {
    	return this.name + message;
  	}
}
var sun = new Star('Sun');  
sun.getMessage(' is shining') // => 'Sun is shining'  

计算属性名和方法

ES6中增加了一个很好的特性:在object literals和class中可以计算属性。

计算属性的方法和[methodNmae(){...}]略有不同,其定义的方法这样的:

var addMethod = 'add',  
  	getMethod = 'get';
var collection = {  
  	items: [],
  	[addMethod](...items) {
    	this.items.push(...items);
  	},
  	[getMethod](index) {
    	return this.items[index];
  	}
};
collection[addMethod]('C', 'Java', 'PHP');  
collection[getMethod](1) // => 'Java'  

[addMethod](...) {...}[getMethod](...) {...}使用了计算属性名快速方法声明。

箭头函数

箭头函数的定义是使用一对小括号,括号内是一系列的参数(param1,param2,...,paramN),后面紧跟=>符号和{...},代码主体放置在这对大括号内。

当箭头函数只有一个参数时,可以省略这对小括号,另外它只包含一个声明时,大括号都可以省略。

下面的示例就是一个箭头函数的基本用法:

var absValue = (number) => {  
  	if (number < 0) {
    	return -number;
  	}
  	return number;
}
absValue(-10); // => 10  
absValue(5);   // => 5

absValue是一个箭头函数,这个函数主要功能就是计算一个数的绝对值。

函数声明使用箭头函数,其中=>具有以下属性:

  • 箭头函数不创建执行自己的上下文(函数表达式或函数声明式相反,创建不创建取决于this的调用)
  • 箭头函数是一个匿名函数:name是一个空字符串''(函数声明式相反,它有一个名字)
  • arguments对象不可使用箭头函数(与其它声明类型相反,其他类型提供arguments对象)

Context transparency

this关键词的使用在JavaScript中让很多同学都感到困惑。(这篇文章详细介绍了this关键词的使用)。

因为函数创建了自己的可执行的上下文(execution context),这也造成一般情况很难确定this所指。

ES6引用箭头函数改善了这种用法(context lexically)。这是一个很好的特性,因为从现在开始函数需要封闭的上下文时没有必要使用.bind(this)或者var self = this

来看一个示例,看this如何继承外部函数:

class Numbers {  
  	constructor(array) {
    	this.array = array;
  	}
  	addNumber(number) {
    	if (number !== undefined) {
       		this.array.push(number);
    	}
    	return (number) => { 
      		console.log(this === numbersObject); // => true
      		this.array.push(number);
    	};
  	}
}
var numbersObject = new Numbers([]);  
numbersObject.addNumber(1);  
var addMethod = numbersObject.addNumber();  
addMethod(5);
console.log(numbersObject.array); // => [1, 5]

Numbers类有一个数字数组,并且提供了一个addNumber()方法,将新数据插入到这个数组中。

addNumber()不带任何参数被调用时,则返回一个闭包,允许插入新的数据。这个闭包是一个箭头函数,它的this就相当于numbersObject。因为其上下文意思取自addNumbers()方法。

如果没有箭头函数,那么需要我们自己手动去修复。这也意味着,要添加.bind()方法:

//...
    return function(number) { 
      console.log(this === numbersObject); // => true
      this.array.push(number);
    }.bind(this);
//...

或者将上下文(context)存给一个变量var self = this:

//...
    var self = this;
    return function(number) { 
      console.log(self === numbersObject); // => true
      self.array.push(number);
    };
//...

context transparency这个属性可以让你在一个封闭的环境内任意使用this

短回调

前面也说过了,当创建的箭头函数只有一个参数,或者主体只有一个声明时,小括号()和花括号{}都可以省去。这有助于创建一个非常短的回调函数。

让我们创建一个函数,如果数组只有0这个元素,将它找出来。

var numbers = [1, 5, 10, 0];  
numbers.some(item => item === 0); // => true 

item => item === 0是一个箭头函数,它看上去非常简单。

有时候嵌套短的箭头函数会让代码阅读起来增加困难。所以最方便的方式是当这它是一个回调函数(没有嵌套)可以使用短的箭头函数方式。如果有必要,添加花括号之来,这样有利于代码的阅读。

函数生成器

生成函数在JavaScript中会返回一个Generator对象。其语法类似于函数表达式、函数声明式和方法声明,不同的是,它需要在function后添加一个*符号。

生成器函数可以按以下这些方式来声明函数:

函数声明function* <name>():

function* indexGenerator() {
    var index = 0;
    while(true) {
        yield index++;
    }
}
var g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1

函数表达式function* ():

var indexGenerator = function* () {  
  	var index = 0;
  	while(true) {
    	yield index++;
  	}
};
var g = indexGenerator();  
console.log(g.next().value); // => 0  
console.log(g.next().value); // => 1 

方法生成*<name>:

var obj = {  
  	*indexGenerator() {
    	var index = 0;
    	while(true) {
      		yield index++;
    	}
  	}
}
var g = obj.indexGenerator();  
console.log(g.next().value); // => 0  
console.log(g.next().value); // => 1  

上面三种方式生成的函数都会返回一个生成器对象g。然后g可以生成一系列的数字。

函数构造器: new Function

在JavaScript函数中第一个类(class object)对象: 函数是一个普通的对象类型是function

这种声明的方式创建相同的函数对象类型,来看一个示例:

function sum1(a, b) {  
  	return a + b;
}
var sum2 = function(a, b) {  
  	return a + b;
}
var sum3 = (a, b) => a + b;  
console.log(typeof sum1 === 'function'); // => true  
console.log(typeof sum2 === 'function'); // => true  
console.log(typeof sum3 === 'function'); // => true  

函数对象类型有一个构造器(constructor):Function

Function当作构造器(constructor)new Function(arg1,arg2,...,argN,bodyString),那么Function 构造器会创建一个新的 Function 对象(new Function)。其中参数arg1,arg2,...,argN会传递给构造器(constructor)成为新函数的参数,而且最后一个参数bodyString用作函数体代码。

来看一个示例,创建一个函数,求两个数的和:

var numberA = 'numberA', numberB = 'numberB';  
var sumFunction = new Function(numberA, numberB,  
   'return numberA + numberB'
);
sumFunction(10, 15) // => 25  

sumFunction创建的Function构造器调用了numberAnumberB两个参数,并且在函数主体内执行return numberA + numberB

这种方式创建的函数不能访问当前的作用域,因为没办法创建闭包。他们总是在全局作用域内创建的。

一个可能就用new Function最佳方式是浏览器或NodeJs脚本访问一个全局对象:

(function() {
   	'use strict';
   	var global = new Function('return this')();
   	console.log(global === window); // => true
   	console.log(this === window);   // => false
})();

如种方式最好

没有孰好孰坏,函数的声明类型的决定要视实际情况而定。但有一些规则还是值得大家一起遵循。

如果要在一个闭包内使用this,那么箭头函数是一个很好的解决方案。另外回调函数是一个简短声明时,箭头函数也是一个很好的选择,因为它的代码短。

当在object literals上需要一个更短的语法时,方法声明是可取的。

new Function这种方法一般不用来声明函数。主要因为它存在很多问题。

我认为这篇文章另一个作用是让大家写出更具可读性的代码,和减少函数使用的bug。因为他们像细胞一样存在任何一个应用程序当中。

本文根据@Dmitri Pavlutin的《Six ways to declare JavaScript functions》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://rainsoft.io/6-ways-to-declare-javascript-functions/

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/6-ways-to-declare-javascript-functions.htmlAir Max 95 Stussy