js中的函数

126 阅读10分钟

1、函数表示

        function sum(a, b) {
            return a + b;
        }
        let sum = function (a, b) {
            return a + b;
        }
        let sum = (a, b) => {
            return a + b;
        }
        let sum = new Function("a", "b", "return a+b");

箭头函数:不能使用arguments,super和new.target,也不能用于构造函数

2、函数名

函数对象都有一个只读的name属性,就是函数名,函数名可能是空串或者anonymous,或者bound xx。

        function sum(a, b) {
            return a + b;
        }
        console.log(sum.name); // sum
        console.log((()=>{}).name); // 空字符串
        console.log((new Function()).name); // anonymous
        console.log(sum.bind(null).name);// bound sum
        let dog = {
            years: 1,
            get age() {
                return this.years; // 使用 this.years 访问属性
            },
            set age(newAge) {
                this.years = newAge; // 使用 this.years 设置属性值
            }
        };
        let cat = {
            years: 1,
            setAge(newAge) {
                this.years = newAge;
            },
            getAge() {
                return this.years;
            }
        }
        console, log(dog.age);
        console.log(cat.getAge);

get age 和 set age是一个整体,age 属性定义了一个 getter 方法和一个 setter 方法,分别用于获取和设置 years 属性的值。java不能的人会认为是第二种。

3、函数参数

可以说js天然就存在方法重载,因为定义方法是可以不写参数,也可以写,写一个写多个都行,因为实际在调用时,可以用arguments类数组对象去访问参数,arguments[0]就是第一个参数。不存在验证命名参数的机制,跟java不一样。即使arguments访问越界,也只是一个undefined给你。

箭头函数不能访问 arguments关键字,而只能命名参数访问,但是作为闭包时访问的外部函数的arguments是可以的。

        function foo() {
            let bar = () => {
                console.log(arguments[0]);
            }
            bar();
        }
        foo(5);

在箭头函数中使用 arguments 是因为箭头函数没有自己的 arguments 对象。箭头函数继承了父作用域(外部函数或全局作用域)的 arguments 对象,因此在箭头函数内部可以访问到父作用域中的 arguments 对象。

没有官方说法的方法重载,方法名一样会覆盖。

默认参数跟arguments的元素不冲突,不是相互补充的关系。

        function foo(name = 'xxx', age = 10) {
            console.log(arguments[0] + " " + arguments[1]);
        }
        foo("xx", 1);// xx 1
        foo("xx");// xx undefined
        foo(1);// 1 undefined
        function foo(name = 'xxx', age = 10) {
            console.log(arguments[0] + " " + `${age}`);
        }
        foo("xx", 1);// xx 1
        foo("xx");// xx 10
        foo(1);// 1 10

4 参数扩展和收集

        function sum() {
            let sum = 0;
            for (let j = 0; j < arguments.length; j++) {
                sum += arguments[j];
            }
            return sum;
        }
        console.log(sum.apply(null, [1, 2])); // 3
        console.log(sum(...[1, 2, 3]));// 6
        console.log(sum([1, 2, 3]));// 01,2,3

这里...就是扩展操作符。当然使用apply也是可以的。最后一个调用为啥是01,2,3,因为直接传入[1, 2, 3],arguments的lenght其实是1,那就是1,2,3,作为一个字符串与0相加,就是01,2,3了。

        function sum(...values) {
            return values.reduce((x, y) => { return x + y }, 0);
        }
        console.log(sum(1, 2, 3));// 6
        console.log(sum([1, 2, 3]));//01,2,3

收集参数就是这里...values。

5 函数声明提升

函数声明会提升到源代码树的顶部,这个函数声明只对单独的函数定义的有效,对函数表达式的没有提升,函数表达式必须得等到代码执行到它那一行,才能在执行上下文中生成函数定义。

        console.log(sum(10, 10)); // 函数声明提升导致可以这么调用
        function sum(a, b) {
            return a + b;
        }
        console.log(sum2(10, 10)); // Uncaught ReferenceError: Cannot access 'sum2' before initialization
        let sum2 = function (a, b) {
            return a + b;
        }

6、 函数作为值或者参数

函数既然是一个对象,那肯定可以作为参数或者返回值的。

        let data = [{
           name: "a", age: 10
        }, {
            name: "b", age: 11
        }]
        function comparePerson(propertyName) {
            return function (obj1, obj2) {
                let a = obj1[propertyName];
                let b = obj2[propertyName];
                if (a > b) {
                    return 1;
                } else {
                    return 0;
                }
            }
        }
        data.sort(comparePerson("age"));
        console.log(data[1].name);

