JS-9:JS函数

181 阅读6分钟

1 定义函数

1.1 具名函数

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

1.2 匿名函数

具名函数不写函数名就是匿名函数,也叫函数表达式,等号右边叫函数表达式:

let a = function(形参1,形参2){
  语句
  return 返回值
}

1.3 箭头函数

let f1 = x => x*x //一个参数,一个语句,可省略圆括号、花括号、return;
let f2 = (x,y) => x+y  //两个参数,要加圆括号;
let f3 = (x,y) => {
  console.log('xxx')
  return x+y
}    //多个语句要加花括号,而且return不能省略;
let f4 = (x,y) => ({name:x,age:y})    //返回对象,需要在花括号外面加圆括号;

1.4 用构造函数

let f = new Function('x','y','return x+y') 基本没人用,但是通过上面代码可以知道,所有函数都是Function构造出来的,包括Object、Array、Function也是。

2 fn和fn()

let fn = () => console.log('hi')
let fn2 = fn
fn2() 

image.png

3 函数调用时机

  • 例1:
let a = 1
function fn(){
  console.log(a)
}
fn()
a = 2    //打印1
  • 例2:
let a = 1
function fn(){
  console.log(a)
}
a = 2    //打印2
fn()
  • 例3:
let a = 1
function fn(){
  setTimeout(()=>{
    console.log(a)
  },0)  
}
fn()
a = 2    //打印2
  • 例4:
let i = 1
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)  
}    //打印6个6
  • 例5:
for(let i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)  
}    //打印0,1,2,3,4,5
  • 例6:
let i 
for(i = 0; i<6; i++){
const x = i 
setTimeout(()=>{ 
console.log(x) 
}) 
}
  • 例7:
let i = 0
for(i = 0; i<6; i++){
!function(i){  
setTimeout(()=>{
  console.log(i)
},0)
}(i)
}
  • 例8:
let i = 0
for(i = 0; i<6; i++){
setTimeout((i)=>{
  console.log(i)
},0,i)
}

4 作用域

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

4.1 全局变量、局部变量

  • 在顶级作用域声明的变量是全局变量;

  • window的属性是全局变量;

  • 其他的都是局部变量;

4.2 作用域嵌套

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

4.3 作用域规则

  • 如果多个作用域有同名变量a,那么:
  1. 查找a的声明时,就向上取最近的作用域,简称:就近原则;

  2. 查找a的过程与函数执行无关,与函数在哪里声明有关;

  3. 但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
let a = 1
function fn1(){
  function fn2(){
    console.log(a)
  }
  function fn3(){
    let a=3
    fn2()  //fn3调用fn2时,因为fn2中没有定义a,所以往声明fn2函数的外面找,即在fn1里面找变量 a ,此时 a=2。而不是在调用 fn2 的地方,即 fn3 里面找。
  }
  let a=2
  return fn3
}
let fn = fn1()
fn()  // 2
  • 函数fn的作用域 image.png

函数声明式在等于号右边的话,则函数fn的作用域也只在等号右边,出了这个范围,函数fn就不存在,要想使用函数,只能用a(),没有等于号,函数就是全局作用域,在哪都能用;

  • 和函数执行没有关系的作用域叫静态作用域(词法作用域);反之叫动态作用域;在确定a是哪个变量的时候不看执行,只有在确定a的值时才要看执行顺序;

image.png

5 闭包

如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。如上面代码的部分:

image.png

6 形式参数

  • 形参只是给参数取名字,形参可多可少; image.png

7 返回值

  • 只有函数有返回值,每个函数有且只有一个返回值:

  • 函数执行完了后才会返回,函数没有return,则返回undefined;

image.png

8 调用栈

8.1 定义

image.png

8.2 递归函数的调用栈

  • 调用栈最长有多少:
function computeMaxCallStackSize(){
  try {
    return 1 + computeMaxCallStackSize();
  } catch (e) {
    //  报错说明 stack overflow 了
    return 1;
  }
}

8.3 爆栈

  • 如果调用栈中压入的帧过多,程序就会崩溃;

9 函数提升

image.png

10 arguments和this

  • 每个函数都有,除了箭头函数;

  • arguments就是包含所有参数的伪数组;

  • 每次调用函数时,都会对应产生一个 arguments ;

  • 尽量不要改 arguments 内的元素;

  • 在 new fn() 调用中,fn 里的 this 指向新生成的对象,这是 new 决定的;

  • 在 fn() 调用中,this 默认指向 window,这是浏览器决定的;

10.1 如何传arguments和this

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

image.png

10.2 this解决什么问题

在写一个函数时,需要得到一个对象,但我们不知道那个对象叫什么名字,因为那个对象可能还没有出生,怎么在不知道一个对象的名字的情况下,拿到一个对象的应用(保存了对象地址的变量);

image.png

10.3 this如何解决

image.png

  • person.sayHi()会隐式地把 person 作为 this 传给 sayHi ,sayHi 可以通过 this 引用 person;

10.4 JS的两种调用法

  • 隐式传递: person.sayHi():会自动把 person 传到函数里,作为 this ; obj.child.fn(1):等价于 obj.child.fn.call(obj.child,1)

  • 显式传递: fn.call(undefined,1,2);

fn.apply(undefined,[1,2])

  • call与apply的区别:apply的参数以数组的形式传递,其余的都相同;
function add(x,y){
  return x+y
}
add.call(undefined, 1,2)
// call的第一个参数要作为this,但是函数中没有用到this,所以用undefined占位,也可以用null,后面的参数则是arguments.
Array.prototype.forEach2 = function(fn){
  for(let i=0;i<this.length;i++){
    fn(this[i],i,this)
  }
}
let array = [1,2,4]
array.prototype.forEach2.call(array,(item)=>console.log(item))  //this就是array;

Array.prototype.forEach2.call({0:'a';1:'b',length:1},(item)=>console.log(item))  //this不一定是数组;

10.5 用.bind绑定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'}),打印出{name: 'frank'} undefined undefined
  • .bind还可以绑定其他参数:
let f3 = f1.bind({name:'frank'},'hi')
f3()  //等价于 f1.call({name:'frank'},hi),打印出{name: 'frank'} hi undefined
  • fn.bind(x,y,z)不会执行fn,而是会返回一个新的函数。新的函数执行时,会调用fn,调用形式为 fn.call(x,y,z),其中 x 是 this,y 和 z 是其它参数。

11 箭头函数

  • 没有arguments 和 this,箭头函数中,this就是一个普通的字符串,和变量a 没有区别;

  • 里面的 this 就是外面的this

console.log(this)  //window
let fn = ()=> console.log(this)
fn()  //window
  • 用 call 也没用

  • 箭头函数返回对象

let f = x=>({name:x})
f('xx')  // 得到:{name: 'xx'}
let f2 = x=>{name:x}
f2('frank')  // 得到:undefined

image.png

要想返回对象,花括号外边要加圆括号,不加的话高亮部分会被看作是代码块;

fn.call({name:'frank'})  //打印出来的还是window;

12 立即执行函数

  • 为什么:ES5时期,为了得到局部变量,原理:

image.png

image.png

  • return一个undefined,undefined前面有+ 号,就变成NaN,

image.png

  • 匿名函数加():JS 代码中唯一需要加分号的地方,其他地方不需要,()会向上看,log返回值为undefined,undefined(function())就会报错;而 !不会向上看,只会向后看,所以,如果有人用()做立即执行函数,最好在函数前面加分号,最好只用 ! 。

image.png

  • ES6之后,要想得到局部变量,直接在 {} 里面用 let 声明即可;