函数是 JavaScript 中的根本组件之一。 一个函数是 JavaScript 过程 — 一组执行工作或计算值的语句。要应用一个函数,你必须将其定义在你心愿调用它的作用域内。
一个JavaScript 函数用function
关键字定义,前面跟着函数名和圆括号。
定义函数
函数申明
一个函数定义(也称为函数申明,或函数语句)由一系列的function
关键字组成,顺次为:
- 函数的名称。
- 函数参数列表,突围在括号中并由逗号分隔。
- 定义函数的 JavaScript 语句,用大括号
{}
括起来。
例如,以下的代码定义了一个简略的square
函数:
function square(number) { return number * number;}
函数square
应用了一个参数,叫作number
。这个函数只有一个语句,它阐明该函数将函数的参数(即number
)自乘后返回。函数的return
语句确定了函数的返回值:
return number * number;
原始参数(比方一个具体的数字)被作为值传递给函数;值被传递给函数,如果被调用函数扭转了这个参数的值,这样的扭转不会影响到全局或调用函数。
如果你传递一个对象(即一个非原始值,例如Array
或用户自定义的对象)作为参数,而函数扭转了这个对象的属性,这样的扭转对函数内部是可见的,如上面的例子所示:
function myFunc(theObject) { theObject.make = "Toyota";}var mycar = {make: "Honda", model: "Accord", year: 1998};var x, y;x = mycar.make; // x获取的值为 "Honda"myFunc(mycar);y = mycar.make; // y获取的值为 "Toyota" // (make属性被函数扭转了)
函数表达式
尽管下面的函数申明在语法上是一个语句,但函数也能够由函数表达式创立。这样的函数能够是匿名的;它不用有一个名称。例如,函数square
也可这样来定义:
const square = function(number) { return number * number; };var x = square(4); // x gets the value 16
然而,函数表达式也能够提供函数名,并且能够用于在函数外部代指其自身,或者在调试器堆栈跟踪中辨认该函数:
const factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};console.log(factorial(3));
当将函数作为参数传递给另一个函数时,函数表达式很不便。上面的例子演示了一个叫map
的函数如何被定义,而后应用一个表达式函数作为其第一个参数进行调用:
function map(f,a) { let result = []; // 创立一个数组 let i; // 申明一个值,用来循环 for (i = 0; i != a.length; i++) result[i] = f(a[i]); return result;}
上面的代码:
function map(f, a) { let result = []; // 创立一个数组 let i; // 申明一个值,用来循环 for (i = 0; i != a.length; i++) result[i] = f(a[i]); return result;}const f = function(x) { return x * x * x;}let numbers = [0,1, 2, 5,10];let cube = map(f,numbers);console.log(cube);
返回 [0, 1, 8, 125, 1000]。
在 JavaScript 中,能够依据条件来定义一个函数。比方上面的代码,当num
等于 0 的时候才会定义 myFunc
:
var myFunc;if (num == 0){ myFunc = function(theObject) { theObject.make = "Toyota" }}
除了上述的定义函数办法外,你也能够在运行时用 Function
结构器由一个字符串来创立一个函数 ,很像 eval()
函数。
当一个函数是一个对象的属性时,称之为办法。理解更多对于对象和办法的常识 应用对象
调用函数
定义一个函数并不会主动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。例如,一旦你定义了函数square
,你能够如下这样调用它:
square(5);
上述语句通过提供参数 5 来调用函数。函数执行完它的语句会返回值25。
函数肯定要处于调用它们的域中,然而函数的申明能够被晋升(呈现在调用语句之后),如下例:
console.log(square(5));/* ... */function square(n) { return n*n }
函数域是指函数申明时的所在的中央,或者函数在顶层被申明时指整个程序。
提醒:留神只有应用如上的语法模式(即 function funcName(){}
)才能够。而上面的代码是有效的。就是说,函数晋升仅实用于函数申明,而不适用于函数表达式。
console.log(square); // square is hoisted with an initial value undefined.console.log(square(5)); // Uncaught TypeError: square is not a functionconst square = function (n) { return n * n;}
函数的参数并不局限于字符串或数字。你也能够将整个对象传递给函数。函数 show_props
函数能够被递归,就是说函数能够调用其自身。例如,上面这个函数就是用递归计算阶乘:
function factorial(n){ if ((n == 0) || (n == 1)) return 1; else return (n * factorial(n - 1));}
你能够计算1-5的阶乘如下:
var a, b, c, d, e;a = factorial(1); // 1赋值给ab = factorial(2); // 2赋值给bc = factorial(3); // 6赋值给cd = factorial(4); // 24赋值给de = factorial(5); // 120赋值给e
还有其它的形式来调用函数。常见的一些情景是某些中央须要动静调用函数,或者函数的实参数量是变动的,或者调用函数的上下文须要指定为在运行时确定的特定对象。显然,函数自身就是对象,因而这些对象也有办法。作为此中情景之一,apply()
办法能够实现这些目标。
函数作用域
在函数内定义的变量不能在函数之外的任何中央拜访,因为变量仅仅在该函数的域的外部有定义。绝对应的,一个函数能够拜访定义在其范畴内的任何变量和函数。换言之,定义在全局域中的函数能够拜访所有定义在全局域中的变量。在另一个函数中定义的函数也能够拜访在其父函数中定义的所有变量和父函数有权拜访的任何其余变量。
// 上面的变量定义在全局作用域(global scope)中var num1 = 20, num2 = 3, name = "Chamahk";// 本函数定义在全局作用域function multiply() { return num1 * num2;}multiply(); // 返回 60// 嵌套函数的例子function getScore() { var num1 = 2, num2 = 3; function add() { return name + " scored " + (num1 + num2); } return add();}getScore(); // 返回 "Chamahk scored 5"
作用域和函数堆栈
递归
一个函数能够指向并调用本身。有三种办法能够达到这个目标:
- 函数名
- `arguments.callee
- 作用域下的一个指向该函数的变量名
例如,思考一下如下的函数定义:
var foo = function bar() { // statements go here};
在这个函数体内,以下的语句是等价的:
bar()
arguments.callee()
(译者注:ES5禁止在严格模式下应用此属性)foo()
调用本身的函数咱们称之为递归函数。在某种意义上说,递归近似于循环。两者都反复执行雷同的代码,并且两者都须要一个终止条件(防止有限循环或者有限递归)。例如以下的循环:
var x = 0;while (x < 10) { // "x < 10" 是循环条件 // do stuff x++;}
能够被转化成一个递归函数和对其的调用:
function loop(x) { if (x >= 10) // "x >= 10" 是退出条件(等同于 "!(x < 10)") return; // 做些什么 loop(x + 1); // 递归调用}loop(0);
不过,有些算法并不能简略的用迭代来实现。例如,获取树结构中所有的节点时,应用递归实现要容易得多:
function walkTree(node) { if (node == null) // return; // do something with node for (var i = 0; i < node.childNodes.length; i++) { walkTree(node.childNodes[i]); }}
跟loop
函数相比,这里每个递归调用都产生了更多的递归。
将递归算法转换为非递归算法是可能的,不过逻辑上通常会更加简单,而且须要应用堆栈。事实上,递归函数就应用了堆栈:函数堆栈。
这种相似堆栈的行为能够在下例中看到:
function foo(i) { if (i < 0) return; console.log('begin:' + i); foo(i - 1); console.log('end:' + i);}foo(3);// 输入:// begin:3// begin:2// begin:1// begin:0// end:0// end:1// end:2// end:3
嵌套函数和闭包
你能够在一个函数外面嵌套另外一个函数。嵌套(外部)函数对其容器(内部)函数是公有的。它本身也造成了一个闭包。一个闭包是一个能够本人领有独立的环境与变量的表达式(通常是函数)。
既然嵌套函数是一个闭包,就意味着一个嵌套函数能够”继承“容器函数的参数和变量。换句话说,外部函数蕴含内部函数的作用域。
能够总结如下:
- 外部函数只能够在内部函数中拜访。
- 外部函数造成了一个闭包:它能够拜访内部函数的参数和变量,然而内部函数却不能应用它的参数和变量。
上面的例子展现了嵌套函数:
function addSquares(a, b) { function square(x) { return x * x; } return square(a) + square(b);}a = addSquares(2, 3); // returns 13b = addSquares(3, 4); // returns 25c = addSquares(4, 5); // returns 41
因为外部函数造成了闭包,因而你能够调用内部函数并为内部函数和外部函数指定参数:
function outside(x) { function inside(y) { return x + y; } return inside;}fn_inside = outside(3); // 能够这样想:给一个函数,使它的值加3result = fn_inside(5); // returns 8result1 = outside(3)(5); // returns 8
保留变量
留神到上例中 inside
被返回时 x
是怎么被保留下来的。一个闭包必须保留它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上从新创立了一遍这个闭包。只有当返回的 inside
没有再被援用时,内存才会被开释。
这与在其余对象中存储援用没什么不同,然而通常不太显著,因为并不能间接设置援用,也不能查看它们。
多层嵌套函数
函数能够被多层嵌套。例如,函数A能够蕴含函数B,函数B能够再蕴含函数C。B和C都造成了闭包,所以B能够拜访A,C能够拜访B和A。因而,闭包能够蕴含多个作用域;他们递归式的蕴含了所有蕴含它的函数作用域。这个称之为作用域链。(稍后会具体解释)
思考一下上面的例子:
function A(x) { function B(y) { function C(z) { console.log(x + y + z); } C(3); } B(2);}A(1); // logs 6 (1 + 2 + 3)
在这个例子外面,C能够拜访B的y和A的x。这是因为:
- B造成了一个蕴含A的闭包,B能够拜访A的参数和变量
- C造成了一个蕴含B的闭包
- B蕴含A,所以C也蕴含A,C能够拜访B和A的参数和变量。换言之,C用这个程序链接了B和A的作用域
反过来却不是这样。A不能拜访C,因为A看不到B中的参数和变量,C是B中的一个变量,所以C是B公有的。
命名抵触
当同一个闭包作用域下两个参数或者变量同名时,就会产生命名抵触。更近的作用域有更高的优先权,所以最近的优先级最高,最远的优先级最低。这就是作用域链。链的第一个元素就是最外面的作用域,最初一个元素便是最外层的作用域。
看以下的例子:
function outside() { var x = 5; function inside(x) { return x * 2; } return inside;}outside()(10); // returns 20 instead of 10
命名抵触产生在return x
上,inside
的参数x
和outside
变量x
产生了抵触。这里的作用链域是{inside
, outside
, 全局对象}。因而inside
的x
具备最高优先权,返回了20(inside
的x
)而不是10(outside
的x
)。
闭包
闭包是 JavaScript 中最弱小的个性之一。JavaScript 容许函数嵌套,并且外部函数能够拜访定义在内部函数中的所有变量和函数,以及内部函数能拜访的所有变量和函数。
然而,内部函数却不可能拜访定义在外部函数中的变量和函数。这给外部函数的变量提供了肯定的安全性。
此外,因为外部函数能够拜访内部函数的作用域,因而当外部函数生存周期大于内部函数时,内部函数中定义的变量和函数的生存周期将比外部函数执行工夫长。当外部函数以某一种形式被任何一个内部函数作用域拜访时,一个闭包就产生了。
var pet = function(name) { //内部函数定义了一个变量"name" var getName = function() { //外部函数能够拜访 内部函数定义的"name" return name; } //返回这个外部函数,从而将其裸露在内部函数作用域 return getName;};myPet = pet("Vivie");myPet(); // 返回后果 "Vivie"
实际上可能会比下面的代码简单的多。在上面这种情景中,返回了一个蕴含能够操作内部函数的外部变量办法的对象。
var createPet = function(name) { var sex; return { setName: function(newName) { name = newName; }, getName: function() { return name; }, getSex: function() { return sex; }, setSex: function(newSex) { if(typeof newSex == "string" && (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) { sex = newSex; } } }}var pet = createPet("Vivie");pet.getName(); // Viviepet.setName("Oliver");pet.setSex("male");pet.getSex(); // malepet.getName(); // Oliver
在下面的代码中,内部函数的name
变量对内嵌函数来说是可获得的,而除了通过内嵌函数自身,没有其它任何办法能够获得内嵌的变量。内嵌函数的内嵌变量就像内嵌函数的保险柜。它们会为内嵌函数保留“稳固”——而又平安——的数据参加运行。而这些内嵌函数甚至不会被调配给一个变量,或者不用肯定要有名字。
var getCode = (function(){ var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify... return function () { return secureCode; };})();getCode(); // Returns the secret code
只管有上述长处,应用闭包时依然要小心防止一些陷阱。如果一个闭包的函数定义了一个和内部函数的某个变量名称雷同的变量,那么这个闭包将无奈援用内部函数的这个变量。
var createPet = function(name) { // Outer function defines a variable called "name" return { setName: function(name) { // Enclosed function also defines a variable called "name" name = name; // ??? How do we access the "name" defined by the outer function ??? } }}
应用 arguments 对象
函数的理论参数会被保留在一个相似数组的arguments对象中。在函数内,你能够按如下形式找出传入的参数:
arguments[i]
其中i
是参数的序数编号(译注:数组索引),以0开始。所以第一个传来的参数会是arguments[0]
。参数的数量由arguments.length
示意。
应用arguments对象,你能够解决比申明的更多的参数来调用函数。这在你当时不晓得会须要将多少参数传递给函数时非常有用。你能够用arguments.length
来取得理论传递给函数的参数的数量,而后用arguments
对象来获得每个参数。
例如,构想有一个用来连贯字符串的函数。惟一当时确定的参数是在连贯后的字符串中用来分隔各个连贯局部的字符(译注:比方例子里的分号“;”)。该函数定义如下:
function myConcat(separator) { var result = ''; // 把值初始化成一个字符串,这样就能够用来保留字符串了!! var i; // iterate through arguments for (i = 1; i < arguments.length; i++) { result += arguments[i] + separator; } return result;}
你能够给这个函数传递任意数量的参数,它会将各个参数连接成一个字符串“列表”:
// returns "red, orange, blue, "myConcat(", ", "red", "orange", "blue");// returns "elephant; giraffe; lion; cheetah; "myConcat("; ", "elephant", "giraffe", "lion", "cheetah");// returns "sage. basil. oregano. pepper. parsley. "myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");
提醒:arguments
变量只是 ”类数组对象“,并不是一个数组。称其为类数组对象是说它有一个索引编号和length
属性。尽管如此,它并不领有全副的Array对象的操作方法。
函数参数
从ECMAScript 6开始,有两个新的类型的参数:默认参数,残余参数。
默认参数
在JavaScript中,函数参数的默认值是undefined
。然而,在某些状况下设置不同的默认值是有用的。这时默认参数能够提供帮忙。
在过来,用于设定默认参数的个别策略是在函数的主体中测试参数值是否为undefined
,如果是则赋予这个参数一个默认值。如果在上面的例子中,调用函数时没有实参传递给b
,那么它的值就是undefined
,于是计算a*b
失去、函数返回的是 NaN
。然而,在上面的例子中,这个曾经被第二行获取解决:
function multiply(a, b) { b = (typeof b !== 'undefined') ? b : 1; return a*b;}multiply(5); // 5
应用默认参数,在函数体的查看就不再须要了。当初,你能够在函数头简略地把1设定为b
的默认值:
function multiply(a, b = 1) { return a*b;}multiply(5); // 5
残余参数
残余参数语法容许将不确定数量的参数示意为数组。在上面的例子中,应用残余参数收集从第二个到最初参数。而后,咱们将这个数组的每一个数与第一个参数相乘。这个例子是应用了一个箭头函数,这将在下一节介绍。
function multiply(multiplier, ...theArgs) { return theArgs.map(x => multiplier * x);}var arr = multiply(2, 1, 2, 3);console.log(arr); // [2, 4, 6]
箭头函数
箭头函数表达式也称胖箭头函数)相比函数表达式具备较短的语法并以词法的形式绑定 this
。箭头函数总是匿名的。
有两个因素会影响引入箭头函数:更简洁的函数和 this
。
更简洁的函数
在一些函数模式中,更简洁的函数很受欢迎。比照一下:
var a = [ "Hydrogen", "Helium", "Lithium", "Beryllium"];var a2 = a.map(function(s){ return s.length });console.log(a2); // logs [ 8, 6, 7, 9 ]var a3 = a.map( s => s.length );console.log(a3); // logs [ 8, 6, 7, 9 ]
this
的词法
在箭头函数呈现之前,每一个新函数都从新定义了本人的 [this]值(在构造函数中是一个新的对象;在严格模式下是未定义的;在作为“对象办法”调用的函数中指向这个对象;等等)。以面向对象的编程格调,这样着实有点宜人。
function Person() { // 构造函数Person()将`this`定义为本身 this.age = 0; setInterval(function growUp() { // 在非严格模式下,growUp()函数将`this`定义为“全局对象”, // 这与Person()定义的`this`不同, // 所以上面的语句不会起到预期的成果。 this.age++; }, 1000);}var p = new Person();
在ECMAScript 3/5里,通过把this
的值赋值给一个变量能够修复这个问题。
function Person() { var self = this; // 有的人习惯用`that`而不是`self`, // 无论你抉择哪一种形式,请放弃前后代码的一致性 self.age = 0; setInterval(function growUp() { // 以下语句能够实现预期的性能 self.age++; }, 1000);}
另外,创立一个束缚函数能够使得 this
值被正确传递给 growUp()
函数。
箭头函数捕获闭包上下文的this
值,所以上面的代码工作失常。
function Person(){ this.age = 0; setInterval(() => { this.age++; // 这里的`this`正确地指向person对象 }, 1000);}var p = new Person();
预约义函数
JavaScript语言有好些个顶级的内建函数:
eval()
eval()
办法会对一串字符串模式的JavaScript代码字符求值。
uneval()
uneval()
办法创立的一个Object
的源代码的字符串示意。
isFinite()
isFinite()
函数判断传入的值是否是无限的数值。 如果需要的话,其参数首先被转换为一个数值。
isNaN()
isNaN()
函数判断一个值是否是NaN
。留神:isNaN
函数外部的强制转换规则
非常乏味; 另一个可供选择的是ECMAScript 6 中定义Number.isNaN()
, 或者应用 typeof
来判断数值类型。
parseFloat()
parseFloat()
函数解析字符串参数,并返回一个浮点数。
parseInt()
parseInt()
函数解析字符串参数,并返回指定的基数(根底数学中的数制)的整数。
decodeURI()
decodeURI()
函数对先前通过encodeURI
函数或者其余相似办法编码过的字符串进行解码。
decodeURIComponent()
decodeURIComponent()
办法对先前通过encodeURIComponent
函数或者其余相似办法编码过的字符串进行解码。
encodeURI()
**encodeURI()**
办法通过用以一个,两个,三个或四个转义序列示意字符的UTF-8编码替换对立资源标识符(URI)的某些字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”代替“字符)。
encodeURIComponent()
encodeURIComponent()
办法通过用以一个,两个,三个或四个转义序列示意字符的UTF-8编码替换对立资源标识符(URI)的每个字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”代替“字符)。
escape()
已废除的 **escape()**
办法计算生成一个新的字符串,其中的某些字符已被替换为十六进制转义序列。应用 encodeURI
或者encodeURIComponent
代替本办法。
unescape()
已废除的 **unescape()**
办法计算生成一个新的字符串,其中的十六进制转义序列将被其示意的字符替换。上述的转义序列就像escape
里介绍的一样。因为 unescape
曾经废除,倡议应用decodeURI()
或者decodeURIComponent
代替本办法。