💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 3.7 函数 函数就是一段可以反复调用的**代码块**。 通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用。 ECMAScript中的函数使用`function`关键字来声明,后跟一组参数以及函数体,这些参数在函数体内像局部变量一样工作。 ~~~ function functionName(arg0, arg1....argN) { statements } ~~~ 函数调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,称为该函数调用表达式的值。 ~~~ function test(name){ return name; } test('tg'); ~~~ 在上面的例子中,name就是形参,调用时的'tg'就是实参。 还可以通过在函数内添加`return`语句来实现返回值。 注意:遇到return语句时,会立即退出函数,即return语句后面的语句不再执行。 ~~~ function test(){ return 1; alert(1); //永远不会被执行 } ~~~ 一个函数中可以包含多个return语句,而且return语句可以不带有任何返回值,最终将返回undefined。 如果函数挂载在一个对象上,将作为对象的一个属性,就称它为对象的方法。 ~~~ var o = { test: function(){} } ~~~ test()就是对象o的方法。 ### 3.7.1 函数定义 JavaScript有**三种**方法,可以定义一个函数。 **1.function命令** ~~~ function name() {} ~~~ name是函数名称标识符。函数名称是函数声明语句必需的部分。不过对于函数表达式来说,名称是可选的:如果存在,该名字只存在于函数体内,并指向该函数对象本身。 圆括号:圆括号内可放置0个或多个用逗号隔开的标识符组成的列表,这些标识符就是函数的参数名称。 花括号:可包含0条或多条JavaScript语句。这些语句构成了函数体。一旦调用函数,就会执行这些语句。 **2.函数表达式** ~~~ var f = function(x){ console.log(x); } ~~~ 采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。 **3.Function()** Function()函数定义还可以通过Function()构造函数来定义 ~~~ var f=new Function('x','y','return x+y'); //等价于 var f=function(x,y){ return x+y; } ~~~ 除了最后一个参数是函数体外,前面的其他参数都是函数的形参。如果函数不包含任何参数,只须给构造函数简单的传入一个字符串---函数体---即可。 不过,Function()构造函数在实际编程中很少会用到。 ### 3.7.2 理解参数 ECMAScript函数不介意传递进来多少个参数,也不在乎传进来参数的类型。原因是ECMAScript中的参数在内部是用**一个数组**来表示的。 **1.可选形参** 在ECMAScript中的函数在调用时,传递的参数可少于函数中的参数,没有传入参数的命名参数的值是**undefined**。 为了保持好的适应性,一般应当给参数赋予一个合理的默认值。 ~~~ function go(x,y){ x = x || 1; y = y || 2; } ~~~ 注意:当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。那些调用你的函数的程序员是没法省略第一个参数并传入第二个实参的。 **2.实参对象** 当调用函数时,传入的实参个数超过函数定义时的形参个数时,是没有办法直接获得未命名值的引用。可以利用标识符`arguments`,其指向实参对象的引用,**实参对象是一个类数组对象**,可以通过数字下标来访问传入函数的实参值,而不用非要通过名字来得到实参。 ~~~ function go(x){ console.log(arguments[0]); console.log(arguments[1]); } go(1,2); //1 //2 ~~~ arguments有一个**length属性**,用以标识其所包含**元素的个数**。 ~~~ function f(x){ console.log(arguments.length); } f(1,2) // 2 ~~~ 通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值。 arguments类数组中每一个元素的值会与对应的命名参数的值保持同步,这种影响是单向的,也可以这样说,如果是修改arguments中的值,会影响到命名参数的值,但是修改命名参数的值,并不会改变arguments中对应的值。 ~~~ function f(x){ console.log(x); // 1 arguments[0]=null; console.log(x); // null } f(1); ~~~ 在上面的例子中,arguments[0]和x指代同一个值,修改其中一个的值会影响到另一个。 注意:如果有同名的参数,则取最后出现的那个值。 **3.按值传参** ECMAScript中所有函数的参数都是**按值传递**的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。 在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象中的一个元素。) 例子: ~~~ var num = 1; function test(count){ count += 10; return count; } var result = test(num); console.log(num); // 1 console.log(result); // 11 ~~~ 在上面的例子中,我们将num作为参数传给了test()函数,即count的值也是1,然后在函数内将count加10,但是由于传递的只是num的值的一个副本,并不会影响num,count和num是独立的,所以最后num的值依旧是1. 在向参数传递引用类型的值时,会先把这个值在内存中的地址**复制给一个局部变量**,若局部变量变化,则局部变量和复制给局部变量路径的全局变量也会发生改变。 ~~~ function test(obj){ obj.name = 'tg'; } var person = new Object(); test(person); console.log(person.name); // "tg" ~~~ 但是,如果局部变量指向了一个新的堆内地址,再改变局部变量的属性时,不会影响全局变量。 看下面的例子: ~~~ function setName(obj) { //相当于省略了var obj = person; obj.name = ‘Nicholas’; obj = new Object(); //重写了obj的值,这时候obj与person引用的对象无关了。 obj.name = ‘Greg’; } var person = new Object(); setName(person); alert(person.name); //‘Nicholas’ ~~~ 在上面的例子中,全局的person和函数内局部的obj在初始传递时,两者指向的是内存中的同一个地址,但是,当在函数内创建了一个新的对象,并赋值给obj(赋值的是新对象的地址)。这个时候,obj指向的就不在是全局对象person,而是指向了新对象的地址,所以给obj添加属性name时,全局对象person的属性不会被改变。 对于上面的例子中的obj,也可以这样说,一旦obj的值发生了变化,那么它就不再指向person在内存中的地址了。 另一个面试中常见的陷阱: ~~~ var foo = { bar: function(){return this.baz;}, baz: 1}; (function(){return typeof arguments[0]();})(foo.bar); //undefined 重写1 (function(){ return typeof arguments[0](); })(function()return this.baz;); 重写2 var a=function(){ return this.baz; } (function(){ return typeof arguments[0](); } )(a); 即 return typeof a(); ~~~ arguments在函数内部,作为函数内部一个变量的属性,调用了a,a没有属性baz,故结果为undefined **4.将对象属性用作实参** 当一个函数包含超过三个形参时,要记住调用函数中实参的正确顺序是件让人头疼的事。不过,我们可以通过**名/值对**的形式传入参数,这样就不需要管参数的顺序了。 ~~~ function f(params){ console.log(params.name); } f({name:'a'}) ~~~ ### 3.7.3 没有重载 * ECMAScript函数没有重载的定义。 * 重载是指为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。 * 如果在ECMAScript中定义了两个名字相同的函数,则该名字只属于后定义的函数。