javascript(函数)
function
函数是一段可以重复调用的代码块。接收相应的参数并可以返回对应的值
函数的声明
javascript 有三种声明函数的方法。function 命令、函数表达式、Function构造器。
(1)function 命令
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只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(函数内部报错时,函数名存在时报错时错误信息中会包含函数名,函数名不存在时会指向引用的值的名称)。因此,下面的形式声明函数也非常常见。

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
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。
圆括号运算符,return 语句和递归
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。
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
函数的属性和方法
name属性
获取函数的原始名称
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属性
函数的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
toString()
输出函数的内容(一切内容、包括注释)
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
arguments 对象
由于 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 输出所以的参数值
callee 属性
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的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题
立即调用的函数表达式(IIFE)
在 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的参数不是字符串,那么会原样返回。
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不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是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。

浙公网安备 33010602011771号