函数的 5 种声明
-
具名函数
具名函数就是具有名字的函数。
function f(x,y){ return x+y } f.name // 'f' function fn3(){ // fn3是一个变量 return 3 }
此时,这个具名函数
fn3
的作用域是全局作用域,所有的地方都可以访问到fn3
。我们举一个例子:一个具名函数传给一个变量,那么这个fn4
的作用域就变了:var fn5 =function fn4(){ } console.log(fn4) // undefinded 因为fn4()作用域部分只是它本身 console.log(fn3) // 值是存在的 作用域是整个windows页面下
-
匿名函数
匿名函数就是声明的时候不要给名字,直接运行的时候会报错。因为你声明了一个函数,但是你却不能引用的到它,所以它就相当于废话,浏览器不会让你过的。
function (){ return 1 }

这个时候只能给它一个引用,这个 fn
就引用了这个函数。
var fn = function (){
return 1
}
而函数就是对象,对象是存在堆内存里面的。一般存的时候只是存它的地址,也就是说 fn
记录的是这个函数的地址,而不是函数本身。
var fn2 = fn
比如说我们可以给 fn2
这样一个赋值,这样的话不是把这个函数复制给 fn2
,而是把函数的地址复制给fn2
。所以现在 fn2
指向了这个函数,fn
也指向了这个函数。如果这个时候你去打印一下 fn
和 fn2
的name
:
var fn = function (){
return 1
}
var fn2 = fn
fn.name // 'fn'
fn2.name // 'fn'
打印以后会发现它们的 name
相同,因为它们是同一个函数。 由此得知,它是匿名函数,但是它有 name
。
-
具名函数赋值
var f f = function f2(x,y){ return x+y } f.name // 'f2' console.log(f2) // f2 is not defined
-
window.Function
var f = new Function('x','y','return x+y') f.name // "anonymous"
-
箭头函数
-
如果只有一个参数
var fn6 = i => i+1 // 接受一个i,返回一个i+1 fn6(7) //8 fn6(8) //9
-
如果有两个参数
var fn6 = (i,j) => i+j fn6(1, 2) // 3 fn6(3, 4) // 7 var fn6 = (i,j) => {console.log(1);return i+j} // 如果后面有两句话就需要用花括号括起来
-
如何调用函数
知道了如何申明一个函数,接下来就是该怎么用了。我们知道数据是直接使用就好了

但是函数不是这样用的,函数叫做调用。调用这个单词的英文是 call
,那如何调用一个函数呢?
function f(x, y){return x + y}
f() 或 f.call()
那函数到底是什么呢?
函数就是一段可以反复调用的代码块,还能接受输入的参数,不同的参数会返回不同的值。
比如我们要求三角形的面积:
function 求三角形的面积(width, height){
var n = width * height
var m = n / 2
return m
}
求三角形的面积(4, 3) // 6
求三角形的面积(5, 3) // 7.5
这样我们要求三角形的面积就比较好求了,不同的三角形只需要调用这个函数,再传入相应的参数。求三角形的面积这个函数就是一段可以反复调用的代码块。
那它在内存中是怎么样表现的呢?函数在内存中要怎么存呢?把它当成字符串存起来

我们来看一下内存图:


函数它是这么一个对象 var f = {}
,它的 name
是 'f'
,它可能会有参数,f.params = ['x', 'y']
。它还有一个函数体,f.functionBody = 'console.log("f")'
。它还有一个 call()
的 API
,f.call = function(){return eval(f.functionBody)}
。然后 f.call()
调用这段代码,它就会打印出 'f'
。
现在可以回到函数到底是什么这个问题了,函数它是一个对象,这个对象可以执行一段代码,可以执行代码的对象就叫做函数。这里说的代码是我们写的代码,不是说它有一个什么方法。为什么说函数是一个对象,那就是因为它把方法体存起来了,然后在调用它的 call()
方法的时候,它会去执行。
为什么用 f.call()
,而不用 f()
?
因为 f.call()
才是真正的调用,f()
是语法糖,为阉割版的call
,无法指定this
和 arguments
。
function f(x, y){
return x + y
}
f.call(undefined, 1, 2) // 3
f.call(undefined, 4, 2) // 6
作用域
- 按照语法树,就近原则
- 我们只能确定变量是哪个变量,但是不能确定变量的值

var global1 = 1
function fn1(param1){
var local1 = 'local1'
var local2 = 'local2')
function fn2(param2){
var local2 = 'inner local2'
console.log(local1)
console.log(local2)
}
function fn3(){
var local2 = 'fn3 local2'
fn2(local2)
}
}
我们看这段代码,首先它声明了一个全局变量 global1
,一个参数为 param1
的函数 fn1
。fn1
里面声明了两个局部变量 local1
和 local2
。然后还声明了一个函数 fn2
,fn2
里面也有一个 local2
同名变量,最后打印出 local1
和 local2
。与 fn2
相同层级的函数 fn3
,且里面也有一个 local2
。
当浏览器看到这样的代码,它不会马上去执行,而是会去做一个抽象语法树(就是它会把语法变成一棵树便于它去执行)。也就是说第一次先不执行,看语法对不对。语法全部对了之后,再从头开始执行。
构建一个词法树(这里只写与变量相关的)

