JS函数
定义函数
定义一个函数
具名函数
1
2
3function 函数名(形式参数1, 形式参数2){
return 返回值
}匿名函数, 具名函数去掉函数名就是匿名函数
1
2
3
4let a = function(x, y){
return x + y
}
// 也叫函数表达式箭头函数
1
2
3
4
5let 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})
// 直接返回对象 需要加个圆括号构造函数
1
2
3
4let f = new Function('x', 'y', 'return x + y')
// 基本没人用, 但是能让你知道函数是谁构造的
// 所有函数都是 Function 构造的
// 包括 Object、Array、Function
函数的要素
每个函数都拥有这些
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
- arguments(除了箭头函数)
- this(除了箭头函数)
调用时机
时机不同, 结果不同
1 | let a = 1; |
1 | let i = 0; |
1 | for(let i = 0; i < 6; i++){ |
作用域
每个函数都会创建一个作用域
1 | // 例1 |
1 | // 例2function f1(){ let a = 1; function f2(){ let a = 2; console.log(a); } console.log(a); a = 3; f2(); console.log(a)}f1()// 打印结果为 /* 1 2 3*/ |
1 | // 例3function f1(){ let a = 1; function f2(){ let a = 2; function f3(){ console.log(a) } a = 22; f3(); } console.log(a); a = 100; f2();}f1();/* 打印机结果为122*/ |
如果多个作用域有同名变量 a
那么查找 a 的声明式, 就向上取最近的作用域, 简称[就近原则]
查找 a 的过程与函数执行无关
但 a 的值与函数执行有关
闭包
1 | function f1(){ let a = 1; function f2(){ // let a = 2; function f3(){ console.log(a) }// /* 如果一个函数用到外部的变量 那么这个函数加这个变量 就叫做 闭包 左边的 a 和 f3 组成了 闭包 */ a = 22; f3(); } console.log(a); a = 100; f2();}f1(); |
形式参数
形式参数意思式非实际参数
1 | function add (x, y){ return x + y;}// 其中 x 和 y 就是形参,因为并不是时机的参数add(1, 2);// 调用 add 时,1 和 2 是实际参数,会被复制给 x y// 上面代码近似等于下面代码function add(){ var x = arguments[0] var y = arguments[1] return x + y} |
返回值
每个函数都有返回值
函数执行完了后才会返回
只有函数有返回值
1 | function hi() { console.log('hi'); }hi();// 没写 return, 所以返回值是 undefinedfunction hi() { return console.log('hi'); }hi()// 返回值为 console.log('hi') 的值,即 undefined |
调用栈
什么是调用栈
JS 引擎在调用一个函数前
需要把函数所在的环境 push 到一个数组里
这个数组叫做调用栈
等函数执行完了, 就会把环境弹 (pop) 出来
然后 return 到之前的环境, 继续执行后续代码
爆栈, 如果调用栈中压入的帧过多, 程序就会奔溃
递归函数
1 | // 阶乘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 * (1))= 4 * (6)= 24// 先递进, 后回归 |
调用栈最长有多少
1 | // 测试调用栈长度function computeMaxCallStackSize() { try { return 1 + computeMaxCallStackSize(); } catch(e) { // 报错说明 stack overflow 了 return 1 }}/*chrom 11409firefox 24740node 12536*/ |
函数提升
1 | function fn(){}// 不管把具名函数声明在哪里, 它都会跑到第一行 |
arguments 和 this
每个函数都有 arguments 和 this ,除了箭头函数
1 | function fn(){ console.log(arguments); console.log(this)}fn()// arguments 是包含所有参数的 伪数组// 如果不给任何条件 this 默认指向 window |
1 | function fn(){ console.log(this)}fn()fn.call(1) // 打印出的 数字 1 被自动转化成对象 1function fn(){ 'use strict' console.log(this)}fn.call(1) // 打印出 数字 1function fn(){ console.log(this); console.log(arguments)}// 传入的第一个参数是 this, 其余的是 argumentsfn.call(1, 2, 4)/*打印结果Number (1)Arguments{0: 2, 1: 4 ...}*/ |
this 是隐藏参数
arguments 是普通参数
this 是参数(个人结论)
假如没有 this
1 | let person = { name: 'frank', sayHi(){ console.log('hello, i am ' + person.name); }}person.sayHi()/* 分析可以用直接保存了对象地址的 变量 获取 'name'这种办法简称为 引用*/ |
问题一
1 | let sayHi = function(){ console.log('hello, i am ' + /* person*/.name)}let person = { name: 'frank', 'sayHi': sayHi.}/*分析person 如果改名,sayHi 函数就挂了sayHi 函数甚至有可能在另一个文件里所以我们不希望 sayHi 函数里出现 person 引用*/ |
问题二
1 | class Perosn { constructot(name){ this.name = name // 这里的 this 是 new 强制指定的 } sayHi(){ console.log(/*????*/) }}/*分析这里只有类,还没创建对象,故不可能获取对象的引用那么如何拿到对象的 name ?*//*需要一种办法拿到对象这样才能获取对象的 name 属性*/ |
一种土方法,用参数
1 | // 对象let person = { name: 'frank', sayHi(p) { console.log('hello, i am ' + p.name) }}person.sayHi(person)// 类class Person { constructor(name){ this.name = name } sayHi(p) { console.log('hello, i am ' + p.name) }}let person = new Person('frank')person.sayHi(person) |
JS 在每个函数里加了 this
1 | // 用 this 获取那个对象let person = { name: 'frank', sayHi(/*this*/){ console.log('hello, i am ' + this.name) }}person.sayHi()/*person.sahHi()相当于 person.sayHi(person)然后 person 被传给 this 了 (person 是个地址)这样每个函数都能用 this 获取一个未知对象的引用了*/// person.sayHi() 会隐式地把 person 作为 this 传给 sayHi// 方便 sayHi 获取 person 对应的对象 |
总结
我们想让函数获取对象的引用
但是并不想通过变量名做到
Python 通过额外的 self 参数做到
JS 通过额外的 this 做到:
person.sayHi() 会把person 自动传给 sayHi, sayHi 可以通过 this 引用 person
其他
- 注意 person.sayHi 和 person.sayHi() 的区别
- 注意 person.sayHi() 的短句 (person.sayHi)()
call 指定 this
1 | // 哪个对let person = { name: 'frank', sayHi(/*this*/){ console.log('hello, i am ' + this.name) }}person.sayHi()Person.sayHi(person)// 省略形式的反而是对的// 两种调用方式person.sayHi()// 会自动把 person 传到函数里, 作为 thisperson.sayHi.call(person)person.sayHi.call({name: 'evan'})// 需要手 动把 person 传到函数里,作为 this// 推荐使用 |
1 | // 例1function add(x, y) { return x + y}add.call(undefined, 1, 2) // 3/*为什么要多写一个 undefined因为第一个参数要作为 this但是代码里没有用 this所以只能用 undefined 占位其实用 null 也可以*/ |
1 | // 例2Array.prototype.forEach2 = function(fn){ for(let i = 0; i < this.length; i++){ fn(this[i], i, this) }}let arr1 = [2, 4, 5]// 两种调用方式arr1.forEach2.call(arr1, (item) = > console.log(item))arr1.forEach2((item) = > console.log(item))/* this 是什么由于大家使用 forEach2 的时候总是会用 arr.forEach2所以 arr1 就被自动传给 foreEach2 了*//* this 一定是数组吗不一定, 比如Array.prototype.forEach2.call({0: 'a', 1: 'b',length: 2})*/ |
this 的两种使用方法
隐式传递
1
fn(1, 2) // 等价于 fn.call(undefined, 1, 2)obj.child.fn(1) // 等价于 obj.child.fn.call(obj.child, 1)
显示传递
1
fn.call(undefined, 1, 2)fn.apply(undefined, [1, 2]) // 数组
**绑定 this **
1 | // 使用 .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'}) |
箭头函数
没有 arguments 和 this
里面的 this 就是外面的 this
1 | console.log(this) // windowlet fn = () => console.log(this)fn() // window// 就算加了 call 也没用fn.call({name: 'frank'}) // window |
立即执行函数
推荐 JS 中的立即执行函数