ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 函数 **1.函数简介** 通过**函数**可以封装任意多条语句,而且可以在任何地方、任何时候调用。 ECMAScript中的函数使用`function`关键字来声明,后跟一组参数以及函数体,这些参数在函数体内像局部变量一样工作。 ``` function functionName(arg0, arg1....argN) { statements } ``` 函数调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,称为该函数调用表达式的值。 ``` function test(name){ return name; } test('tg'); ``` 在上面的例子中,name就是形参,调用时的'tg'就是实参。 除了实参之外,每次调用还会拥有另一个值---本次**调用的上下文**---这就是`this`关键字的值。 我们还可以通过在函数内添加`return`语句来实现返回值。 **注意**:遇到`return`语句时,会立即退出函数,也就是说,`return`语句后面的语句不再执行。 ``` function test(){ return 1; alert(1); //永远不会被执行 } ``` 一个函数中可以包含多个`return`语句,而且`return`语句可以不带有任何返回值,最终将返回`undefined`。 ``` function test(num){ if(num > 2){ return num; }else{ return ; } } test(3); // 3 test(1); //undefined ``` 如果函数挂载在一个对象上,将作为对象的一个属性,就称它为对象的方法。 ``` var o = { test: function(){} } ``` test()就是对象o的方法。 **2、函数定义(声明)** 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()构造函数在实际编程中很少会用到。 **注意点**:如果同一个函数被多次定义(声明),后面的定义(声明)就会覆盖前面的定义(声明) ``` function f(){ console.log(1); } f() //1 function f(){ console.log(2); } f() //2 ``` 函数可以调用自身,这就是递归(recursion) ``` function f(x){ if(x>2){ console.log(x); return f(x-1); }else{ return 1; } } f(4); // 4 //3 ``` 不能在条件语句中声明函数 (在ES6中的块级作用域是允许声明函数的。) **3、函数命名** 任何合法的JavaScript标识符都可以用做一个函数的名称。 函数名称通常是动词或以动词为前缀的词组。 通常函数名的第一个字符为小写。当函数名包含多个单词时,可采取**下划线法**,比如:like_this();也可以采取**驼峰法**,也就是除了第一个单词之外的单词首字母使用大写字母,比如:likeThis(); **4、被提前** 就像变量的“被提前”一样,函数声明语句也会“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用。 ``` f() function f(){} ``` 其实JavaScript是这样解释的: ``` function f(){} f() ``` 注意:在函数提升中,函数体也会跟着提升(不像变量一样,只会提升变量声明),这也是我们可以引用后面声明的函数的原因。 此外,以表达式定义的函数并没有“被提前”,而是以变量的形式“被提前”。 ``` f(); var f = function (){}; // TypeError: f is not a function ``` 变量其实是分为声明,赋值两部分的,上面的代码等同于下面的形式 ``` var f; f(); f = function() {}; ``` 调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。 **5、嵌套函数** JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量,这就是JavaScript的`闭包`。 ``` function test(){ var name = 'tg'; function test2(){ var age = 10; console.log(name); // "tg" } console.log(age); // Uncaught ReferenceError: age is not defined } test(); ``` 从上面的例子可得,test2()可以访问name,但是如果在test()内,test2()外访问age,就会报错。 **6、函数调用** 构成函数主体的JavaScript代码在定义时并不会执行,只有调用该函数,它们才会执行。有4种方式调用JavaScript函数: - 作为函数 - 作为方法 - 作为构造函数 - 通过它们的call()和apply()方法间接调用 **6.1函数调用** 函数可以通过函数名来调用,后跟一对圆括号和参数(圆括号中的参数如果有多个,用逗号隔开) ``` function test(){} test() ``` **6.2方法调用** ``` var o = { f: function(){} } o.f(); ``` **6.3构造函数调用** 如果函数或者方法调用之前带有关键字`new`,它就构成构造函数调用。 凡是没有形参的构造函数调用都可以省略圆括号(但不推荐)。 ``` var o=new Object(); var o=new Object; ``` **7、函数的实参和形参** **7.1 可选形参** 在ECMAScript中的函数在调用时,传递的参数可少于函数中的参数,没有传入参数的命名参数的值是undefined。 为了保持好的适应性,一般应当给参数赋予一个合理的默认值。 ``` function go(x,y){ x = x || 1; y = y || 2; } ``` 注意:当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。那些调用你的函数的程序员是没法省略第一个参数并传入第二个实参的。 **7.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并不是真正的数组,它是一个类数组对象。每个实参对象都包含以数字为索引的一组元素以及length属性。 通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值。 在函数体内,我们可以通过`arguments`对象来访问这个参数类数组,我们可以使用方括号语法访问它的每一个参数(比如arguments[0]),它还有一个length属性,表示传递进来的参数个数。 `arguments`类数组中每一个元素的值会与对应的命名参数的值保持同步,这种影响是单向的,也可以这样说,如果是修改`arguments`中的值,会影响到命名参数的值,但是修改命名参数的值,并不会改变`arguments`中对应的值。 ``` function f(x){ console.log(x); // 1 arguments[0]=null; console.log(x); // null } f(1); ``` 在上面的例子中,arguments[0]和x指代同一个值,修改其中一个的值会影响到另一个。 注意:如果有同名的参数,则取最后出现的那个值。 ``` function f(x,x){ console.log(x); } f(1,2) // 2 ``` `callee`和`caller`属性 arguments对象带有一个`callee`属性,返回它所对应的原函数。 在一个函数调用另一个函数时,被调用函数会自动生成一个`caller`属性,指向调用它的函数对象。如果该函数当前未被调用,或并非被其他函数调用,则`caller`为null。 再次提醒,`arguments`并不是真正的数组,它只是类数组对象(有length属性且可使用索引来访问子项)。但我们可以借助Array类的原型对象的slice方法,将其转为真正的数组: ``` Array.prototype.slice.call(arguments, 0); //更简洁的写法 [].slice.call(arguments, 0); ``` **7.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 test(obj){ obj.name = 'tg'; obj = new Object(); obj.name = 'tg2'; } var person = new Object(); test(person); console.log(person.name); // "tg" ``` 在上面的例子中,全局的`person`和函数内局部的`obj`在初始传递时,两者指向的是内存中的同一个地址,但是,当在函数内创建了一个新的对象,并赋值给`obj`(赋值的是新对象的地址)。这个时候,`obj`指向的就不在是全局对象`person`,而是指向了新对象的地址,所以给obj添加属性name时,全局对象person的属性不会被改变。 对于上面的例子中的obj,也可以这样说,一旦obj的值发生了变化,那么它就不再指向person在内存中的地址了。 **8、将对象属性用做实参** 当一个函数包含超过三个形参时,要记住调用函数中实参的正确顺序是件让人头疼的事。不过,我们可以通过名/值对的形式传入参数,这样就不需要管参数的顺序了。 ``` function f(params){ console.log(params.name); } f({name:'a'}) ``` **9、作为值的函数** 在JavaScript中,我们可以将函数赋值给变量。 ``` function f(){} var a=f; ``` **10、函数作用域** 作用域(scope)指的是变量存在的范围。 Javascript只有两种作用域:一种是**全局作用域**,变量在整个程序中一直存在,所有地方都可以读取;另一种是**函数作用域**,变量只在函数内部存在。 在函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。 ``` var a=1; function f(){ console.log(a) } f() //1 ``` 上面的代码中,函数f内部可以读取全局变量a。 在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。 ``` function f(){ var a=1; } v //ReferenceError: v is not defined ``` 上面代码中,变量v在函数内部定义,所以是一个局部变量,函数之外就无法读取。 函数内部定义的变量,会在该作用域内覆盖同名全局变量。 ``` var a=1; function f(){ var a=2; console.log(a); } f() //2 a //1 ``` 注意:对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。 函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。 **11、函数内部的变量提升 ** 与全局作用域一样,函数作用域内部也会产生“变量提升”现象。 `var`命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。 ``` function f(x){ if(x>10){ var a = x -1; } } //等同于 function f(x){ var a; if(x>10){ a = x - 1; } } ``` **12、没有重载** ECMAScript函数没有重载的定义。 重载是指为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。 对于ECMAScript函数,如果定义了两个同名的,后定义的函数会覆盖先定义的函数。 **13、函数属性、方法和构造函数** **13.1 函数的属性、方法** **(1)name属性** name属性返回紧跟在function关键字之后的那个函数名。 ``` function f(){} f.name //f ``` **(2)length属性** 函数的length属性是只读属性,代表函数形参的数量,也就是在函数定义时给出的形参个数。 ``` function f(x,y){} f.length //2 ``` **(3)prototype属性** 每一个函数都包含一个prototype属性,这个属性指向一个对象的引用,这个对象称做“原型对象”(prototype object)。 **(4)call()** 语法: ``` call([thisObj[,arg1[, arg2[, [,.argN]]]]]) ``` 定义:调用一个对象的一个方法,以另一个对象替换当前对象。 说明: call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。 **(5)apply()** 语法: ``` apply([thisObj[,argArray]]) ``` 定义:应用某一对象的一个方法,用另一个对象替换当前对象。 说明: 如果 argArray 不是一个有效的数组或者不是 arguments 对象,那么将导致一个 TypeError。 如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数。 bind()方法 bind()方法是在ECMAScript 5中新增的方法。 toString()方法 函数的toString方法返回函数的源码。 ``` function f(){ return 1; } f.toString() //function f(){ // return 1; //} ``` **(6)bind()** bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。 ``` var bar=function(){ console.log(this.x); } var foo={ x:3 } bar(); bar.bind(foo)(); /*或*/ var func=bar.bind(foo); func(); 输出: undefined 3 ``` 注意:bind()返回的是函数。 **13.2 构造函数** 构造函数和普通函数的定义并没有太大区别,不过我们使用`new`关键字来生成构造函数的实例对象。 ``` function Test(){} var t = new Test(); ``` 对于构造函数,一般首字母大写,便于和普通函数区别开来。 定义每个函数都会主动获取一个`prototype`属性,该属性拥有一个对象--该函数的原型,该原型有一个`constructor`属性,指向其当前所属的函数。 **14、闭包** JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量,这就是JavaScript的`闭包`。 `闭包`会保存函数作用域中的状态,即使这个函数已经执行完毕。 `闭包`的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即`闭包`可以使得它诞生环境一直存在。 `闭包`的创建依赖于函数。 ``` function f(a){ return function(){ return a++; }; } var c=f(1); console.log(c()); //1 console.log(c()); //2 console.log(c()); //3 ``` 闭包的另一个用处,是封装对象的私有属性和私有方法。 **15、立即调用的函数表达式(IIFE)** 在Javascript中,一对圆括号()是一种运算符,跟在函数名之后,表示调用该函数。 ``` (function(){ statement }()) ``` 上面的函数会立即调用。 **注意**:上面代码的圆括号的用法,function之前的左圆括号是必需的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。而使用圆括号,JavaScript解释器才会正确地将其解析为函数定义表达式。 当然,下面的方法也会以表达式来处理函数定义的方法。 ``` !function(){}(); ~function(){}(); -function(){}(); +function(){}(); ``` 通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个: - 一是不必为函数命名,避免了污染全局变量; - 二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。 **16、eval命令** eval命令的作用是,将字符串当作语句执行。 ``` eval('var a=1'); a //1 ``` eval没有自己的作用域,都在当前作用域内执行 JavaScript规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。 ``` (function(){ 'use strict'; eval('var a=1'); console.log(a); //ReferenceError: a is not defined })(); ``` **17、严格模式下的函数** - 不能把函数命名为eval或arguments - 不能把参数命名为eval或arguments - 不能出现两个命名参数同名的情况 如果出现上面三种情况,都会导致语法错误,代码无法执行。