跳到主要内容

JS 函数基础与递归原理

四种定义函数

  1. 匿名函数
  2. 具名函数
  3. 赋值函数
  4. 箭头函数

匿名函数

function (){
console.log('hi')
}

具名函数

function sayHi() {
console.log('hi')
}

赋值函数

let sayHi = function () {
console.log('hi')
}

箭头函数

箭头函数表达式的语法比函数表达式更简洁。更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

let sayHi = () => console.log('hi')

变量

全局变量

  1. 顶级作用域里声明的变量
  2. window.xxx
let a = 1
window.a = 1

局部变量

  1. { }包括起来的代码为局部变量,除了使用window.a = 1声明变量,也不能使用var声明(历史遗留问题)
  2. 作用域可以嵌套
{
let b = 1
}
b
// Uncaught ReferenceError: b is not defined

**作用域嵌套规则:**默认就近原则。查找声明时,向上取最近的作用域,查找声明过程与函数执行无关,声明值才是与函数执行相关。 静态作用域也叫「词法作用域」(编译原理的知识) 闭包:一个函数用到外部变量,那么这一组组合就形成==闭包==

function showName() {
let content = '变量被调用的时候就形成闭包'
function displayName() {
alert(content) //闭包
}
return displayName
}

let fn = showName()
fn()

函数提升

具名函数function fn()不管在何处声明,最终 JS 引擎解析的时候都会提升到前面。

add(3, 5)
function add(x, y) {
return x + y
}
// 8

匿名函数 let fn = funticon(){}fn 只是赋值,并不是函数名称,实际上该function并没有名称,这种==匿名函数的声明并不进行函数提升。==

add(3, 5)
let add = function (x, y) {
return x + y
}
// Uncaught ReferenceError: add is not defined

形式参数

形式参数的意思就是非实际参数,在函数内部相当于==变量声明==

function add(x, y) {
return x + y
}

// 以上函数与 ↓ 函数近乎等价
function add() {
var x = arguments[0]
var y = arguments[1]
return x + y
}

疑问:形参与对象、值有函数执行后有不同的结果

let a = 1 // 数据存储区域不同,“1”放在“stni "区
let b = { value: 1 }

形参只是给参数起名字,形参可多可少

function add(x) {
return x + arguments[1]
}
add(1, 2)
//3

函数返回值

return返回值为undefined

console.log('hi')
hi // 打印值
undefined // 返回值

**⚠️ 注意:**只有函数才有返回值


JS 的调用时机

JS 定义一个函数后并不会自动的执行它。函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。

调用函数才会以给定的参数真正执行这些动作。例如:定义了函数add,并在之后调用函数

function add(x, y) {
// 定义一个函数为”add“
return x + y
}
add(1, 2) // 传入参数并执行函数

定义与执行不一定同时进行,其中的过程就存在着==调用时机==,不同时间调用函数会有不同的效果

function f() {
console.log(a)
}
let a = 0
f() // 0
a = 1
function f() {
console.log(a)
}
let a = 0
a = 1
f() // 1

循环结构

其中for...let是最简洁的循环结构

for (i = 0; i < 6; i++) {
console.log(i)
}

除了for...let的循环体,还有function可以运用

function f(n, b) {
if (b === undefined) {
;(b = n), (n = 0)
}
if (n <= b) {
console.log(n)
f(n + 1, b)
}
}
// 以下两种函数调用
f(5)
f(0, 5)

function f(n, b) {
b === undefined ? ((b = n), (n = 0)) : null
n <= b ? (console.log(n), f(n + 1, b)) : null
}

function f(n = 0) {
n <= 10 ? (console.log(n), f(n + 1)) : null
}
f()

let f = n => (n <= 10 ? (console.log(n), f(n + 1)) : undefined)

setTImeout

setTimeout() 是设定一个指定等候时间 (单位是千分之一秒, millisecond),时间到浏览器就会执行一个指定的代码。

setTimeout("alert('对不起, 要你久候')", 3000)

⚠️ 注意:变态的情况是即便将setTimeout等待时间设置为0,JS 也会在所有代码都执行完再马上执行setTimeout(console.log('hi'),0)

可以理解为 JS 将所有代码顺序执行,一旦设置setTimeout后,该代码的顺序会延后执行。

for (var i = 0; i < 6; i++) {
// 使用var,后续会解析
setTimeout(() => {
console.log(i)
}, 0)
}
// 6 6 6 6 6 6

i<6; i++条件执行完i = 6跳出循环体,然后setTimeout才会一条条的执行,所以最终打印出 6 个 6。

针对这种情况,新版 ES6 才会独立为for...let的循环体进行优化。

for (let i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// 这样才会打印出 0 1 2 3 4 5

证明 setTimeout 的变态行为,先声明变量i一旦不使用for...let循环结构,就会出现 6 个 6 的问题

let i
for (i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}

调用栈

JS 引擎在调用一个函数前,需要将函数所在的环境push到一个数组里,等函数执行完,把环境pop出来,然后return到调用前的环境里,执行后续的代码。

递归

也就是 JS 执行函数时会进入另外一个空间执行函数,这个空间成为调用栈。若一个函数里面嵌套自己,最终会形成多个调用栈,一层层递进运算再回退运算称为递归运算

function f(n) {
return n != 1 ? n * f(n - 1) : 1
}
f(4) // 24

函数内部的运算过程

f(4)
= 4 * f(4-1)
= 4 * (3 * f(3-1))
= 4 * (3 * (2 * f(2-1)))
= 4 * (3 * (2 *( 1 * f(1)))) // f(1),n = 1 不满足条件,停止调用

递进完成后,进行回归运算

f(4)
= 4 * f( 4-1 )
= 4 * ( 3 * f( 3-1 ))
= 4 * ( 3 * ( 2 * f( 2-1 )))
= 4 * ( 3 * ( 2 *( 1 * f( 1 )))) // f(1),n = 1 不满足条件,停止调用
-----------
= 4 * ( 3 * (2 *( 1 )))
= 4 * ( 3 * ( 2 ))
= 4 * ( 6 )
= 24

---上面每一个=都代表将函数push到调用栈里面。---每一个=都代表将函数pop到调用栈里,直到最后一个值运算完成,才将结果return到函数调用前的环境里。

如果函数需要过多的调用栈,超出浏览器 JS 引擎的范围就会==爆栈==。

function f(n) {
return n != 1 ? n + f(n - 1) : 1 // 相乘的值过大,运行相加来验证
}

end.