JavaScript 函数基础——函数定义的各种方式以及函数的作用


函数的主要作用就是把程序中许多相似的操作封装起来,达到复用的目的,需要时调用一下函数即可,不用再编写过多的重复代码,大大简化了后续相似的操作。
本文整理了 JavaScript 中函数的各种定义(创建)方式,以及基本的使用。

一、函数声明

函数使用 function 关键字来声明,基本语法如下:

function 函数名(参数1, 参数2, ...) {
    // 代码编写在这里
    
    return value;
}

可以通过函数名加上()来调用函数,就像这样:

function sayHi() {
	console.log('Hello!');
}

sayHi(); // 输出 'Hello!' 
sayHi(); // 可复用

二、作用域

函数会形成一个作用域,换句话说就是,函数中声明的变量只在该函数内部可见。

function fn() {
    let a = 1;
    console.log(a); 
}

fn();// 输出 1
console.log(a); // 报错,外部无法访问函数内部的变量

外部无法访问函数内部的变量,但是函数可以访问外部的变量,也可以对其修改。

let a = 1;

function fn() {
    a += 1;
    console.log(a); 
}

fn();// 输出 2
console.log(a); // 2 ,外部的 a 已经被修改

如果函数内部声明了跟外部同名的变量,它们不是在同一个作用域中的,是不同的两个变量。函数中访问同名变量时,会使用函数内部变量,忽略外部变量。

在函数之外声明的变量(也不在{}之内)被称为全局变量,它属于全局作用域。全局变量在所有函数中都是可见的,但是也有弊端——它们有被意外修改的风险,尽量减少全局变量的使用是一种很好的做法。

想要了解更多关于作用域的内容,请参见 → 作用域

三、参数

我们可以通过参数将数据传递给函数,通过传递的数据影响执行结果,使函数更灵活、复用性更强。

function foo(a, b) {
    console.log([a, b]);
}

foo(1, 2); // 输出 [1, 2]

这个例子中,ab 属于函数中的局部变量,只能在函数中访问。调用函数时,传递的数据会根据位置来匹配对应,分别赋值给 ab

创建函数时,function 函数名 后面括号中设定的参数被称为形参;调用函数时,函数名后面括号中传入的参数被称为实参。上面例子中,ab 是形参,传入的 12 是实参。

参数默认值

如果调用函数时缺少提供实参,那么形参默认值为 undefined

有时候我们想要设置默认值,在 ES6 之前还不支持显式地设置默认值,只能采用变通的方式:

function sayHi(name) {
    name = name || 'everyone';
	console.log( 'Hello ' + name + '!');
}

sayHi(); // 输出 'Hello everyone!' 

通过检查参数值的方式判断有没有赋值,上面的做法虽然简便,但缺点在于如果传入的参数对应布尔值为 false ,实参就不起作用了。需要更精确的话可以用 if 语句或者三元表达式,判断参数是否等于 undefined,如果是则说明这个参数缺失 :

// if 语句判断
function sayHi(name) {
	if (name === undefined) {
		name = 'everyone';
	}
    
	console.log( 'Hello ' + name + '!');
}

// 三元表达式判断
function sayHi(name) {
	name =  (name !== undefined) ? name : 'everyone';
	
    console.log( 'Hello ' + name + '!');
}

ES6 就方便了许多,因为它支持了显式的设置默认值的方式,就像这样:

function sayHi(name = 'everyone') { // 定义函数时,直接给形参赋值
	console.log( 'Hello ' + name + '!');
}

sayHi(); // 输出 'Hello everyone!' 
sayHi('Tony'); // 输出 'Hello Tony!' 
sayHi(undefined); // 输出 'Hello everyone!'

这些结果表明了,它也是通过参数是否等于 undefined 来判定参数是否缺失的。

默认值不但可以是一个值,它还可以是任意合法的表达式,甚至是函数调用:

function sayHi(name = 'every'+'one') {
	console.log( 'Hello ' + name + '!');
}

function sayHi(name = foo()) {
	console.log( 'Hello ' + name + '!');
}

关于函数参数还有一些其他的细节和技巧,具体可参见 → 函数参数

四、返回值

函数中可以使用 return 关键字将一个值作为调用的结果返回。例如:

function sum(a, b) {
    return a + b;
}

let result = sum(1, 2); // 用变量来接收返回的值

console.log(result); // 3

