首先要明确函数是对象
一、函数
1、定义一个函数
- 具名函数:
function fn(x,y){return x+y} - 匿名函数:
- 上述两者可以合起来写:
let a=function fn(x,y){return x+y},此时fn只能作用在“=”号右边,fn(2,3)报错 - 箭头函数:
- 用构造函数:
let f=new Function('x','y','return x+y'),很少用,但能知道函数是由Function构造出来的
2、函数自身VS函数调用/fn VS fn()
- 有圆括号才能调用
- 分析该代码:
- fn保存了该匿名函数的地址
- 这个地址被复制给了fn2
- fn2()调用了该匿名函数
- fn和fn2都是函数的引用而已
- 真正的函数并不是fn也不是fn2
二、函数的要素
每个函数都有:调用时机、作用域、闭包、形式参数、返回值、调用栈、函数提升、arguments(除了箭头函数)、this(除了箭头函数)
1、调用时机(时机不同,结果不同)
- 上述两段代码区别在于,let在for外和内,打印出的结果不同
- i已经执行完成到6了,setTimeout才开始执行,要执行6次,故打印出6个6
- 当将let放入for里时,JS每次都会“复制”下j,所以setTimeout时,有6个j,分别为0,1,2,3,4,5,执行6次,即打印出0,1,2,3,4,5
- 总的来说,执行时要想清楚顺序
2、作用域(每个函数都会默认创建一个作用域)
3、闭包
- 如果一个函数用到了外部的变量,那么这个函数+这个变量,就叫做闭包
4、形式参数(非实际参数)
fanction add(x,y){
return x+y
}
add(1,2)
- x,y就是形参,不代表任何实际的值,只表示参数的顺序
- 1,2就是实际参数,会被赋值(复制地址给)x,y
- 形式参数的本质就是变量声明,上述代码近似等价于
function add(){
var x=arguments[0] //参数的第一个
var y=arguments[1]
return x+y
}
- 形参可多可少,形参只是给参数取名字
5、返回值(每个函数都有返回值)
- 函数执行完了才会返回
- 只有函数有返回值(1+2返回值为3×,1+2值为3√)
6、调用栈
- JS引擎在调用一个函数前,需要把函数所在的环境push进到一个数组里,这个数组就叫做调用栈
- 等函数执行完后,就会把环境pop弹出来,然后return到之前的环境,继续执行后续代码
- 如果f(10000),要压10000次栈,f(20000)就20000次,这个调用栈就很长,如果调用栈不够用了,就称为爆栈(如果调用栈中压入的帧过多,程序崩溃)
- 一般调用栈最长有多少?chrome 12578,Firefox 26773,Node 12536
7、函数提升
function fn(){},不管你把具名函数声明在哪里,它都会跑到第一行let fn=function(){},这是赋值,右边的匿名函数不会提升- 所以,要惯用let,若用var则不会报错,导致混乱
8、arguments和this(每个函数都有,箭头函数除外)
8.1、arguments
- arguments是一个保存所有参数的伪数组(无push、pop属性)
- 每次调用函数时,都会对应产生一个arguments
function fn(){console.log(arguments)},如何传arguments,直接传参即可
8.2、this
function fn(){console.log(this)},默认指向window,有所有的全局变量- 如何传this,fn.call(),如果传的不是对象,JS会自动封装成对象,有对应Number、String等属性
- 如果不想如此,可将函数改为(但没人会这么用)
8.3、arguments和this
- this是第一个参数,arguments是除第一外的所有参数
- this是隐藏参数,arguments是普通参数
8.4、如何研究this(不用this去实现和this一样的功能)
- 假设没有this
let person={
name:'jack'
sayHi(){
console.log(`你好,我叫`+person.name)
}
}
- 可以用直接保存了对象地址的变量获取name,这种方法叫引用
- 那如果
let sayHi=function(){
console.log(`你好,我叫`+person.name)
}
let person={
name:'jack'
'sayHi':sayHi
}
- 若person改名,sayHi函数里的person也要对应改名,否则报错
- sayHi函数甚至有可能在另一个文件里
- 所以,不希望sayHi函数里出现person
- 再如果
class Person{
constructor(name){
this.name=name //这里的this是new强制指定的
}
sayHi(){
console.log(???) //只创建了类,还没有创建对象,如何拿到对象的name
}
}
- 解决上述两个如果,用土办法,用参数
let person={
name:'jack'
sayHi(p){
console.log(`你好,我叫`+p.name)
}
}
person.sayHi(person)
class Person{
constructor(name){
this.name=name
}
sayHi(p){
console.log(`你好,我叫`+p.name)
}
}
- JS不这样做,JS在每个函数里加了this(用this获取那个未知的对象)
let person={
name:'jack'
sayHi(){
console.log(`你好,我叫`+this.name)
}
}
person.sayHi()
- person.sayHi()相当于person.sayHi(person),写代码时只能写第一种,不能写第二种
- 把this给你,把person给this,相当于把person给你
- this就是最终调用这个函数的对象
- 这样,每个函数都能用this获取一个未知对象的引用了
- 总的来说,person.sayHi()会隐式地把person作为this传给sayHi
8.5、this的两种调用
- person.sayHi(),会自动把person传到函数里,作为this
- 手动传person到函数里,作为this;好处是传什么就是什么(必须用这个调用方法)
person.sayHi.call(person) //jack
person.sayHi.call({name:1}) //1
person.sayHi.call({name:'rose'}) //rose
8.6、this的一些情况/延伸应用
- 没有用到this的情况,要将第一位this占位,用null也行
function add(x+y){
return x+y
}
add.call(undefined,1,2) //3
- 重写forEach(遍历数组)
- forEach里的this一定是数组吗?不一定,也可以传伪数组
8.7、this的两种使用方法
- 隐式传递
fn(1,2) //等价于fn.call(undefined,1,2)
obj.child.fn(1) //等价于obj.child.fn.call(obj.child,1)
- 显式传递
fn.call(undefined,1,2)
fn.apply(undefined,[1,2])
8.8、绑定this,使用.bind
function f1(p1,p2){
console.log(this,p1,p2)
}
let f2=f1.bind({name:'jack'}) //f2就是f1绑定了this后的新版本
f2() //等价于f1.call({name:'jack'})
- .bind还可以绑定所有参数,如上还可绑定p1,p2
let f3=f1.bind({name:'jack'},'hi')
f3() //等价于f1.call({name:'jack'},'hi')
9、箭头函数和立即执行函数
9.1、箭头函数
- 箭头函数没有arguments和this
- 里面的this就是外面的this
console.log(this) //window
let fn()=>console.log(this)
fn() //window
fn.call(1) //window
9.2、立即执行函数
- 原来ES5时代,为了得到一个局部变量,需要额外声明一个全局函数
function fn(){
var a=2
console.log(a)
}
fn() //2
- 不想要这个全局函数,那就把它变成匿名,然后立即加个()执行它
function(){
var a=2
console.log(a)
}()
- 但JS不认这个语法,所以在函数前加个运算符即可,!、()、+、-都可
+ function(){
var a=2
console.log(a)
}() //2
- 但是有些运算符会往上走影响代码,所以永远推荐使用!来解决
- 但当你看见队友使用()来立即执行,你就在上一行加个;号,这是JS唯一一个使用;的地方