函数是对象
定义一个函数
- 具名函数
function 函数名(形式参数1, 形式参数2){
语句
return 返回值
}
function fn(x,y){
语句
return 返回值
}
注意一下特殊情况:
let a = function fn(x,y){return x+y}
fn(1,2) //此时输入fn(1,2),会报错
可以用a(1,2)来调用fn
- 匿名函数
上面的具名函数,去掉函数名就是匿名函数
let a = function(x, y){ //注意,这里a只是容纳了右边函数的地址
return x+y
}
也叫函数表达式
- 箭头函数
let f1 = x => x*x //let f1 = (输入参数) => (输出参数)
let f2 = (x,y) => x+y // 也可以有两个参数,注意!圆括号不能省
let f3 = (x,y) => {return x+y} // 如果加了花括号,return不能省
例如:
let fn = (x,y) => {
console.log('hi')
return x+y //当里面有>=两句话的时候,就要加return和花括号
}
let f4 = (x,y) => {name: x, age: y}
//如果要返回对象的话,这样写会出错,需要加个圆括号
因为花括号在js里面优先被当作"块的起始",比如{foo:1}这只是个代码块(标签label),不是对象
let f4 = (x,y) => ({name:x, age: y})
//这样就没问题了
- 用构造函数
let f = new Function('x', 'y', 'return x+y')
//基本没人用,但是能让你知道函数是谁构造的
所有函数都是 Function 构造出来的,包括 Object、Array、Function 也是
函数自身 V.S. 函数调用
fn V.S. fn()
- 函数自身 代码:
let fn = () => console.log('hi')
fn //结果: 不会有任何结果,因为 fn 没有执行
- 函数调用 代码:
let fn = () => console.log('hi')
fn() //结果: 打印出 hi,有圆括号才是调用
代码:
let fn = () => console.log('hi')
let fn2 = fn
fn2()
- 结果:
- fn 保存了匿名函数的地址
- 这个地址被复制给了 fn2
- fn2() 调用了匿名函数
- fn 和 fn2 都是匿名函数的引用而已(如果你存了一个东西的地址,那你就是那个东西的引用而已)
- 真正的函数既不是 fn 也不是 fn2,而是() => console.log('hi')
函数的要素
每个函数都有这些东西:
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(除了箭头函数)
- this(除了箭头函数)
调用时机
例1:
let a = 1
function fn(){
console.log(a)
}
问:打印出多少?
答:不知,因为没有调用代码
例2:
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a = 2
问:打印出多少?2
例3:
let i = 0 //这里let写在外面
for(i = 0; i<6; i++){ //for里面是i
setTimeout(()=>{ //setTimeout意思是过一会儿执行,先执行for,for的优先级比较高
console.log(i)
},0)
}
问打印出多少? 答:不是 0、1、2、3、4、5 ,而是 6 个 6
for(let i = 0; i<6; i++){ //for和let一起用有奇效。。
setTimeout(()=>{
console.log(i)
},0)
}
问打印出多少? 答:是 0、1、2、3、4、5
因为 JS 在 for 和 let 一起用的时候会加东西 每次循环会多创建一个 i
例4:
let arr = [1,2,3,4,5,6]
arr.forEach(function(x,y){
console.log(y)
})
用forEach一样能打印出0、1、2、3、4、5
作用域
每个函数都会默认创建一个作用域
function fn(){
let a = 1
}
console.log(a) // a 不存在
问:是不是因为 fn 没执行导致
答:就算 fn 执行了,也访问不到作用域里面的 a
全局变量 V.S. 局部变量
- 在顶级作用域声明的变量是全局变量
- window 的属性是全局变量
- 其他都是局部变量
- 局部变量
function fn(){
//如果在一个函数里面声明(let/const)一个变量,
let a = 1 那么这个变量就是局部变量,因为它只在作用域里面生效,
出了作用域就不生效了。所以是局部的
}
fn()
console.log(a) // a 还是不存在
- 全局变量
let b = 1 //在顶级作用域声明了b,所以b是全局变量
function f1(){
window.c = 2 //在window上挂c,也是全局变量,可以随便放,
不一定要放外面,放函数里面也可以
let a = 1 //在函数里声明a,只能在函数的作用域里面生效
}
f1()
function f2(){
console.log(c)
}
f2() //输入函数调用c
函数可嵌套,作用域也可嵌套
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
闭包
形式参数
- 形式参数的意思就是非实际参数
function add(x, y){
return x+y
}
f(1,2)
// 其中 x 和 y 就是形参,因为并不是实际的参数 add(1,2)
// 调用 add 时,1 和 2 是实际参数,会被赋值给 x y
赋值的意思就是根据内存图的概念,把参数在stack里面所对应的值全部复制过去
- 形参可认为是变量声明
// 上面代码近似等价于下面代码
function add(){
var x = arguments[0] //声明一个x等于参数的第0个
var y = arguments[1] //声明一个y等于参数的第1个
return x+y
}
- 形参可多可少 形参只是给参数取名字
function add(x){
return x+ arguments[0] //只声明一个变量也可以,
arguments[0]意思是参数的第0个
}
add(1,2)
//输出结果3
function add(x,y,z){ //第3个参数就算不用也没关系
return x+y
}
f(1,2) //就算有3个形式参数,就调用2个实际参数也可以
返回值
- 每个函数都有返回值 返回值就是return后面的那个值,return就是返回的意思
function hi(){ console.log('hi') }
hi() //没写 return,所以返回值是 undefined
function hi(){ return console.log('hi') }
hi()
返回值为 console.log('hi') 的值,即 undefined
- 函数执行完了后才会返回
- 只有函数有返回值
调用栈
什么是调用栈?
- JS 引擎在调用一个函数前,
- 需要把函数所在的环境 push 到一个数组里 (压栈),
- 这个数组叫作调用栈。
- 等函数执行完了,
- 把环境弹(pop)出来 (弹栈),
- return 到之前的环境,继续执行后续代码。
举例:
console.log(1)
console.log('1+2的结果为' + add(1,2))
console.log(2)
递归函数
- 阶乘
function f(n){
return n !== 1 ? n* f(n-1) : 1
}
-
理解递归
先递进,再回归
-
递归函数的调用栈 递归函数的调用栈很长
画出阶乘(4) 的调用栈
- 调用栈最长有多少 在浏览器里面输入下列代码调用函数可以得知:
function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) {
// 报错说明 stack overflow 了
return 1;
}
}
- Chrome 12578
- Firefox 26773
- Node 12536
- 爆栈 如果调用栈中压入的帧过多,程序就会崩溃
函数提升
- 什么是函数提升?
function fn(){}
不管你把具名函数声明在哪里,它都会跑到第一行
例如:
add(1,2)
function add(1,2){
return x+y
}
//函数跑到第一行,在add(1,2)前面,输出3
例2:
let add = 1
function add(){}
//会报错,因为函数会跑到第一行
变成:
function add(){}
let add = 1
//但是add已经被函数声明了,let就不能再声明,这样会报错
例3:
var add
function add(){}
//输入add
返回的是函数:add(){}
因为var只是声明了一个add,并没有赋值,函数赋值了所以函数比较厉害。。
但是如果改成var add = 1
输入add,返回的是1
- 什么不是函数提升
let fn = function(){}
这是赋值,右边的匿名函数声明不会提升
arguments 和 this
每个函数都有,除了箭头函数
代码:
function fn(){
console.log(arguments)
console.log(this)
}
打印出this和arguments可知:
arguments是个伪数组,因为它没有数组的共有属性
如果不给任何条件的话,this默认指向window
- 如何传 arguments
function fn(){
console.log(arguments)
}
fn(1,2,3)
调用 fn 即可传 arguments
fn(1,2,3) 那么 arguments 就是 [1,2,3] 伪数组
- 如何传 this
目前可以用
fn.call(xxx, 1,2,3)
传 this 和 arguments
xxx是传到this的,后面的1,2,3都存在arguments里面
而且 xxx 会被自动转化成对象(JS 的糟粕)
如果传的this不是对象,那么JS会自动把它封装成对象
undefined的话,this默认指向window
除非在定义函数的时候加上'use strict'
例如:
function fn(){
'use strict'
console.log(this)
}
fn(1) //输出1
this 是隐藏参数,arguments 是普通参数(this 是参数(此结论参考知乎大佬的))
假设没有 this
代码:
let person = {
name: 'xxx',
sayHi(){
console.log(`你好,我叫` + person.name)
}
}
分析: 我们可以用直接保存了对象地址的变量(person)获取 'name', 我们把这种办法简称为引用。
let person = {name: 'xxx'},person是变量,它保存了=号后面这个对象的地址 代码
- 问题1:
let sayHi = function(){
console.log(`你好,我叫` + person.name)
}
let person = {
name: 'xxx',
'sayHi': sayHi
}
分析:
- person 如果改名,sayHi 函数就挂了
- sayHi 函数甚至有可能在另一个文件里面
- 所以我们不希望 sayHi 函数里出现 person 引用
- 问题2:
class Person{
constructor(name){
this.name = name
// 这里的 this 是 new 强制指定的
}
sayHi(){
console.log(???)
}
}
分析: 这里只有类,还没创建对象,故不可能获取对象的引用。
那么如何拿到对象的 name ? 一种土办法,用参数
对象:
let person = {
name: 'frank',
sayHi(p){
console.log(`你好,我叫` + p.name) //先用形参p来顶替未知的对象
}
}
person.sayHi(person) //这里的实际参数person对应的是函数里的形参p,
通过实际参数来调用变量person,通过变量person引用对象
类:
class Person{
constructor(name){ this.name = name }
sayHi(p){
console.log(`你好,我叫` + p.name)
}
}
person.sayHi(person) //同理,类也一样
Python也是用这种方法:
class Person:
def __init__(self, name): # 构造函数
self.name = name
def sayHi(self):
print('Hi, I am ' + self.name)
person = Person('frank')
person.sayHi()
特点:
- 每个函数都接受一个额外的 self, 这个 self 就是传进来的对象
- 只不过 Python 会偷偷帮你传对象
- person.sayHi() 等价于 person.sayHi(person)
- person 就被传给 self 了
JS 在每个函数里加了 this
- 用 this 获取那个对象
- person.sayHi()相当于person.sayHi(person)
- 然后 person 被传给 this 了(person 是个地址)
- 这样,每个函数都能用 this 获取一个未知对象的引用了 person.sayHi()会隐式地把 person 作为 this 传给 sayHi(方便 sayHi 获取 person 对应的对象 )
- this的总结:
- 我们想让函数获取对象的引用,但是并不想通过变量名做到
- Python 通过额外的 self 参数做到
- JS 通过额外的 this 做到:person.sayHi() 会把 person 自动传给 sayHi, sayHi 可以通过 this 引用 person 其他:
- 注意 person.sayHi 和 person.sayHi() 的区别
- 注意 person.sayHi() 的断句 (person.sayHi)()
两种调用
person.sayHi()
person.sayHi(person) //这种调用写法式错的
省略形式反而对了,完整形式反而是错的? 所以提供了两种调用方法。
- 小白调用法 person.sayHi()
会自动把 person 传到函数里,作为 this
- 大师调用法(推荐使用) person.sayHi.call(person)
需要自己手动把 person 传到函数里,作为 this
例1:
function add(x,y){
return x+y
}
//没有用到 this
add.call(undefined, 1,2) // 3
为什么要多写一个undefined?
- 因为第一个参数要作为 this
- 但是代码里没有用 this
- 所以只能用 undefined 占位
- 其实用 null 也可以
例2:
Array.prototype.forEach2 = function(fn){
for(let i=0;i<this.length;i++){
fn(this[i], i, this)
}
}
- this 是什么 由于大家使用 forEach2 的时候总是会用 arr.forEach2 所以 arr 就被自动传给 forEach2 了
- this 一定是数组吗 不一定,比如 Array.prototype.forEach2.call({0:'a',1:'b'})
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'})
- .bind 还可以绑定其他参数
let f3 = f1.bind({name:'frank'}, 'hi')
f3() // 等价于 f1.call({name:'frank'}, hi)
箭头函数(没有 arguments 和 this)
- 里面的 this 就是外面的 this
console.log(this) // window
let fn = () => console.log(this)
fn() // window
- 就算你加 call 都没有
fn.call({name:'frank'}) // window
)
里面的 this 就是外面的 this
console.log(this) // window
let fn = () => console.log(this)
fn() // window
就算你加 call 都没有
fn.call({name:'frank'}) // window
立即执行函数(用的少)
- 原理
- ES 5 时代,为了得到局部变量,必须引入一个函数
- 但是这个函数如果有名字,就得不偿失
- 于是这个函数必须是匿名函数
- 声明匿名函数,然后立即加个 () 执行它
- 但是 JS 标准认为这种语法不合法
- 所以 JS 程序员寻求各种办法
- 最终发现,只要在匿名函数前面加个运算符即可
- !、~、()、+、- 都可以
- 但是这里面有些运算符会往上走
- 所以推荐永远用 ! 来解决