概念
函数是一种对象
定义一个函数
具名函数
function 函数名(形式参数1,形式参数2){
语句
return返回值
}
匿名函数
- 上面的具名函数去掉函数名就是匿名函数
leta=function(x,y){ return x+y }- 也叫函数表达式
- 注意 函数在等号右边,它的作用域只在右边。没有等号,才会全局作用
箭头函数
- 单个参数:
let f1 = x => x*x - 多个个参数:
let f2 = (x,y,z,...) => x+y+z+...参数圆括号不能省 - 返回多语句: 需自行添加return,因为多语句JS无法确定返回值
let f3 = (x,y) => {
console.log('hi') 语句1
return x+y 语句2
}
- 输入参数返回对象:
let f4 = (x,y) => ({name:x, age: y})直接返回对象,JS判定''{''是执行版块的开始,而不是对象,加(),才会识别为一个整体,所以圆括号不能省 - 构造函数
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
函数的要素
调用时机 (时机不同,结果不同)
- 例1
let a = 1
function fn(){
console.log(a)
}
打印结果未知,因为没有调用代码
- 例2
let a = 1
function fn(){
console.log(a)
}
fn()
打印结果为1
- 例3
let a = 1
function fn(){
console.log(a)
}
a = 2
fn()
打印结果为2
- 例4
let a = 1
function fn(){
console.log(a)
}
fn() 调用
a = 2 新赋值,还未调用
结果为1
- 例5
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a = 2
结果为2
- 例6 打印出6个6
et i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
结果:打印6个6
执行机制
- 首先根据循环条件,setTimeout 执行6次
- setTimeout执行原理是先等i从0到6。等一会i的值为6,即打印出6
- 总体逻辑:等一会儿打印(打印6)执行了6次(打印6个6)
- 例7 打印出0,1,2,3,4,5
for (let i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
let会在每次循环的时候将当前i的值放入setTimeout中,不会再变化
打印出0,1,2,3,4,5,与传统的for循环没有区别
- 其它实现方法
let i = 0
for (i = 0; i < 6; i++) {
let j = i
setTimeout(() => {
console.log(j)
}, 0)
}
作用域
每个函数都会默认创建一个作用域
- 例1
function fn(){
let a = 1
}
console.log(a)
a 不存在
就算fn执行了,也访问不到作用域里的a
a的作用域只在{ }内
- 例2
function fn(){
let a = 1
}
fn()
console.log(a)
a还是不存在,let a=1 的作用域在{ }内
全局变量 V.S.局部变量
- 在顶级作用域声明的变量是全局变量
- 在window的属性是全局变量
- 其他都是局部变量
作用域的 函数可嵌套
例1:
- 执行机制
- 调用函数f1
- 函数f1先打印a,再调用f2(以下序号代表执行顺序)
- f1打印a,作用域内a=1,打印1
- f2打印a,作用域内a=2,打印2 例2:
- 执行机制
- 调用f1(以下序号代表执行顺序)
- f1内先打印f1内a的值
- a赋值为100,待定,不影响函数调用,根据实际情况
- 调用f2
- f2执行 a赋值22,赋值是否调用待定
- f2执行调用f3
- f3执行打印a,f3内没有a的值
- 由于a先赋值了22,后调用f3,所以就近原则,a为100,打印22
- 总结:总体执行顺序为先执行f1→a=100→执行f2,具体内容由外向内展开,找出作用域和调用函数顺序
- 调用f1(以下序号代表执行顺序)
闭包
如果一个函数用到了外部的变量
那么这个函数加这个变量
就叫做闭包
下图的 a 和 f3 组成了闭包
形式参数
- 形式参数的意思就是非实际参数
function add(x, y){ x和y就是形参,因为并不是实际参数
return x+y
}
add(1,2) 调用add时,1和2是实际参数,会赋值给x,y, 本质是复制了地址到x和y
结果为3
- 形参可认为是变量声明 以上代码等价于:
function add(){
var x = arguments[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 没加return
- 正确写法 1+2的值为3
调用栈
概念
JS 引擎在调用一个函数前, 需要把函数所在的环境 push 到一个数组里, 这个数组叫做调用栈, 等函数执行完了,就会把环境弹(pop)出来, 然后 return 到之前的环境,继续执行后续代码
例:调用机制
递归函数
- 阶乘
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
先递进到控制条件 n=1,再回归,返回值24
- 递归函数的调用栈 以阶乘(4)为例
- 爆栈
如果调用栈中压入的帧过多,程序就会崩溃
- 检测代码
function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) {
// 报错说明 stack overflow 了
return 1;
}
}
函数提升
概念
- function fn(){ }
- 不管把具名函数声明在哪里,它都会跑到第一行
两种顺序都是function先声明了add,let后声明add,但是let不允许再次声明
arguments和this
每个函数都有除了箭头函数
argumens
特性
arguments原型里面没有数组的共有属性,所以它是包含所有参数的伪数组(没有push,join等属性)
this
如果不给任何条件,this默认指向window
fn.call(XXX)传参
XXX会被自动转化成对象
图中的1不是值,而是对象
去除转换效果
加入
'use atrict',禁止转化成对象
this、arguments传参
图1
图二
第一个参数传给this,后面的参数传给arguments
person.sayHi()案例的this(难点)
假设没有this
let person = {
name: 'frank',
sayHi(){ 函数还未调用执行
console.log(`你好,我叫` + person.name)
}
}
- 分析
我们可以用直接保存了对象地址的变量获取'name',这种方法简称为引用
问题一
- person 如果改名,sayHi函数就不能运行了
- sayHi函数甚至可能在另外一个文件呢里面
- 所以我们不希望sayHi函数里出现person引用
问题二
class Person{
constructor(name){
this.name = name
// 这里的 this 是 new 强制指定的
}
sayHi(){
console.log(???)
}
}
分析
- 这里只有类,没有创建对象,故不可能获取对象的引用
需要一种办法拿到对象,这样才能获取对象的name属性
种土办法,用参数
对象
let person = {
name: 'frank',
sayHi(p){
console.log(`你好,我叫` + p.name)
}
}
person.sayHi(person)
- sayHi函数虽然写在对象里面,但是与写在外面没有什么区别,因为还没有调用
- p为函数的形式参数,person作为参数
类
class Person{
constructor(name){ this.name = name }
sayHi(p){
console.log(`你好,我叫` + p.name)
}
}
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
let person = {
name: 'frank',
sayHi(this){ 里面的this不写
console.log(`你好,我叫` + this.name)
}
}
加了this后,
person.say()相当于person.sayHi(person)
然后person被传给this了(person是个地址)
这样每个函数都能用this获取一个未知对象的引用了
个人理解:this只不过是这个函数自带的形参,它的作用是作为中间的媒介,调用person的地址,获取person对应的对象
思想person.sayHi()会隐式地把person作为this传给sayHi,方便sayHi获取person对应的对象
总结
- 我们想让函数获取对象的引用
- 但是并不想通过变量名做到
- Python通过额外的this做到
- JS通过额外的this做到:
- person.sayHi() 会把 person 自动传给 sayHi, sayHi 可以通过 this 引用 person
注意
- 注意 person.sayHi 和 person.sayHi() 的区别
- 注意 person.sayHi() 的断句 (person.sayHi) ( )
this 调用原理图
案例:
class Person{
constructor(name){
this.name = name
// 这里的 this 是 new 强制指定的
}
sayHi(){
console.log(???)
}
}
这里的p是一个中间对象,而JS引擎将这个中间对象地址传给了this,让this扮演P这个角色
另一个问题
- person.sayHi()
- person.sayHi(person)
- 省略形式反而对了,完整形式反而是错的
两种调用方法
方法一
- person.sayHi()
- 会自动把perso传到函数里作为this
方法二
- person.sayHi.call(person)
- 需要自己动手把person传到函数里,作为this
call指定this
传参传的是什么,this就是什么
- 传的参数为对象{name:1},这时的this就为{name:1},this.name就为1,打印出1 例
- 案例中没有this,传参的时候第一个数据选择任意数据都可以(用于this的占位),后面才是所需要的具体参数(arguments) 例:打印数组的元素
let array = [1, 2, 3] 声明一个数组
Array.prototype.forEach2 = function(fn) { 数组原型中加入forEach2函数,参数为fn
for (let i = 0; i < this.length; i++) { 循环次数为未知数组的长度,this过渡
fn(this[i], i) fn函数的参数为key以及对应的value
} this作为过渡数组,等待传入
}
array.forEach2.call(arry, (item) => console.log(item)) array就是this,右侧参数即为fn
this一定为数组吗?
- 不一定,比如
Array.prototype.forEach2.call({0:'a',1:'b'})
这里的this为{0:'a',length:1}伪数组,所以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'})
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
立即执行函数
原理
- ES5时代,为了得到局部变量,先引入一个函数
- 但是这个函数如果有名字,就得不偿失
- 于是这个函数必须是匿名函数
- 声明匿名函数,然后立即加个 () 执行它
- 但是 JS 标准认为这种语法不合法
- 只要在匿名函数前面加个运算符即可
- !、~、()、+、- 都可以
- 但是这里面有些运算符会往上走 推荐用!