JS函数

148 阅读6分钟

首先要明确函数是对象

一、函数

1、定义一个函数

  • 具名函数:function fn(x,y){return x+y}
  • 匿名函数:微信图片_20230322103328.jpg
  • 上述两者可以合起来写:let a=function fn(x,y){return x+y},此时fn只能作用在“=”号右边,fn(2,3)报错
  • 箭头函数:微信图片_20230322104234.jpg
  • 用构造函数:let f=new Function('x','y','return x+y'),很少用,但能知道函数是由Function构造出来的

2、函数自身VS函数调用/fn VS fn()

  • 有圆括号才能调用
  • 分析该代码:微信图片_20230322105249.png
  1. fn保存了该匿名函数的地址
  2. 这个地址被复制给了fn2
  3. fn2()调用了该匿名函数
  4. fn和fn2都是函数的引用而已
  5. 真正的函数并不是fn也不是fn2

二、函数的要素

每个函数都有:调用时机、作用域、闭包、形式参数、返回值、调用栈、函数提升、arguments(除了箭头函数)、this(除了箭头函数)

1、调用时机(时机不同,结果不同)

微信图片_20230322111031.png

  1. 上述两段代码区别在于,let在for外和内,打印出的结果不同
  2. i已经执行完成到6了,setTimeout才开始执行,要执行6次,故打印出6个6
  3. 当将let放入for里时,JS每次都会“复制”下j,所以setTimeout时,有6个j,分别为0,1,2,3,4,5,执行6次,即打印出0,1,2,3,4,5
  4. 总的来说,执行时要想清楚顺序

2、作用域(每个函数都会默认创建一个作用域)

微信图片_20230322113215.jpg 微信图片_20230322113015.jpg 微信图片_20230322145608.jpg

3、闭包

  • 如果一个函数用到了外部的变量,那么这个函数+这个变量,就叫做闭包 微信图片_20230322145832.jpg

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
}
  • 形参可多可少,形参只是给参数取名字 微信图片_20230322150915.jpg

5、返回值(每个函数都有返回值)

微信图片_20230322151127.jpg

  • 函数执行完了才会返回
  • 只有函数有返回值(1+2返回值为3×,1+2值为3√)

6、调用栈

  • JS引擎在调用一个函数前,需要把函数所在的环境push进到一个数组里,这个数组就叫做调用栈
  • 等函数执行完后,就会把环境pop弹出来,然后return到之前的环境,继续执行后续代码 微信图片_20230322152109.jpg
  • 如果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,直接传参即可 微信图片_20230322153530.png
8.2、this
  • function fn(){console.log(this)},默认指向window,有所有的全局变量 微信图片_20230322154420.png
  • 如何传this,fn.call(),如果传的不是对象,JS会自动封装成对象,有对应Number、String等属性 微信图片_20230322154718.png
  • 如果不想如此,可将函数改为(但没人会这么用) 微信图片_20230322155039.png
8.3、arguments和this

微信图片_20230322155358.png

  • this是第一个参数,arguments是除第一外的所有参数
  • this是隐藏参数,arguments是普通参数
8.4、如何研究this(不用this去实现和this一样的功能)
  1. 假设没有this
let person={
  name:'jack'
  sayHi(){
    console.log(`你好,我叫`+person.name)
  }
}
  • 可以用直接保存了对象地址的变量获取name,这种方法叫引用
  1. 那如果
let sayHi=function(){
  console.log(`你好,我叫`+person.name)
}
let person={
  name:'jack'
  'sayHi':sayHi
}
  • 若person改名,sayHi函数里的person也要对应改名,否则报错
  • sayHi函数甚至有可能在另一个文件里
  • 所以,不希望sayHi函数里出现person
  1. 再如果
class Person{
  constructor(name){
    this.name=name  //这里的this是new强制指定的
  }
  sayHi(){
  console.log(???)  //只创建了类,还没有创建对象,如何拿到对象的name
  }
}
  1. 解决上述两个如果,用土办法,用参数
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)
  }
}
  1. 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就是最终调用这个函数的对象
  1. 这样,每个函数都能用this获取一个未知对象的引用了
  • 总的来说,person.sayHi()会隐式地把person作为this传给sayHi
8.5、this的两种调用
  1. person.sayHi(),会自动把person传到函数里,作为this
  2. 手动传person到函数里,作为this;好处是传什么就是什么(必须用这个调用方法)
person.sayHi.call(person)  //jack
person.sayHi.call({name:1})  //1
person.sayHi.call({name:'rose'})  //rose
8.6、this的一些情况/延伸应用
  1. 没有用到this的情况,要将第一位this占位,用null也行
function add(x+y){
  return x+y
}
add.call(undefined,1,2)  //3
  1. 重写forEach(遍历数组) 微信图片_20230322194854.png
  • forEach里的this一定是数组吗?不一定,也可以传伪数组
8.7、this的两种使用方法
  1. 隐式传递
fn(1,2)  //等价于fn.call(undefined,1,2)
obj.child.fn(1)  //等价于obj.child.fn.call(obj.child,1)
  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、立即执行函数
  1. 原来ES5时代,为了得到一个局部变量,需要额外声明一个全局函数
function fn(){
  var a=2
  console.log(a)
}
fn()  //2
  1. 不想要这个全局函数,那就把它变成匿名,然后立即加个()执行它
function(){
  var a=2
  console.log(a)
}()
  1. 但JS不认这个语法,所以在函数前加个运算符即可,!、()、+、-都可
+ function(){
  var a=2
  console.log(a)
}()  //2
  1. 但是有些运算符会往上走影响代码,所以永远推荐使用!来解决
  2. 但当你看见队友使用()来立即执行,你就在上一行加个;号,这是JS唯一一个使用;的地方