1、定义函数
- 具名函数
function 函数名(形参1,形参2){
语句
return 返回值
}
-
匿名函数
- 上面的具名函数,去掉函数名就是匿名函数
- let a = function(x,y){return x+y}
- 右边部分也叫函数表达式,左边部分是声明一个变量a
- 注意:如果在"="号右边的函数有函数名,那么这个函数名“fn”的作用域只存在于该等号右边,无法在其他地方使用
- 示例1:

- 示例2:

-
箭头函数
let f1 = 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')
- 没人用,但是能让你知道函数是谁构造的
- 所有函数都是Function构造出来的
- 包括Object、Array、Function 也是
2、函数的要素
每个函数都有这些东西
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(除了箭头函数)
- this(除了箭头函数)
1. 调用时机
- 例1:
let i=0
for(i=0; i<6;i++){
setTimeout(()=>{console.log(i)},0)
} //输出6个6
- 例2:
for(let i=0; i<6;i++){
setTimeout(()=>{console.log(i)},0)
} //输出0,1,2,3,4,5
- 这是因为JS在for和let一起用的时候会加东西,每次循环会多创建一个i,创建了6个i
2. 作用域
- 全局变量:
- 在顶级作用域中声明的变量
- window的属性
- 示例:
let a=1
function fn1(){
window.b = 2
}
fn1()
function fn2(){
console.log(a,b) //输出 1 2
}
- 作用域规则:
- 如果多个作用域有同名变量a
- 那么查找a的声明时,就向上取最近的作用域
- 也就是“就近原则”
- 查找a的过程与函数执行无关
- 这种与函数执行无关的作用域,称为“静态作用域”,也叫“词法作用域”
- 但a的值与函数的执行有关
- 如果多个作用域有同名变量a
3. 闭包
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()
- 如果一个函数用到了外部的变量,那么这个函数 + 这个变量,就叫做闭包,上面例子中的a 和f3 就组成了闭包
4. 形式参数
- 形式参数的意思就是非实际参数
- 示例:
function add(x,y){
return x+y
}
//其中x 和y 就是形参,因为不是实际的参数
add(1,2)
//调用 add 时,1和2是实际参数,会被赋值给x 和 y
- 形参可多可少,只是给参数取名字而已
- 示例1:
function add(x){
return x + arguments[1]
}
- 示例2:
function add(x,y,z){
return x+y
}
5. 返回值
- 每个函数都有返回值
console.log()的返回值为undefined- 函数执行完了后,才会返回
- 只有函数有返回值
6. 调用栈
-
什么是调用栈
- JS引擎在调用一个函数前
- 需要把函数所在的环境push到一个数组里
- 这个数组叫做调用栈
- 等函数执行完了,就会把环境弹(pop)出来
- 然后return到值钱的环境,继续执行后续代码
-
举例:
console.log(1)
console.log('1+2的结果为'+add(1,2))
console.log(2)

