JavaScript青少年简明教程:函数及其相关知识(上)
在JavaScript中,函数是一段可以重复使用的代码块,它执行特定的任务并可能返回结果。
内置函数(Built-in Functions)
内置函数是编程语言中预先定义的函数,也可以称为标准库函数(Standard Library Functions)。这些函数由编程语言的引擎或标准库提供,开发者可以直接使用它们来完成常见的任务,而无需自己实现相应的功能。例如,JavaScript的Math对象就包含了一系列数学相关的内置函数,如Math.abs()、Math.sin()等。
本节重点介绍自定义函数。
自定义函数
先看常见的命名函数
命名函数使用function关键字来定义,后面跟着函数名和一对圆括号内的参数列表,以及大括号内的函数体。格式:
function functionName(parameter1, parameter2, ...) {
// 函数体
// 在这里编写要执行的代码
}
其中,functionName 是函数的名称;parameter1, parameter2, ... 是函数的参数——形参。函数体内编写了要执行的代码。
命名函数的调用:
functionName(argument1, argument2, ...);
其中,functionName 是函数的名称;argument1,argument2, ... 是实参。
例如
// 定义函数
function greet(name) {
return "Hello, " + name;
}
let message = greet("Alice"); // 调用函数
console.log(message);
JavaScript的函数是定义和组织代码的基本构建块之一。一个函数是一组语句的集合,可以在代码中多次调用。函数能够接受输入(称为参数)并返回输出(称为返回值)。
函数的调用
要执行函数中的代码,你需要调用(或执行)该函数。这可以通过在函数名后添加括号(即使该函数不接受任何参数)来完成。
基本函数语法
functionName(argument1, argument2, ..., argumentN)
functionName: 是你要调用的函数的名称。
(): 圆括号是函数调用的标志。
argument1, argument2, ..., argumentN: 是传递给函数的参数(如果有的话)
例如:
// 定义函数
function add(a, b) {
return a + b;
}
// 调用时传递数字
add(1, 2); // sum 是 3
顺便指出下面情况
也可以传递字符串,尽管这不是期望的用法
add('1', '2'); // result 是 '12',因为字符串连接了
说明:在 JavaScript 中,+ 运算符既可以用于数字相加,也可以用于字符串拼接。如果 a 或 b 中有一个是字符串,+ 会进行字符串拼接。
嵌套调用
add(2, add(3, 4)); // 输出: 9
嵌套函数调用并不是 JavaScript 独有的特性。许多其他编程语言(如Python、C++、Java等)也支持这种方式。
形参(parameter)和实参(argument)是在函数定义和函数调用时使用的术语。
形参是在定义函数时,括号内部列出的变量名称。它们是函数内部使用的占位符,用于接收将来调用该函数时传入的值。
实参是在调用函数时,括号内部传递给函数形参的具体值。
实参会依次映射到形参上。
函数的参数传递
在JavaScript中,函数参数都是按值传递的。也就是说,传递给函数的是参数值的拷贝。但是对于不同数据类型,这个“值”的内容是有区别的,对于对象参数,这个“值”是对象的引用。
参数传递方面,在 JavaScript中,参数传递方式是按值传递——传递的是副本。具体说来:1)当将一个原始(基本)类型(primitive type)数据如数字、字符串、布尔值等作为参数传递给函数时,实际上是将该值的一个副本传递给函数——将实参值复制给形参,实参和形参相互独立互不干扰。函数内部对该副本的修改不会影响到原始的值。2)当将一个引用数据类型(对象类型)(如对象、数组等)作为参数传递给函数时,传递的是该对象的引用(地址)的副本——将实参引用的地址值复制给形参,形参和实参指向同一个对象的地址,改变形参所指向的对象的属性将影响实参所指向的对象。需要注意,在引用类型的参数传递中,并不会改变形参的值(即引用的地址),而是通过形参改变它所指向的对象的属性。
☆当传递原始数据类型时,实际上是将原始值的副本传递给了函数或其他变量。这意味着函数或其他变量操作的是原始值的副本,而不是原始值本身。
这种传递方式使得函数内部对原始值的修改不会影响到函数外部的变量,因为函数内部操作的是原始值的副本,而不是原始值本身。
例如:
function addTen(num) { // num 是一个局部变量
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30
解析如下:
此例展示了在 JavaScript 中传递变量的值的方式。
例中定义了一个函数 addTen,它接受一个参数 num。
在此主要关注变量情况:
首先,定义了一个变量 count,并赋值为 20。
接下来,调用 addTen 函数,并将 count 作为参数传递进去。这里需要注意的是,虽然 count 的值是 20,但是在函数中传递的是 count 的值的副本,而不是 count 本身。
在函数内部,num 的值被增加了 10,变成了 30。然后,函数返回了这个新的值,并将其赋给了变量 result。
最后,通过 console.log 打印了 count 和 result 的值。由于在函数中传递的是 count 的值的副本,所以 count 的值没有发生变化,仍然是 20。而 result 的值是函数返回的新值,即 30。图解如下:
☆当传递引用数据类型(reference type)数据如对象、数组时,实际上是将引用的地址传递给了函数。这意味着函数可以通过引用来访问和修改原始对象。引用是指向存储在内存中的对象的地址,所以传递引用时,传递的是指向对象的指针,而不是对象本身的实际值。
这种传递方式使得我们可以在函数内部修改原始对象,并且这些修改会在函数外部可见。因为函数和外部变量都指向同一个对象,所以对对象的修改会影响到所有引用该对象的变量。
例如:
function changeName(obj) {
obj.name = 'Bob';
}
let person = { name: 'Alice' };
console.log(person.name); // 输出 'Alice'
changeName(person);
console.log(person.name); // 输出 'Bob'
解析如下:
此例展示了在 JavaScript 中传递对象的引用的方式。
例中定义了一个函数 changeName,它接受一个参数 obj。
在此主要关注变量情况:
首先,定义了一个对象 person,并赋值为 { name: 'Alice' }。这个对象有一个 name 属性,其初始值为 ‘Alice’。
接下来,通过 console.log 打印了 person.name 的值,即 ‘Alice’。
然后,调用 changeName 函数,并将 person 对象作为参数传递进去。这里需要注意的是,虽然 person 是一个对象,但是在函数中传递的是 person 对象的引用,而不是对象本身的副本。
在函数内部,通过修改 obj 的 name 属性,将其值改为 ‘Bob’。由于 obj 是 person 对象的引用,所以这个修改也会影响到 person 对象本身。
最后,通过 console.log 打印了 person.name 的值,即 ‘Bob’。由于在函数中修改了 person 对象的 name 属性,所以 person.name 的值变成了 ‘Bob’。图解如下:
总结之,传递引用时,传递的是引用的地址,而不是实际的值。传递原始数据类型时,传递的是实际的值的副本。这种差异导致了在函数内部对引用数据类型的修改会影响到函数外部的变量,而对原始数据类型的修改不会影响到函数外部的变量。
为加深理解,下面再给出两个示例说明。
例1、对于原始类型(primitive type)如数字、布尔值、字符串等,传递的就是这个值本身。在函数体内对形参的任何修改,都不会影响实参变量的值。例如:
function changeNum(x) {
x = 100; // 修改形参x的值为100
}
let num = 10;
changeNum(num);
console.log(num); // 输出10
在这个例子中,num变量的值10作为实参传递给changeNum函数。但在函数内部将形参x重新赋值为100,并不会影响到外部实参num的值。所以最后打印出来的num值仍然是10。
例2、对于引用类型(reference type)如对象、数组等,传递的值是对象的引用。如果在函数体内通过形参引用修改了对象的属性,实参对象的内容也会被修改,因为它们引用的是同一个对象。但如果在函数内部给形参变量重新赋值另一个对象,实参对象本身是不受影响的。如:
function changeArr(arr) {
arr.push(4); // 修改数组内容
arr = [1, 2]; // 重新赋值数组
}
let numbers = [1, 2, 3];
changeArr(numbers);
console.log(numbers); // 输出 [1, 2, 3, 4]
在这个例子中,将数组 [1, 2, 3] 作为实参传递给 changeArr 函数。在函数内部,通过形参 arr 调用 push 方法,向数组追加了一个新元素 4。由于形参和实参引用的是同一个数组,所以实参 numbers 的内容也发生了改变。接下来,将一个新的数组 [1, 2] 赋值给了形参 arr。但是,这个赋值操作只在函数内部生效,实参 numbers 指向的数组没有改变。所以最后输出的 numbers 是 [1, 2, 3, 4]。
理解清楚JavaScript的参数传递方式,可以帮助我们更好地掌控代码的执行过程,减少隐蔽的bug。
学习过其它语言的朋友注意,与Java、C#语言不同,JavaScript的参数传递不检查实参数量和形参数量是否匹配。在JavaScript中,如果你调用的函数提供的参数(实参)少于声明的参数(形参),那么未被传值的形参将被赋予undefined。如果提供的实参多于形参,那么多余的实参将不会被函数内部直接使用,但它们仍然可以通过arguments对象(在非箭头函数中)或剩余参数(rest parameters,使用...语法)来访问。
当实参数量少于形参数量时,未被传值的形参将被自动赋值为undefined,例如:
function multiply(a, b) {
console.log(a, b); // 输出: 2, undefined
return a * b;
}
console.log(multiply(2)); // 输出: NaN
当实参数量多于形参数量时,多余的实参虽然不能直接在函数内部通过形参名称访问,但仍然可以通过arguments对象或剩余参数来获取:
function sum(a, b) {
let result = a + b;
for (let i = 2; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
console.log(sum(1, 2, 3, 4)); // 输出: 10
在这个例子中,sum函数只有两个形参a和b,但我们传递了四个实参。通过arguments对象,我们可以在函数内部访问所有实参。
使用剩余参数可以更方便地处理多余的实参:
function sum(a, b, ...numbers) {
let result = a + b;
for (let value of numbers) {
console.log(value);
}
return result;
}
console.log(sum(1, 2, 10, 20));
这里,这里 a 接收 1,b 接收 2,而 ...numbers 收集 10 和 20。
for...of 循环打印了 10 和 20
函数返回 result,即 3
输出:
10
20
3
变量作用域(scope)
JavaScript 中的作用域(scope)指的是变量可访问范围。主要有以下几种作用域:
☆全局作用域
全局作用域是最外层的作用域,在任何地方都可以访问全局作用域中的变量。如:
var globalVar = "I'm global";
function testScope() {
console.log(globalVar); // 可以访问全局变量
}
testScope(); // 输出: I'm global
console.log(globalVar); // 输出: I'm global
☆函数作用域
函数作用域是指在函数内部声明的变量,只能在该函数内部访问。如:
function testFunctionScope() {
var localVar = "I'm local";
console.log(localVar); // 可以访问
}
testFunctionScope(); // 输出: I'm local
// console.log(localVar); // 错误:localVar is not defined
☆块级作用域 (ES6引入)
使用let和const关键字声明的变量具有块级作用域,只在声明它们的块(由 {} 包围)内有效。如:
if (true) {
let blockVar = "I'm in a block";
console.log(blockVar); // 输出: I'm in a block
}
// console.log(blockVar); // 错误:blockVar is not defined
for (let i = 0; i < 3; i++) {
console.log(i); // 输出: 0, 1, 2
}
// console.log(i); // 错误:i is not defined
☆词法作用域(Lexical Scope)
词法作用域,也称为静态作用域,是指变量的作用域在代码编写时(词法分析时)就已经确定,而不是在运行时确定。在 JavaScript 中,函数的作用域在函数定义时就已经确定,而不是在函数调用时。
关键点:
内部函数可以访问外部函数的变量。
作用域是基于函数的嵌套关系,而不是函数的调用关系。
这种机制使得闭包成为可能。词法作用域的一个重要应用。
示例:
let globalVar = "I'm global";
function outerFunction() {
let outerVar = "I'm from outer";
function innerFunction() {
let innerVar = "I'm inner";
console.log(globalVar); // 可以访问全局变量
console.log(outerVar); // 可以访问外部函数的变量
console.log(innerVar); // 可以访问自己的变量
}
innerFunction();
// console.log(innerVar); // 错误:innerVar 在这里不可访问
}
outerFunction();
// console.log(outerVar); // 错误:outerVar 在这里不可访问
在这个例子中,innerFunction 可以访问它自己的变量、外部函数 outerFunction 的变量,以及全局变量。这就是词法作用域的体现。
变量提升
“语法基础”一节中提到过变量提升,学过函数理解更容易了。
变量提升是 JavaScript 中的一种机制,它将变量和函数的声明移动到它们所在作用域的顶部。这意味着无论你在哪里声明变量和函数,它们都会被视为在当前作用域的开始处声明。
关键点:
只有声明会被提升,赋值不会。
函数声明会被完整地提升。
变量声明使用 var 关键字时会被提升,但 let 和 const 不会(它们会产生暂时性死区)。
console.log(x); // 输出:undefined
var x = 5;
// 上面的代码等同于:
var x;
console.log(x);
x = 5;
// 函数提升
sayHello(); // 输出:"Hello!"
function sayHello() {
console.log("Hello!");
}
// let 和 const 不会被提升
console.log(y); // 抛出 ReferenceError
let y = 10;
函数表达式(Function Expression)
【function expression - JavaScript | MDN 】
函数表达式也是定义函数的一种方式,但函数是赋值给一个变量的。
JavaScript 函数表达式是一种在表达式中定义函数的方式。它允许将函数作为值进行赋值、传递给其他函数或存储在变量中。
函数表达式的语法如下:
letVariableName = function [name](parameters) {
// 函数体
};
其中,name 是函数的名称,函数名,该名称仅是函数体的局部名称——在外部作用域中不可见。可以省略。当省略函数名的时候,该函数就成为了匿名(anonymous)函数。parameters 是函数的参数列表,而函数体则是具体的代码块。
函数表达式的调用:
VariableName(argument)
其中,argument,实参,是在调用函数时传递给函数形参的具体值。
匿名函数表达式,例如:
const x = function (y) {
return y * y;
};
console.log( x(2)); //输出 4
具名函数表达式,例如:
let sum = function add(a, b) {
return a + b;
};
console.log(sum(3,5)); // 输出 8
//console.log(add(3,5)); // 这会抛出错误: add is not defined,因为add在这里不可见
注意,具名函数表达式,这里的函数名在外部作用域中不可见。
这种特性的主要用途递归等:递归函数可以在内部调用自己,而不需要依赖外部的变量名。例如:
const factorial = function calc(n) {
if (n <= 1) return 1;
return n * calc(n - 1); // 这里可以使用 calc
};
console.log(factorial(5)); // 输出: 120
//console.log(calc(5)); // ReferenceError: calc is not defined
在这个例子中,calc 只在函数内部可见,这允许函数进行递归调用,同时不会污染外部作用域。关于递归函数后面将介绍。
ES6引入了箭头函数,这是匿名函数的一种简洁写法。下一节介绍。