JS函数和你不知道的this
四种方式定义函数
具名函数
function 函数名(形式参数1, 形式参数2) {
语句
return 返回值
}
匿名函数
- 上面的具名函数,去掉函数名就是匿名函数
let a = function(x, y) {return x+y}
- 也叫函数表达式
箭头函数
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也是
基本没人用,但是能让你知道函数是谁构造的
函数的要素
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(除了箭头函数)
- this(除了箭头函数)
调用时机
来看三个例子,看看JS有多操蛋。
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)
}
打印多少? 不是0,1,2,3,4,5.而是6个6。循环到每一个i都是过一会打印出i。过一会就是指要把所有循环走完。那么当循环走完,i就为6,所以要打印出6个6
for(let i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
}, 0)
}
打印多少?
是0,1,2,3,4,5. 因为JS在for和let一起用的时候会加东西。每次循环会多复制一个i留在那里,不跟随新的i变化。(彳亍口巴)。
那这样setTimeout还有什么意义?
好问题,🈚️意义。
作用域:就近原则 & 闭包
function fn(){
let a = 1
}
fn()
console.log(a) // a不存在
全局变量:
window.c = 1
如果多个作用域有同名变量a, 那么查找a的声明时,就向上取最近的作用域,查找a的过程与函数执行无关,但a的值与函数执行有关。
和函数执行没有关系的作用域叫做静态作用域(词法作用域)。和函数执行有关系的叫做动态作用域,JS🈚️动态作用域。
最后看个例子来理解作用域:
如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。上面代码4-7行中的a和f3就组成了闭包。
形式参数和返回值
- 形参可认为是变量声明,类似于:
function add(){
var x = arguments[0]
var y = arguments[1]
return x+y
}
- 没return返回值是undefined
递归、调用栈与爆栈
什么是调用栈
- JS引擎在调用一个函数前
- 需要把函数所在的环境push到一个数组里
- 这个数组叫做调用栈
- 等函数执行完了,就会把环境pop出来
- 然后return到之前的环境,继续执行后续代码
递归函数
// 阶乘
function f(n){
// return n === 1 ? 1 : n * f(n-1)
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
先递进,再回归
想想递归的调用栈(压栈和弹栈):
爆栈
如果调用栈中压入的帧太多,程序就会崩溃。
调用栈大小: Chrome 12578, Firefox 26773, Node 12536(Node和Chrome用的JS引擎都是V8,所以差不多)
函数提升
不管具名函数声明在哪,都会跑到第一行
arguments和this
- arguments是一个包含该函数所有普通参数的伪数组
- 调用当前函数,this默认表示window
- 加上
’use strict'
就不会让this自动将传入的参数转化为对象了 (没人这么做)
实际上this是第一个参数,arguments是从第一个参数往后的所有参数。
this是隐藏参数,arguments是普通参数(个人结论)
person.sayHi()会隐式地把person作为this传给sayHi
两种调用法
- 小白调用法
person.sayHi()
// 会自动把person传到函数里,作为this
- 大师调用法
person.sayHi.call(person)
// 需要自己手动把person传到函数里,作为this
- 例子
function add(x,y) {
return x +y
}
// 不用this
add.call(undefined, 1, 2) // 传undefined是占位,因为第一个参数要作为this。
用this:
上面的例子,用array作为this,调用了forEach2
两种写法再探究:
一个是显示地指定array是this,一个是隐式地指定array是this。
总结一下this的两种使用方法
- 隐式传递
fn(1,2) //等价于fn.call(undefined, 1, 2)
obj.child.fn(1) //等价于obj.child.fn.call(obj.child,1)
- 显式传递
fn.call(undefined, 1, 2)
fn.apply(undefined, [1,2])
绑定this
- 使用
.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'})
就调f2相当于调f1,唯一的区别是你把this绑定了
有什么用? Vue和React中会经常用到。
.bind
还可以绑定其他参数
let f3 = f1.bind({name:'frank'}, 'hi')
f3() // 等价于f1.call({name:'frank'}, hi)
箭头函数
箭头函数没有arguments和this
- 里面的this就是外面的this
console.log(this) //外面的this是window
let fn = () => console.log(this)
fn() // window
- 就算你加call都没有用
fn.call({name:'frank'}) // window,没有用,指定什么都没有用,还是window
就如同:
let b = 1
let fn = () => console.log(b)
b是外面的b,this也是外面的this(window)
- 同样的,箭头函数没有arguments,舒服:
let fn = () => console.log(arguments)
fn(1,2,3)
// Uncaught ReferenceError: arguments is not defined at fn
立即执行函数(生成局部变量的方法)
只有JS有的变态玩意,现在用得少
原理
- ES5时代,为了得到局部变量,必须引入一个函数
- 但是这个函数如果有名字,就还会引入一个全局函数,得不偿失
- 于是这个函数必须是匿名函数
- 声明匿名函数,然后立即加个()执行它
- 但是JS标准认为这种语法不合法
- 所以JS程序员寻求各种办法
- 最终发现,只要在匿名函数前面加个运算符即可
- !、~、()、+、-都可以
- 但是这里面有些运算符会往上走
- 所以推荐永远用!来解决
//ES5老方法
!function (){
var a =2
console.log(a)
} ()
//ES6最新方法
{
let a = 2
}
//不推荐()
console.log('hi') //如果立即执行函数前面这句代码
(function (){
var a =2
console.log(a)
} ()) //Uncaught TypeError: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)
} ())
结论,你就用ES6语法就完事了。