定义一个函数
所有函数都是
**Function**
构造出来的,包括**Object**
、**Array**
、**Function**
也是
/* 具名函数 */
function 函数名(形式参数1, 形式参数2){
语句
return 返回值
}
/* 匿名函数 */
let a = function(x,y){return x+y} // 等于号 左边是赋值,右边是函数表达式
/* 箭头函数 */
let f1 = x => x*x // 箭头函数 左边 x 是输入参数,右边 x*x 是输出参数
let f2 = (x,y) => x+y // 圆括号不能省
let f3 = (x,y) => {return x+y} // 加了花括号,return 不能省
let f4 = (x,y) => ({name:x, age:y}) // 直接返回对象会出错,需要加个圆括号
/* 构造函数 */
let f = new Function('x','y','return x+y') // 基本没人用,但能知道函数是谁构造的
let a = function(x,y){return x+y}
- 如果函数声明是在
=
右边,那么整个作用域就只能在函数右边,其他地方要用这个函数就用 a
- 如果函数声明是在
function(x,y){return x+y}
函数调用 fn()
代码 1
let fn = () console.log('hi')
fn()
结果:打印 hi ,有圆括号才是调用
代码 2
let fn = () => console.log('hi')
let fn2 = fn
fn2()
结果:
fn
保存了匿名函数的地址,这个地址被复制给了fn2
fn2
调用了匿名函数fn
和fn2
都是匿名函数的引用而已,真正的函数既不是fn
也不是fn2
函数的要素
- 调用时机、作用域、闭包、形式参数、返回值、调用栈、
- 函数提升、arguments (除了箭头函数)、this (除了箭头函数)
调用时机
- 时机不同,结果不同。示例: ```javascript 例 1 let a = 1 function fn(){ console.log(a) } // 没有调用函数
例 2 let a = 1 function fn(){ console.log(a) } fn() // 打印出 1
例 3 let a = 1 function fn(){ console.log(a) } a = 2 fn() // 打印出 2
例 4 let a = 1 function fn(){ console.log(a) } fn() a = 2 // 打印出 1
例 5 let a = 1 function fn(){ setTimeout(()=>{ console.log(a) },0) } fn() a = 2 // 打印出 2 例 6 let i = 0 for(i = 0; i<6; i++){ setTimeout(()=>{ // 延时函数 console.log(i) },0) } // 不是打印 0、1、2、3、4、5,而是 6 个 6 // 循环先执行完,再去打印
例 7 for(let i = 0; i<6; i++){ setTimeout(()=>{ console.log(i) },0) // 打印出 0、1、2、3、4、5 // 因为 JS 在 for 和 let 一起用的时候会加东西,每次循环会多创建一个 i (为了迎合新人而做出的处理)
- `**setTimeout**` :用于在指定的毫秒数后调用函数或计算表达式。(没明确毫秒数时,尽快执行完当前代码,再打印)
- **语法**有两种:
- `setTimeout (要执行的代码, 等待的毫秒数)`
- `setTimeout (JavaScript 函数, 等待的毫秒数)`
<a name="iV6xA"></a>
### 作用域:就近原则 & 闭包
- 每个函数都会默认创建一个作用域
```javascript
function fn(){
let a = 1
}
console.log(a) // a 不存在
// 就算 fn 执行了,也访问不到作用域里面的 a
// 如果在函数里面声明了一个 let 或者 const,那么这个变量就是局部变量,因为它只在函数块里能生效
全局变量 V.S. 局部变量
- 全局变量:
- 在顶级作用域声明的变量
- 挂到 window 上的属性是全局变量
-
函数可嵌套
作用域也可嵌套。 示例:
function f1(){
let a = 1
function f2(){
let a = 2
console.log(a)
}
console.log(a)
a = 3
f2()
}
f1()
作用域规则
如果多个作用域有同名变量
a
如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包
- 下面代码中的
a
和f3
组成了闭包
形式参数
形式参数的意思就是非实际参数
function add(x, y){
return x+y
} // 其中 x 和 y 就是形参,因为并不是实际的参数
add(1, 2) // 调用 add 时。1 和 2 是实际参数,会被赋值给 x y
形参的本质是声明变量
// 上面代码近似等价于下面的代码
function add(){
var x = arguments[0]
var y = arguments[1]
return x+y
}
形参可多可少,只是给参数取名字
示例
/* 写法一 */
function add(x){
return x+ arguments[i] // arguments 意为所有参数
}
add(1,2) --> // 3
/* 写法二 */
function add(x,y,z){
return x+y
}
add(3,4) --> // 7
返回值
- 每个函数都有返回值
- 返回值是在执行后才会返回的 ```javascript function hi(){ console,log(‘hi’)} hi() // 没写 return,所以返回值是 undefined
function hi(){ renturn console.log(‘hi’)} hi() // 返回值为 console.log(‘hi’) 的值,即 undefined
- **只有函数有返回值**
- 打印值和返回值不是一个东西,**返回值**是 return 后面的值
- `~~1 + 2~~`~~ 返回值为 3~~
- `1 + 2` 值为 3
<a name="kZghT"></a>
### 调用栈
<a name="jgYzI"></a>
#### 什么是调用栈
- JS 引擎在调用一个函数前,需要把函数所在的环境 push 到一个**数组**里,这个数组叫做**调用栈。**
- 等函数执行完了,就会把环境**弹(pop)**出来
- 然后 return 到之前的环境,继续执行后续代码
<a name="nIVLl"></a>
#### 调用栈的作用
- 调用栈实际上是记录进入一个函数之后,回的话回去哪里?
- 每次进入一个函数都得记下来等一会回到哪,所以要把回到的地址写到栈里。如果进入到函数后还要进入一个函数,就要把这个函数的地址再放到栈里面,函数执行完后就要弹栈,知道回到哪,此为**压栈**和**弹栈**。
<a name="yv3th"></a>
#### 递归函数
> **先递进,再回归**
> - 递进的过程就是**压栈**的过程
> - 回归的过程就是**弹栈**的过程
- **阶乘**
```javascript
function f(n){
return n !== 1 ? n* f(n-1) : 1
}
理解递归
f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24
递归函数的调用栈
如何测出浏览器调用栈最长有多长?
function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) {
// 报错说明 stack overflow 了
return 1;
}
}
Chrome 12578
- Firefox 26773
- Node 12536
- 爆栈 :如果调用栈中压入的帧过多,程序就会崩溃
函数提升
什么是函数提升
说明 | 截图示例 |
---|---|
function fn(){} 不管把具名函数声明在哪里,它都会跑到第一行 |
![]() 这就是函数提升 |
【注意】:
**let**
不允许在已经有一个函数add
的情况下,再声明add
,因为函数会跑到前面去,变成这个顺序
**let**
有个特性:- 如果这个变量已经存在就不许再声明了,不能有同名的函数和
**let**
变量,但是**var**
变量没有这个问题。 **let**
必须先声明再使用,否则报错,示例:

