JS函数

141 阅读10分钟

函数是对象

定义一个函数

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

注意一下特殊情况:

let a = function fn(x,y){return x+y}

fn(1,2)    //此时输入fn(1,2),会报错

可以用a(1,2)来调用fn  

image.png

  • 匿名函数

上面的具名函数,去掉函数名就是匿名函数

let a = function(x, y){   //注意,这里a只是容纳了右边函数的地址
    return x+y 
}

也叫函数表达式

  • 箭头函数
let f1 = x => x*x              //let f1 = (输入参数) => (输出参数)

let f2 = (x,y) => x+y          // 也可以有两个参数,注意!圆括号不能省
let f3 = (x,y) => {return x+y} // 如果加了花括号,return不能省

例如:
let fn = (x,y) => {
console.log('hi')
return x+y           //当里面有>=两句话的时候,就要加return和花括号
}
let f4 = (x,y) => {name: x, age: y}

//如果要返回对象的话,这样写会出错,需要加个圆括号
因为花括号在js里面优先被当作"块的起始",比如{foo:1}这只是个代码块(标签label),不是对象

let f4 = (x,y) => ({name:x, age: y}) 
//这样就没问题了
  • 用构造函数
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()
  • 结果:
  1. fn 保存了匿名函数的地址
  2. 这个地址被复制给了 fn2
  3. fn2() 调用了匿名函数
  4. fn 和 fn2 都是匿名函数的引用而已(如果你存了一个东西的地址,那你就是那个东西的引用而已)
  5. 真正的函数既不是 fn 也不是 fn2,而是() => console.log('hi')

函数的要素

每个函数都有这些东西:

  1. 调用时机
  2. 作用域
  3. 闭包
  4. 形式参数
  5. 返回值
  6. 调用栈
  7. 函数提升
  8. arguments(除了箭头函数)
  9. this(除了箭头函数)

调用时机

例1:

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

问:打印出多少?

答:不知,因为没有调用代码

例2:

let a = 1
function fn(){
  setTimeout(()=>{
    console.log(a)
  },0)
}

fn()
a = 2

问:打印出多少?2

例3:

let i = 0                //这里let写在外面
for(i = 0; i<6; i++){    //for里面是i
  setTimeout(()=>{      //setTimeout意思是过一会儿执行,先执行for,for的优先级比较高
    console.log(i)
  },0)
}

问打印出多少? 答:不是 0、1、2、3、4、5 ,而是 6 个 6

for(let i = 0; i<6; i++){     //for和let一起用有奇效。。
  setTimeout(()=>{
    console.log(i)
  },0)
}

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

因为 JS 在 for 和 let 一起用的时候会加东西 每次循环会多创建一个 i

例4:

let arr = [1,2,3,4,5,6]
arr.forEach(function(x,y){    
    console.log(y)
  })

用forEach一样能打印出0、1、2、3、4、5

作用域

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

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

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

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

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

  1. 在顶级作用域声明的变量是全局变量
  2. window 的属性是全局变量
  3. 其他都是局部变量
  • 局部变量
function fn(){
                   //如果在一个函数里面声明(let/const)一个变量,
   let a = 1       那么这个变量就是局部变量,因为它只在作用域里面生效,
                   出了作用域就不生效了。所以是局部的
}
fn()
console.log(a)    // a 还是不存在
  • 全局变量
let b = 1        //在顶级作用域声明了b,所以b是全局变量

function f1(){
window.c = 2     //在window上挂c,也是全局变量,可以随便放,
                   不一定要放外面,放函数里面也可以
let a = 1        //在函数里声明a,只能在函数的作用域里面生效
}

f1()

function f2(){
console.log(c)
}

f2()   //输入函数调用c

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

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

  console.log(a)
  a = 3   //没啥用,迷惑老子
  f2()
}
f1()  //输出1,2
  • 作用域规则
  1. 如果多个作用域有同名变量 a那么查找 a 的声明时,就向上最近的作用域,简称「就近原则」

  2. 查找 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() //输出1、22

闭包

image.png

形式参数

  • 形式参数的意思就是非实际参数
function add(x, y){
  return x+y
}
f(1,2)

// 其中 x 和 y 就是形参,因为并不是实际的参数 add(1,2)

// 调用 add 时,1 和 2 是实际参数,会被赋值给 x y

赋值的意思就是根据内存图的概念,把参数在stack里面所对应的值全部复制过去

image.png

  • 形参可认为是变量声明
// 上面代码近似等价于下面代码
function add(){
  var x = arguments[0]   //声明一个x等于参数的第0个
  var y = arguments[1]   //声明一个y等于参数的第1个
  return x+y
}
  • 形参可多可少 形参只是给参数取名字
function add(x){
    return x+ arguments[0]   //只声明一个变量也可以,
                               arguments[0]意思是参数的第0个
}
add(1,2)
//输出结果3
function add(x,y,z){   //第3个参数就算不用也没关系
    return x+y         
}
f(1,2)         //就算有3个形式参数,就调用2个实际参数也可以

返回值

  • 每个函数都有返回值 返回值就是return后面的那个值,return就是返回的意思
function hi(){ console.log('hi') }

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

hi()

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

image.png

  1. 函数执行完了后才会返回
  2. 只有函数有返回值

调用栈

什么是调用栈?

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

举例:

console.log(1)
console.log('1+2的结果为' + add(1,2))
console.log(2)

image.png

递归函数

  • 阶乘
function f(n){
  return n !== 1 ? n* f(n-1) : 1
}
  • 理解递归 image.png 先递进再回归

  • 递归函数的调用栈 递归函数的调用栈很长