-
进入函数前,会把一会要回来的地址写进栈中,这个过程叫做压栈
-
执行完函数后,会根据栈中记录的地址回到原来的位置,这个过程叫做弹栈
-
递归函数:
function fn(){
return n !== 1? n*(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
-
爆栈:如果调用栈中,压入的栈过多,程序就会崩溃
7. 函数提升
-
什么是函数提升
- function fn(){}
- 不管把这个具名函数声明在哪里,它都会跑到第一行
- 示例:

-
什么不是函数提升
- let fn = function(){}
- 这是赋值,右边的匿名函数声明不会提升
- 示例:

8. arguments(每个函数都有,除了箭头函数)
- arguments:包含所有参数的伪数组(没有数组的共有属性)
- 如何传arguments
- 调用fn即可传arguments
- fn(1,2,3) 那么arguments 就是[1,2,3]伪数组
- 示例:

9. this(每个函数都有,除了箭头函数)
- this:this默认指向window(基本不使用),示例:

- 如何传this:
- 目前可以使用fn.call(xxx,1,2,3)传this和arguments,其中“xxx”就是this,“1,2,3”就是arguments,而且“xxx”会被自动转化为对象,
- 示例:

- 如果传的this不是对象,那么JS会自动把它封装成对象,示例:

- 如果不让它转化为对象,需要在函数中添加语句:"use strict",示例:

- 结论:this是隐藏参数,arguments是普通参数,this是参数
- JS在每个函数里都加了this,用this获取那个对象
let person = {
name: 'wbh' ,
sayHi(this){
console.log(`你好,我叫` + this.name)
}
}
- person.sayHi()也就相当于person.Hi(person)
- 然后 person 被传给了 this 了(person是一个地址)
- 这样,每个函数都能用 this 来获取一个位置对象的引用了
- person.sayHi() 会隐式的把 person 作为 this 传给 sayHi
- sayHi 可以通过 this 引用person
- 也就是说,this就是最终调用sayHi的对象
两种调用方法:
- 小白调用法
- person.sayHi()
- 会自动把 person 传到函数里,作为this
- 大师调用法
- person.sayHi.call(person)
- 需要自己手动把 person 传到函数里,作为this
- 示例:

- 也就是说,把 {'name':'xxx'} 这个对象,作为 this,那么 console.log(this.name) 的输出结果就是 xxx
例1:
function add(x,y){
return x+y
}
add.call(undefined,1,2) // 3 ,使用了大师调用法
- 为什么要用一个undefined:
- 因为第一个参数要作为this
- 但是代码中没有this
- 所以用undefined占位,null也可以
例2:
Array.prototype.forEach2 = function(fn){
console.log(this)
}
let array = [1,2,3]
array.forEach2.call(array) //[1,2,3],大师写法
array.forEach2() //[1,2,3],小白写法
Array.prototype.forEach2 = function(fn){
for(let i=0;i<this.length;i++){
fn(this[i],i,this)
}
}
let array =[1,2,3]
array.forEach2.call(array,function(x,y,z){
console.log(x,y,z) //1 0 [1,2,3]
}) //2 1 [1,2,3]
//3 2 [1,2,3]
array.forEach2((x,y,z)=>console.log(x,y,z)) //小白写法
示例:

- this 一定是数组吗?不一定,如:
Array.prototype.forEach2.call({0:'a',1:'b',length:2},(item)=>console.log(item)) //a b
其中 {0:'a',1:'b',length:2} 是一个伪数组
- this 的两种使用方法:
隐式传递:
fn(1,2) //等价于fn.call(undefined,1,2)
obj.child.fn(1) //等价于obj.child.fn.call(obj.child,1)
显式传递:
fn.call(undefinde,1,2)
fn.apply(undefined,[1,2])
- 绑定this
- 使用 .bind 可以让 this 不被改变
function f1(p1,p2){
console.log(this,p1,p2)
}
let f2 = f1.bind({name:'wbh'}) //那么 f2 就是 f1 绑定了 this 后的新函数
f2() //等价于f1.call({name.'wbh'})
示例1:



{name.'wbh'}已经被绑定在f2上
- .bind 还可以绑定其他参数
let f3 = f1.bind({name:'wbh'},'h1') //'hi'相当于形参p1
f3() //等价于 f1.call({name:'wbh'},'h1')
let f4 = f1.bind({name:'wbh'},'h1','hello') //'hello' 相当于形参p2
f4() //等价于f1.call({name:'wbh'},'h1','hello')
示例:

3、箭头函数(没有arguments和this)
- 里面的this就是外面的this
console.log(this) //window
let fn =()=> console.log(this)
fn() //window
- 就算你加call都没用
fn.call({name:'wbh'}) //window
- arguments,示例:

4、立即执行函数
- 原理:
- 在ES5时代,为了获得一个局部变量,必须引入一个函数,这个函数必须是一个匿名函数,声明后,立即加个 () 执行它,示例:
function (){ //但是JS认为这样的语法不合法
var a = 2
console.log(a)
}()
- 于是,只要在匿名函数前加一个运算符即可,!、~、()、+、- 都可以
- 优先推荐加 !
!function (){
var a = 2
console.log(a)
}() //2
- 现在新的标准局部变量声明方式:
{
let a = 1 console.log(a) //2
}
console.log(a) //报错