咱们可以看看sort接收的方法对象是啥:

sort(compareFn?: ((a: { name: string; age: number; }, b: { name: string; age: number; }) => number) | undefined): { name: string; age: number; }[] 这个方法跟kt的有啥区别?kt严重抄袭js。

7、 函数内部

arguments对象还有一个callee属性,指向arguments对象所在函数的指针,就是函数名。这里用arguments.callee就是取代factorial,用来解耦函数名的。

        function factorial(num) {
            if (num <= 1) {
                return 1;
            } else {
                return num * arguments.callee(num - 1);
            }
        }
        let trueFac = factorial;
        console.log(trueFac(5)); // 120

但是在严苛模式下arguments.callee不能访问,此时可以使用命名函数表达式来处理。

        let factorial = (function f(num) {
            if (num <= 1) {
                return 1;
            } else {
                return num * f(num - 1);
            }
        })
        console.log(factorial(5)); // 120

caller属性,这个是调用者方法名。

new.target可以用来检测是否是new 关键字调用的new.target属性。

       function King(){
          if(!new.target){
            throw 'king must be intantiated using "new"'
          }
          console.log(new.target); // 打印King方法
       }
       new King();
       King(); // Uncaught king must be intantiated using "new"

this值需要区分箭头函数和普通函数.

        window.color = 'red';
        function a() {
            return this.color;
        }
        let obj = {};
        obj.a = a;
        console.log(obj.a()); // undefined 
        console.log(a());// red

        window.color = 'red';
        let b = () => { return this.color };
        let obj2 = {};
        obj2.b = b;
        console.log(obj2.b()); // red
        console.log(b());// red

        window.royalthName = "xxx";
        function King() {
            this.royalthName = "yyy";
            setTimeout(() => console.log(this.royalthName), 1000);
        }
        new King(); // yyy

        window.royalthName = "xxx";
        function Queen() {
            this.royalthName = "yyy";
            setTimeout(function () { console.log(this.royalthName); }, 1000);
        }
        new Queen(); // xxx

从上面的4个例子,分不清this到底是window还是当前上下文了吧。

第一个例子中,出现undefined 说明this就是obj对象,不是window。

第二个例子中,出现两个都是red,原因是箭头函数定义在window上下文中,所以this保存的是window。

第三个例子中,箭头函数定义在King函数中,所以this是定义King函数时的上下文。

第四个例子中,this是在匿名函数中,默认指向全局对象,是不会保存定义Queue函数时的上下文的,所以this这里是window。

既然this这么难整,有没有办法指定this呢,有!apply、call、bind方法可以做到指定this的能力。
        window.color = 'red';
        function b() {
            console.log(`${this.color}`);
        }
        let obj2 = {
            color: "blue"
        };
        b.call(obj2); // blue
        b.call(window); // red
        b.call(this);// red

apply和call方法的区别就是apply传参传的是数组,call必须是展开所有的参数调用。

每个方法都有length和prototype属性。length就是参数的个数。

8、尾调用优化

只在严苛模式下优化,返回值是一个函数,尾调用函数调用完后无其他逻辑,且该尾调用函数如果有闭包也不捕捉外部函数作用域中的变量。

尾调用优化的例子就是以斐波那契函数为例,除了可以用for循环改写外,还可以改成尾调用优化。

        function nac(num) {
            if (num < 2) {
                return num;
            }
            return nac(num - 1) + nac(num - 2);
        }
        console.log(nac(5)); // 5
        
       // 改写成尾调用优化的例子:
        function fib(n) {
            return fibnac(0, 1, n);
        }
        function fibnac(a, b, n) {
            if (n === 0) {
                return a;
            }
            return fibnac(b, a + b, n - 1);
        }
        console.log(fib(5)); // 5

9、 闭包

闭包和匿名函数区别?

闭包(Closure)和匿名函数(Anonymous function)是两个不同的概念,它们可以同时存在也可以独立存在。

闭包:闭包是指在函数内部可以访问外部作用域的变量的情况。换句话说,闭包是由函数和函数内部能访问的外部变量组合而成的。当一个函数返回另一个函数时,内部函数会保持对外部作用域的引用,即使外部函数执行完毕,内部函数仍然可以访问外部函数的变量。这种机制称为闭包。闭包可以用于隐藏信息、实现私有变量等。例如:

