JavaScript函数(上)

301 阅读10分钟

函数的定义

函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。

函数的本质

函数的本质是一种特殊的对象

函数的声明

  1. 方法一:Function Expression函数表达式。这种写法将一个anonymous function匿名函数赋值给变量a,左边是赋值,右边是函数表达式。
let a = function(x,y){
    return x + y
}

采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。下面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错。

let print = function x(){
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function
  1. 方法二:function命令。使用function命令来新建一个函数,带有函数名,也叫做named function具名函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
function 函数名(parameters形式参数1,parameters形式参数2){
    语句
    return 返回值
}
  1. 方法三:Function构造函数。这种声明函数的方式非常不直观,几乎无人使用,但是能让你知道函数是谁构造的,所有的函数都是function构造的,包括Object,Array,Function。下面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。
let add = new Function(
  'x',
  'y',
  'return x + y'
);

// 等同于
function add(x, y) {
  return x + y;
}
  1. 方法四:Arrow Function箭头函数
    • 方式1:其中x为输入的参数,x*x为函数体。箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像下面的,只包含一个表达式,连{ ... }return都省略掉了。例如,let f1 = x => x*x
    • 方式2:如果参数不是一个,就需要用括号()括起来, 例如let f2 = (x,y) => x+y
    • 方式3:还有一种可以包含多条语句,这时候就需要使用{ ... }return注意,如果加了{...}就不能省略return,因为JS会判断不出哪个语句需要被返回 例如,let f3 = (x,y) => {return x + y}
    • 方式4:如果要返回一个对象,就要注意,如果是单个表达式,会出错因为和函数体的{ ... }或者说block块级区域 代码的语法有冲突,所以要加一个()包裹起来例如,let f4 = (x,y) =>({name:x, age:y})

函数的重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。 下面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2

函数本身vs函数调用

fn函数本身

下面的代码不会有任何结果,因为函数fn只是被定义,还未被调用

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

fn()函数调用

下面的代码会被执行,因为()才是调用函数

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

进阶知识

变量fn保存了匿名函数的地址,然后这个地址被复制到了fn2,fn2()调用了匿名函数,真正的函数既不是fn,也不是fn2,他们都只是匿名函数的引用而已

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

函数调用时机

实例1

问题: 请问打印出多少?

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

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

实例2

问题: 请问打印出多少?

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

答案: 打印出1,因为调用了函数fn

实例3

问题: 请问打印出多少?

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

答案: 打印出2,因为fn()a = 2之后才被执行

实例4

问题: 请问打印出多少?

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

答案: 打印出1,因为fn()a = 2之前被执行

实例5

问题: 请问打印出多少?

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

答案: 打印出2,因为setTimout()a = 2 完成之后才执行

实例6:for循环中诡异的SetTimout

问题: 请问打印出多少?

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

答案: 66,因为for循环的时候,每次执行一次i,触发setTimout()函数,等for循环结束了,i6的时候,6setTimeout()才最终执行

实例7:let解决诡异的SetTimeout

问题: 请问打印出多少?

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

答案: 0,1,2,3,4,5 因为for循环体中加入setTimeout定时器,for语句每次循环时,定时器都处于等待状态,for循环执行一次,定时器就得到一次机会,直到循环执行完毕,定时器才开始执行,循环几次定时器就执行几次,而let只能声明一次,所以每次都是新的变量,也可以理解为每次循环都被被复制了一个i,加上循环的i,一共有6i

原理分析

  1. 规则:同步优先、异步靠边、回调垫底
  2. js的执行机制js是单线程环境,从上到下、依次执行,即 同步执行;在实例6中,for循环是同步代码,setTimeout是异步代码。js在执行代码的过程中,碰到同步代码会依次执行,碰到异步代码就会将其放入任务队列中进行等待,当同步代码执行完毕后再开始执行异步代码,即 异步执行。
  3. js作用域问题:当同步代码执行完毕后,开始执行异步的setTimeout代码,执行setTimeout时需要从当前作用域内寻找一个变量i,此时for循环已执行完毕,当前 i=6,所以执行setTimeout时输出为6,任务队列中的剩余5setTimeout也依次执行,输出为6
  4. 实例6中的letfor循环的外部。而在实例7中,let只在代码块内才有效,let只能声明一次。变量i是用let声明的,当前的i只在本轮循环中有效,每次循环的i 其实都是一个新的变量,所以setTimeout定时器里面的i,其实是不同的变量,即最后输出0-5。(若每次循环的变量i都是重新声明的,如何知道前一个循环的值?这是因为JavaScript 引擎内部会记住前一个循环值)

实例8:实例7的let的替代解决方法

  • 方法1-闭包。通过闭包,将i的变量驻留在内存中,当输出j的时候,引用的是外部函数的i,所以执行setTimout时已经确定了里面的输出了
for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    })(i);
}
  • 方法2-拆分setTimeout结构。将setTimeout的定义和调用分别放到不同部分