return 关键字是可选的,它有两个作用:

  1. 指定返回值。如果不使用 return ,或者用 return 而不指定返回的值,则默认返回 undefined
  2. 结束函数的执行。当函数执行到 return 语句,函数会立即停止执行并退出,return 语句后面哪怕还有代码也不会继续执行。
function fn1(){
    
}
function fn2(){
    return
}

console.log( fn1() === undefined ); // ture
console.log( fn2() === undefined ); // ture

return可以在函数中存在多个,例如:

function diff(num1, num2) {
    if (num1 < num2) {
        return num2 - num1; // 若条件符合则执行此条语句,
    }
    
    return num1 - num2;
}

diff(1,2); // 1
diff(3,1); // 2

上面例子中,当 num1 < num2 成立,则执行完第一条 return 语句并终止函数执行,否则执行第二条 return 语句。

注意!不要在 return 和返回值之间换行。对于返回一条比较长的表达式,你可能会出于可读性的目的把它放在新的一行,但是:

return // 等于 return;
	(some + long + expression + or + whatever * f(a) + f(b))

这样不行,因为 JavaScript 默认会在 return 语句最后加上分号;,实际上这样会返回 undefined。如果实在想跨行,那么应该在 return 这一行开始写,或者按照如下方式加上括号:

return (
	some + long + expression
	+ or +
	whatever * f(a) + f(b)
	)

推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如 果函数有时候返回值,有时候有不返回值,会给调试代码带来不便。

五、其他形式的函数

JavaScript 中创建函数的方式不止一种。

函数表达式

在 JavaScript 中,函数不是“神奇的语言结构”,而是一种特殊的值,就像 10 或者 [1, 2, 3] 这些一样。因此,有另一种创建函数的方式被称为函数表达式。就像这样:

let sayHi = function() {
	console.log('Hello!');
}

可以看到这种创建函数的方式和其他赋值一样,把一个匿名函数赋值给一个变量。不管函数是采用何种定义方式,都只是一个存储在变量 sayHi 的值。

匿名函数即 function 关键字后面没有标识符。如果一个函数有其他方式能够被调用,那么 function 关键字后面可以省略标识符。但如果是函数声明来创建函数,则不能省略。

函数表达式和函数声明除了创建方式不同之外,还有两个不同之处。

  1. 在预解析时它们的“提升”行为不同。

    函数声明:会提升整个函数体,所以能在定义之前调用。

    函数表达式:跟其他变量一样,只提升标识符,不提升其初始化。

想要了解更多关于预解析时 “提升” 的内容,请参见 → 变量提升和函数提升

  1. 严格模式下,函数声明如果在{}中,它的声明会被限制在块级作用域内。

以下代码期望的是根据条件声明函数,但是并不能达到目的:

'use strict';
let a = 1;
if (a) {
	foo(); // 1
	function foo() {
		console.log(1);
	}
} else {
	function foo() {
		console.log(0);
	}
}

foo(); // 报错,foo 没有定义

它的作用域已经被限制在块级作用域内,只在{}内可见。那怎么样才能实现条件声明呢,可以使用函数表达式:

'use strict';
let a = 1;
let foo;
if (a) {
	foo = function () {
		console.log(1);
	}
} else {
	foo = function () {
		console.log(0);
	}
}

foo(); // 输出 1

命名函数表达式

刚才所提到的函数表达式,它是匿名的。相对的,也可以在 function 关键字后面加上名字,这种被称为命名函数表达式(NFE,Named Function Expression)

它看起来是这样:

let sayHi = function fn() {
	console.log('Hello!');
}

sayHi(); // 输出 'Hello!'

它依然是函数表达式,那么多了这一个名字的意义是什么呢?

这里 fn 有两个特殊的地方:

  1. 它允许函数在内部引用自己。
  2. 在函数外部不可见。
let sayHi = function fn( name ) {
	if (name) {
		console.log('Hello' + name + !);
	} else {
		fn('everyone!'); // 调用自身
	}
};

sayHi(); // Hello, everyone

func(); // 报错, 在函数外部不可见

为什么不用 sayHi 来引用自己,而要用 fn 来引用自己呢?sayHi 当然在大多数情况下都可以引用自己,但如果是下面这种情况:

let sayHi = function( name ) {
	if (name) {
		console.log('Hello' + name + !);
	} else {
		sayHi('everyone!');
	}
};
let welcome = sayHi;
sayHi = null;

welcome(); // 报错,sayHi 不再是一个函数

sayHi 是有可能被函数外部代码改变的,当它不再指向这个函数,那么函数就会报错。但如果提供了一个内部的函数名字 ,就可以避免这个问题。另外,在函数内部也不可改变它,这里的 fn 是一个常量。

