定义一个函数
JavaScript 函数是执行特定任务的代码块,会在某些代码调用它时被执行。其本身是一种对象。
一个 JavaScript 函数用function关键字定义,后面跟着函数名和圆括号。
具名函数
function 函数名(形式参数1, 形式参数2){
语句
return 返回值
}
例如:
function fn(x, y){
return x+y
}
匿名函数
具名函数去掉函数名就是匿名函数,如下所示:
let a = function(x, y){
return x+y
}
也叫函数表达式,(函数没有名字,变量a容纳了函数的地址)
箭头函数
箭头左边是输入的函数,右边是输出的函数
let f1 = x => x*x
let f2 = (x,y) => x+y // 圆括号不能省
let f3 = (x,y) => {return x+y} // 花括号不能省
let f4 = (x,y) => ({name:x, age: y}) //直接返回对象会出错,需要加个圆括号
构造函数
let f = new Function('x', 'y', 'return x+y')
很少有人用构造函数来定义一个函数,但是能让知道函数是谁构造的
所有函数都是 Function 构造出来的,包括 Object、Array、Function 也是
函数调用
函数在被调用时才会执行,函数自身是不会执行的。如下
let fn = () => console.log('hi')
let fn2 = fn
fn2()
想要fn2执行,第三行如果输入函数自身fn2,函数不会有任何结果,因为 fn 没有执行(被调用)。输入fn()才会打印出 hi。有圆括号才是调用。
- 对于上述代码需要明确的是:
fn保存了匿名函数的地址- 这个地址被复制给了
fn2 fn2()调用了匿名函数fn和fn2都是匿名函数的引用而已- 真正的函数既不是
fn也不是fn2
函数要素
每个函数都有这些要素
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(除了箭头函数)
- this(除了箭头函数)
调用时机
JavaScript - 函数的执行时机 - 掘金 (juejin.cn)
作用域
每个函数都会默认创建一个作用域,作用域是指变量可以生效的范围。
- 作用域可分为全局作用域和局部作用域,相应的变量也可分为全局变量和局部变量
- 在全局作用域中定义的变量可以在任何地方使用,在局部作用域中定义的变量只能在这个局部作用域内部使用。
- 在顶级作用域声明的变量是全局变量,window 的属性是全局变量,其他都是局部变量
例如:
function fn(){
let a = 1
}
fn()
console.log(a)
这里的 a 是局部变量,只作用于函数fn里面,console.log(a)是打印不出 a 的
let a = 1
function fn(){
console.log(a)
}
fn()
console.log(a)
这里的 a 是全局变量,可以在任何地方使用
注意:
- 即使window的属性写在某个函数里面也是全局变量
- Object / parseInt 可以直接用是因为他们在window上
函数可嵌套,作用域也可嵌套
例如:
function f1(){
let a = 1
function f2(){
let a = 2
console.log(a)
}
console.log(a)
a = 3
f2()
}
f1()
会打印出 1 、2
规则:
- 如果多个作用域有同名变量 a
- 那么查找 a 的声明时,就向上取最近的作用域,简称「就近原则」
- 查找 a 的过程与函数执行无关,但 a 的值与函数执行有关
注:和函数执行没有关系的作用域叫做静态作用域,也叫词法作用域
再例如:
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 、 22
闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
例如:
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()
函数 f3 和let a = 2中的 a 组成了闭包
形式参数
形式参数的意思就是非实际参数,可认为是变量声明
例如:
function add(x, y){
return x+y
}
add(1,2)
其中 x 和 y 就是形式参数,因为并不是实际的参数。调用 add 时,1 和 2 是实际参数,会被赋值给 x 和 y
上面代码近似等价于下面代码
function add(){
var x = arguments[0]
var y = arguments[1]
return x+y
}
add(1,2)
形式参数只是给参数取名字,可多可少
例如:
返回值
返回值是指函数执行完毕后返回的值。
- 每个函数都有返回值,这个返回值可以通过关键字
return进行设置 - 函数执行完了后才会返回,即只有执行了以后才会有返回值
- 只有函数有返回值
- 若未显式地设置函数的返回值,那函数会默认返回一个undefined
- 在函数中,一旦执行完成
return语句,那么整个函数就结束了,后续语句将不再执行
例如:
function hi(){ console.log('hi') }
hi()
没写 return,所以返回值是 undefined
function hi(){ return console.log('hi') }
hi()
返回值为 console.log('hi') 的值,console.log('hi') 的值是 undefined
调用栈
JS 引擎在调用一个函数前需要把函数所在的环境 push (推)到一个数组里,这个数组叫做调用栈。 等函数执行完了,就会把环境弹(pop)出来,然后 return 到之前的环境,继续执行后续代码。
递归函数的调用栈
例如写一个4的阶乘
function f(n){
return n !== 1 ? n* f(n-1) : 1
}
fn(4)
对于fn(4)我们可以理解为
f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24
先递进再回归,它的调用栈如下图所示(压4次,弹4次)
调用栈最多有多长?
- Chrome 12578
- Firefox 26773
- Node 12536
爆栈:如果调用栈中压入的帧过多,程序就会崩溃
函数提升
函数提升指不管把具名函数声明在哪里,它都会跑到第一行
例如:
(先使用,后声明。但是没有BUG)
注意:
let fn = function(){}
这是赋值,右边的匿名函数声明不会提升
arguments
arguments 是一个包含所有普通参数的伪数组。
- 每个函数都有,除了箭头函数
- 每次调用函数时,都会对应产生一个 arguments
- 我们应该尽量不对 arguments 内的元素进行修改,修改 arguments 会让代码变得令人疑惑
如何传 arguments
- 调用 fn 即可传 arguments
- fn(1,2,3) 那么 arguments 就是 [1,2,3] 伪数组
this
对于 this 可以理解为JS 通过 this 做到让函数获取对象的引用。谁调用函数,谁就是this。除了箭头函数,任何函数都有 this
关于 this 的规则:
- 如果不给任何的条件,那么 this 默认指向 window
- 如果传一个对象, this 就是这个对象
- 如果传的不是对象,JS会把传的东西封装成对象;如果加上
use strict。那么传的this是什么就是什么 - 如果传undefined, 那么 this 是 window
如何传 this:
- 用
fn.call()传 this
例如:
上面说过如果传的不是对象,JS会把传的东西封装成对象;但是如果加上use strict。那么传的this是什么就是什么
关于 call
目前可以用 fn.call(xxx, 1,2,3) 传 this 和 arguments,而且 xxx 会被自动转化成对象
(this 是第一个参数; arguments是后面的参数)
this 的调用
上面说过可以用call来传 this ,在传输时要注意需要自己手动把“参数”传到函数里,作为 this ;传什么,this 就是什么。一般不要空写,不建议使 this 自动把“参数”传到函数里。
例一:
let person = {
name: 'frank',
sayHi(){
console.log(`你好,我叫` + this.name)
}
}
person.sayHi.call(person)
person.sayHi.call(person)最好不要写成person.sayHi.call()。
例二:
例三:
为什么要多写一个 undefined ?
- 因为第一个参数要作为 this
- 但是代码里没有用 this
- 所以只能用 undefined 占位
- 其实用 null 也可以
this 的两种使用方法
- 隐式传递
fn(1,2)等价于fn.call(undefined, 1, 2)obj.child.fn(1)等价obj.child.fn.call(obj.child, 1)- (如果是对象的属性的函数调用,this是函数前面的这部分)
- 显示传递
fn.call(undefined, 1,2)fn.apply(undefined, [1,2])
绑定 this
- 使用
.bind可以让 this 不被改变
例如:
f2 就是 f1 绑定了 this 之后的新函数;
f2()等价于 f1.call({name:'frank'})
.bind还可以绑定其他参数
例如:
在这里
{name:'frank'}是隐藏参数(或者说this), 'hi'是p1
箭头函数的 this 和 arguments
箭头函数没有 arguments 和 this ,可以理解为新的语法里用箭头函数干掉了 arguments 和 this
- 箭头函数的this就是一个普通变量。里面的 this 就是外面的 this
- 箭头函数没有 arguments
this总结
(假设 fn 是一个普通函数, arrow 是一个箭头函数)
- 在 new fn() 调用中,fn 里的 this 指向新生成的对象,这是 new 决定的
- 在 fn() 调用中, this 默认指向 window,这是浏览器决定的
- 在 obj.fn() 调用中, this 默认指向 obj,这是 JS 的隐式传 this
- 在 fn.call(xxx) 调用中,this 就是 xxx,这是开发者通过 call 显式指定的 this
- 在 arrow() 调用中,arrow 里面的 this 就是 arrow 外面的 this,因为箭头函数里面没有自己的 this
- 在 arrow.call(xxx) 调用中,arrow 里面的 this 还是 arrow 外面的 this,因为箭头函数里面没有自己的 this
资料来源:饥人谷