JS函数

117 阅读10分钟

概念

函数是一种对象

定义一个函数

具名函数

function 函数名(形式参数1,形式参数2){
语句
return返回值
}

1632278638(1).png

匿名函数

  • 上面的具名函数去掉函数名就是匿名函数
  • leta=function(x,y){ return x+y }
  • 也叫函数表达式
  • 注意 函数在等号右边,它的作用域只在右边。没有等号,才会全局作用

1632278830(1).png

箭头函数

  • 单个参数:let f1 = x => x*x
  • 多个个参数:let f2 = (x,y,z,...) => x+y+z+... 参数圆括号不能省
  • 返回多语句: 需自行添加return,因为多语句JS无法确定返回值
let f3 = (x,y) => {
console.log('hi')       语句1
return x+y              语句2
} 
  • 输入参数返回对象:let f4 = (x,y) => ({name:x, age: y}) 直接返回对象,JS判定''{''是执行版块的开始,而不是对象,加(),才会识别为一个整体,所以圆括号不能省
  • 构造函数
  • let f = new Function('x', 'y', 'return x+y')解释了函数是由谁构造出来的,基本没人用
  • 所有函数都是Function构造出来的,包括Object、Array、Function也是

函数自身V.S.函数调用 fn V.S. fn()

函数自身

  • 代码:
let fn = () => console.log('hi')
fn
  • 结果:不会有任何结果,因为fn没有执行

函数调用

  • 代码:
let fn = () => console.log('hi')
fn()
  • 结果:打印出hi,只有圆括号才是调用

再进一步

  • 代码:
let fn = () => console.log('hi')
let fn2 = fn
fn2()

  • 结果
    • fn保存了匿名函数的地址
    • 这个地址复制给了fn2
    • fn2()调用了匿名函数
    • fn和fn2都是匿名函数的引用而已 (引用的含义:存了地址,被称为引用)
    • 真正的函数既不是fn也不是fn2

    函数的要素

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

  • 例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 打印出6个6
et i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
 结果:打印66

执行机制

  1. 首先根据循环条件,setTimeout 执行6次
  2. setTimeout执行原理是先等i从0到6。等一会i的值为6,即打印出6
  3. 总体逻辑:等一会儿打印(打印6)执行了6次(打印6个6)
  • 例7 打印出0,1,2,3,4,5
for (let i = 0; i < 6; i++) {
    setTimeout(() => {
        console.log(i)
    }, 0)
let会在每次循环的时候将当前i的值放入setTimeout中,不会再变化
打印出0,1,2,3,4,5,与传统的for循环没有区别


  • 其它实现方法
let i = 0
for (i = 0; i < 6; i++) {
    let j = i
    setTimeout(() => {
        console.log(j)
    }, 0)
}


作用域

每个函数都会默认创建一个作用域

  • 例1
function fn(){
  let a = 1
}
console.log(a) 

a 不存在
就算fn执行了,也访问不到作用域里的a
a的作用域只在{   }内
  • 例2
function fn(){
  let a = 1
}
fn()
console.log(a)
a还是不存在,let a=1 的作用域在{  }内

全局变量 V.S.局部变量

  • 在顶级作用域声明的变量是全局变量
  • 在window的属性是全局变量
  • 其他都是局部变量

作用域的 函数可嵌套

例1:

1632302840(1).png

  • 执行机制
    • 调用函数f1
    • 函数f1先打印a,再调用f2(以下序号代表执行顺序)
      1. f1打印a,作用域内a=1,打印1
      2. f2打印a,作用域内a=2,打印2 例2:

7ffc674f1ccef508174aafe926d59e4.png

  • 执行机制
    • 调用f1(以下序号代表执行顺序)
      1. f1内先打印f1内a的值
      2. a赋值为100,待定,不影响函数调用,根据实际情况
      3. 调用f2
      4. f2执行 a赋值22,赋值是否调用待定
      5. f2执行调用f3
      6. f3执行打印a,f3内没有a的值
      7. 由于a先赋值了22,后调用f3,所以就近原则,a为100,打印22
    • 总结:总体执行顺序为先执行f1→a=100→执行f2,具体内容由外向内展开,找出作用域和调用函数顺序

闭包

如果一个函数用到了外部的变量 那么这个函数加这个变量 就叫做闭包
下图的 a 和 f3 组成了闭包

cc21dc35b0809df5271904c1ddf8604.png

形式参数

  • 形式参数的意思就是非实际参数
function add(x, y){        x和y就是形参,因为并不是实际参数
  return x+y             
}
add(1,2)                调用add时,12是实际参数,会赋值给x,y,   本质是复制了地址到x和y

结果为3

  • 形参可认为是变量声明 以上代码等价于:
function add(){
  var x = arguments[0]
  var y = arguments[1]
  return x+y
}
  • 形参可多可少 形参只是给参数取名字

小结:不论参数是数值还是对象,复制到形参内的只是参数的地址

返回值

  • 每个函数都有返回值
function hi(){ console.log('hi') }
hi()


没写 return,所以返回值是 undefined
function hi(){ return console.log('hi') }
hi()


返回值为 console.log('hi') 的值,即 undefined
  • 函数执行完了之后才会返回
  • 只有函数有返回值
    • 错误示范 1+2 返回值为3 没加return
    • 正确写法 1+2的值为3

    调用栈

    概念

JS 引擎在调用一个函数前, 需要把函数所在的环境 push 到一个数组里, 这个数组叫做调用栈, 等函数执行完了,就会把环境弹(pop)出来, 然后 return 到之前的环境,继续执行后续代码

例:调用机制

1696715fc5673594545dd360ab7f1ef.png

递归函数

  • 阶乘
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

先递进到控制条件 n=1,再回归,返回值24
  • 递归函数的调用栈 以阶乘(4)为例

1632364457(1).png

  • 爆栈 如果调用栈中压入的帧过多,程序就会崩溃
    • 检测代码
function computeMaxCallStackSize() {
  try {
    return 1 + computeMaxCallStackSize();
  } catch (e) {
    // 报错说明 stack overflow 了
    return 1;
  }
}

函数提升

概念

  • function fn(){ }
  • 不管把具名函数声明在哪里,它都会跑到第一行

862aba9791533a93eca9c94fbb0d8a8.png

577dafad6a35019f2941956e0e2b236.png 两种顺序都是function先声明了add,let后声明add,但是let不允许再次声明

arguments和this

每个函数都有除了箭头函数

argumens

特性 f90471213a794d19c841f22d34917e3.png arguments原型里面没有数组的共有属性,所以它是包含所有参数的伪数组(没有push,join等属性)

this

b5edf0b407fe881c344e41d040d83f3.png 如果不给任何条件,this默认指向window

fn.call(XXX)传参

XXX会被自动转化成对象 image.png 图中的1不是值,而是对象
去除转换效果

76fa10ff4313cf6be2fa47037ba90fa.png 加入'use atrict',禁止转化成对象

this、arguments传参

图1 1632379212(1).png 图二 1632378916(1).png 第一个参数传给this,后面的参数传给arguments

person.sayHi()案例的this(难点)

假设没有this

let person = {
  name: 'frank',    
  sayHi(){                   函数还未调用执行
    console.log(`你好,我叫` + person.name)
  }
}
  • 分析
    我们可以用直接保存了对象地址的变量获取'name',这种方法简称为引用

问题一

  • person 如果改名,sayHi函数就不能运行了
  • sayHi函数甚至可能在另外一个文件呢里面
  • 所以我们不希望sayHi函数里出现person引用

问题二

class Person{
  constructor(name){
    this.name = name 
    // 这里的 this 是 new 强制指定的
  }
  sayHi(){
    console.log(???)
  }
}
分析
  • 这里只有类,没有创建对象,故不可能获取对象的引用

需要一种办法拿到对象,这样才能获取对象的name属性

种土办法,用参数
对象
let person = {
  name: 'frank',
  sayHi(p){
    console.log(`你好,我叫` + p.name)
  }
}
person.sayHi(person)    
  • sayHi函数虽然写在对象里面,但是与写在外面没有什么区别,因为还没有调用
  • p为函数的形式参数,person作为参数
class Person{
  constructor(name){ this.name = name }
  sayHi(p){
    console.log(`你好,我叫` + p.name)
  }
}
Python 代码
class Person:
  def __init__(self, name):                   构造函数
    self.name = name

  def sayHi(self):
    print('Hi, I am ' + self.name)
person = Person('frank')
person.sayHi()

特点
  • 每个函数都接受一个额外的self
  • 这个self就是传进来的对象
  • 只不过Python会偷偷帮你传对象
  • person.sayHi() 等价于 person.sayHi(person)
  • person就被传给self了

JS在每个函数里加了this

let person = {
  name: 'frank',
  sayHi(this){                   里面的this不写
    console.log(`你好,我叫` + this.name)
  }
}

加了this后, person.say()相当于person.sayHi(person)
然后person被传给this了(person是个地址)
这样每个函数都能用this获取一个未知对象的引用了
个人理解:this只不过是这个函数自带的形参,它的作用是作为中间的媒介,调用person的地址,获取person对应的对象

思想person.sayHi()会隐式地把person作为this传给sayHi,方便sayHi获取person对应的对象

总结

  • 我们想让函数获取对象的引用
  • 但是并不想通过变量名做到
  • Python通过额外的this做到
  • JS通过额外的this做到:
    • person.sayHi() 会把 person 自动传给 sayHi, sayHi 可以通过 this 引用 person

注意

  • 注意 person.sayHi 和 person.sayHi() 的区别
  • 注意 person.sayHi() 的断句 (person.sayHi) ( )

this 调用原理图

案例:

class Person{
  constructor(name){
    this.name = name 
    // 这里的 this 是 new 强制指定的
  }
  sayHi(){
    console.log(???)
  }
}

cb1e0b466e1e2e7fd4a5126029b22dc.png
这里的p是一个中间对象,而JS引擎将这个中间对象地址传给了this,让this扮演P这个角色

另一个问题

  • person.sayHi()
  • person.sayHi(person)
  • 省略形式反而对了,完整形式反而是错的

两种调用方法

方法一

  • person.sayHi()
  • 会自动把perso传到函数里作为this

方法二

  • person.sayHi.call(person)
  • 需要自己动手把person传到函数里,作为this

call指定this

传参传的是什么,this就是什么 1632456134(1).png

  • 传的参数为对象{name:1},这时的this就为{name:1},this.name就为1,打印出1 例

1632456959(1).png

  • 案例中没有this,传参的时候第一个数据选择任意数据都可以(用于this的占位),后面才是所需要的具体参数(arguments) 例:打印数组的元素
let array = [1, 2, 3]                                       声明一个数组
Array.prototype.forEach2 = function(fn) {                   数组原型中加入forEach2函数,参数为fn
    for (let i = 0; i < this.length; i++) {                 循环次数为未知数组的长度,this过渡
        fn(this[i], i)                                      fn函数的参数为key以及对应的value
    }                                                       this作为过渡数组,等待传入 

}
array.forEach2.call(arry, (item) => console.log(item))     array就是this,右侧参数即为fn  

this一定为数组吗?

  • 不一定,比如Array.prototype.forEach2.call({0:'a',1:'b'})

1632462457(1).png 这里的this为{0:'a',length:1}伪数组,所以this为可以任意指定的参数

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])

绑定this

使用。bind可以让this不被改变

function f1(p1, p2){
  console.log(this, p1, p2)
}
let f2 = f1.bind({name:'frank'})
 那么 f2 就是 f1 绑定了 this 之后的新函数
f2() 等价于 f1.call({name:'frank'})

bind还可以绑定其他参数

let f3 = f1.bind({name:'frank'}, 'hi')
f3()    等价于 f1.call({name:'frank'}, hi)


箭头函数

没有arguments和this

里面的this就是外面的this

  • console.log(this) 结果 window
  • let fn = () => console.log(this) fn() 结果为window

就算加call都没用

  • fn.call({name:'frank'}) 结果 window

立即执行函数

原理

  • ES5时代,为了得到局部变量,先引入一个函数
  • 但是这个函数如果有名字,就得不偿失
  • 于是这个函数必须是匿名函数
  • 声明匿名函数,然后立即加个 () 执行它
  • 但是 JS 标准认为这种语法不合法
  • 只要在匿名函数前面加个运算符即可
  • !、~、()、+、- 都可以
  • 但是这里面有些运算符会往上走 推荐用!