函数的定义方式
具名函数
- function 函数名(形式参数1,形式参数2) {语句 return 返回值}
function add(x,y) {return x+y}
匿名函数
let f1 = function(x,y){return x+y}- let 变量名 = function (形式参数1,形式参数2) {语句 return 返回值}
- 和具名函数相比去掉了函数名,但最起码得有一个变量名存放这个对象
- 因为匿名函数没有名字,所以必须给这个函数表达式赋值给变量。变量储存了这个匿名函数的地址而已本质上和函数名是一样的
用构造函数
- let 变量名 = new Function('形式参数1','形式参数2',语句,'return 返回值')
- 使用这个方法可以了解内存
箭头函数
- let 变量名 = (形式参数) => {语句 return 返回值}
- 因为箭头函数没有名字,所以必须把这个箭头函数赋值给变量f1。变量储存了这个箭头函数的地址而已
- 形式参数在只有一个时,可以省略圆括号()
- 只有返回值时,{}和return可以省。但是注意如果此时返回值是个对象,{}省了是会有bug的!在js中花括号优先被当成块,那在对象外面加个()就可以了
注意
- 但是如果
let f1 = function fn (x,y){return x+y}fn作用域十分有限,在外面只能通过f1调用 - 函数名在函数内部总是可见得,函数作用域在等于号右边,如果只是普通具名函数那么则是全局函数
var foo = function bar() {
bar(); // 正常运行
}
bar(); // 出错:ReferenceError
函数自身fn VS 函数调用(执行)fn()
- fn只是表示把变量fn保存的地址指向的那个函数长啥样弄出来我看看,变量名就是地址的别称。
let fn = () => {console.log('hi')}
fn // () => {console.log('hi')}
fn() // hi
fn(1) // hi
fn2 = fn
fn2 // () => {console.log('hi')}
fn2() // hi
- fn保存了匿名函数的地址,fn是保存函数地址的内存的地址的别称(js内存图)
- 这个地址被复制给了fn2
- fn2()调用了匿名函数
- fn和fn2都是匿名函数的引用而已
- 真正的函数既不是fn也不是fn2,而是() => {console.log('hi')}
- 理解这个是理解递归的关键
函数的要素
调用时机
- 函数定义是不会调用函数的,只有函数名()才会调用,结合上一篇文章的map函数,在参数函数时是不会调用的
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a = 2
// 2
//过一会打印出a,过一会就是指要等所有代码执行完
- 重点就在于setTimeout的执行时机,是等所有代码执行完之后的0秒
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
//打印出6个6
//循环到每一个i都是过一会打印出i。过一会就是指要把所有循环走完。那么当循环走完,i就为6,所以要打印出6个6
- 上面代码只有一个全局变量i
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
//打印出0、1、2、3、4、5
//为了迎合新人的思想特地做出了更新
//在JS中如果for和let一起用就会出现每次循环多创建一个i的情况
- 上面代码实际上等于下面代码,for(let i=0;i<5;i++)这句话的圆括号之间,有一个隐藏的作用域
- for循环的小括号是一个作用域,而花括号又是一个作用域,而小括号的作用域是包裹住了大括号作用域的,let i = 隐藏作用域中的i//相当于加了这一行在里面花括号,之后每次for循环i都是新的但值是和外面一层作用域值是一样的
- 隐藏作用域负责i值得改变,内部作用域负责创建新的i并且接受隐藏作用域i的值
for(let i = 0; i<6; i++){
let i = 隐藏作用域中的i //相当于加了这一行
setTimeout(()=>{
console.log(i)
},0)
}
变量作用域
- 函数都会创建一个局部变量作用域
- ES6中let可以用块级作用域
- 在顶级作用域声明的变量是全局变量
- window的属性是全局变量,即使在函数了定义window属性,也是全局变量,解释了Object等函数可以到处用
- 全局变量在任何作用域存在,局部变量只在局部作用域里存在生存
- 如果多个作用域有同名变量a,那么查找a的声明时,就向上看取最近的作用域(自己身处的那个作用域当然是最近的,没有再向上找最近的作用域),简称「就近原则」。查找a的声明的过程(也相当于作用域的范围)与函数执行无关,但a的值与函数执行有关
- 一个作用域与函数的执行无关叫静态作用域(词法作用域)(js就是词法)
- 一个作用域与函数的执行有关叫动态作用域
闭包
- 如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包
- 如上图的a和f3组成了闭包
函数参数
- 形式参数的意思就是非实际参数。形参可认为是变量声明。形参可多可少
- 传值传地址,如果实际参数是地址那么传递的只是对象的地址,与c语言联想
function add(x, y){return x+y}
//其中X和y就是形参,因为并不是实际的参数
add(1,2)
//调用add时,1和2是实际参数,会被赋值给xy
相当于声明了两个变量等待赋值,等价于
function add(){
var X = arguments[0]
var y = arguments[1]
return x+y
- js代码很随意,无所谓你有几个你有多少,多出来就相当于多声明了几个变量,回答了上一篇博客问题,函数不知道你需要的参数是函数
- 没有返回值的函数返回undefined
函数栈
- 具体看匿名程序媛写的很棒
- 当我们调用函数时,浏览器就会进入函数,,问题就在于进入了之后得出结果,那么把结果放在哪里呢?栈的作用就来了,每次调用函数时就把执行上下文放入栈中,使得进入函数算出的结果出函数时可以知道放在哪。
递归
function f(n){
return n !== 1 ? n* f(n-1) : 1
}
- 怎么样理解递归呢?重点就是函数和函数调用不一样,很多人不理解为什么一个函数没有写完就可以调用它,实际上等你调用它不就写完了吗,f只是函数而已,里面的函数调用只有在调用最外层函数才会执行,这个时候不就写完了吗
- 递归里加个条件来停止递归,每次递归条件就符合一点直到它完全符合不在调用递归
- 超出最多压栈次数,就会爆栈,程序就会崩溃
- 一个调用栈最多可以压栈几次:调用栈的长度是有限的
函数声明
- 不管你把具名函数声明在哪里,他都会跑到第一行去,
add(1,2)
function add(x,y){return x+y}
//3
//因为具名函数其实会跑到第一行,所以add函数以及被定义了。所以可以执行add(1,2)
- 高于变量提升
let add=1
function add(x,y){return x+y}
//报错,因为具名函数其实在第一行,以及把add作为函数名了,之后let想要声明add是不行的,add已经被用了。
- 上面代码会出错,出错意味着代码就不会执行,在解释阶段就直接停止了
- 用var会覆盖声明,尽量不要用,后面的会覆盖前面的
add(1,2)
let fn = function (x,y){return x+y}
- let 声明会提升到块顶部
- 从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)
- 如果你在 TDZ 内使用该变量,JS 就会报错
- 具体是因为let把声明分为了三部分,创建,初始化,使用,let只是把创建提升了,在TDZ时还只是undefined,这个时候调用当然会出错
this和arguments
arguments
- arguments只出现与函数当中,是函数的一个属性
- arguments是一个包含该函数所有普通参数的伪数组
- 每次调用函数时,都会对应产生一个 arguments
- 我们应该尽量不对 arguments 内的元素进行修改,修改 arguments 会让代码变得令人疑惑
- 可以通过对arguments对参数进行控制
- 每次调用函数就会为函数开辟内存空间,执行完之后抹去内存
this
场景1:全局环境下的 this
- 函数在浏览器全局环境中被简单调用,非严格模式下this指向window; 在use strict指明严格模式的情况下就是undefined
场景2:上下文对象调用中的 this
- this指向最后调用它的对象
call
- 使用fn.call(x,1,2,3),那么第一个参数x会传给this(而且这个参数x会被自动转化为对象),后面的参数会传给arguments。一般情况下this是场景2
- call其实是函数原型的一个方法,浏览器会把第一个参数会传给this,后面的参数会传给arguments,可以通过函数的call属性调用函数
this背景
- 我们想让方法中有一个变量,这个变量指向一个将要被创建的父对象,父对象中有个属性是我们需要。
let person = {
name: 'frank',
sayHi(){
console.log(`你好,我叫` + person.name)
}
}
//因为我们只是定义了这个函数sayHi,但是却没有调用它,所以在定义函数satHi时可以引用还没完全声明好的person父对象。因为要是调用了这个函数,那么肯定父对象person也早就声明好了。
//一个函数在调用时里面的东西必须声明好,但定义时有没有声明好不重要
- 上面代码存在问题,父对象改名,函数就挂了,而且使用类方法时没有指定父对象,该怎么办
- 我们需要一个办法来让父对象的方法获得父对象里的属性
- 方法一:在父对象的方法上加入参数,参数代表父对象(python用)
- 方法二:使用this(js用)
那么this就相当于sayhi的一个隐藏参数,把父对象地址偷偷传给this
相当于在sayhi函数内部,偷偷声明了一个this,然后把父对象地址偷偷传给this
在实际调用中就会发生下面的情况:
我们调用person. sayHi()
相当于我们调用person. sayHi(this)函数
相当于person. sayHi (person)
在函数内部
var this = arguments[0]
然后person被传给参数 this 了(person的地址)
这样,每个函数都能用this 获取父对象的引用了
call调用
- person.sayHi.call(xxx)
- 第一个参数xxx作为形式参数this的实际参数
- 如果没有就传undefined
bind绑定
- 用.bind绑定函数的this
- 用.bind绑定函数的其他参数
function f1(p1, p2){
console.log(this, p1, p2)
}
let f2 = f1.bind({name:'frank'})
// 那么f2就是f1绑定了this之后的新函数
f2() // 等价于f1.call({name:'frank'})
let f3 = f1.bind({name:'frank'}, 'hi')
f3() // f1.call({name:'frank'}, hi)
箭头函数
- 箭头函数没有自己的this。箭头函数里面的this就是该函数外面的this,就算你加call都没用。
fn.call({name:'frank'}) // window - 箭头函数没有arguments
立即执行函数
-
在ES6以前没有没有块级作用域和let
-
只能通过var和函数搭配声明局部变量
-
问题就在于如果定义一个具名函数不还得需要一个全局变量吗,我们想要代码到各个源代码可以执行,这就需要匿名函数
-
并且立即执行,这样里面代码才会生效,在js代码中直接下面代码是错误的
function(){
var a =2
console.log(a)
} ()
!function(){
var a =2
console.log(a)
} ()
- 解决办法就是在函数前加点符号,也可以给函数加个括号,这样会有隐患看下面代码
//不推荐()
console.log('hi') //如果立即执行函数前面这句代码
(function (){
var a =2
console.log(a)
} ()) //报错:console.log(...) is not a function
//因为js中回车没意义,所以把下面的立即执行函数接到前面去了,所以相当于
undefined(function (){
var a =2
console.log(a)
} ()) //把undefined当一个函数来执行了,哪来的undefined这个函数,所以报错
//补救措施
console.log('hi'); //在他俩之间加分号。注意这是js语言中唯一两句代码之间需要加分号的情况!!!
(function (){
var a =2
console.log(a)
} ())
this
JavaScript 提供了call、apply、bind这三个方法,来切换/固定this的指向。
- 函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。
- apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。
- bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。
var d = new Date();
d.getTime()
var print = d.getTime;
print() // 报错blind解决