函数的定义与要素
函数定义
-
具名函数
function 函数名(形式参数1,形式参数2){
语句
return 返回值
} -
匿名函数
去掉函数名就是匿名函数
声明一个变量把它赋值为函数表达式,a只是容纳了它的地址
let a = function(x,y){return x+y}
声明变量=函数表达式 -
箭头函数
let f1 = x => x乘x //x是输入右边是输出
let f2 = (x,y)=>x*y // 多个参数
let f3 = (x,y)=>{console.log('hi')
return x+y}如果有花括号需要写return
接受一个值x 返回一个对象
花括号优先被认成块作用域,必须加个小括号告诉他是一个对象
let f4 = x =>({name:x}) -
构造函数(所有函数都是Function构造出来的包括Object/Array/Function)
let f = new Function('x','y','return x + y') - fn和fn()
fn是函数自身,
fn()调用函数
函数要素
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(箭头函数除外)
- this(箭头函数除外)
- call指定this
- 立即执行函数
调用时机
时机不同 结果不同
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a=2
打印出2
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
打印出6个6
为什么明明setTimeout的时间设置为了0,还是打印出6个6?
在 js 中,定时器是一个异步任务。 JS 会优先执行当前的同步任务,在同步代码执行结束后才会去处理任务队列中的异步任务。这样,即便定时器设置了0,也是在忙完手头的事情之后才会去读取任务队列。
JS 有同步任务和异步任务。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指:不进入主线程、而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
for(let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}JS在for和let一起用的时候加了一些东西所以
打印出 0 1 2 3 4,
每次循环会多创建一个i
作用域
每个函数都会创建一个作用域
function fn(){
let a = 1
}
console.log(a)//a 不存在,访问不到作用域里面的a
let 作用域就在它最近的花括号里
全局变量:
- 在顶级作用域声明的变量是全局变量
- 挂到window上的属性是全局变量
比如Object可以直接用,也可以自己写window.c=xxx。
局部变量:
- 函数里声明lety或const就是一个局部变量,出了作用域不生效。
作用域规则:
如果多个作用域有同名的变量a
- 查找a的声明时,向上取最近的作用域(就近原则)
- 查找a的过程与函数执行无关,但a的值与函数的执行有关。
静态作用域:
- 函数的作用域在函数定义时就已经确定了,跟函数执行没有关系。
- 函数的作用域在运行时才确定,跟函数执行有关系。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
JavaScript采用静态作用域的方式访问变量,所以打印出1.
JavaScript之静态作用域 - 简书 (jianshu.com)
闭包:JS函数会就近寻找最近的变量
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()
如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。上面f2中的a和f3组成闭包。
形式参数
形式参数的意思就是非实际参数
let a = 1
let b =2
function add(x, y){
x+1
y+2
return x+y
}
其中x和y就是形参,因为并不是实际的参数
add(a,b)
调用 add 时,a 和 b 是实际参数,会被赋值给 x y
- JS传参
传值:在上面的代码中,a和b在赋值给x、y的时候,会被复制一份传进去。函数中对形参的修改并不会影响到实际参数本身。函数执行完后还是a =1 b =2。
传址:但是传递的参数时对象时。此时传进去的就是对象的地址,对对象做出的修改会影响到对象本身。
其实无所谓传值和传址。就是把stack内存中的东西复制过去而已。
形参可认为是变量声明。上面代码近似等价于下面代码。
function add(){
var x=arguments[0]//参数的第0个
var y=arguments[1]
return x+y
}
返回值
- 每个函数都有返回值
function hi(){console.log('hi') }
hi()
没写return,所以返回值是undefined
function hi(){ return console.log('hi')}
hi()
返回值为console.log('hi')的值,即undefined - 函数执行完了后才会返回
- 只有函数有返回值
错误:1+2返回值为3
正确:1+2值为3
调用栈
什么是调用栈
- JS引擎在调用一个函数前
- 需要把函数所在的环境push到一个数组里
- 这个数组叫做调用栈
- 等函数执行完了,就会把环境弹(pop)出来
- 然后return到之前的环境,继续执行后续代码
作用:
- 由于我每次进入一个函数我都得记下来我等会回到哪,所以把回到的地址写到栈里边。如果进入一个函数之后还要进入一个函数,就再把这个地址放到栈里边,函数执行完了就弹栈 。
举例
function first() {
console.log('first')
function second() {
console.log("second")
}
second();
third();
}
function third() {
console.log("third")
}
// 调用 first
first();
调用的是函数 first, 此时 first 进入函数栈,接着在 first 中调用函数 second,second 入栈,当 second 执行完毕后,second 出栈,third 入栈,接着 third 执行完出栈,执行 first 其他代码,直至 first 执行完,函数栈清空。
递归函数:
阶乘
function f(n){
return n !==1 ? n*f(n-1) : 1
}
f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2* (1)))
= 4 * (3 * (2))
= 4 * (6)
=24
先递进,后回归
- 递归的调用栈
1、进入f(4)并压栈
2、得到4 * f(3) 进入f(3)并压栈(把4也记录下来)
3、得到3 * f(2) 进入f(2)并压栈(把3也记录下来)
4、得到2* (1)进入f(1)并压栈(把2也记录下来)
5、得到1
6、弹栈2得到2
7、弹栈3得到6
8、弹栈4得到24
9、弹栈回到第一行 - 我们可以看到f(4)压栈4次
- 所以f(10000)压栈10000次
爆栈:如果调用栈中压入的帧过多,程序就会崩溃
栈的长度:
function computeMaxCallStackSize(){
try{
return 1+ computeMaxCallStackSize()
}catch(e){
//报错说明 stack overflow
return 1
}
}
函数提升
定义:
不管你把具名函数function fn(){}声明在哪里,它都会跑到第一行。
这种不行
let fn = function(){}
这是赋值,右边的匿名函数声明不会提升
let不允许有一个函数add的时候再声明一个add,var可以:
let add = 1
function add(){}
报错
var add = 1
function add(){}
console.log(add)
此时add是1
arguments和this
每个函数都有,除了箭头函数
argements:
- 包含所有参数的伪数组
- 通过array.from 可以把它变为数组
- 调用fn即可传arguments
- fn(1,2)那么arguments就是【1,2】伪数组
打印出来看一下
function fn(){console.log(arguments)}
this
- 打印出来看一下
function fn(){console.log(this)} - 我们发现如果单纯打印this不给任何条件的情况下this默认指向window
如何传this和arguments
- 使用fn.call(1,2,3,4)传this和arguments
1是this(第一个参数) 2,3,4是arguments - xxx如果不是对象JS会自动把他转化成对象(JS糟粕)
- 在函数中加'use strick'即可制止
this要解决的问题: - 我们写一个函数的时候需要得到一个对象
- 但是不知道对象的名字,可能那个对象还没出生
- 怎样在不知道一个对象的名字情况下拿到对象的引用
let sayHi = function(){
console.log(person.name)
}
我在写这个函数之前我怎么能确定后面这个变量是person呢
let person = {
name:'frank',
'sayHi':sayHi
}
如果我person改名字了,sayHi函数就挂了
sayHi甚至可能在另一个文件中
所以我们不希望sayHi函数出现person的引用
class Person{
constructor(name){
this.name = name//这里this强制指定的
}
sayHi(){
console.log(???)
}
}
这里只有类,还没有创建对象,故不可能拿到对象的引用
JS是如何解决问题的
- JS在每个函数里加了this
- 在任何函数里面可以用this获取那个你目前还不知道名字的对象
let person = {
name:'frank',
sayHi(){
console.log(this.name)
这里的this就表示以后出现的那个对象
}
}
- person.sayHi()相当于person.sayHi(person)然后person地址被传给了this
- person.sayHi()会隐式地把person作为this传给sayHI,方便sayHi获取person对应的对象
函数的两种调用方法
- person.sayHi()会自动把person传到函数里作为this
- person.sayHi.call(person)手动把person传到函数里作为this(推荐)
call指定this
this写foreach
默认把array当作this
this就是一个参数而已,我传什么就是什么默认就是array
this的两种使用方法
- 隐式传递
fn(1,2)=== fn.call(undefined,1,2)
obj.child.fn(1) ===等价于obj.child.fn.call(obj,child,1) - 显示传递 call apply
fn.call(undefined,1,2)
fn.apply(undefined,[1,2])
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'})
其他参数
let f3 = f1.bind({name:'frank'},'hi')
f2()===f1.call({name:'frank'},'hi')
箭头函数
没有arguments和this
箭头函数里面的this就是外面的this加call也没用
- console.log(this) //this是window
- let a = ()=> console.log(this)
a() //还是window - a.call(1) //还是window
立即执行函数(没啥用)
- 以前只有var的时候 如果想声明一个局部变量必须在一个函数里声明才是局部(所以现在直接let就好了)
- 我就想要一个局部变量不想要全局函数
-
采用匿名函数并直接调用,会报错所以前面加个!