js中的函数
函数是一段可以重复调用的代码块,也是一个对象,为了解决代码重复的问题。
函数的5种声明方式
1.具名函数
function f(x,y){
return x+y
}
这里function的作用相当于var,var用来声明一个变量,而function用来声明一个函数。var声明的变量可以有多种类型,而function声明的函数只能是function。
2. 匿名函数赋给变量
var f = function(x,y){
return x+y
}
在内存中开辟一段空间用来存储这个匿名函数,然后把它的地址赋给var声明的变量f。如果此时执行f = 1
,那么这个匿名变量就失去了与栈内存的联系,会被浏览器回收。
3. 具名函数赋给变量
var f = function f2(x,y){
return x+y
}
这里比较容易迷惑,但是确实有这么变态的声明方式。你以为同时声明了f2和f两个函数吗?不是的,f2根本就不存在!!!f2只活在这个该死的函数内部。在函数的外部,你只能通过f.name看见f2这个字眼。也就是说,这个需要用f.call()来调用的函数,函数名居然是f2!!
4. 使用全局对象window.Function声明
var f = new Function('x','y','return x+y')
加不加new效果相同,反正也没人用这种诡异的东西来声明一个函数。前边不论有多少个字符串都是这个函数的参数,只有最后一个字符串是函数体。
字符串拼接的时候可以加变量,比如
var n = 2
var f = new Function('x','y','return x+' + n + '+y')
f(1, 2) //1+2+2=5
5.箭头函数
var f = (x,y) => {
return x+y
}
这个应该是比较省事的一种吧!括号里边的是参数,后边大括号里边是函数体。如果函数体里边只有一句你可以将大括号和return一起省掉,变成这种形式var f = (x,y) => x+y
,要么不要去掉要么return和大括号一起去掉。如果参数只有一个那么你还可以省掉小括号var f = n => n*n
。
tips
函数一定会有一个返回值,就算你不写返回值,浏览器也会自动给你加上一个return undefined
。也因此,你在控制台写语句的时候经常返回undefined。像下图:
我们在控制台打印出'打印'
这两个字,这条语句的返回值是undefined。console.log()的返回值和打印出的东西并不相同,打印出的东西是你传入的东西,而这条语句的返回值总是undefined。
所有的函数都有一个name属性。
function f(){} //f.name === 'f'
var f = function(){} //f.name === 'f'
var f = function f2(){} //f.name === 'f2'
var f = new Function() //f.name === 'anonymous' !!!!!这个单词的中文意思是“匿名”
var f = () => {} //f.name === 'f'
函数的使用
一个基本类型的数据,声明了之后便可以直接使用。比如var n = 3; n = n + 1
,而函数的调用需要用到函数的共有方法call。但是如果你只写一个f,那它只是一个简单的对象,并不能做什么。函数有一种简单的调用方法f()
,这里不提。我们要说的是f.call(),通过调用call方法来使用这个函数。
函数在内存中的存储方式:
在堆内存中函数开辟了一段空间,里边存有函数的参数和函数体,以及__proto__
属性,整个的函数体是以字符串的形式存储的,也就是那种很鸡肋的声明方法中的最后一个参数。函数的共有属性里包括有调用函数的call方法,call方法可以把函数体存的字符串当成代码执行。eval()
有着同样的效果,可以将字符串转成代码执行。比如eval('1+1')
得到的结果是2,eval('1+"1"')
得到的结果是11。
所以我们可以把call方法想象成这个样子
f.call = function(){
eval(f.fbody)
}
//////f是一个变量,f.call是一个属性,f.call()是一个方法
使用f.call()进行函数的调用时,第一个参数是这个函数的本身,也就是在函数体内部的this。后边的参数才是正经传入的参数,也就是组成伪数组arguments的数据。你可以用this得到第一个参数,用arguments[i](i为参数的index)得到后边的参数。
普通模式下第一个参数如果是undefined浏览器会将其封装为window对象(看上去是Window但this===window
的结果为true),但如果在严格模式下
function f(x,y){
'use strict'
console.log(this)
console.log(arguments)
return undefined
}
你传入的是什么得到的就是什么,而不会多此一举地把它们封装成对象。因为this本来就是一个参数,并不是对象。
call stack
function a(){
console.log('a1')
b.call()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c.call()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
上边的代码执行结果依次是a1 b1 c b2 a2
,什么是call stack?在你进入到c.call()之后怎么知道是回去打印a2还是b2呢?正在执行的语句被压入stack中,然后等待它上边的都走了再继续执行出栈。这就是call stack(调用栈)。所谓的栈溢出(stack over flow)就是栈里存的东西太多溢出来了。
作用域
不知道你们有没有听过一句话:如果不写var那声明的就是全局变量。事实上,如果你写了一句a = 1
浏览器会优先认为它是一条赋值语句,然后沿着作用范围的这棵树找,如果找到了最上层还没找到变量a,那么才会在最上层声明变量a。所以我们看到的结果就是声明了全局变量。
js中一个函数就是一个作用范围,我们可以把函数看作是树的结点,这就构成了一个表示作用范围的树形结构。在使用一个变量的时候,它会沿着这个作用范围一层一层地找直至找到这个变量的声明为止,采用的是就近原则。
如果一个函数使用了它范围之外的值,那么这个函数和这个变量就构成了闭包。
一个很经典的题
var liTags = document.querySelectorAll('li') //假设这个页面有6个li
for(var i = 0; i<liTags.length; i++){
liTags[i].onclick = function(){
console.log(i) // 点击第3个 li 时,打印出来的是 2 还是 6 ?
}
}
事实上,在页面加载完之后onclick这个事件还没有执行,也就是说,在for循环的i已经等于6的时候,onclick这个事件还没有发生。function这个容器里边的内容一直都在改变,它是动态的,执行这个函数的时候,我们打印出来的是i最终的值6,而不是你想当然的结果。
本来想把之前的解释删掉的,因为之前虽然强行解释了但是对打印出来的i
的结果都还有些懵。但是好像删掉之后又没什么好写的了,因为这个问题懂的时候好像就没什么好解释的了,似乎最后打印出的是6是自然而然的结果。而且写的好像也没啥错2333
为了更好理解一些可以画内存图如下(emmm点击事件什么的其实是存在heap里边的,画错了蓝鹅不想改了好费劲而且对要表达的东西没什么太大的影响,所以意会一下就好……)
变量i
和事件都存储在stack内存中,而函数都存在了heap内存里,在事件发生的时候进行函数的调用,此时函数对全局变量i
进行调用,所以打印出的只能是i
的最终结果,也就是6。