函数是一段能够重复调用的代码块。接收相应的参数并能够返回对应的值javascript
javascript 有三种声明函数的方法。function 命令、函数表达式、Function构造器。java
(1)function 命令es6
function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。 数组
function add(x, y) { console.log(x + y) } add(3,4);//7
上面的代码命名了一个add函数,输出两个数字相加的和,之后使用add()这种形式,就能够调用相应的代码。这叫作函数的声明(Function Declaration)。安全
(2)函数表达式闭包
除了用function命令声明函数,还能够采用变量赋值的写法。 函数
let add=function (x, y) { console.log(x + y) }; add(3,4);
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),由于赋值语句的等号右侧只能放表达式。性能
采用函数表达式声明函数时,function命令后面不带有函数名。若是加上函数名,该函数名只在函数体内部有效,在函数体外部无效。 优化
let add=function addAdd(x, y) { console.log(addAdd) }; console.log(addAdd) //报错 未定义 add(3,4);// 输出函数自己
上面代码在函数表达式中,加入了函数名addAdd。这个addAdd只在函数体内部可用,指代函数表达式自己,其余地方都不可用。这种写法的用处有两个,一是能够在函数体内部调用自身,二是方便除错(函数内部报错时,函数名存在时报错时错误信息中会包含函数名,函数名不存在时会指向引用的值的名称)。所以,下面的形式声明函数也很是常见。 spa
let add = function add(x, y) { console.log(x + y) }; add(3, 4);
须要注意的是,函数的表达式须要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来讲,这两种声明函数的方式,差异很细微,能够近似认为是等价的。
(3) Function 构造器
let add=new Function( 'x','y','console.log(x+y)' ) add(3,4);//7 let say=new Function( 'console.log("hello")' ) say();//hello
你能够传递任意数量的参数给Function构造函数,只有最后一个参数会被当作函数体,若是只有一个参数,该参数就是函数体。
Function构造函数能够不使用new命令,返回结果彻底同样。 总的来讲,这种声明函数的方式很是不直观,几乎无人使用。
若是同一个函数被屡次声明,后面的声明就会覆盖前面的声明
function add(x,y){ console.log(x+y); } add(3,4);//12 function add(x,y){ console.log(x*y) } add(3,4);//12
上面代码中,后一次的函数声明覆盖了前面一次。并且,因为函数名的提高(参见下文),前一次声明在任什么时候候都是无效的,这一点要特别注意。
调用函数时,要使用圆括号运算符。圆括号之中,能够加入函数的参数。
function add(x,y){ return x+y
console.log('result')//不会执行 } let result=add(3,4); console.log(result)//7
上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。 函数体内部的return语句,表示返回。JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值,后面即便还有语句,也不会获得执行。也就是说,return语句所带的那个表达式,就是函数的返回值。return语句不是必需的,若是没有的话,该函数就不返回任何值,或者说返回undefined。
函数能够调用自身,这就是递归(recursion)。下面就是经过递归,计算数字的阶乘
function fact(i) { if (i == 1) { return 1 } return i * fact(i - 1) } console.log(fact(3)) //6
上面的代码就是
fact(3)==>3*fact(3-1)==>3*fact(2) fact(2)==>2*fact(2-1)==>3*fact(1) fact(1)==>1 fact(3)==>3*2*1==>6
一等公民:通常来讲,若是某程序设计语言中的一个值能够做为参数传递,能够从子程序中返回,能够赋值给变量
二等公民:能够做为参数传递,可是不能从子程序中返回,也不能赋给变量
三等公民:它的值连做为参数传递都不行
JavaScript 语言将函数看做一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可使用值的地方,就能使用函数。好比,能够把函数赋值给变量和对象的属性,也能够看成参数传入其余函数,或者做为函数的结果返回。函数只是一个能够执行的值,此外并没有特殊之处。 因为函数与其余数据类型地位平等,因此在 JavaScript 语言中又称函数为第一等公民
function add(x, y) { console.log(x + y); } // 将函数赋值给一个变量 let operator = add; operator(3, 4); //7 // 将函数做为参数和返回值 function addOperat(fn) { return fn; } addOperat(add)(3, 4) //7
JavaScript 引擎将函数名视同变量名,因此采用function命令声明函数时,整个函数会像变量声明同样,被提高到代码头部、(另外两种声明是不会存在这种状况的)因此,下面的代码不会报错。
let operator = add; operator(3, 4); //7 function add(x, y) { console.log(x + y); }
下面这两种状况都会报错
// add 未定义 add(); let add=function(){ console.log(1) }
// add 不是一个函数 let add; add(); add=function(){ console.log(1) }
所以,若是同时采用function命令和赋值语句声明同一个函数,最后老是采用赋值语句的定义。由于函数表达式声明的函数不会存在函数名的变量提高
// 这里不使用 let f var f=function(){ console.log(1) }; function f(){ console.log(2) } f();//1
获取函数的原始名称
function f(){} console.log(f.name);//f let f2=function (){}; console.log(f2.name);//f2 let f4=function f3(){}; console.log(f4.name);//f3
函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数
function f(){} console.log(f.length);//0 let f2=function (a,b){}; console.log(f2.length);//2 let f4=function f3(a){}; console.log(f4.length);//1
输出函数的内容(一切内容、包括注释)
function add(x,y){ let a=x; let b=y; console.log(x+y) } console.log(add.toString())// 输出函数本省 console.log(Math.ceil.toString()) //对于那些原生的函数,toString()方法返回function (){[native code]}
做用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种做用域:一种是全局做用域,变量在整个程序中一直存在,全部地方均可以读取;另外一种是函数做用域,变量只在函数内部存在。ES6 又新增了块级做用域。
全局做用域,在任何地方均可以访问
let age='18'; function getAge(){ console.log(age) } console.log(age);//18 getAge();// 18 age全局做用域 函数内部也可访问
函数做用域,只在函数内部能够访问
function getAge(){ var age='qzy'; console.log(age) } getAge()//qzy console.log(age)// age未定义 函数内部定义的变量 只有函数内部能够访问
块级做用域 es6 新增的 let 声明变量
{ let age='25' } console.log(age) // age未定义
函数内部定义的变量,会在该做用域内覆盖同名全局变量。
let age=18; function getAge(){ let age=25 console.log(age) } console.log(age);//18 getAge();//25
注意,对于var命令来讲,局部变量只能在函数内部声明,在其余区块中声明,一概都是全局变量。
if(true){ var age=25; } console.log(age);//25
与全局做用域同样,函数做用域内部也会产生“变量提高”现象。var命令声明的变量,无论在什么位置,变量声明都会被提高到函数体的头部。
function ageLevel(age){ if(age>100){ var level='高手' } console.log(level) } ageLevel(101);//高手 // 等价于 function ageLevelEqual(age){ var level; if(age>100){ level='高手' } console.log(level) } ageLevelEqual(101);//高手
函数自己也是一个值,也有本身的做用域。它的做用域与变量同样,就是其声明时所在的做用域,与其运行时所在的做用域无关
let age=25; function getAge(){ console.log(age) } function setAge(){ let age=26; getAge(); } setAge();//25
getAge函数是在最外层声明的,因此它绑定的做用域是最外层,在函数内部变量age不会影响到其绑定的最外层的值
一样的,函数体内部声明的函数,做用域绑定函数体内部。
let age = 25; function setAge() { let age = 26; function getAge() { console.log(age) } // 将声明的函数返回 return getAge } setAge()();// 26
正是这种机制,构成了下文要讲解的“闭包”现象。
函数运行的时候,有时须要提供外部数据,不一样的外部数据会获得不一样的结果,这种外部数据就叫参数。函数参数不是必需的,JavaScript 容许省略参数。
function square(x){ return x*x } console.log(square(2));//4 console.log(square(3));//9
函数的参数传递方式分为两种:传值传递、传址传递
传值传递:函数参数若是是原始类型的值(数值、字符串、布尔值),则采用这种方式,此时在函数体内修改参数值,不会影响到函数外部
let age=25; function setAge(age){ age=26; } console.log(age);//25
传址传递:若是函数参数是复合类型的值(数组、对象、其余函数),则采用这种方式,传入函数的原始值的地址,此时在函数体内修改参数值,会影响到函数外部(有特殊的)
let person={ age:25 }; function setPerson(person){ person.age=26; } setPerson(person); console.log(person.age);//26
person 为一个对象 在函数背部修改其中的属性,外面的变量中的值也作相应的改变
注意,若是函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
let person={ age:25 }; function setPerson(person){ person={ age:26 }; } setPerson(person); console.log(person.age);//25
在函数f内部,参数对象person被整个替换成另外一个值。这时不会影响到原始值。这是由于,形式参数person的值原本是是参数person的地址,从新对person赋值致使person指向另外一个地址,保存在原地址上的值固然不受影响。
若是有同名的参数,则取最后出现的那个值。
function add(a,a){ console.log(a) } add(1);// undefined 形参为两个 同名取最后一个 可是未传递 因此未定义 add(1,2);// 2 add(1,2,3);// 2
因为 JavaScript 容许函数有不定数目的参数,因此须要一种机制,能够在函数体内部读取全部参数。这就是arguments对象的由来。 arguments对象包含了函数运行时的全部参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可使用。
function add(one){ let len=arguments.length;// 获取传入的参数个数 for(let i=0;i<len;i++){ console.log(arguments[i]) } } add(1,2,3,4,5)
注:arguments对象能够在运行时修改,修改后对应的形参就是修改后的值;可是在开启严格模式下能够修改,可是不会影响到形参,
var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } console.log(f(1, 1)) // 2
与数组的关系,须要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(好比slice和forEach),不能在arguments对象上直接使用。 若是要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种经常使用的转换方法:slice方法和逐一填入新数组。
function add(one){ let args = Array.prototype.slice.call(arguments); args.forEach(list=>{ console.log(list) }) } add(1,2,3,4,5)
function add(one) { let args = []; for (let i = 0; i < arguments.length; i++) { args.push(arguments[i]); } args.forEach((list)=>{ console.log(list) }) } add(1, 2, 3, 4, 5)
均可以使用forEach 输出因此的参数值
arguments对象带有一个callee属性,返回它所对应的原函数,能够经过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,所以不建议使用
function add() { console.log(arguments.callee==add) } add();//true
闭包(closure)是 JavaScript 语言的一个难点,也是它的特点,不少高级应用都要依靠闭包实现。 理解闭包,首先必须理解变量做用域。前面提到,JavaScript 有两种做用域:全局做用域和函数做用域。函数内部能够直接读取全局变量。
函数内部能够读取函数外部的变量,可是函数外部是不能读取到函数内部的变量的,这个在上面已经实验过了
若是出于种种缘由,须要获得函数内的局部变量。正常状况下,这是办不到的,只有经过变通方法才能实现。那就是在函数的内部,再定义一个函数
function person(){ let age=18; function getAge(){ return age } return getAge } let p=person();// 接收getAge let pAge=p(); console.log(pAge);// 18
上面的代码 咱们在person内部定义了一个age字段 ,可是咱们想在外部取不到这个值,这个时候咱们能够在函数内部定义一个函数,这个函数在person 函数内部,是能够取到person内部的值的,而后咱们再把函数暴露出来,就能够获取到这个值了,那此时getAge函数就是闭包,即可以读取其余函数内部变量的函数。因为在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,所以能够把闭包简单理解成“定义在一个函数内部的函数”,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。
闭包的最大用处有两个,一个是能够读取函数内部的变量,另外一个就是让这些变量始终保持在内存中,即闭包可使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果
function createIn(count){ return function (){ console.log(++count); } } let result=createIn(0); result();//1 result()//2 result()//3
上面代码中,count是函数createIn的内部变量。经过闭包,count的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中能够看到,闭包result使得函数createIn的内部环境一直存在。因此,闭包能够看做是函数内部做用域的一个接口。
为何会这样呢?缘由就在于result始终在内存中,而result的存在依赖于createIn,所以也始终在内存中,不会在调用结束后,被垃圾回收机制回收
闭包的另外一个用处,是封装对象的私有属性和私有方法。
function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; } var p1 = Person('张三'); p1.setAge(25); console.log(p1.getAge()) // 25
上面代码中,函数Person的内部变量_age,经过闭包getAge和setAge,变成了返回对象p1的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,因此内存消耗很大。所以不能滥用闭包,不然会形成网页的性能问题
在 JavaScript 中,圆括号()是一种运算符,跟在函数名以后,表示调用该函数。好比,print()就表示调用print函数。 有时,咱们须要在定义函数以后,当即调用该函数。这时,你不能在函数的定义以后加上圆括号,这会产生语法错误。
function say(){ /* code */ }();// Uncaught SyntaxError: Unexpected token )
产生这个错误的缘由是,function这个关键字便可以看成语句,也能够看成表达式。
// 语句 function f() {} // 表达式 var f = function f() {}
为了不解析上的歧义,JavaScript 引擎规定,若是function关键字出如今行首,一概解释成语句。那不是的就是函数表达式喽。所以,JavaScript 引擎看到行首是function关键字以后,认为这一段都是函数的定义,不该该以圆括号结尾,因此就报错了。
下面函数表达式后面跟上圆括号是能够当即执行的。
// 表达式 直接输出1 var f = function () { console.log(1) }()
解决方法就是不要让function出如今行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
(function (){console.log(1)})();//1 (function (){console.log(2)}());//2
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,因此就避免了错误。这就叫作“当即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。 注意,上面两种写法最后的分号都是必须的。若是省略分号,遇到连着两个 IIFE,可能就会报错
(function (){console.log(1)})()//1 (function (){console.log(2)}())//2 // Uncaught TypeError: (intermediate value)(...) is not a function
上面代码的两行之间没有分号,JavaScript 会将它们连在一块儿解释,将第二行解释为第一行的参数
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生一样的效果,只要让引擎认为它是表达式便可。
let i =function (){console.log(1)}();//1 true && function (){console.log(2)}();//2 +function (){console.log(3)}();//3
一般状况下,只对匿名函数使用这种“当即执行的函数表达式”。它的目的有两个:一是没必要为函数命名,避免了污染全局变量;二是 IIFE 内部造成了一个单独的做用域,能够封装一些外部没法读取的私有变量。
eval命令接受一个字符串做为参数,并将这个字符串看成语句执行。若是参数字符串没法看成语句运行,那么就会报错。放在eval中的字符串,应该有独自存在的意义,不能用来与eval之外的命令配合使用。举例来讲,下面的代码将会报错。若是eval的参数不是字符串,那么会原样返回。
eval('var a=1'); console.log(a);//1 console.log(eval(123));//123
eval('3x');//报错
eval('return')//报错
eval没有本身的做用域,都在当前做用域内执行,所以可能会修改当前做用域的变量的值,形成安全问题
var a=0; eval('var a=1'); console.log(a);//1
为了防止这种风险,JavaScript 规定,若是使用严格模式,eval内部声明的变量,不会影响到外部做用域。
'use strict' var a=0; eval('var a=1'); console.log(a);//0
若是我不声明,直接修改a,能够看到仍是能够修改为功的。
'use strict' var a=0; eval('a=1'); console.log(a);//1
上面代码中,严格模式下,eval内部仍是改写了外部变量,可见安全风险依然存在。一般状况下,eval最多见的场合是解析 JSON 数据的字符串,不过正确的作法应该是使用原生的JSON.parse方法。
let str = '{"name": "qzy", "age": 25}'; let obj = eval('('+str+')');//let obj=JSON.parse(str); console.log(obj); // Object {name: "hanzichi", age: 10}
eval 解析时为何要加圆括号呢 ?
let obj='{}'; console.log(eval(obj));//undefined console.log(eval('('+obj+')'));//{}
上面说过eval 会执行输入的字符串,obj是以{开头、}结尾的。那js 引擎就会把这当成代码块,执行里面的语句。
前面说过eval不利于引擎优化执行速度。更麻烦的是,还有下面这种状况,引擎在静态代码分析的阶段,根本没法分辨执行的是eval。
"use strict" let my=eval; my('var a=1'); console.log(a);//1
为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一概是全局做用域。详细的看下一篇对象的讲解
var a = 1; function f() { var a = 2; var e = eval; e('console.log(a)'); } f() // 1
就一句话,没事别用eval。