认识函数

什么是函数呢,新手最早接触到的函数有:
- alert()函数
- 浏览器弹出一个窗口
- prompt 函数
- 在浏览器弹出并接收用户输入
- console.log
- 在控制台打印内容
- String/Number/Boolean 等

当我们在谈函数时,到底在说什么
- 函数其实就是某段代码的封装,这段代码会帮助我们完成某一个功能
- 默认情况下 javascript 引擎或者浏览器会给我们提供一些已经实现好的函数
- 我们也可以自己编写属于自己的函数

函数使用步骤

函数的使用包含两个步骤
- 声明函数 — 封装 独立的功能
- 调用函数 — 享受 封装的成果

声明函数,在 JavaScript 也可以称为定义函数
- 声明函数的过程是对某些功能的封装过程
- 在开发中我们会根据自己的需求定义很多的函数

调用函数,也可以称为函数调用
- 调用函数时让已存在的函数为我们所用
- 这些函数可以是自己刚刚封装号的某个功能函数
- 也可以调用默认提供或者其他第三方库中定义好的函数

函数的作用
- 在开发中,使用函数可以提高编写的效率以及代码的复用

声明函数和调用函数

声明函数使用 function 关键字:这种写法称为函数的定义

注意
- 函数名的命名规则和变量名的命名规则是一致的
- 函数要尽量见名知意(并且函数通常是一些行为(action),所以使用动词会更多一些)
- 函数定义完后里面的代码不会执行,函数必须要调用才会执行

 // 声明一个函数
    function sayHello () {
      console.log('hello')
      console.log('how do you do!')
    }
    // 调用函数
    sayHello()
    // 函数可以在任何时候,进行多次调用
    sayHello()

函数的参数

函数的参数的使用
- 函数,把 具有独立功能的代码块组织起来为一个小模块,在需要的时候调用
- 函数的参数,增强函数的通用性,针对 相同的数据处理逻辑,能够适应更多的数据
- 在函数内部,把参数当作变量使用,进行需要的数据处理
- 函数调用时,按照函数定义的参数顺序,把希望在函数内部处理的数据,通过参数传递

形参和实参
- 形参(parameter):定义函数时,小括号中的参数,是用来接收参数的,在函数内部可以作为变量使用
- 实参(argument):调用函数时,小括号内的参数,用来把数据传递到函数内部

// name,age, height 称之为函数的参数(形参,形式参数)
function printInfo (name, age, height) {
  console.log(`my name is ${name}`)
  console.log(`age is ${age}`)
  console.log(`height is ${height}`)
}

//'rin', 19, 188 也称之为函数的参数(实参,实际参数)
printInfo('rin', 19, 188)
printInfo('kobe', 49, 1.95)

函数的返回值

函数一般可以接收参数,同时也会返回一些数据
- 使用 return 关键字返回结果
- 一旦在函数中使用 return ,那么当前函数会终止
- 如果函数中没有 return 语句,那么函数默认的返回值是 undefined
- 函数中有 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined

function sum (num1, num2) {
  var result = num1 + num2
  // 定义具体的返回值
  return result
}
var total = sun(20, 30)
console.log('total:', total);

定义一个数字格式化函数

var playCount1 = 5123 // 5123
var playCount2 = 51234563 // 5120万
var playCount3 = 7951234563 // 79亿

// 封装一个工具 对数字格式化
function formatCount (count) {
  var result = 0
  if (count >= 10_0000_0000) { // 超过多少的时候转换
	result = Math.floor(count / 1_0000_0000) + '亿'
  } else if (count >= 10_0000) {
	result = Math.floor(count / 1_0000) + '万'
  } else {
	result = count
  }
  return result
}

console.log(formatCount(playCount1))
console.log(formatCount(playCount2))
console.log(formatCount(playCount3));

arguments 参数

事实上函数有一个特别的对象:arguments 对象
- 默认情况下,arguments 对象是所有(非箭头)函数都可用的局部变量
- 该对象中存放着所有的调用者传入的参数,从 0 位置开始,依次存放
- arguments 变量的类型是一个 object 类型(array-like),不是一个数组,但是和数组的用法看起来很像
- 如果调用者传入的参数多于函数接收的参数,可以通过 arguments 去获取所有的参数

function foo (name, age) {
  console.log('传入的参数:', name, age)

  // 函数中都存在一个局部变量 arguments
  console.log(arguments)
  // arguments 是一个对象
  console.log(typeof arguments)
  // 对象内部包含了所有传入的参数
  /* console.log(arguments[0])
  console.log(arguments[1]) */
  // 对 arguments 来进行遍历
  for (var i = 0; i < arguments.length; i++) {
	console.log(arguments[i])
  }
}

function sum () {
  var total = 0
  for (var i = 0; i < arguments.length; i++) {
	total += arguments[i]
  }
  return total
}

foo('rin', 17, 1.68, '东京')
console.log(sum(12, 34, 57, 87));

函数递归

函数中调用函数

在开发中,函数内部是可以调用另一个函数的

既然函数中可以调用另一个函数,那么函数是否可以调用自己呢?
- 当然可以
- 但是函数调用自己必须要有结束条件,否则产生无限调用,导致程序错误