- 如果这个变量已经存在就不许再声明了,不能有同名的函数和
什么不是函数提升
let fn = function(){}
- 这是赋值,右边的匿名函数声明不会提升
arguments 和 this
arguments 和 this 每个函数都有,除了箭头函数。
function fn(){
console.log(arguments)
console.log(this)
}
arguments
是包含所有参数的伪数组
如何把伪数组变成数组?
**Array.from**
可以把任何不是数组的东西变成数组。
如何传 arguments
如果不给任何的条件,那么
**this**
默认指向**window**
。(一般不用这个默认,属于 JS 糟粕)- 如果传的
this
不是对象,那么 JS 会自动封装成对象。( JS 的糟粕) - 如何传 this
- 可以用
**fn.call**(xxx, 1,2,3)
传 this 和 arguments - 而且
xxx
会被自动封装成对象 - 如何禁用
this
自动封装成对象的特性?- 声明函数时加一句
**'use strict'**
:不要乱加东西 | 没加**'use strict'**
的效果 | 加了**'use strict'**
的效果 | 传**undefined**
的效果 | | —- | —- | —- | | 自动将 1 变成了对象 1| 传什么就是什么
| 传了undefined
就会变成全局变量 window|
- 声明函数时加一句
- 可以用
**this**
是隐藏参数**arguments**
是普通参数
图示 | 说明 |
---|---|
![]() |
**this** 是第一个参数**arguments** 是第一个参数往后的所有参数 |
在写函数时怎么得到未来要创建的对象的引用?
JS 在每个函数里加了
**this**
,用this
获取一个未知对象的引用(如果一个变量保存了一个对象的地址那就是引用)
let person = {
name: 'tk',
sayHi() { // 圆括号里有隐藏参数 this
console.log(`你好,我叫` + this.name)
}
}
person.sayHi()
person.sayHi()
会隐式地把person
(person 是个地址) 作为this
传给sayHi
,方便sayHi
获取person
对应的对象
this 的两种调用
-
小白调用法**person.sayHi()**
会自动把person
传到函数里,作为this
- 隐藏太多细节,只适合小白
[x] 大师调用法「推荐」
- 不会自动传任何东西
**person.sayHi.call(person)**
需要自己手动把person
传到函数里,作为this
function add(x,y){
return x+y
}
// 如果没有用到 this ,可用 undefined 占位
add.call(undefined,1,2) // 3
为什么要多写一个
**undefined**
- 因为第一个参数要作为
this
,但是代码里没有用 this,所有只能用undefined/null
占位
- 因为第一个参数要作为
this 的两种使用方法
- 隐式传递
**fn(1,2)**
等价于**fn.call(undefined,1,2**)
:把**undefined**
作为**this**
**obj.child.fn(1)**
等价于**obj.child.fn.call(obj.child,1**)
:把**obj.call**
作为**this**
,**1**
为第二个参数- 如果是一个对象的属性里面的函数,那么就等价于把这个函数前面的这个部分作为
**this**
- 如果是一个对象的属性里面的函数,那么就等价于把这个函数前面的这个部分作为
- 显示传递
**fn.call(undefined,1,2)**
**fn.apply(undefined),[1,2]**
**call**
和**apply**
的区别:需要在this
后面的其他参数上加中括号**[]**
,因为后面的参数要用数组的形式来表示,只是形式不同,其他都一样。
绑定 this
如果不确定
**this**
可以强制绑定
使用
**.bind**
可以让**this**
不被改变function f1(p1,p2){
console.log(this, p1, p2)
}
let f2 = f1.bind({name:'tk'}) // f2 就是 f1 绑定了 this 之后的新函数 (也就是 f2 是 this 绑定之后的版本)
// f2() 等价于 f1.call({name:'tk'})
**.bind**
还可以绑定其他参数let f3 = f1.bind({name:'tk'},'hi')
// f3() 等价于 f1.call({name:'tk'},hi)
箭头函数
JS 在最新版把 this 给干掉了
- this 功能复杂且隐晦,新版的 JS 函数放弃用 this,直接用箭头函数
箭头函数没有 arguments
和 this
箭头函数里面的
**this**
就是外面的**this**
- 箭头函数的
**this**
就是一个普通的变量,外面是什么里面就是什么(外面的 this 默认为 window) - 怎么证明箭头函数没有
**this**
?- 不论对箭头函数做任何的
**call**
或**.bind**
操作,它的this
永远都是最开始获取到的定义this
(也就是window
)/* 代码示例 */
console.log(this) // 默认的 this 是 外面的 window
let fn = ()=> console.log(this) // 声明一个箭头函数让它打印出 this
fn() // 不管在任何时候打印出 fn,它的值都是 window
// 就算加上 call 或 .bind 都没用,打印出的还是 window
- 不论对箭头函数做任何的
总结:
每个函数都有这些东西
- 调用时机:决定变量的值。
- 作用域:每个函数都有作用域,若同时有多个作用域,那就采取就近原则。
- 闭包:就是一个函数和一个变量合起来。
- 形式参数:就是对参数取名字而已,相当于声明一个变量。
- 返回值:每个函数都有返回值。
- 调用栈:每个函数进去之前都要压一个栈,出来之后要弹栈。
- 函数提升:函数永远都会跑到最前面去。
- arguments(除了箭头函数):是包含所有参数的一个伪数组
- this(除了箭头函数):用来引用一个还不存在的东西,实际上
**this**
就是**call**
的一个参数,只要坚持用call
,那么this
就完全没用歧义,如果不用call
那this
就充满了歧义。
立即执行函数
- 只有 JS 才有的变态玩意,现在用的少
原理
- ES 5 时代,用
**var**
为了得到局部变量,必须引用一个函数,但是这个函数如果有名字,就得不偿失了 - 后来发现只要在匿名函数前面加个运算符即可,
!、~、()、+、-
都可以。 - 但是有些运算符会往上走,所有推荐永远用
!
来解决,如果用了()
就在圆括号前面加个分号;
来解决。 - 新版 JS 用
**let**
就没有这个问题了