function timer(i) {
    setTimeout( console.log( i ), i*1000 );
}
for (var i=1; i<=5;i++) {
    timer(i);
}
  • 方法3-设置setTimeout第三个参数。由于每次传入的参数是从for循环里面取到的值,所以会依次输出15
for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
     }, i*1000, i );
}

scope作用域

定义

scope作用域指的是变量存在的范围,每个函数都会默认创建一个作用域

分类

JavaScript有三种作用域:

  1. 全局作用域,变量在整个程序中一直存在,所有地方都可以读取
  2. 函数作用域,变量只在函数内部存在了
  3. 块级block作用域, 和let搭配,外层作用域无法获取到内层作用域,非常安全明了。即使外层和内层都使用相同变量名,也都互不干扰

全局变量vs局部变量

  • 在全局作用域,声明的是全局变量, 例如window的属性就是全局变量
  • 其他的都为局部变量

实例1

function fn(){
    let a = 1
}
fn()
console.log(a) // 报错,因为全局变量a不存在,函数fn 访问不到局部变量a

原因: 因为变量a只存在作用域中,外部访问不到

作用域的规则

  • 函数可以嵌套,作用域也可以嵌套
  • 如果有多个作用域有同名变量a,那么查找a的声明时,就向上取最近的作用域,简称'就近原则'
  • 查找a的过程与函数执行无关,a的值与函数执行有关

实例2

function f1(){
    let a = 1;
    
    function f2(){
        let a  = 2
        console.log(a)
    }
    console.log(a)
    a = 3
    f2()
}
f1()
// 1 这个a的值是f1()里面的a
// 2 这个a的值是f2()里面的a

实例3

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 这个a的值是f1()里面的a
// 22 这个a的值是f3()执行f2里面的a

closure闭包

定义

函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。

原理

在实例3中,就存在了closure闭包, 如果一个函数用到了外部的变量,那在这里,外部变量af3函数就组成了闭包

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

作用

闭包常常用来间接访问一个变量。通过暴露一个函数,让别人可以间接访问这个变量。 另一个就是让这些变量的值始终保持在内存中,不会在外部函数被调用后被自动清除。

优缺点

优点:避免全局变量的污染;能够读取函数内部的变量;在内存中维护一个变量

缺点:

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  2. 闭包会在父函数外部,改变父函数内部变量的值。解决方法是, 不要随便改变父函数内部变量的值。

应用场景:实现封装;模拟面向对象,不同的对象(类的实例)拥有独立的成员及状态,互不干涉

parameters形式参数

定义

形式参数就是函数定义的时候规定的参数,简单的说,就是给参数取名字

原理

在下面的代码中,函数声明时候规定的参数xy就是形式参数,并不是实际的参数。当输入实际参数12的时候,两个参数会被复制给xy,然后xy进行操作。

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

形式参数也可以认为是变量声明,下面的代码和上面的代码是等价的。JS 中,形式参数可多可少,不影响运行

function add(){
    var x = arguments[0]
    var y = arguments[1]
    return x + y
}

返回值

定义

返回值指函数执行完毕后返回的值

原理

返回值的类型包括:Number数字 String字符串 Boolean布尔值 function函数 Ojbect对象, 默认返回 undefined只有在函数执行了之后才会有返回值,没有执行,就没有返回值。在下面的例子中,函数没有写return,所以返回值是undefined

function hi(){
    console.log('hi')
}
hi()  // undefined

在下面这个例子中,也是返回undefined, 因为console.log('hi')的值是undefined,console.log()方法只有打印值,没有返回值,返回的只有undefined

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

更多信息

「每日一题」JS 中的闭包是什么?

(阮一峰)块级作用域

网道 JS函数

关于for循环中使用setTimeout的四种解决方案