函数的递归

事实上,函数调用自己还有一个专门的名称,叫做递归(Recursion)

递归是一种重要的编程思想
- 将一个复杂的任务 ,转化成可以重复执行的普通任务

例子:实现一个自己的幂函数 pow
- 我们可以先用 for 循环实现

递归实现思路
- 这是因为在数学上:
- 那么对函数的调用我们也可以进行划分

// 递归实现(必须有一个结束条件)
function pow (x, n) {
  // 在某一个条件下,让他不在继续调用
  if (n === 1) return x
  return x * pow(x, n - 1)
}
console.log(pow(2, 3))
console.log(pow(3, 3));

递归实现斐波那契数列

// 数列:1 1 2 3 5 8 13 21 34 55 ... x

// 位置:1 2 3 4 5 6  7  8  9 10 ... n

/*  
 function fibonacci (n) {
   if (n === 1 || n === 2) return 1
   return fibonacci(n - 1) + fibonacci(n - 2)
 }
  */

// 2.斐波那契for循环实现
function fibonacci (n) {
  if (n === 1 || n === 2) return 1
  var n1 = 1
  var n2 = 1
  var result = 0

  for (var i = 3; i <= n; i++) {
	result = n1 + n2 // 2 3 5 8 13 21
	n1 = n2 // 1 2 3 5 8
	n2 = result // 2 3 5 8 13 
  }
  return result
}

console.log(fibonacci(5))
console.log(fibonacci(10));

局部变量和外部变量

在 JavaScript(ES5之前)中没有块级作用域的概念,但是函数可以定义自己的作用域
- 作用域(Scope)表示一些标识符的作用有效范围(所以也有被翻译为有效范围的)
- 函数的作用域表示在函数内部定义的变量,只能在函数内部访问到

  // message 在哪一个范围内可以被使用,称之为 message 的作用域(scope)
  // 全局变量:全局作用域
  var message = 'hello world'
  if (true) {
    console.log(message)
  }

  function foo () {
    console.log('在foo中访问', message)
  }

  foo()

  // ES5之前是没有块级作用域的(var定义的变量时没有块级作用域的)
  {
    var count = 100
    console.log('在代码块中访问count:', count)
  }
  console.log('在代码块外面也能访问count:', count)
  
  // for 循环的代码块也是没有自己的作用域的
  for (var i = 0; i < 3; i++) {
    var foo = 'foo'
  }
  console.log('for循环的代码块也能访问:', foo)
  console.log('for循环的外面访问i:', i) // 3

  // ES5之前函数代码块是会形成自己的作用域的
  // 意味着函数内部的变量外面是无法访问的
  function test () {
    var bar = 'bar'
  }
  test()
  // console.log('test函数外面访问bar:', bar);

  function sayHello () {
    var nickname = 'kobe'
    console.log('sayHello内部访问nickname:', nickname)
    
    function hi () {
      console.log('hi function~')
      console.log('在hi函数访问nickname:', nickname)
    }
    hi()
  }
  sayHello()
  // console.log('sayHello外面访问nickname:',nickname);

外部变量和局部变量
- 定义在函数内部的变量,被称之为局部变量(Local Variables)
- 定义在函数外部的变量,称之为外部变量(Outer Variables)

什么是全局变量
- 在函数之外声明的变量(在script中声明的),称之为全局变量
- 全局变量在任何函数中都是可见的
- 通过 var 声明的全局变量会在 window 对象上添加一个属性

函数中,访问变量的顺序是怎样的呢?
- 优先访问自己函数中的变量,没有时找外部函数或者全局的变量,如果还没有就会找到 windowwindow 上也没有就会报错

函数表达式(Function Expressions)

在 JavaScript中,函数并不是一种语法结构,而是一种特殊的值。
- 前面定义函数的方式,我们称之为函数的声明(Function Declaration)

// 函数声明(声明语句)
function foo(){console.log('foo函数')}

还有另一种写法是函数表达式

var bar = function(){
	console.log('bar函数')
}

注意,function 关键字后面没有函数名
- 函数表达式允许省略函数名

无论函数是如何创建的,函数都是一个值(这个值的类型是一个对象)

在JavaScript中,我们可以将函数看做为头等公民

函数声明 vs 函数表达式

首先,语法不同
- 函数声明:在代码中声明为单独的语句的函数
- 函数表达式:在一个表达式中或另一个语法结构中创建的函数

其次,JavaScript 创建的时机是不同的
- 函数表达式是在代码执行到达时被创建,并且仅在那一刻起可用
- 函数声明被定义之前,它就可以被调用
- 这是内部算法的原因
- 当 JavaScript 准备运行脚本的时候,首先会在脚本中寻找全局函数声明,并且创建这些函数

开发中如何选择
- 当我们需要声明一个函数时,首先考虑函数声明语法
- 它能够为代码提供更多的灵活性,因为我们可以在声明这些函数之前调用它

JavaScript头等函数

