函数的要素
每个函数都有以下要素:调用时机、作用域、闭包、形式参数、返回值、调用栈、函数提升、arguments(除了箭头函数)、this(除了箭头函数)
1. 调用时机
一个函数的调用时机不同,得到的结果就不同。
示例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,因为 setTimeout 是异步执行的,要等 a = 2 执行完了再执行 fn。
示例6
let i = 0
for (i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
上面代码会打印6个6,而不是0,1,2,3,4,5,由于 setTimeout 是异步执行的,所以 for 循环执行结束后(i = 6)再执行setTimeout打印出 i,且打印六次。
示例7
for (let i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
上面代码会打印出0,1,2,3,4,5,使用 let 声明的变量在块级作用域内能强制执行更新变量。
2. 作用域:就近原则 & 闭包
每个函数都会默认创建一个作用域。
function fn() {
let a = 1
}
console.log(a) //a 不存在
fn()
console.log(a) // a 还是不存在
上面代码中,就算 fn 执行了,也访问不到作用域里面的 a,a 是局部变量。
关于全局变量和局部变量:
- 在顶级作用域声明的变量是全局变量
- window 的属性是全局变量
- 其他都是局部变量
函数可以嵌套,作用域也可以嵌套。
function f1() {
let a = 1
function f2() {
let a = 2
console.log(a) //打印出2
}
console.log(a) //打印出1
a = 3
f2()
}
f1()
// 1
// 2
作用域规则:如果多个作用域有同名变量 a,那么查找 a 的声明时,就向上取最近的作用域,简称 “就近原则”,查找 a 的过程与函数执行无关,但 a 的值与函数执行有关。
function f1() {
let a = 1
function f2() {
let a = 2
function f3() {
console.log(a)
}
a = 22
f3()
}
console.log(a)
a = 100
f2()
}
f1()
// 1
// 22
如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包,上面代码中的 let a = 2 和 f3 组成了闭包。
3. 形式参数
形式参数指非实际参数。
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[1]
}
add(1,2) //3
4. 返回值
- 每个函数都有返回值
- 函数执行完了后才会返回
- 只有函数才有返回值
function hi() {
console.log('hi')
}
hi()
//没写 return,所以返回值是 undefined
function hi() {
return console.log('hi')
}
hi()
//返回值为 console.log('hi') 的值,即 undefined
5. 调用栈(Call stack)
JS 引擎在调用一个函数前,需要把函数所在的环境压(push)到一个数组里,这个数组叫做调用栈,等函数执行完了,就会把环境从调用栈里弹(pop)出来,然后 return 到之前的环境,继续执行后续代码。MDN Call stack(调用栈)
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
上面代码是一个有关于阶乘的递归函数,递归就是先递进(压栈),再回归(弹栈),递归函数的调用栈很长,调用栈的长度是有限的,如果调用栈中压入的帧过多,程序就会崩溃,这就是爆栈。
6. 函数提升
函数提升就是,不管把具名函数声明在哪里,它都会跑到第一行。
add(1, 2)
function add(x, y) {
return x + y
}
//3
如果将一个匿名函数赋值给变量时,这时的匿名函数声明不会提升。
let fn = function() {} //这是赋值,右边的匿名函数声明不会提升
注意,采用function命令和var赋值语句声明同一个函数时,由于存在函数提升,最后会采用var赋值语句的定义。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
上面例子中,表面上后面声明的函数f,应该覆盖前面的var赋值语句,但是由于存在函数提升,实际上正好反过来。
7. arguments 和 this
每个函数都有 arguments 和 this,除了箭头函数。
arguments:arguments 对象是一个包含所有普通参数的伪数组。 arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。
this: 如果不给任何条件,this 默认指向 window。
function fn() {
console.log(arguments)
console.log(this)
}
如何传 arguments?
- 调用 fn 即可传 arguments
fn(1,2,3);那么 arguments 就是 [1,2,3] 伪数组
如何传 this?
- 可以用
fn.call(xxx, 1,2,3)传this和arguments - 而且
xxx会被自动转化成对象。
function fn() {
console.log(this)
}
fn.call(1) //Number {1},如果传的this不是对象,JS会自动封装成对象
添加'use strict',就不会被转化成对象。
function fn(){
'use strict'
console.log(this)
}
fn.call(1) //1
8. 关于 this
JS 在每个函数里加了 this。
let person = {
name: 'frank',
sayHi() {
console.log(`你好,我叫` + this.name) //this就是最终调用sayHi()的那个对象
}
}
person.sayHi() 会隐式地把 person 作为 this 自动传给 sayHi,sayHi 可以通过 this 引用 person,以方便 sayHi 获取 person 对应的对象。
函数的两种调用方式
person.sayHi()会自动把 person 传到函数里,作为 thisperson.sayHi.call(person)需要自己手动把 person 传到函数里,作为 this
当使用 call 调用函数而又没有用到 this 时,因为第一个参数要作为 this,但是代码里没有用 this,所以只能用 undefined 占位,其实用 null 也可以。
function add(x, y) {
return x + y
}
add.call(undefined, 1, 2) // 3
this 的两种使用方式
(1)隐式传递
fn(1,2)// 等价于 fn.call(undefined, 1, 2)obj.child.fn(1)// 等价于 obj.child.fn.call(obj.child, 1)
(2)显示传递
fn.call(undefined, 1,2)fn.apply(undefined, [1,2])
绑定 this
使用 .bind 可以让 this 不被改变。
function f1(p1, p2) {
console.log(this, p1, p2)
}
let f2 = f1.bind({
name: 'jack'
})
//f2 就是 f1 绑定了 this 之后的新函数
f2() // 等价于 f1.call({name:'jack'})
.bind 还可以绑定其他参数。
let f3 = f1.bind({name: 'frank'}, 'hi')
f3() // 等价于 f1.call({name:'frank'}, hi)
箭头函数
箭头函数没有 arguments 和 this。
箭头函数里面的 this 就是外面的 this (相当于一个普通变量),就算使用 call 也没有 this 和 arguments。
console.log(this) // window
let fn = () => console.log(this)
fn() // window
fn.call({name:'frank'}) // window
let fn2 = () => console.log(arguments)
fn2(1, 2, 3) // ReferenceError: arguments is not defined
立即执行函数(现在用的少)
ES 5 时代,为了得到局部变量,必须引入一个函数,但是这个函数如果有名字,就得不偿失。
于是这个函数必须是匿名函数,声明匿名函数,然后立即加个 () 执行它,但是 JS 标准认为这种语法不合法。
所以 JS 程序员寻求各种办法,最终发现,只要在匿名函数前面加个运算符即可,!、~、()、+、- 都可以,但是这里面有些运算符会往上走,所以推荐永远用 !来解决。
! function() {
var a = 2
console.log(a)
}()
//打印出2,并返回true
JS 新语法使用一个代码块和 let 即可声明一个局部变量。
{
let a = 2
console.log(a)
} //打印出2
console.log(a) //ReferenceError: a is not defined