JavaScript函数

156 阅读9分钟

函数的 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 也指向了这个函数。如果这个时候你去打印一下 fnfn2name

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()APIf.call = function(){return eval(f.functionBody)} 。然后 f.call() 调用这段代码,它就会打印出 'f'

现在可以回到函数到底是什么这个问题了,函数它是一个对象,这个对象可以执行一段代码,可以执行代码的对象就叫做函数。这里说的代码是我们写的代码,不是说它有一个什么方法。为什么说函数是一个对象,那就是因为它把方法体存起来了,然后在调用它的 call() 方法的时候,它会去执行。

为什么用 f.call(),而不用 f()

因为 f.call() 才是真正的调用,f() 是语法糖,为阉割版的call,无法指定thisarguments

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 的函数 fn1fn1 里面声明了两个局部变量 local1local2。然后还声明了一个函数 fn2fn2 里面也有一个 local2 同名变量,最后打印出 local1local2 。与 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),这个时候 a2

词法作用域确定的是两个变量的关系。

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')

看上面这段代码,根据分析词法树浏览器首先做的不是运行代码,而是分析有哪些声明,把声明的都提到前面去。它解析完当前有哪些变量写了哪些语法之后,才开始真正的执行。

  1. 首先执行 a ,进入 function a( ){ },函数 a( ) 里打印出 console.log('a1') 中的 'a1',退出;
  2. 进入函数 b 执行,(如果返回的话就打印出 'a2'),打印出 'b1'
  3. 进入函数 c 执行,(如果返回的话就打印出 'b2' ),打印出 'c',
  4. 此时返回 c 值,c 执行完就打印出 'b2',函数 b( ) 退出;
  5. b 执行完打印出 'a2',返回 a 值,最后,执行 console.log('end')

Call Stack 到底是什么呢?

我们知道 js 是单线程的,所以它在执行一长串代码的时候,它就会把当前的环境都记住,比如说能访问哪些变量。然后它突然看到一个函数,这个时候它就要切换环境了。因为它要进入这个函数,这个函数的代码并不在这里,存在另一块内存。那它进入这个函数之前,它有可能会忘掉怎么回来,这个时候它就在这里做个记号。但是它要做很多记号,于是它把每一层记号放到一个栈里面,这个栈就叫做调用栈。只要它进入一层调用栈,那么栈里面就会多一个关于它进入时候的记录,于是它就进入新的函数开始执行了。如果新的函数还有函数,它就把第二个函数又放到栈里面,等它回来的时候就先回那个最后进入的地方。

thisarguments

在进入一个函数的时候,除了记录下函数当前的位置地址(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 的第一个参数。

函数在进入的一瞬间要做三个事情

  1. 记录函数当前的位置,放在 Call Stack 里面
  2. 记录下 this,可以传也可以不传,不传就会默认的变为 window
  3. arguments,传什么就把什么放到数组里面,不传为空数组

thisarguments,用 call 传参数,call 的第一个参数为 thiscall 的后面所有参数都会放在arguments 里,包装成数组。

function f(){
    'use strict'
    console.log(this)
    console.log(arguments)
    return undefined
}

f.call(1,2,3) // this 为 1,arguments 为 [2,3]

在普通模式下如果 thisundefined,浏览器会自动把 this 变成 window 。如果是在严格模式下 'use strict'call 的第一个参数是什么 this 就是什么。

arguments 是一个伪数组,它的 __proto__ 不是 Array.prototype