function outerFunction() {
  let outerVariable = 'I am outer variable';
  function innerFunction() {
    console.log(outerVariable);
  }
  return innerFunction;
}
const closureFunction = outerFunction(); // 内部函数 innerFunction 形成闭包
closureFunction(); // 输出: I am outer variable
  1. 匿名函数:匿名函数是指没有名称的函数。在 JavaScript 中,可以使用函数表达式来创建匿名函数。匿名函数可以赋值给变量,也可以作为函数参数传递,或者在需要函数的地方直接定义和调用。匿名函数可以用于简单的逻辑处理、回调函数等场景。例如:
const anonymousFunction = function() {
  console.log('This is an anonymous function');
};

anonymousFunction(); // 输出: This is an anonymous function

虽然闭包和匿名函数可以同时出现,但它们是不同的概念。闭包是一种特定的执行方式,而匿名函数是一种函数的形式。在 JavaScript 中,经常会使用匿名函数来创建闭包,以实现一些特定的功能或逻辑。

9.1 、闭包占用的内存不释放:

闭包出现的话,外部函数的活动对象(arguments等对象)会保存在内存中,直到匿名函数销毁才销毁。

  function comparePerson(propertyName) {
            return function (obj1, obj2) {
                let a = obj1[propertyName];
                let b = obj2[propertyName];
                if (a > b) {
                    return 1;
                } else {
                    return 0;
                }
            }
        }
        let compares = comparePerson("age");
        let result = compares({
            name: "a", age: 10
        }, {
            name: "b", age: 11
        });
        compares = null;  // 主动置于null,可以及时释放闭包占用的内存。

9.2 、闭包下的this可以被正确访问

        window.id = 1;
        let obj = {
            id: 2,
            b() {
                return function () {
                    return this.id;
                }
            }
        }
        console.log(obj.b()()); // 1 访问到了window上的id
        let obj2 = {
            id: 2,
            b() {
                let that = this;
                return function () {
                    return that.id;
                }
            }
        }
        console.log(obj2.b()()); // 2

10 、常见的内存泄漏

  1. 未清理的事件监听器
  • 在不再需要的时候,使用 removeEventListener 方法移除事件监听器。
// 修复内存泄漏
const button = document.getElementById('myButton');
function handleClick() {
  console.log('Button clicked');
}
button.addEventListener('click', handleClick);
// 当不再需要监听器时,使用 removeEventListener 移除它
button.removeEventListener('click', handleClick);
  1. 闭包中的变量引用
  • 尽量避免在闭包中持有大量数据。如果必须在闭包中引用外部变量,确保在使用完毕后将其置为 null,这样可以帮助 JavaScript 引擎及时回收内存。
// 修复内存泄漏
function createClosure() {
  const data = new Array(10000).fill('Some data'); // 大量数据
  return function() {
    // 在闭包中引用外部 data 变量
    console.log(data.length);
    // 使用完毕后将 data 置为 null,帮助 JavaScript 引擎回收内存
    data = null;
  };
}
  1. 未清理的定时器或者间隔器
  • 使用 clearInterval 或者 clearTimeout 清除不再需要的定时器或者间隔器。
// 修复内存泄漏
let counter = 0;
const intervalId = setInterval(function() {
  console.log('Counter:', counter++);
}, 1000);

// 在不再需要时,清除定时器 intervalId
clearInterval(intervalId);

11 、块级作用域和立即调用的函数表达式和块级作用域变量关键字let

        for (var i = 0; i < 5; i++) {
        }
        console.log(i); // 5 飘出来的i
        
        // 立即调用的函数表达式
        (function () {
            for (var k = 0; k < 5; k++) {
            }
        }) 
        console.log(k); // k is not defined

        // let是块级作用域变量let
        for (let j = 0; j < 5; j++) {

        }
        console.log(j); // j is not defined

12、 函数表达式的修饰

匿名函数表达式可以使用 letvar 声明,甚至可以不使用任何声明关键字,但在某些情况下可能会有不同的作用域和生命周期。

下面是一些示例:

  1. 使用 let
let sayHello = function() {
  console.log('Hello!');
};

sayHello(); // 输出: Hello!

使用 let 声明的变量具有块级作用域,这意味着它们只在声明它们的块内可见。在大多数情况下,推荐使用 let 来声明变量,因为它有助于避免变量提升和作用域问题。

  1. 使用 var
var sayHello = function() {
  console.log('Hello!');
};

sayHello(); // 输出: Hello!

使用 var 声明的变量具有函数级作用域,在函数内部的任何地方都是可见的。但是,在ES6之后,letconst 更好地替代了 var,因为它们提供了更好的作用域控制。

  1. 不使用声明关键字:
sayHello = function() {
  console.log('Hello!');
};

sayHello(); // 输出: Hello!

如果不使用声明关键字,sayHello 将被视为全局变量,这意味着它将成为全局对象的属性。这样的话,如果在代码的其他地方重复定义了相同名称的变量,可能会造成意外的覆盖或冲突。

13、 私有变量

        function MyObj() {
            let name = "xx";
            function privateA() {
                console.log('A');
            }
            this.publicB = function () {
                privateA();
                return name;
            }
        }
        let obj = new MyObj();
        obj.publicB();
        obj.privateA();// obj.privateA is not a function

这里name和privateA都是在函数中声明的,都是私有的,publicB因为是使用this添加到对象上的,所以是公有的,这里的publicB是一个特权方法,可以访问私有变量。

        function MyObj() {
            let name = "xx";
            function privateA() {
                console.log('A');
            }
            publicB = function () {
                privateA();
                return name;
            }
        }
        let obj = new MyObj();
         publicB(); // A
        obj.privateA();// obj.privateA is not a function

同样,去掉this, publicB就是一个没有关键字声明的变量,属于全局作用域的变量。也是特权方法。

        function MyObj() {
            let name = "xx";
            a:1;
        }
        let obj = new MyObj();
        console.log(obj.name); // undefined
        console.log(obj.a);// undefined

14、 对象字面量定义公有和私有函数、变量

const person = {
  // 公有属性
  name: 'John',
  age: 30,
  // 公有方法
  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
};
// 调用公有方法
person.greet(); // 输出: Hello, my name is John and I am 30 years old.
const person = (function() {
  // 私有成员
  let privateAge = 30;
  // 私有方法
  function increaseAge() {
    privateAge++;
  }
  // 返回一个包含公有方法的对象
  return {
    // 公有方法
    greet() {
      console.log(`Hello, I am ${privateAge} years old.`);
    },
    // 公有方法可以调用私有方法和成员
    birthday() {
      increaseAge();
      console.log('Happy birthday!');
    }
  };
})();
// 调用公有方法
person.greet(); // 输出: Hello, I am 30 years old.
person.birthday(); // 输出: Happy birthday!
person.greet(); // 输出: Hello, I am 31 years old.

使用了立即执行函数创建了一个闭包,闭包内部定义了私有变量 privateAge 和私有方法 increaseAge。然后,返回一个包含公有方法的对象字面量。在返回的对象中,公有方法 greetbirthday 可以访问并操作闭包内部的私有成员和方法,但外部无法直接访问或修改这些私有成员。这样就实现了对象字面量中的私有方法和成员。

15、 模块模式和模块增强模式

模块模式(Module Pattern)和模块增强模式(Module Augmentation Pattern)都是 JavaScript 中用于创建模块的常见模式,它们都旨在封装一组相关的功能,并提供公共接口供外部使用。虽然它们的目标相同,但在实现方式和使用场景上有一些区别。

模块模式(Module Pattern)

模块模式是一种利用闭包创建私有变量和函数,并返回一个公共接口的模式。通过使用立即执行函数,可以创建一个封闭的作用域,其中的变量和函数对外部不可见,从而实现了信息的封装和保护。这种模式适合于创建单例对象或包含一组相关功能的模块。

const myModule = (function() {
  let privateVariable = 'I am private';

  function privateFunction() {
    console.log('This is a private function');
  }

  return {
    publicMethod: function() {
      console.log('Public method accessing private variable:', privateVariable);
      privateFunction();
    },
    publicVariable: 'I am public'
  };
})();

模块增强模式(Module Augmentation Pattern)

模块增强模式是在现有的模块上进行扩展或增强的一种模式。通常,我们会将已有的模块作为参数传递给另一个函数,在这个函数中对模块进行扩展,并返回一个新的模块。这种模式适用于需要动态地修改现有模块或将多个模块合并成一个模块的情况。

const enhancedModule = (function(originalModule) {
  originalModule.newMethod = function() {
    console.log('This is a new method');
  };

  return originalModule;
})(myModule);

在这个示例中,我们将 myModule 作为参数传递给一个函数,该函数会在现有模块的基础上添加一个新的方法,然后返回一个增强后的模块。

区别和适用场景
  • 模块模式更适合于创建独立的模块或单例对象,它通过闭包实现了信息的封装和保护,可以有效地避免全局作用域的污染。
  • 模块增强模式更适合于动态地修改现有模块或将多个模块合并成一个模块,它允许在不修改原始模块的情况下扩展模块的功能。