JS函数

定义函数

定义一个函数

  • 具名函数

    1
    2
    3
    function 函数名(形式参数1, 形式参数2){
    return 返回值
    }
  • 匿名函数, 具名函数去掉函数名就是匿名函数

    1
    2
    3
    4
    let a = function(x, y){
    return x + y
    }
    // 也叫函数表达式
  • 箭头函数

    1
    2
    3
    4
    5
    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})
    // 直接返回对象 需要加个圆括号
  • 构造函数

    1
    2
    3
    4
    let f = new Function('x', 'y', 'return x + y')
    // 基本没人用, 但是能让你知道函数是谁构造的
    // 所有函数都是 Function 构造的
    // 包括 Object、Array、Function

函数的要素

每个函数都拥有这些

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

时机不同, 结果不同

1
2
3
4
5
6
7
8
9
let a = 1;
function fn(){
setTimeout( () => {
console.log(a)
}, 0)
}
fn()
a = 2
// 打印结果为 2
1
2
3
4
5
6
7
let i = 0;
for(i = 0; i < 6; i++){
setTimeoug( () => {
console.log(i)
}, 0)
}
// 打印结果为 6个6
1
2
3
4
5
6
7
8
for(let i = 0; i < 6; i++){
setTimeoug( () => {
console.log(i)
}, 0)
}
// 打印结果为 0, 1, 2, 3, 4, 5
// 因为 JS 在 for 和 let 一起用的时候会加东西
// 每次循环会多创建一个 i

作用域

每个函数都会创建一个作用域

深入理解 JS 作用域和作用域链

1
2
3
4
5
// 例1
function fn(){ let a = 1; }
console.log(a) // a 不存在
// 问: 是不是因为 fn 没执行导致
// 答: 就算 fn 执行了,也访问不到作用域里面的 a
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 的值与函数执行有关

闭包

JS中的闭包是什么

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 到之前的环境, 继续执行后续代码

Snipaste_2021-09-02_17-04-35
调用栈 图示

爆栈, 如果调用栈中压入的帧过多, 程序就会奔溃

递归函数

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// 先递进, 后回归
Snipaste_2021-09-02_17-36-31
递归函数调用栈 图示

调用栈最长有多少

1
// 测试调用栈长度function computeMaxCallStackSize() {    try {        return 1 + computeMaxCallStackSize();    } catch(e) {        // 报错说明 stack overflow 了        return 1    }}/*chrom 11409firefox 24740node 12536*/

函数提升

1
function fn(){}// 不管把具名函数声明在哪里, 它都会跑到第一行

JS 变量提升和函数提升

前端面试必考-JS 变量提升和函数提升详解

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 ...}*/
Snipaste_2021-09-02_21-14-03
arguments 和 this

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'})

快速理解 JS 中 this 的用法与陷阱

箭头函数

没有 arguments 和 this

里面的 this 就是外面的 this

1
console.log(this) // windowlet fn = () => console.log(this)fn() // window// 就算加了 call 也没用fn.call({name: 'frank'}) // window

立即执行函数

Snipaste_2021-09-03_23-05-46
立即执行函数

推荐 JS 中的立即执行函数