画出阶乘(4) 的调用栈 image.png

  • 调用栈最长有多少 在浏览器里面输入下列代码调用函数可以得知:
function computeMaxCallStackSize() {
  try {
    return 1 + computeMaxCallStackSize();
  } catch (e) {
    // 报错说明 stack overflow 了
    return 1;
  }
}
  1. Chrome 12578
  2. Firefox 26773
  3. Node 12536
  • 爆栈 如果调用栈中压入的帧过多,程序就会崩溃

函数提升

  • 什么是函数提升? function fn(){}

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

例如:

add(1,2)
function add(1,2){
   return x+y
}  
//函数跑到第一行,在add(1,2)前面,输出3

例2:

let add = 1
function add(){}
//会报错,因为函数会跑到第一行
变成:
function add(){}
let add = 1
//但是add已经被函数声明了,let就不能再声明,这样会报错

例3:

var add
function add(){}

//输入add
返回的是函数:add(){}
因为var只是声明了一个add,并没有赋值,函数赋值了所以函数比较厉害。。

但是如果改成var add = 1
输入add,返回的是1
  • 什么不是函数提升 let fn = function(){}

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

arguments 和 this

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

代码:

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

打印出this和arguments可知:

arguments是个伪数组因为它没有数组的共有属性

如果不给任何条件的话this默认指向window

  • 如何传 arguments
function fn(){
  console.log(arguments)
  }
fn(1,2,3)

调用 fn 即可传 arguments

fn(1,2,3) 那么 arguments 就是 [1,2,3] 伪数组

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

xxx是传到this的,后面的1,2,3都存在arguments里面

而且 xxx 会被自动转化成对象(JS 的糟粕)

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

image.png

undefined的话,this默认指向window image.png

除非在定义函数的时候加上'use strict'

例如:

function fn(){
  'use strict'
  console.log(this)
  }
fn(1)  //输出1

this 是隐藏参数,arguments 是普通参数(this 是参数(此结论参考知乎大佬的))

假设没有 this

代码:

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

分析: 我们可以用直接保存了对象地址的变量(person)获取 'name', 我们把这种办法简称为引用。

let person = {name: 'xxx'},person是变量,它保存了=号后面这个对象的地址 代码

  • 问题1:
let sayHi = function(){
  console.log(`你好,我叫` + person.name)
}
let person = {
  name: 'xxx',
  'sayHi': sayHi
}

分析:

  1. person 如果改名,sayHi 函数就挂了
  2. sayHi 函数甚至有可能在另一个文件里面
  3. 所以我们不希望 sayHi 函数里出现 person 引用
  • 问题2:
class Person{
  constructor(name){
    this.name = name 
    // 这里的 this 是 new 强制指定的
  }
  sayHi(){
    console.log(???)
  }
}

分析: 这里只有类,还没创建对象,故不可能获取对象的引用。

那么如何拿到对象的 name ? 一种土办法,用参数

对象:
let person = {
  name: 'frank',
  sayHi(p){
    console.log(`你好,我叫` + p.name)    //先用形参p来顶替未知的对象
  }
}
person.sayHi(person)   //这里的实际参数person对应的是函数里的形参p,
                       通过实际参数来调用变量person,通过变量person引用对象

类:
class Person{
  constructor(name){ this.name = name }
  sayHi(p){
    console.log(`你好,我叫` + p.name)
  }
}
person.sayHi(person)  //同理,类也一样

Python也是用这种方法:

class Person:
  def __init__(self, name): # 构造函数
    self.name = name

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

特点:

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

JS 在每个函数里加了 this

  • 用 this 获取那个对象 image.png
  1. person.sayHi()相当于person.sayHi(person)
  2. 然后 person 被传给 this 了(person 是个地址)
  3. 这样,每个函数都能用 this 获取一个未知对象的引用了 person.sayHi()会隐式地把 person 作为 this 传给 sayHi(方便 sayHi 获取 person 对应的对象 )
  • this的总结:
  1. 我们想让函数获取对象的引用,但是并不想通过变量名做到
  2. Python 通过额外的 self 参数做到
  3. JS 通过额外的 this 做到:person.sayHi() 会把 person 自动传给 sayHi, sayHi 可以通过 this 引用 person 其他:
  • 注意 person.sayHi 和 person.sayHi() 的区别
  • 注意 person.sayHi() 的断句 (person.sayHi)()

两种调用

person.sayHi()
person.sayHi(person)   //这种调用写法式错的

省略形式反而对了,完整形式反而是错的? 所以提供了两种调用方法。

  • 小白调用法 person.sayHi()

会自动把 person 传到函数里,作为 this

  • 大师调用法(推荐使用) 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 的两种使用方法

  • 隐式传递
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
)
里面的 this 就是外面的 this
console.log(this) // window
let fn = () => console.log(this) 
fn() // window
就算你加 call 都没有
fn.call({name:'frank'}) // window

立即执行函数(用的少)

  • 原理
  • ES 5 时代,为了得到局部变量,必须引入一个函数
  • 但是这个函数如果有名字,就得不偿失
  • 于是这个函数必须是匿名函数
  • 声明匿名函数,然后立即加个 () 执行它
  • 但是 JS 标准认为这种语法不合法
  • 所以 JS 程序员寻求各种办法
  • 最终发现,只要在匿名函数前面加个运算符即可
  • !、~、()、+、- 都可以
  • 但是这里面有些运算符会往上走
  • 所以推荐永远用 ! 来解决