JavaScript函数
函数基本概念
这些概念,对初学者而言,不可能一下子理解,开始先大体知道、了解即可,后面将详细解释,等耐心学习实践一段时间后,再回过头理解领悟其含义就容易了。
为完成某一功能的程序指令(语句)的集合,称为函数。
☆JavaScript函数的分类
1、自定义函数(我们自己编写的函数),如:function funName(){},本讲重点之。
2、系统函数(JavaScript自带的函数),如alert函数。
注意:和C语言、Visual Basic语言等函数定义不可以嵌套,调用可以嵌套不同,JavaScript函数定义和调用都可以嵌套,这一点和python函数一样。
在 JavaScript中,函数是头等(first-class)对象,每个函数其实都是一个Function对象,因为它们可以像任何其他对象一样具有属性(property)和方法(method)。它们与其他对象的区别在于函数可以被调用。
如果一个函数中没有使用return语句,则它默认返回undefined。要想返回一个特定的值,则函数必须使用 return 语句来指定一个要返回的值。(使用new关键字调用一个构造函数除外)。
调用函数时,传递给函数的值被称为函数的实参,对应位置的函数参数名叫作形参。
下面展开介绍。
JavaScript函数是对象
在 JavaScript 中,虽然使用 typeof 操作符判断函数类型将返回 "function" ,但是将JavaScript 函数描述为一个对象更加准确,JavaScript 函数有(property)和方法(method)。length 属性返回函数调用过程接收到的参数个数。toString() 方法将函数作为一个字符串返回。
function myFun(a, b) {
return a * b;
}
在浏览器“控制台”(console)中测试上面这个函数:
函数名本质就是一个变量名,是指向某个Function对象的引用。
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。
JavaScript自定义函数的定义(简称函数定义,也称为函数声明,或函数语句)
定义函数有多种方式:
JavaScript 有三种基本定义函数的方式
(1)函数的常规定义(声明定义),语法
function functionName(arg0, arg1, ... argN) {
statements
}
函数名与左括号之间没有空格!关键字 function、函数名(functionName)、一组参数arg0, arg1, ... argN放在一对圆括号内,以及置于大括号中的待执行代码statements。如:
function product(a, b) {
return a * b;
}
var z = product(4, 3); //调用
上面的代码命名了一个myFunction函数,以后使用myFunction()这种形式,就可以调用相应的代码。这叫做函数的定义。
当这样定义一个函数时,javascript实际上在后台为你创建了一个对象。这个对象的名称就是函数名本身。这个对象的类型是function。
函数返回值使用return关键字, 没有return语句则默认返回undefined。
声明式函数的调用可以在定义之前或者定义之后,因为可以函数提升(何为函数提升?见后面“JavaScript函数提升”部分)。
下面给出在网页中使用的代码:
<!DOCTYPE html>
<html>
<head>
<script>
function myFunction() {
function product(a, b) {
return a * b;
}
var z = product(4, 3); //调用
alert(z);
}
</script>
</head>
<body>
<button onclick="myFunction()">Try it</button>
</body>
</html>
保存为网页文件(扩展名为.html的文件),可运行之看看效果。
(2)函数表达式定义(赋值式定义)
语法
var myFunction = function name([param[, param[, ... param]]]) {
statements
}
如:
var x = function (a, b) {
return a * b};
var z = x(4, 3); //调用
这种写法是将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。
赋值式函数的调用只能在定义之后。
下面给出在网页中使用的代码:
<!DOCTYPE html>
<html>
<head>
<script>
function myFunction() {
var x = function (a, b) {
return a * b
}
var z = x(4, 3); //调用
alert(z);
}
</script>
</head>
<body>
<button onclick="myFunction()">Try it</button>
</body>
</html>
保存为网页文件(扩展名为.html的文件),可运行之看看效果。
(3)Function 对象(类)创建函数,也称为 函数构造器(Function())定义函数
用 Function 类直接创建函数,这说明JavaScript函数实际上是功能完整的对象。用 Function 类直接创建函数的语法如下:
var function_name = new function(arg1, arg2, ..., argN, function_body)
第三种定义函数的方式是Function构造函数。这种定义函数的方式非常不直观,几乎无人使用。通过Function定义的函数效率远不如通过函数常规定义语句或字面量表达式定义两种方式。
var myFunction = new Function( 'a', 'b', 'return a * b');
var y = myFunction(4, 3); //调用
对Function 对象(类)后面还会介绍。
下面给出在网页中使用的代码:
<!DOCTYPE html>
<html>
<head>
<script>
function myFunction() {
var myFunction = new Function( 'a', 'b', 'return a * b');
var y = myFunction(4, 3); //调用
alert(y);
}
</script>
</head>
<body>
<button onclick="myFunction()">Try it</button>
</body>
</html>
保存为网页文件(扩展名为.html的文件),可运行之看看效果。
return语句
函数体内部的return语句,表示返回。JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。
通常情况,return语句对于一个函数是很有必要的,因为往往需要函数在一系列的代码执行后会得到一个期望的返回值,而此值就是通过return语句返回,并且将控制权返回给主调函数。
语法格式:
return 表达式
通常情况下return后面跟有表达式,但是并不是绝对的,例如:
return;
此情况就是单纯的将控制权转交给主调函数继续执行。
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。
function add(x, y) {
return x + y;
}
add(1, 1) // 2
需要注意return语句的特点,即其后面的语句不执行,具体示例代码如下:
function f(a,b,c)
{
console.log(a); //执行
return;
console.log(b); //不执行
console.log(c); //不执行
}
f(1,2,3); //调用
只打印(显示)出a值,而b和c值并未打印,因为前面有return语句。
JavaScript函数返回值类型
return后的值将会作为函数的执行结果返回,可以定义一个变量,来接收该结果。JavaScript函数返回值类型有两种:值类型和引用类型。JavaScript的return还可以返回一个函数(Returning a function)。
☆值类型使用的是值传递方式,即传递数据的副本。一般情况下,函数返回的非对象数据都使用值返回方式,如下面的代码所示:
fcuntion sum( a , b ) //
{
return a + b; // 返回两个数之和
}
var c = sum( 1, 2 ); //调用
☆引用类型返回的是数据的地址,而不是数据本身 。通常返回复合类型数据时使用引用传递方式,如下代码所示 :
fcuntion getNameList() 定义函数,以获取名单
{
var List = new Array("Lily", "Petter", "Jetson"); // 名单
return List; // 返回名单引用
}
var nameList = getNameList(); // 调用
nameList = null; // 删除引用
☆JavaScript的return还可以返回一个函数(Returning a function)
function getSum() // 定义加法函数
{
function sum( a, b ) // 定义私有函数
{
return a+b; // 返回两个数之和
}
return sum; // 返回私有函数的地址
}
var sumOfTwo = getSum(); // 取得私有函数地址
var total = sumOfTwo( 1, 2 ); // 求和
或写为:
function getSum() // 定义加法函数
{
return function sum( a, b ) // 定义私有函数
{
return a+b; // 返回两个数之和
}
}
var sumOfTwo = getSum(); // 取得私有函数地址
var total = sumOfTwo( 1, 2 ); // 求和
形参与实参
形参
形参实际上就是函数内部使用的变量,在函数外部不能使用
在定义函数()中每写一个单词,就相当于在函数内部定义一个可以使用的变量,多个单词之前使用,隔开,如:
function fun(num1, num2) {
// 在函数内部就可以使用 num1 和 num2 这两个变量
}
var fun1 = function(num1, num2) {
// 在函数内部就可以使用 num1 和 num2 这两个变量
实参
在函数调用时为形参赋值使用
多个参数的时候一般需要一一对应,如:
function fn(num1, num2){
//这里形参是num1和num2
}
fn(100,200); // 调用fn函数,此时num1=100,num2=200
一般来说,函数的形参和实参个数是相等的,但在JavaScript中没有规定两者必须相等。如果形参个数大于实参,那么多出的形参值为undefined。相反,如果实参个数大于形参,那么多出的实参就无法被形参访问,而被忽略掉。
参数类型
在 JavaScript中,参数传递方式是按值传递——传递的是副本。1)当将一个值类型(如数字、字符串、布尔值等)作为参数传递给函数时,实际上是将该值的一个副本传递给函数。函数内部对该副本的修改不会影响到原始的值。2)当将一个引用类型(如对象、数组等)作为参数传递给函数时,传递的是该对象的引用(地址)的副本。这意味着函数内部对该引用的修改将会影响到原始的对象,这可能会产生一种错觉,让人误以为 JavaScript 是按引用传递的。
【如果实参是一个包含原始值(数字,字符串,布尔值)的变量,则就算函数在内部改变了对应形参的值,返回后,该实参变量的值也不会改变。
如果实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变。】
JS的“参数本质上是按值传递给函数的——因此,即使函数体的代码为传递给函数的参数赋了新值,这个改变也不会反映到全局或调用该函数的代码中。” https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Functions
函数的参数传递,遵从 按值传递 原则,可以记忆为传递的是 栈内存 中的值,要么是一个原始值,要么是一个地址。具体可见 ECMAScript 中变量的复制,以及函数的参数传递 https://juejin.cn/post/6953878356836384805
JavaScript函数传参原理,参见 JavaScript函数传参原理详解——值传递还是引用传递_小敏哥的专栏-CSDN博客_js函数传参是值传参还是引用
JavaScript中arguments这个特殊参数
arguments是函数(不包括箭头函数)内置的一个类数组对象,数组元素为函数的参数。可用于获取函数的可变参数(不定参数)列表——适用于函数参数无法确定个数的情况;
下面给出一个例子:
function say() {
console.log( arguments[0] + arguments[1] + arguments[2]);
}
say("wang"," ","wang"); //hello wang
再给出一个例子
function sum() {
var sum = 0;
var args = arguments;
for (var i in args) {
sum += args[i];
}
return sum;
}
console.log(sum(1,2,3,4)); //10
运行之,参见下图:
arguments有几个属性length和callee,length表示调用方传入了多少个参数,callee表示当前函数体本身。第三个属性是个Symbol类型的键,该类型的值都是独一无二的,该键指向的值是一个values函数。
☆利用argument的length属性可以返回参数的个数
function cNumbArg () {
return arguments.length;
}
cNumbArg(25,2255,"您好");//返回3
☆测试callee表示当前函数体本身——对下面的例子而言指函数fun:
function fun(){
console.log('arguments.callee === fun的值:',arguments.callee === fun);
}
fun(); // arguments.callee === fun的值: true
☆测试第三个属性
function fun(){
console.log(arguments[Symbol.iterator]);
let iterator = arguments[Symbol.iterator]();
console.log('iterator:',iterator);
console.log(iterator.next());
}
fun();
你可以在运行测试之看看。
在ES6之前,我们通常使用arguments来实现不定参数,比如前面提到的例子:
function sum() {
var sum = 0;
var args = arguments;
for (var i in args) {
sum += args[i];
}
return sum;
}
console.log(sum(1,2,3,4)); //10
使用这种方式虽然能实现不定参数的效果,但是缺点也很明显:
1)不能够从函数定义头直接看出使用了可变参数
2)如果在函数头里增加了一个参数,那么可能函数体都得重改,如:
function sum(notice) {
var sum = 0;
var args = arguments;
for (var i=1; i<args.length; ++i) {
sum += args[i];
}
return notice+sum;
}
console.log(sum(1,2,3,4)); //10
可以注意到:这里i的值从1开始递增,如果我们再增加了参数,那么函数体就需要进一步修改,就很不方便。
为了解决arguments存在的不足,ES6中引入了不定参数,如:
function sum(...numbers) {
return numbers.reduce((before, val) => before+val);
}
console.log(sum(1,2,3,4)); //10
运行之,参见下图:
如此一来,以上的两个问题便都解决了。
使用不定参数,需要注意如下问题:
1)只有最后一个参数可以被标记为不定参数,在函数调用的时候,额外的参数都会被放进不定参数所对应的数组里,如sum(notice, ...numbers),当调用sum('Sum is', 1, 2, 3)的时候,1、2、3三个数都会被存放进numbers对应的数组里。
2)当没有额外的参数的时候,不定参数不会为undefined,而是[]空数组。
ES6中还引入了默认参数,当一个参数未被传值时,则使用默认参数。如:
function toast(message, delay = 1000) {
// ...
}
和其他语言中不同的是,默认值表达式在求值时是自左向右的,所以 右边的参数可以引用到左边的参数,如:
function double(a = 1, b = a) {
console.log(a+b);
}
double(5); // 10
默认参数还有几个细节需要注意:
1)传递undefined等效于不传值,即toast('Hello', undefined)等效于toast('Hello') 。
2)没有默认值的参数,隐式默认为undefined,即toast()中,message的取值为undefined。
JavaScript函数提升
JavaScript函数提升是指允许函数调用在函数定义(函数声明)之前,例如:
foo();
function foo() {
console.log('I am hoisted');
}
JavaScript引擎是把函数声明整个地提升到了当前作用域的顶部,预编译之后的代码逻辑如下:
function foo() {
console.log('I am hoisted');
}
foo();
JavaScript作用域(scope)
在 JavaScript 中, 作用域(scope,或译有效范围)就是变量和函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
下面先看几个例子
function outFun() {
var varS = "varS是outFun函数中的一个变量";
console.log(varS);
}
console.log(varS); //报错:varS' is not defined
outFun(); //输出:varS是outFun函数中的一个变量"
从此例可以看出:函数内的变量,在函数外不能访问使用。
再看一个例子:
var outVariable = "outVariable是最外层函数外的变量"; //全局变量
function outFun() { //外层函数
var inVariable = "inVariable是外层函数outFun中的变量"; //外层函数中的变量
function innerFun() { //内层函数
console.log(inVariable); //内层函数可以访问到外层函数中的变量
}
innerFun(); //
}
console.log(outVariable); //输出:outVariable是最外层函数外的变量
outFun(); //输出:inVariable是外层函数outFun中的变量
console.log(inVariable); //报错:inVariable is not defined
innerFun(); //报错:innerFun is not defined
从此例可以看出:全局变量有的值可在整个程序中访问,内层函数可以访问到外层函数中的变量等特点。
☆全局作用域(Global Scope)
(1)不在任何函数内定义的变量就具有全局作用域。
(2)实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性。
(3)window对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top 等。
☆局部作用域(Local Scope)
JavaScript的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,称为函数(局部)作用域。
变量具有局部或者全局作用域,这导致运行时变量的访问来自不同的作用域。全局作用域的生存周期与应用程序相同。局部作用域只在该函数调用执行期间存在。
全局变量
全局变量有全局作用域,它的值可在整个程序中访问和修改。
(1)在函数定义外声明的变量是全局变量。
(2)全局变量有 全局作用域,它的值可在整个程序中访问和修改。
(3)如果变量在函数内没有声明(没有使用 var、let关键字),该变量为全局变量。
局部变量
局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。每当执行函数时,都会创建和销毁该变量。函数外无法访问函数内的变量,函数内却可以访问函数外的变量。
(1)在函数定义内声明的变量是局部变量。
(2)因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
(3)每当执行函数时,都会创建和销毁该变量,且无法通过函数之外的任何代码访问该变量。
(4)函数外无法访问函数内的变量,函数内却可以访问函数外的变量。
(5)局部变量和全局变量重名时,局部变量会覆盖全局变量。
局部变量的优先级是高于全局变量的,但是虽然名字相同,局部变量不会影响到同名全局变量在函数外的值。
下面给出例子。
函数外部不能访问函数内部局部变量(私有属性)。全局变量和局部变量可以使用相同的名称,但它们是不同的变量,全局变量和局部变量示例代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>变量的作用域例子1</title>
</head>
<body>
<script>
var a=10; //声明全局变量
var b=a+5; //声明全局变量
function showVars()
{
var a=20; //声明局部变量
var b=a+5; //声明局部变量
return "函数内部的a=" + a + "\n" + "函数内部的b=" +b;
}
var str=showVars();
alert(str+ "\n"+"函数外部的a=" + a + "\n" + "函数外部的b=" +b)
</script>
</body>
</html>
将上述代码保存文件名为:变量的作用域例子1.html,双击之,运行结果参见下图:
块级作用域
var 不支持块级作用域。使用var关键字在函数内声明的变量,就可以在整个函数内部使用。
为了解决块级作用域,ES6引入了 let 和 const 关键字,可以声明一个块级作用域的变量。let支持块级作用域,用let关键字在代码块中声明变量,在代码块范围内生效。
var、let、const的区别
var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
let、const 声明并不会被提升到当前代码块的顶部,因此需要将 let、const 声明放置到顶部,以便让变量在整个代码块内部可用。如:
function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
let、const 禁止在同一作用域内重复声明,否则会抛出错误。如:
{
var count = 30;
let count = 40;
}
将抛出错误:
在Microsoft Edge浏览器中抛出错误:Use before declaration
在Google Chrome浏览器中抛出错误:Uncaught SyntaxError: Identifier 'count' has already been declared
变量除变量作用域外,还存在变量作用域链。变量作用域链是指变量的查找过程,即每一段JavaScript代码(包含函数)都会有一个与之关联的作用域链。作用域链的开始位置为调用变量的位置,然后再一层层向外链接,下面通过示例代码理解作用域链,示例代码如图:
可以看到程序中有两个bar变量,当bar被调用时,将通过作用域链开始位置进行查找,先找到最近定义的变量位置,即第4行代码,作用域链停止查找,直接返回结果,即456。如果没有第4行代码,作用域链在开始位置找不到结果,就会向作用域的外层继续查找,直到找到想要的变量,第2行代码,返回123。如果作用域链上最终找不到在将抛出一个异常。
函数作用域
作用域指作用范围,JavaScript函数也是具备作用域的。假设在foo函数中定义了bar函数,即bar函数只能在foo函数内进行调用,而不能在foo函数外调用,参见下图:
变量的隐式声明
JavaScript变量允许不声明就使用
当没有声明,直接给变量赋值时,会隐式地给变量声明,此时这个变量作为全局变量存在——作用于整个页面的,因此即使在函数作用域中的变量也是全局变量。
function test(){
a=3;
console.log(a);//3
}
test();
console.log(a);//3
隐式声明的话就没有变量声明提前的功能了。所以下面的使用是会报错:
function test(){
console.log(a);//ReferenceError: a is not defined
a=3;
}
test();
关于JavaScript 的变量作用域,还可参见:现代 JavaScript 的变量作用域 现代 JavaScript 的变量作用域 · Issue #2 · OFED/translation · GitHub
箭头函数表达式( arrow function expression)
ES6标准新增了一种新的函数:Arrow Function(箭头函数)也称为箭头函数表达式( arrow function expression)。
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this(注:但会捕获其所在上下文的 this 值,作为自己的 this 值),arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
语法:
([param] [, param]) => { statements }
param => expression
其中
param 参数名称,箭头函数放 参数 的地方就在 () 内, 没有参数,() 必须写, 一个参数,() 可写可不写,多个参数,() 必须写。
箭头函数放 函数体 的地方在 {}内,函数体 就 一句 {} 可写可不写,函数体 不止一句,{} 必须写。
如果不知道,() {} 写不写,该不该省略,那就写,写了不会错。
statements 或 expression 多个声明statements需要用大括号括起来,而单个表达式时则不需要。表达式expression也是该函数的隐式返回值。
可参见:https://segmentfault.com/a/1190000012067545
箭头函数的书写顺序 参数 => 函数体
例子:
var sum = (a, b) => { return a+b;}
sum(2, 3); 调用
下面给出在网页中使用的代码:
<html>
<head>
<meta charset="utf-8">
<script>
function secondFunction(){
var sum = (a, b) => { return a+b;} //箭头函数
document.write(sum(2, 3));
}
</script>
</head>
<body>
<p>单击以下按钮调用函数</p>
<form>
<input type="button" onclick="secondFunction()" value="调用函数">
</form>
</body>
</html>
将上述代码保存文件名为:箭头函数例子.html,双击之可看运行结果。
嵌套函数(Nested Functions)
JavaScript允许函数的嵌套定义,即在函数体中定义函数。例如:
<html>
<head>
<meta charset="utf-8">
<script>
function hypotenuse(a, b) {
function square(x) {
return x*x;
} //内部函数
var val;
val = square(a) + square(b);
return val;
}
function secondFunction(){
var result;
result = hypotenuse(2, 3);
document.write ( result );
}
</script>
</head>
<body>
<p>单击以下按钮调用函数</p>
<form>
<input type="button" onclick="secondFunction()" value="调用函数">
</form>
</body>
</html>
将上述代码保存文件名为:嵌套函数例子.html,双击之可看运行结果。
递归
递归函数是一种非常重要的编程技术,当年我在学习其他编程技术(如C、C++、Java等)都经常用到。在定义递归函数时,需要2个必要条件:
- 一个结束递归的条件;
- 一个递归调用的语句;
如:
用递归计算阶乘(factorial)
function factorial(n){
if(n == 1){//跳出条件
return 1;
}
return n*factorial(n-1); //调用自身
}
//调用:计算5的阶乘
console.log( factorial(5)); //120
附录、
JavaScript的 this 关键字 JavaScript系列之this是什么 - 知乎