盒子
盒子
文章目录
  1. 什么是函数
    1. 具名函数来定义
    2. 匿名函数来定义
    3. 具名函数定义了又赋值给了变量
    4. window.Function来构造
    5. 箭头函数
  2. 函数的一些必备知识
    1. 函数的name属性
    2. 函数如何调用
    3. 这就是藏着的this
    4. arguments
  3. 函数的call stack
    1. 普通调用
    2. 嵌套调用
    3. 递归调用
  4. 函数作用域

初探JS的函数

什么是函数

函数是对象的一种,也是一段可以重复使用的代码块,开发人员为了完成某项功能,把相关代码块放到一起。

函数内部可以传参,也可以被当做参数传递

目前定义函数有五种方法

具名函数来定义

1
2
3
4
function f(x, y){
return x + y
}
f.name //'f'

匿名函数来定义

1
2
3
4
5
var f
f = function(x, y){
return x + y
}
f.name //'f'

具名函数定义了又赋值给了变量

1
2
3
4
5
var f1
f1 = function f(a, b){
return a + b
}
f1.name //'f'

要注意:虽然f1.name='f',但是f只在函数内部可用,实际上函数的名字还是f1

window.Function来构造

1
2
var f2 = new Function('x', 'y', 'return x + y')
f2.name //'anonymous'

箭头函数

1
2
3
var f3 = (x, y) => {return x - y}
var sum = (x, y) => x + y //函数体内只有一行代码,可以省略大括号和return
var n2 = n => n*n //只有一个参数,可以省略小括号

常用的定义方法是1、2、5这三种方法。

函数的一些必备知识

函数的name属性

由上面的五种定义方法,我们可以知道函数具有name属性,而且不同的定义方法,name属性也很奇葩。

函数如何调用

为了理解后面的this,推荐使用call()方法,而不是使用常见的f()

以第一种定义方法为例

1
2
f.call(undefined, 1, 3)
4

call()方法的第一个参数就是this,后面的参数才是函数的执行参数。

下面用代码检验一下

1
2
3
4
5
6
7
8
function f1(m, n){
console.log(this)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} //不是应该打印undefined,为啥是window呢?
4 //这才是函数的执行内容

执行f1.call(undefined, 1, 3)后,this不是应该打印出undefined吗,为啥打印了Window呢(注意实际上是个小写的window,不是浏览器打印的大写的Window),可以用代码验证打印的就是小写的window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f1(m, n){
console.log(this === window)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
true //说明是小写的window
4
function f1(m, n){
console.log(this === Window)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
false //并不是大写的Window
4

我真是服啦,那window和Window有啥区别呢。真是蛋疼啊,竟然考虑这个问题……

答案就是 var object = new Object,那var window = new Window。而且Window毫无探讨的意义,倒是这个window是个全局属性,多少有点用。


有时候自己真是有点钻牛角尖,钻进去后,还不会举一反三。如果立刻想到obj的例子就不用浪费时间了。


这就是藏着的this

这是因为浏览器捣的鬼,他把undefined变成了window。接下来使用严格模式,让undefined现身

1
2
3
4
5
6
7
8
9
function f1(m, n){
'use strict'
console.log(this)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
undefined //这个undefined就是call()方法的第一个参数undefined
4
  • 而且call()的第一个参数是啥,this就是啥
1
2
3
4
5
6
7
8
9
function f1(m, n){
'use strict'
console.log(this)
console.log(m + n)
}
undefine
f1.call('我是啥this就是啥', 1, 3)
我是啥this就是啥 //打印的依然是call()的第一个参数
4

arguments

前面分析了call()的第一个参数,那后俩参数是啥呢。

对,你没猜错,那就是arguments。

当你写call(undefined, 1, 3)的时候。undefined可以被认为是this[1, 3]就是arguments

函数的call stack

上面我们接触了call()方法,现在我们学习一下当有多个函数调用的时候,JavaScript解析器是如何调用栈的。

MDN的解释如下

调用栈是解析器(如浏览器中的的javascript解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)

  • 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
  • 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致“堆栈溢出”错误。

以下是通过三个方面去理解call stack这个概念的。

普通调用

代码如下,直观的动图可以看上述的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function a(){
console.log('a')
return 'a'
}

function b(){
console.log('b')
return 'b'
}

function c(){
console.log('c')
return 'c'
}

a.call()
b.call()
c.call()

如上的代码,先有三个函数声明,然后是三个调用。浏览器先执行a.call(),然后执行b.call(),c.call(),下面结合图具体详细分析。

普通调用

  • 第一步:浏览器入口是a.call(),a函数入栈,执行a函数内部代码
  • 第二步:console.log(‘a’)执行完毕,就出栈,接着a函数结束,出栈死亡
  • 第三步:b.call()入栈,执行b函数内部代码
  • 第四步: console.log(‘b’)执行完毕就出栈,接着b函数结束,出栈死亡
  • 第五步:c.call()入栈,执行c函数内部代码
  • 第六步:console.log(‘c’)执行完毕就出栈,接着c函数结束,出栈死亡。
  • 整个代码结束,浏览器恢复平静。

嵌套调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()
console.log('end')

嵌套调用

  • 第一步:浏览器的入口还是a.call(),a.call()入栈,执行a函数内部的代码
  • 第二步: a函数的第一行语句console.log(‘a1’),入栈,打印出a1,这句话就出栈死亡。此时a函数继续执行下面的代码。
  • 第三步: a函数的第二行语句b.call()入栈。执行b函数内部的代码。
    • 第四步:进入b函数内部,b函数的第一行语句console.log(‘b1’)入栈,打印出b1,就出栈死亡。
    • 第五步:b函数的第二行c.call()入栈,又进入c函数内部
      • 第六步:进入c函数的内部,第一行语句console.log(‘c’)入栈,打印出c,就出栈死亡。
      • 第七步:c函数执行完毕,出栈死亡。
    • 第八步:回到b函数内部,执行第三行代码console.log(‘b2’)入栈,打印出b2,出栈死亡。
    • 第九步: b函数执行完毕,出栈死亡。
  • 第十步: 回到a函数内部,执行第三行代码console.log(‘a2’),入栈,打印出a2,就出栈死亡。
  • 第十一步:a函数执行完毕,出栈死亡。
  • 第十二步:console.log(‘end’)入栈,打印出end,出栈死亡。
  • 整个代码运行完,浏览器归于平静。

递归调用

递归调用就是上面的嵌套调用的复杂变化,细心点,分析就能明白具体的代码顺序。

函数作用域

除了全局变量,其他变量只能在自己的函数内部被访问到,其他区域无法访问。通过几个面试题来学习一下。

  • 第一道面试题
1
2
3
4
5
6
var a = 1
function f1(){
alert(a) // 是多少
var a = 2
}
f1.call()

问:alert出什么东西?

这种题切忌上去就做,容易打错成了 a是2 一定要先把变量提升。变成如下这样的

1
2
3
4
5
6
7
var a = 1
function f1(){
var a
alert(a)
a = 2
}
f1.call()

这样一提升就知道啦,答案:a是undefined

  • 第二道面试题
1
2
3
4
5
6
7
8
9
var a = 1
function f1(){
var a = 2
f2.call()
}
function f2(){
console.log(a) // 是多少
}
f1.call()

问:a是多少

这个题用就近原则好做。

树形结构

用树形结构来分析,当上面的代码被浏览器渲染之后

  • 全局变量里面有:var a = 1,f1、f2函数
  • f1函数作用域里面又重新声明了一个var a = 2
  • f2函数作用域里面是console.log(a)

所以打印的那个a就是全局的a,答案是a=1