头等函数(first-class function)是指在程序设计语言中,函数被当做头等公民
- 这意味着,函数可以被作为别的函数的参数、函数的返回值、赋值给变量或存储在数据结构中
- 也有人主张应该包括支持匿名函数

通常我们对头等公民的编程方式,称之为函数式编程
- JavaScript 就是符合函数式编程的语言,这也是 JavaScript 的一大特点

比如:函数可以在变量和变量之间相互进行赋值

// 函数作为一等公民
// 1.函数库被赋值给变量(函数表达式写法)
var foo1 = function () {
  console.log('foo1函数被执行')
}
// foo1()

// 2.让函数在变量直接来回传递
// var foo2 = foo1
// foo2()

// 3.函数可以接受另一个函数
/*  function bar (fn) {
   console.log('fn:', fn)
   fn()
 }
 bar(foo1) */

// 4.函数可以作为另一个函数的返回值
/* function sayHello (name) {
  function hi () {
	console.log('hi:' + name)
  }
  return hi
}

var fn = sayHello('kobe') // 科里化
fn() */

// 5.将函数存储在另一个数据结构中
var obj = {
  name: 'why',
  eating: function () {
	console.log('eating')
  }
}

obj.eating()

function bar1 () {
  console.log('bar1函数执行')
}

function bar2 () {
  console.log('bar2函数执行')
}

function bar3 () {
  console.log('bar3函数执行')
}

// 事件总线的封装
var fns = [bar1, bar2, bar3]

// 函数式编程:使用函数来作为头等公民使用函数,这种编程方式就是函数式编程

回调函数(Callback Function)

既然函数可以作为一个值互相赋值,那么也可以传递给另一个函数

foo 这种函数外面也可以称为高阶函数(Higher-order function)

高阶函数必须至少满足两个条件之一
- 接受一个或多个函数作为输入
- 输出一个函数

匿名(anonymous)函数的理解
- 在传入一个函数时,我们没有指定这个函数的名词或者通过函数表达式来指定函数对应的变量,那么这个函数就称之为匿名函数

/* function request (url, callback) {
  console.log('根据URL向服务器发送网络请求')
  console.log('需要花费比较长的时间才能拿到对应的结果')
  var list = ['javascript权威', '现代javascript', 'javascript高程']
  callback(list)
}

function handleResult (res) {
  console.log('在handleResult中拿到结果', res)
}

request('https://www.baidu.com/abc/getSearchResult', handleResult) */

function request (url, callback) {
  console.log('根据URL向服务器发送网络请求')
  console.log('需要花费比较长的时间才能拿到对应的结果')
  var list = ['javascript权威', '现代javascript', 'javascript高程']
  callback(list)
}

// 传入的函数是没有名字的,匿名函数
request('url', function (res) {
  console.log('在handleResult中拿到结果', res)
})

立即执行函数

什么是立即执行函数
- 专业名字:Immediately-Invoked Function ExpressionIIFE 立即调用函数表达式)
- 表达的含义是一个函数定义完后被立即执行
- 第一部分是定义了一个匿名函数,这个函数有自己独立的作用域
- 第二部分是后面的 (),表示这个函数被执行了

这个东西有什么用
- 会创建一个独立的执行上下文环境,可以避免外界访问或者修改内部的变量,也避免了对内部变量的修改

<button class="btn">按钮1</button>
<button class="btn">按钮2</button>
<button class="btn">按钮3</button>
<button class="btn">按钮4</button>

<script>
// 获取一个按钮监听点击
// 1.拿到 html 元素
/*  var btnEl = document.querySelector('.btn')
 console.log(btnEl)
 // 2.监听对应按钮的点击
 btnEl.onclick = function () {
   console.log('点击了按钮1')
 } */
 
// 获取所有的按钮监听点击
var btnEls = document.querySelectorAll('.btn')
for (var i = 0; i < btnEls.length; i++) {
  var btn = btnEls[i];
  (function (m) {
	btn.onclick = function () {
	  console.log(`按钮${m + 1}发生了点击`)
	}
  })(i)
}

// 上面代码的另一种写法
var btnEls = document.querySelectorAll('.btn')
for (var i = 0; i < btnEls.length; i++) {
  (function (m) {
	btnEls[m].onclick = function () {
	  console.log(`按钮${m + 1}发生了点击`)
	}
  })(i)
}

立即执行函数的其他写法

立即执行函数必须是一个表达式(整体),不能是函数声明
- 下面这种写法会出错,因为是一个函数声明,不是一个函数表达式
- 当圆括号出现在匿名函数的末尾想要调用函数时,它会默认将函数当作是函数声明

当圆括号包裹函数时,它会默认将函数作为表达式去解析,而不是函数声明

下面也是一个函数表达式,所以可以执行

代码风格

// 1.foo和小括号直接不需要空格
// 2.多个参数,之后加上一个空格
// 3.小括号和大括号中间直接有一个空格
// 4.{}和其他函数定义在同一行
function foo(m, n) {

}

foo(20, 30)

if (true) {

} else {

}

for (var i = 0; i < 5; i++) {

}

// 模板字符串可以换行
var message = `hhhhhhhhhhhhhhhh,
				  红红火火恍恍惚惚`

Q.E.D.