JS 函数

157 阅读6分钟

函数是对象

定义一个函数

  1. 具名函数
function 函数名(形式参数1, 形式参数2){
  语句
  return 返回值
}
  1. 匿名函数,也叫函数表达式
let a = function(x, y){ return x+y }

如果函数是在等于号右边的,那么函数的作用域就只在右边,出了右边的范围就不存在了

image.png

  1. 箭头函数
let f1 = x => x*x 
let f2 = (x,y) => x+y // 圆括号不能省
let f3 = (x,y) => {return x+y} // 花括号不能省
let f4 = (x,y) => ({name:x, age: y}) //直接返回对象会出错,需要加个圆括号(头疼)
  1. 用构造函数
let f = new Function('x', 'y', 'return x+y')

基本没人用,但是能让你知道函数是谁构造的。 所有函数都是 Function 构造出来的, 包括 Object、Array、Function 也是

函数的要素

每个函数都有这些东西

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

时机不同,结果不同

例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:

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}

问打印出多少? 答: 6 个 6

例7:

for(let i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}

问打印出多少? 答: 0、1、2、3、4、5

作用域

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

例子1:

function fn(){
  let a = 1
}
console.log(a) // a 不存在

问:是不是因为 fn 没执行导致

答:就算 fn 执行了,也访问不到作用域里面的 a

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

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

函数可嵌套,作用域也可嵌套

function f1(){
  let a = 1
  function f2(){
    let a = 2
    console.log(a)
  }
  console.log(a)  // 1
  a = 3
  f2()  // 2
}
f1()

作用域规则

  • 如果多个作用域有同名变量 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()

下面代码中a 和 f3 组成了闭包

let a = 2
function f3(){
  console.log(a)
}

如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包

形式参数

形式参数的意思就是非实际参数

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 hi(){ console.log('hi') }
hi()

没写 return,所以返回值是 undefined

function hi(){ return console.log('hi') }
hi()

返回值为 console.log('hi') 的值,即 undefined

  • 函数执行完了后才会返回
  • 只有函数有返回值
  • 1+2 返回值为 3
  • 1+2 值为 3

调用栈

  1. 什么是调用栈
  • JS 引擎在调用一个函数前
  • 需要把函数所在的环境 push 到一个数组里
  • 这个数组叫做调用栈
  • 等函数执行完了,就会把环境弹(pop)出来
  • 然后 return 到之前的环境,继续执行后续代码
  1. 举例
console.log(1)
console.log('1+2的结果为' + add(1,2))
console.log(2)

image.png

  1. 递归函数

阶乘

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

先递进,再回归

image.png

  1. 调用栈最长有多少
function computeMaxCallStackSize() {
  try {
    return 1 + computeMaxCallStackSize();
  } catch (e) {
    // 报错说明 stack overflow 了
    return 1;
  }
}
  • Chrome 12578
  • Firefox 26773
  • Node 12536
  1. 爆栈:如果调用栈中压入的帧过多,程序就会崩溃

函数提升

  1. 什么是函数提升 function fn(){}

不管你把具名函数声明在哪里,它都会跑到第一行

  1. 什么不是函数提升 let fn = function(){}

这是赋值,右边的匿名函数声明不会提升

arguments 和 this

每个函数都有arguments 和 this,除了箭头函数

arguments是伪数组

function fn(){
  console.log(arguments)
  console.log(this)
}

image.png

image.png

  • 如何传 this 目前可以用 fn.call(xxx, 1,2,3) 传 this 和 arguments

image.png

如果传的 xxx 不是对象,js会自动转化成对象(JS 的糟粕)

如何禁止自动转化为对象

function fn(){
  'use strict'    //  加上这句
  console.log(arguments)
  console.log(this)
}

this 是隐藏参数

arguments 是普通参数

JS 在每个函数里加了 this 用 this 获取那个对象

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

person.sayHi()
// 相当于
person.sayHi(person)

然后 person 被传给 this 了(person 是个地址)

这样,每个函数都能用 this 获取一个未知对象的引用了

person.sayHi()会隐式地把 person 作为 this 传给 sayHi

两种调用方法

  1. 小白调用法 person.sayHi() 会自动把 person 传到函数里,作为 this

  2. 大师调用法 person.sayHi.call(person) 需要自己手动把 person 传到函数里,作为 this

应该学习哪种? 学习大师调用法,因为小白调用法你早就会了

例1:

function add(x,y){
  return x+y
}

没有用到 this add.call(undefined, 1,2) // 3

为什么要多写一个 undefined?因为第一个参数要作为 this,但是代码里没有用 this,所以只能用 undefined 占位,其实用 null 也可以

例2:

Array.prototype.forEach2 = function(fn){
  for(let i=0;i<this.length;i++){
    fn(this[i], i, this)
  }
}
  • this 是什么?由于大家使用 forEach2 的时候总是会用 arr.forEach2,所以 arr 就被自动传给 forEach2 了

  • this 一定是数组吗?不一定,比如 Array.prototype.forEach2.call({0:'a',1:'b'})

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

绑定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

立即执行函数

只有JS有这玩意,现在用的少

原理

ES 5 时代,为了得到局部变量,必须引入一个函数,但是这个函数如果有名字,就得不偿失,于是这个函数必须是匿名函数,声明匿名函数,然后立即加个 () 执行它。但是 JS 标准认为这种语法不合法,所以 JS 程序员寻求各种办法。最终发现,只要在匿名函数前面加个运算符即可。

!、~、()、+、- 都可以

但是这里面有些运算符会往上走,所以方方推荐永远用 ! 来解决