函数对象
新建函数
具名函数
- 语法:
function 函数名(形参1, 形参2){
语句
return 返回值
}
- 具名函数的作用域
let a = function fn(a, b){return a + b}
fn(1, 2)
function fn(a, b){return a + b}
fn(1, 2)
- 如果函数声明在等于号右边,那么fn()的作用域只有等于号右边,这种情况下想要调用函数只能使用a()。
- 这种情况下声明出来的函数仍是具名函数,它的name值为'fn',但是只能通过a()调用。
匿名函数
- 具名函数去掉函数名就是匿名函数:
function(形参1, 形参2){
语句
return 返回值
}
- 也叫函数表达式。
- 匿名函数和真正的函数是不同的,前者只是函数地址的引用:
function trueFn(){return 'hello world'}
let a = () => 'hello world'
console.dir(trueFn)
console.dir(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})
- 箭头函数也是匿名函数。
- 如果有2个及以上参数,则圆括号不能省略。
- 如果函数体有2个以上语句,则花括号不能省略。这种情况下return不能省略。
- JS中优先把花括号视为函数,因此直接返回花括号包裹的对象会报错。这种情况下要使用圆括号把圆括号包裹起来。
Function()
let fn = new Function('x', 'y', 'return x+y')
函数自身和函数调用
函数的引用
let fn = () => console.log('hello world')
let fn2 = fn
fn2()
- fn并不是真正的函数,它只是保存了匿名函数的地址,并将地址复制给了fn2。
- fn和fn2都只是匿名函数的引用,不是真正的函数。
函数的要素
调用时机
- setTimeout()
let a = 1
function fn() {
setTimeout(() => {
console.log(a)
}, 0)
}
fn()
a = 2
- setTimeout()的计时会在所有可执行代码执行完毕后才开始,无论给的延迟参数有多小,哪怕是0,
- let-for-setTimeout也是遵循上述规则。
let i
for(i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
}, 0)
}
- 除了for-let-setTimeout,这是刻意设计的例外。
for(let i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
}, 0)
}
- JS在for-let语句中,每次循环会多创建一个i。
- console.log()在正式开始执行的时候,每次都会调用不同的i。
作用域
作用域概述
- 通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。 作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
- 全局作用域:全局有效。作用于所有代码执行的环境(整个script标签内部)或者一个独立的JS文件。
- 局部作用域:局部有效。仅作用于函数或代码块的内部。
变量的作用域
- 全局变量:在全局作用域中声明的变量。全局变量在任何区域都可以访问和修改。
- window的属性就是全局变量,其他的都是局部变量。
- 如果是通过
window.变量名来赋值的,在任何区域都可以进行变量声明。
- 局部变量:在函数或代码块内部声明的变量。局部变量只能在当前花括号内部访问和修改。
作用域链
- 只要是代码,就至少有一个作用域。
- 如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
- 根据在局部作用域内部可以访问外部变量的这种机制,就称作作用域链,采取就近原则的方式来查找变量最终值。
- 就近原则:如果多个作用域有同名变量a,在查找a的声明时,就向上取最近的作用域。
作用域的特殊情况
- 在局部或块级作用域内部,没有声明的情况下直接给变量赋值,也当做全局变量看。
- 但是函数内部的形参可以看做是函数内部的局部变量,而不会被视为全局变量。
闭包
- 如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包。
function f1(){
let a = 1
function f2(){
console.log(a)
}
}
f1()
形式参数
- 形式参数:声明函数时写在fn()里的叫形参。
- 实际参数:调用函数时写在fn()里的叫实参。
- 形参可以视为函数内部的变量声明(
let a),实参可以视为给这个变量赋值(a = 1)。
- 实参的传递实际上是把stack中的内容复制了一份,赋值给形参。
返回值
- 每个函数都有返回值,只有函数才有返回值。
- 没写return的情况下,返回值为undefined。
- 返回值≠输出值,
console.log('hi')的输出值为'hi',返回值为undefined。
- 函数执行完了才会有返回值。
返回值和值
- 返回值和值在调用出后的效果是一样的。
- 返回值和值的区别在于:需不需要通过函数来调用。
- 在JS语境下,由于没有私有成员。对象内部的属性,能够被直接调用,一般来说都是值。而非对象内部的属性,大部分情况下需要函数来调用,一般来说都是返回值。
调用栈
函数提升
- 只要通过
function fn(){}来声明具名函数,不管把函数声明在哪里,它都会跑到第一行。
add(1, 2)
function add(x, y) {
return x+y
}
- 如果同时有
function fn(){}和var fn = 1,也是先执行函数声明,再指定变量声明。
var fn = 1
function fn(){}
console.log(fn)
- 如果同时有
function fn(){}和var fn,此时由于var只是声明,并没有给定具体的内容,而function()在声明的同时会给与内容,因此function()赋值成功且不会被var覆盖。
var fn
function fn(){}
console.log(fn)
- 变量声明并且赋值函数的情况下,语句会被视为变量声明语句,不会发生函数提升:
fn(1,2)
let a = function(a, b){return a + b}
arguments和this
- 每个函数都有arguments和this,除了箭头函数。
argument
- 包含函数所有参数的伪数组。
- 如何给argument赋值:调用函数即可。
fn(1,2,3)中,argument就是[1,2,3]伪数组。
- 尽量不要对arguments内的元素进行修改。
this
call()
- 用call()来传递this:
let obj = {
str: 'hello world',
sayHi(){
console.log(this.str)
}
}
obj.sayHi.call(obj)
- 在使用call()时,即不使用this,也要传递一个undefined或null来占位。
function add(x,y){
return x+y
}
add.call(undefined, 1,2)
- Array.prototype.forEach()也是用this来写的:
Array.prototype.forEach = furnction(fn){
for(let i = 0; i < this.length; i++){
fn(this[i], i)
}
}
bind()
- 使用bind()可以绑定this,让this不被改变:
function f1(p1, p2){
console.log(this, p1, p2)
}
let f2 = f1.bind({name: 'frank'})
- f2()就是f1()绑定了this之后的新函数。
- f2()等价于
f1.call({name: 'frank'}),只不过this已经被绑定了。
- bind()可以绑定所有的参数:
let f3 = f1.bind({name: 'frank'}, 'hello')
f3('world')
- f3()等价于
f1.call({name: 'frank'}, 'hello')。
箭头函数
- 箭头函数没有argument和this。
- 箭头函数中的this就是window,即使call()也无法改变箭头函数的this。
立即执行函数
- 过去,想要在不声明全局变量的情况下,得到一个局部变量。通过声明匿名函数并直接调用,可以不生成全局变量:
! function(){
let a = 'hello world'
console.log(a)
}()
- 但是
function(){}()会被JS引擎识别错误,通过在function()前增加特殊标识符可以避免这个问题:
+ function(){}()
- function(){}()
* function(){}()
! function(){}()
- 最好使用
!,其他符号都有可能产生BUG,比如+会尝试把函数同字符串拼接起来。
- 但是在新版JS中,直接通过代码块就可以解决:
{
let a = 'hello world'
console.log(a)
}
拓展
静态作用域
- 函数执行对变量查找没有关系的作用域。
- 这种作用域中,增加或删除函数不会影响原有的变量查找。
- JS的作用域都是静态作用域。
递归函数
function f(n) {
return n !== 1 ? n*f(n -1) : 1
}
- 递归:先递进,再回归。
- 递进的过程实际就是压栈的过程。
- 回归的过程时机就是弹栈的过程。