函数的 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.Functionvar 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。