函数是什么
函数本身就是一个比较特别的对象!!!
定义一个函数
具名函数
function 函数名(){
语句
return 返回值
}
匿名函数
let a = function () {
语句
return 返回值
}
- 可以发现,具名函数和匿名函数的区别就是 function 后面是否直接跟了一个函数名。除了立即执行函数,匿名函数必须同时声明一个变量来存储这个函数的地址。
function () {语句return 返回值}称作“函数表达式”。- 匿名函数与具名函数的怪异结合声明函数:
let a = function fn(){}这中声明函数的方式最终结果还是一个匿名函数的声明,只能a()来调用函数,而不能fn()调用函数,因为fn这个变量的作用域只在let a = function fn(){}等号右边那一部分,其他地方无法调用这个fn。
箭头函数
-
箭头函数的完整结构。()内传入参数;{}中的内容,包括{}是函数体。
let fn = (x,y)=>{ console.log(x+y) return x+y } -
箭头函数只有1个参数的时候可以省略 (),没有参数或2个及2个以上的参数()不可省略。
let fn = x =>{ console.log(x) return x } -
箭头函数函数体中只有一句 return语句 ,则可以省略 {}及return
let fn = (x,y)=>x+y // return x+y -
在3.的基础上,如果函数体直接返回的是一个对象则会出现歧义。
let fn = x => { name:x } // 本意是直接返回{ 'name': x }这个对象 // 但是JS对{}符号首先会认为它是函数块,name:x这个结构会被认为是label // 正确写法,给对象添加一个()号 let fn = x =>({ name:x })
构造函数
-
构造函数是最规范的写法,不过由于使用麻烦,所以基本没人使用。好处就是能一眼看出来,所有函数都是由 Function 构造函数构造而成的。
let fn = new Function('x','y','return x+y') // 'x' 'y'是形式参数,'return x+y'是函数体。
函数本身与函数调用
let fn = ()=>{console.log('hello')}
fn2 = fn
fn()
fn2()
- 直接一个 函数名 指的是函数本身,而 函数名() 则是函数调用
- 上面 fn 是 fn函数本身,fn() 是调用函数fn。
- fn其实只是一个保存了
()=>{console.log('hello')}这个函数对象的地址的变量。 fn2 = fn只是把 fn 保存的地址复制给了 fn2- fn、fn2都只是
()=>{console.log('hello')}这个匿名函数的引用,真正的函数其实只是一个存储在Heap区的一个对象。
函数的要素一:调用时机
-
函数调用前,引用的变量是否已经声明?
-
函数调用时,变量的值是多少?
-
setTimeout的调用:
let a = 1 function fn(){ setTimeout(()=>{ console.log(a) },0) } fn() a = 2setTimeout的执行涉及到JS执行队列的问题,因为setTimeout中的函数的执行顺序是在整个代码块执行之后,所以实际上
console.log(a)是在a = 2之后执行的,所以最后输出的是2。 -
在for循环中的setTimeout的调用与变量的问题:
let a = 1 for (let i = 1 ; i < 6; i++ , a++) { setTimeout(() => { console.log('i:' + i) console.log('a:' + a) }, 0) } // i:1 2 3 4 5 // a:6 6 6 6 6在上一节我们知道了setTimeout中的函数是在主程序执行后才执行。
let声明的变量i,在for循环中会在内部不断地创建块级作用域,来存储每个i。所以每个setTimeout的回调函数在引用变量i的时候,会就近使用块级作用域的i。
而当setTimeout的回调函数在引用变量a的时候,因为变量a是全局变量,而在就近的作用域中并没有另一个变量名为a的变量,所以直接在全局变量中引用a。
函数的要素二:作用域
什么是作用域?
-
作用域:限制变量名、函数名等名字的的使用范围就是作用域。
-
每个函数调用的时候都会默认创建一个作用域。作用域的范围就是函数的{}包裹的范围。
function fn(){ let a = 1 } fn() console.log(a) // 报错,a未被定义
作用域、全局变量及局部变量
- 全局变量:哪里都能访问的变量;
- 局部变量:有访问范围限制的变量。
- 顶级作用声明的变量是全局变量
- window的属性是全局变量,为什么Object、parseInt可以随时使用,因为他们是window的一个属性。
- 其他都是局部变量
函数的嵌套与作用域的嵌套
function f1() { // f1作用域开始
let a = 1
function f2() { // f2作用域开始
let a = 2
console.log(a)
} // f2作用域结束
console.log(a)
a = 3
f2()
} // f1作用域结束
f1()
- 可以见到f2作用域是嵌套在f1作用域中的,和它们函数的嵌套关系相同。
作用域的规则
当多个作用域中同时存在同名变量a
- 在查找a时,根据作用域的嵌套,从内而外向上取最近的作用域中的变量a。简称:就近原则。
- JS中的作用域是静态作用域,查找a的过程与函数执行无关。
- 而 a 的值与函数执行有关。
根据这个规则,下面这个代码输出结果。
function f1() {
let a = 1
function f2() {
let a = 2
function f3() {
console.log(a) // 22
}
a = 22
f3() // 注意调用f3的时候a的值已经重新赋值22
}
console.log(a) // 1
a = 100
f2()
}
f1()
// 输出结果与顺序:
// 1
// 22
函数的要素三:闭包
什么是闭包?
- 如果一个函数用到了其外部的变量,那么这个函数加这个外部的变量就称作闭包。
函数的要素四:形式参数
什么是形式参数?
-
形式参数的意思就是非实际参数,可以理解为一个容器,当函数调用的时候实际参数会被赋值给这些形式参数。
function add(x,y){ return x+y } add(1,2)上面的函数,其中 x y 就是形式参数。当调用add(1,2)时,1 和 2是实际参数,这两个实际参数会分别赋值给 x y。
-
形参的数量不代表实际参数的数量,形参可多可少,只是给参数取个名字。
函数的要素五:返回值
-
每个函数都有返回值,如果没有指定返回的内容,JS函数默然返回一个undefined(
return undefined) -
函数执行完后才会返回
-
函数 return 后,后面的函数内容不再继续执行
function fn(){ return 'hello' console.log('world') } fn() // 没有输出'world' -
返回值和值不是同一个东西。
return 1+2函数返回3;1+21+2的值是3。
函数的要素六:调用栈
什么是调用栈?
- JS引擎在调用一个函数之前,会把函数执行完后应该返回的地址存储(push)在一个数组中(压栈)。这个数组就是调用栈。
- 当函数执行完成后就会根据调用栈从后往前把地址pop出来(弹栈),当将调用栈所有地址都弹出后,即回到主程序,继续执行后续的代码。
- 调用栈有最大限度,Chrome、Node:12500左右,Firefox:26500左右。当程序运行超出调用栈的最大限度,称作爆栈或栈溢出。
- 调用栈最常关联的就是递归函数,一般也只有递归函数才可能出现爆栈的现象。所以在使用递归函数的时候,要确保函数都递归终点,否则必定爆栈。
调用栈的存储与弹出的过程
function fn(x){
return x!==1 x*f(x):1
}
// 用调用栈分析fn(4)的实现过程
// 压栈:1.此时在主程序环境中。调用栈push当前 fn(3) 所处地址后,进入f(3)
// 弹栈:3.f(3)得到返回值 6 后,根据调用栈地址返回之前f(3)所处位置。此时已经返回到主程序的环境中,继续执行后续程序。
fn(3){
return 3 * f(2)
// 压栈:2.调用栈push f(2) 所处地址后,进入f(2)
// 弹栈:2.f(2)得到返回值 2 后,根据调用栈地址返回之前f(2)所处位置
f(2){
return 2 * f(1)
// 压栈:3.调用栈push f(1)所处地址后,进入f(1)
// 弹栈:1.f(1)得到返回值 1 后,根据调用栈地址返回之前f(1)所处位置
f(1){
return 1
}
}
}
// *上面的代码非正常代码,只是为了方便描述
函数的要素七:具名函数的提升
-
具名函数会有函数提升的现象
// fn()前,已经声明了 fn 但是还没给函数赋值 fn() function fn(){ console.log('hello') } // 输出'hello'正常逻辑来说 fn() 应该报错,但还是具名函数声明的函数会有函数提升的现象,即把声明函数这行代码提升到主程序的最前方。但是提升的只是声明,赋值并没有提升。
-
匿名函数没有函数提升现象
fn() // 报错 let fn = function(){ console.log('hello') }
函数的要素八:arguments和this
arguments
-
arguments是函数调用时所有参数的一个伪数组集合,也是一个对象。如:arguments[0]就是该函数第一个参数。
function fn(){ return arguments[0]+arguments[1] // 这里也可以用形参代替 } fn('love','peace') // 返回 'lovepeace' // 即 arguments[0] = 'love' arguments[1] = 'peace' -
传arguments:在函数调用的时候直接通过传入实参即可以传入arguments。
function fn(){ console.log(arguments) } fn(1,2,3,4) -
arguments是(除了箭头函数)函数的一个普通参数。arguments对象只能在函数中使用。
-
一般我们在不能确定参数数量的时候使用arguments对象。
this
-
this的含义:当前执行代码的环境对象。this可以认为是隐藏在函数中一个属性。
-
为什么我们需要this?因为我们需要在对象没声明之前拿到对象!!
-
如果没有this,我们需要怎么拿到没声明的对象?
class Person{ constructor(name){ this.name = name // 这个this的指向是new语句的效果 } sayHi(self){ // 没有this的时候,我们通过参数来拿到没有声明的对象 console.log('我是'+self.name) } } let p1 = new Person('jack') p1.sayHi(p1) // 这里我们把声明的p1当作实参传入函数 -
一些语言如Python就通过上面的方法获取那个未声明的对象,而JS则在每个函数中添加了this来获取那个未声明的对象。
let person = { name:'frank', sayHi(){console.log('我是'+this.name)} // this被赋值person的地址 } person.sayHi() // 相当于 person.sayHi(person){this = person} // 只是用于解释,这个写法不规范
涉及this的函数调用
-
小白调用法:
- xxx.sayHi() 这个调用方法会自动把 xxx对象 传到函数中,作为this。
-
大师调用法:call
- xxx.sayHi.call(xxx,其它参数),这个方法的第一个参数就是手动指定的this。可以是自己xxx,也可以是其他对象。
- 如果函数用不到this咋办?一般传入 undefined 或 null占位。
-
call apply bind的使用与区别
- call 和 apply的作用类似,但是使用形式稍有不同。call 和 apply的第一个参数是this的指向,后面参数是调用函数的所传入的实参。
- bind则可以绑定this指向的对象,以及绑定其他参数。bind的第一个参数是this的指向,后面参数是绑定的调用函数的实参,绑定实参后,调用函数时不需要传入已经绑定的实参。
// call 与 apply function add(x,y,z){return x+y-z} add.call(undefined,1,2,3) // 因为用不上this,所以使用undefined占位 add.apply(undefined,[1,2,3]) // bind function fn(p1,p2){ console.log(this,p1,p2) } // 给fn2赋值了绑定了 this指向为{'name':'jack'} ,第一个实参为10的 fn函数 fn2 = fn.bind({'name':'jack'},10) // fn2调用的时候只需要传入第3个参数就可以了 fn2() // {name: "jack"} 10 undefined 因为没有传第三个参数所以undefined fn2(12) // {name: "jack"} 10 12 // fn2(12)相当于 fn.call({'name':'jack'},10,12)
fn()中的this指向哪里?
- 根据上面的内容:obj.sayHi()中的this指向的很显然是obj这个对象。
- 那么一个最简单的函数 fn() 中的this指向哪里呢?
- 答案:window。
- 在顶级作用域声明的函数都挂载到了window这个对象上,所以fn() 即 window.fn() 那么,fn()中的this自然指向window。
箭头函数与this
-
箭头函数的this永远指向的是其外面this的指向,因为箭头函数没有this。
-
即使使用call调用箭头函数也没有用,因为call只是改变了this的指向,没有this就根本无法改变指向。
let fn1 = { show(){ console.log(this) } } let fn2 = { show:()=>{ console.log(this) } } fn1.show() // fn1 fn2.show() // window fn1.show.call({'name':'frank'}) // {'name':'frank'} fn2.show.call({'name':'frank'}) // window
立即执行函数
-
ES6以前的产物,为了生成一个局部变量,必须使用一个函数
-
如果使用变量接收了这个函数就造成了资源的浪费
-
所以我们声明一个没有名字的而且马上执行的函数来实现这个效果。
-
在匿名函数前加个运算符都能实现立即执行函数,如:!、+、-、()、~
// 立即执行函数 (function(){var a = 1})() // ()实现的有个弊端 console.log() (function(){var a = 1})() // 上面的代码会出现 undefined(function(){var a = 1})()的结果,然后就是报错 // 如果非要用()则必须在立即执行函数前加 ; -
推荐永远使用 ! 来实现立即执行函数
!function(){var a = 1}()