当我们在使用变量的时候,比如说在 fn2
里面去打印 local1
,那么它打印的是哪个 local1
呢,是怎么确定的呢?是根据这个词法树来确定的。fn2
里面只有一个 local2
,所以找不到 local1
。找不到的话就到比 fn2
高一层级的 fn1
上找,然后找到了 local1
。先在当前作用域找,没找到的话再到你上一层作用域找,如果这一层找不到就再往上找。
fn3
也声明了一个 local2
,它在调用 fn2
的时候把 local2
带进去了。这里的 local2
会影响 fn2
里面的local2
吗?
答案是完全不会的,没有任何影响。一个函数里面能访问哪些变量,在做词法树分析的时候就已经确定了。也就是说跟调用不调用都没有关系,就算不调用也能确定。要注意的一点是词法树只是用来分析变量是不是这个变量,不是用来分析这个变量的值是不是这个变量的值,分析的是语义跟值没有关系。
来看一个题目:
var a = 1
function b(){
console.log(a)
}
b
里面的 a
是不是外面的 a
呢?我们分析一下语法就知道,这个 a
肯定是外面的 a
,因为它自己没有声明a
。 但是 a
的值是不是外面 a
的值(打印出的一定是 1
吗)?答案是否定的,这个 a
是外面的 a
,但是不代表这个值不会变。比如说这样做
var a = 1
function b(){
console.log(a)
}
a = 2
b()
首先 a=1
,然后声明了一个函数 b
,但是还没有执行 b
(也就是说 console.log(a)
暂时放置一边不动)。再让 a=2
,这个时候去执行 b
,当执行 b
的时候才会去执行 console.log(a)
,这个时候 a
是 2
。
词法作用域确定的是两个变量的关系。
Call Stack
Stack
就是栈,一种先进后出的数据结构,跟队列正好相反。
function a(){
function a(){
console.log('a1')
b()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a()
console.log('end')
看上面这段代码,根据分析词法树浏览器首先做的不是运行代码,而是分析有哪些声明,把声明的都提到前面去。它解析完当前有哪些变量写了哪些语法之后,才开始真正的执行。
- 首先执行
a
,进入function a( ){ }
,函数a( )
里打印出console.log('a1')
中的'a1'
,退出; - 进入函数
b
执行,(如果返回的话就打印出'a2'
),打印出'b1'
; - 进入函数
c
执行,(如果返回的话就打印出'b2'
),打印出'c'
, - 此时返回
c
值,c
执行完就打印出'b2'
,函数b( )
退出; b
执行完打印出'a2'
,返回a
值,最后,执行console.log('end')
Call Stack
到底是什么呢?
我们知道
js
是单线程的,所以它在执行一长串代码的时候,它就会把当前的环境都记住,比如说能访问哪些变量。然后它突然看到一个函数,这个时候它就要切换环境了。因为它要进入这个函数,这个函数的代码并不在这里,存在另一块内存。那它进入这个函数之前,它有可能会忘掉怎么回来,这个时候它就在这里做个记号。但是它要做很多记号,于是它把每一层记号放到一个栈里面,这个栈就叫做调用栈。只要它进入一层调用栈,那么栈里面就会多一个关于它进入时候的记录,于是它就进入新的函数开始执行了。如果新的函数还有函数,它就把第二个函数又放到栈里面,等它回来的时候就先回那个最后进入的地方。
this
和 arguments
在进入一个函数的时候,除了记录下函数当前的位置地址(call stack
),还要记录传给这个函数的参数有哪些?
function f(){
console.log(this)
console.log(arguments)
}
f.call() // window
f.call({name:'frank'}) // {name: 'frank'}, []
f.call({name:'frank'},1) // {name: 'frank'}, [1]
f.call({name:'frank'},1,2) // {name: 'frank'}, [1,2]
我们声明一个函数f然后调用它,我们调用的时候当然是要把 f
当前的环境(位置)存到 Call Stack
。除此之外还要准备两个东西
- 第一个是
this
,如果没有写的话就是undefined
,最后会变成window
- 第二个是
arguments
的伪数组 (没有的话则为空数组)

总结:this
,是 call
的第一个参数。
函数在进入的一瞬间要做三个事情
- 记录函数当前的位置,放在
Call Stack
里面 - 记录下
this
,可以传也可以不传,不传就会默认的变为window
arguments
,传什么就把什么放到数组里面,不传为空数组
this
和 arguments
,用 call
传参数,call
的第一个参数为 this
,call
的后面所有参数都会放在arguments
里,包装成数组。
function f(){
'use strict'
console.log(this)
console.log(arguments)
return undefined
}
f.call(1,2,3) // this 为 1,arguments 为 [2,3]

this
是 undefined
,浏览器会自动把 this
变成 window
。如果是在严格模式下 'use strict'
,call
的第一个参数是什么 this
就是什么。
arguments
是一个伪数组,它的 __proto__
不是 Array.prototype
。