let sayHi = function ( name ) {
 	sayHi = 1;
 	console.log(sayHi);
 };

sayHi(); // 输出 1
sayHi(); // 报错,sayHi 不再是函数


let sayHi = function fn( name ) {
 	fn = 1;
 	console.log(fn);
 };

sayHi(); // 报错,Assignment to constant variable. 常量不可改变

立即调用函数表达式

前面提到的函数表达式,它们需要通过函数名去调用才会执行,还有另一种方式可以执行函数表达式,这种方式通常被称为立即调用函数表达式(IIFE,immediately-invoked function expressions)

通常是这样的:

(function () {
    console.log('IIFE');
})(); // 最后的括号代表调用

(function () {
    console.log('IIFE');
}());  // 括号第二种放置方式,功能是一样的

函数首先被包裹在一对括号内,使其成为一个表达式,所以不需要函数名,而第二个括号代表调用这个函数。

除了使用括号,还有其他方式可以使函数形成一个表达式:

!function() {
  alert('利用 ! 运算符形成表达式');
}();

+function() {
  alert('利用 + 运算符形成表达式');
}();

立即调用函数表达式的主要作用是用来创建一个局部的作用域,当然它也可以进行参数传递。

let a = 1;

(function (value) {
    console.log(value);
})(a);

立即调用函数表达式的主要作用是用来形成一个局部的作用域,作为模仿块级作用域的存在,在 ES6 支持块级作用域之后,已经不再需要创建立即调用函数表达式这样的方式。 但我们依然要知道它,能够帮助我们去理解可能会遇到的旧标准代码。

箭头函数

ES6 提供了新的创建函数方式,这种方式类似于函数表达式但更为方便。它就是箭头函数,基本语法如下:

let sum = (a, b) => {
    return a + b;
}

在只有一个参数的情况下,可以省略括号

let sum = x => { return ++x; }

如果只有一行代码,甚至可以省略大括号{}return ,它会隐式地返回这行代码的结果。

let sum = x => ++x;

箭头函数还有其他有趣的特性,具体可参见 → 箭头函数

new Function 语法

最后一种创建函数的方式是利用构造函数 Function

let sum = new Function('a', 'b', 'return a + b');

console.log(sum(1, 2)); // 3

Function 构造函数接收字符串参数,将字符串转换成函数。最后一个参数为函数体,而之前的参数都是函数参数

下面是一个没有参数的例子:

let sayHi = new Function('console.log("Hello")');

sayHi(); // 输出 'Hello'

这样的函数在内部不能访问外层作用域(非全局)的变量,只能直接访问全局变量(不是传递参数而是直接引用)。

let value = "global";

function getValue() {
	let value = "test";

	let fn = new Function('console.log(value)');
	
    return fn;
}

getValue()(); // 输出 'global' ; 如果没有全局变量 value,会报错:value 未定义

这种定义函数的方式并不推荐使用,因为它会被解释两次:第一次是将它当做常规的代码,第二次是解释传给构造函数的字符串。明显影响了性能。

六、何时使用函数

函数复用性的特点十分明显,通常会认为用函数只是封装多次使用的代码。但把一段逻辑相关的代码封装在一起也很有用,即使只调用一次。例如下面一个获取随机整数的函数:

function getRandomInt(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min)) + min; // 不含最大值,含最小值
}

let result = getRandomInt(1, 10);

在理解代码逻辑时,只需要把关注点放在 let result = getRandomInt(1, 10); 语句上,代码结构更为清晰,可读性更强。

总结

  1. 函数的创建有 4 种方式:函数声明、函数表达式、箭头函数、new Function 语法。
  2. 函数声明和函数表达式区别在于 “提升” 和在{ }中的表现。函数声明会提升整个函数体,严格模式下会被限制在块级作用域中;而函数表达式的提升和可见范围取决于变量的声明方式。
  3. 如果函数要在内部要引用自己,推荐使用函数声明创建函数,或者使用命名函数表达式。
  4. 立即调用函数表达式主要是用来模拟块级作用域,编写新的代码不需要用到它,学习它主要是为了帮助理解旧标准的代码。
  5. 有时候即使一段逻辑相关的代码只会出现一次,把它们封装成一个函数也很有意义。
- END -

感谢阅读,如果有疑问或发现错误,欢迎在评论区留言讨论哦,与大家共同进步~

如果感觉本文能给到你一点点帮助,欢迎点赞、收藏加关注支持博主~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值