5.函数

130 阅读14分钟

函数语法

重复代码:让程序难以维护

函数主要用于减少重复代码

创建(定义、声明)函数

function 函数名(){
    // 函数体
}

函数体的代码不会直接运行,必须要手动调用函数,才能运行其中的代码。

调用函数

运行函数体

函数名();

函数提升

通过字面量声明的函数,会提升到脚本块的顶部。

通过字面量声明的函数,会成为全局对象的属性。

其他特点

通过typeof 函数名,得到的结果是"function"

函数内部声明的变量:

  1. 如果不使用var声明,和全局变量一致,表示给全局对象添加属性
  2. 如果使用var声明,变量提升到所在函数的顶部,函数外部不可以使用该变量

函数中声明的变量,仅能在函数中使用,在外部无效

参数

参数表示函数运行的未知条件,需要调用者告知的数据

// 参数的有效返回在函数体中
function 函数名(形参1, 形参2, ...){
    
}

函数名(实参)

如果实参没有传递,则对应的形参为undefined

返回值

函数运行后,得到的结果,调用函数时,调用表达式的值就是函数的返回值

return 会直接结束整个函数的运行

return 后面如果不跟任何数据,返回undefined

如果函数中没有书写return,则该函数会在末尾自动return undefined。

文档注释

/**
 *
 *
*/

作用域和闭包

作用域

作用域表示一个代码区域,也表示一个运行环境

JS中,有两种作用域:

  1. 全局作用域

直接在脚本中书写的代码

在全局作用域中声明的变量,会被提升到脚本块的顶部,并且会成为全局对象的属性。

  1. 函数作用域

函数中的代码

在函数作用域中声明的变量,会被提升到函数的顶部,并且不会成为全局对象的属性.

因此,函数中声明的变量不会导致全局对象的污染

尽量的把功能封装在函数中

但是,当函数成为一个表达式时,它既不会提升,也不会污染全局对象。

将函数变为一个函数表达式的方式之一,将函数用小括号括起来。

然而,这样一来,函数无法通过名称调用。

如果书写一个函数表达式,然后将立即调用,该函数称之为立即执行函数 IIFE(Imdiately Invoked Function Expression)。

由于大部分情况下,函数表达式的函数名没有实际意义,因此,可以省略函数名。

没有名字的函数,称之为匿名函数

思考下面的代码为什么不能正常调用?

  <script>
        var sayHello = function test6(){
            alert("今天是周六");
        };
        test6();
    </script>

原因是上面的一句话,函数无法通过函数表达式。

创建函数表达式的方式目前看来有两种

  1. var a = function 函数名();
  2. var a = function();
  3. 立即调用函数表达式 (IIFE)
(function() {
    // 函数体
})();

IIFE是一种立即执行的匿名函数表达式,它在声明后立即执行。这有助于创建私有作用域,防止变量污染全局作用域。

4.箭头函数

var myFunction = () => {
    // 函数体
};

为什么这里末尾会有个分号?

你可能想知道,为什么函数表达式结尾有一个分号 ;,而函数声明没有:

function sayHi() {
  // ...
}

let sayHi = function() {
  // ...
};

答案很简单:

  • 在代码块的结尾不需要加分号 ;,像 if { ... }for { }function f { } 等语法结构后面都不用加。
  • 函数表达式是在语句内部的:let sayHi = ...;,作为一个值。它不是代码块而是一个赋值语句。不管值是什么,都建议在语句末尾添加分号 ;。所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句。

函数表达式 vs 函数声明

函数声明:在主代码流中声明为单独的语句的函数。

// 函数声明
function sum(a, b) {
  return a + b;
}

函数表达式:在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式 = 右侧创建的:

// 函数表达式
let sum = function(a, b) {
  return a + b;
};

更细微的差别是,JavaScript 引擎会在 什么时候 创建函数。 函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。 一旦代码执行到赋值表达式 let sum = function… 的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。 函数声明则不同。 在函数声明被定义之前,它就可以被调用。 例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。 这是内部算法的原故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。 在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。 例如下面的代码会正常工作:

sayHi("John"); // Hello, John

function sayHi(name) {
  alert( `Hello, ${name}` );
}

函数声明 sayHi 是在 JavaScript 准备运行脚本时被创建的,在这个脚本的任何位置都可见。

……如果它是一个函数表达式,它就不会工作:

sayHi("John"); // error!

let sayHi = function(name) {  // (*) no magic any more
  alert( `Hello, ${name}` );
};

函数表达式在代码执行到它时才会被创建。只会发生在 (*) 行。为时已晚。 函数声明的另外一个特殊的功能是它们的块级作用域。 严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。 例如,想象一下我们需要依赖于在代码运行过程中获得的变量 age 声明一个函数 welcome()。并且我们计划在之后的某个时间使用它。 如果我们使用函数声明,以下则代码不能如愿工作:

let age = prompt("What is your age?", 18);

// 有条件地声明一个函数
if (age < 18) {

  function welcome() {
    alert("Hello!");
  }

} else {

  function welcome() {
    alert("Greetings!");
  }

}

// ……稍后使用
welcome(); // Error: welcome is not defined

这是因为函数声明只在它所在的代码块中可见。

下面是另一个例子:

let age = 16; // 拿 16 作为例子

if (age < 18) {
  welcome();               // \   (运行)
                           //  |
  function welcome() {     //  |  
    alert("Hello!");       //  |  函数声明在声明它的代码块内任意位置都可用
  }                        //  |  
                           //  |
  welcome();               // /   (运行)

} else {

  function welcome() {
    alert("Greetings!");
  }
}

// 在这里,我们在花括号外部调用函数,我们看不到它们内部的函数声明。


welcome(); // Error: welcome is not defined

我们怎么才能让 welcome 在 if 外可见呢?

正确的做法是使用函数表达式,并将 welcome 赋值给在 if 外声明的变量,并具有正确的可见性。

下面的代码可以如愿运行:

let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {

  welcome = function() {
    alert("Hello!");
  };

} else {

  welcome = function() {
    alert("Greetings!");
  };

}

welcome(); // 现在可以了

什么时候选择函数声明与函数表达式? 根据经验,当我们需要声明一个函数时,首先考虑函数声明语法。它能够为组织代码提供更多的灵活性。因为我们可以在声明这些函数之前调用这些函数。 这对代码可读性也更好,因为在代码中查找 function f(…) {…} 比 let f = function(…) {…} 更容易。函数声明更“醒目”。 ……但是,如果由于某种原因而导致函数声明不适合我们(我们刚刚看过上面的例子),那么应该使用函数表达式。

总结

函数是值。它们可以在代码的任何地方被分配,复制或声明。 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。 函数表达式在执行流程到达时创建。

在大多数情况下,当我们需要声明一个函数时,最好使用函数声明,因为函数在被声明之前也是可见的。这使我们在代码组织方面更具灵活性,通常也会使得代码可读性更高。 所以,仅当函数声明不适合对应的任务时,才应使用函数表达式。在本章中,我们已经看到了几个例子,以后还会看到更多的例子。

作用域中可以使用的变量

全局作用域只能使用全局作用域中声明的变量(包括函数)

函数作用域不仅能使用自身作用域中声明的变量(包括函数),还能使用外部环境的变量(包括函数)

有的时候,某个函数比较复杂,在编写的过程,可能需要另外一些函数来辅助它完成一些功能,而这些函数仅仅会被该函数使用,不会在其他位置使用,则可以将这些函数声明到该函数的内部。

函数内部声明的变量和外部冲突时,使用内部的。

闭包

闭包(closure),是一种现象,内部函数,可以使用外部函数环境中的变量。 此处需要看的文章

闭包的产生的条件: 1.外部函数和内部函数,外包函数里面声明变量,内部函数里面使用这个变量。

最典型的应用:


function outer() {
        let a = 1;
        function f(){
            a++;
            console.log(a);
        }
        return f;
 }
 
 var method = outer();
 method();

闭包可以简单的理解为上面的函数f和变量a,函数f访问变量a就叫闭包。 闭包右两个重要的特点: 1.可以访问函数的内部变量。一般来说只有全局的变量才能在外部进行访问。定义在函数内部的变量,正常是不能访问的到,可以通过闭包去访问到。

2.让这些变量始终保持在内存中,而不是说当前函数调用完,变量就没了。有的时候我们需要在外部(函数外部)去调这些函数,而函数调用完之后的结果我们希望能保存起来,第一种方法我们其实可以在函数外部声明一个全局变量。而这里的闭包虽然是个局部变量,也可以达到同样的效果,而且避免了对全局对象的变量污染。 上面的方法每次执行完都会将a的值存取来。

值得提醒的是这里不论是否有没有return f;这句都是闭包。

如何在对象中定义属性和方法?

<script>
        var obj = {
            a: "asfd",
            b: 234,
            c: function() {
                console.log("ccccc");
            },
            d: function() {
                console.log("dddd");
            }
            myMethod() { 
             console.log("eee");
            }
        };

        // console.log(obj.c, typeof obj.c);
        
        obj.text = "这是一段测试文字";
        console.log(obj.text);
        
        obj["abc"] = "这是第二段测试文字";
        console.log(obj.abc);
        console.log(obj["abc"]);
        
         var e = "ddd";
         obj[e] = "haha";
         console.log(obj.ddd);//可以正常访问
         console.log(obj.e);//不能正常访问
    
        var b = obj.c;
        b();
        obj.d();
  </script>
    

下面先讲解属性的定义的 属性的定义首先可以分为两种: 1.在对象内部定义 内部定义就是 属性名 :属性值,这里注意两点,第一属性名前面不需要加 var,这里是定义属性而不是定义变量 第二 定义的属性的之间用 ,号隔开。

2.在对象外部定义,比方说上面的obj.text,在oc中一般要先定义,JS语言比较灵活,可以直接使用,js语言帮忙定义。

此外除了还可以使用上面的["属性名"],访问可以使用 对象.属性名 或者 对象["属性名"] 来访问。

3.还可以用变量名作为属性名

继续看对象里面方法的定义

a.最常规的方式是在对象里面使用下面的方式来定义

 c: function() {
                console.log("ccccc");
            },

b.ES6及以后版本中,你可以使用更短的语法,可以使用下面的语法来定义

myMethod() { 
             console.log("eee");
            }

c.使用箭头函数,写法如下

myMethod: () => { // 方法体 }

函数表达式和this

函数表达式

JS中,函数也是一个数据,语法上,函数可以用于任何需要数据的地方

JS中,函数是一等公民

函数是一个引用类型,将其赋值给某个变量时,变量中保存的是函数的地址

this关键字

this无法被赋值

  1. 在全局作用域中,this关键字固定指向全局对象。(就是script 最外层的作用域)
  2. 在函数作用域中,取决于函数是如何被调用的
    1. 函数直接调用,this指向全局对象
    2. 通过一个对象的属性调用,格式为对象.属性()对象["属性"](),this指向对象
    3. 构造函数,后面通过 new 构造函数创建对象,构造函数里面的this指向生成的新对象。

按照目前自己的理解,this就分两种情况,函数里面和对象中,函数中就指向的是window,通过对象里面是指向当前所在的对象。

下面这句是最核心的。

哪个对象调用函数,函数里面的 this 指向哪个对象this只有在调用到的时候才能确定指向,不能在定义的位置和时间确定

this指向示例1

<script>
        function test(){
            console.log(this);
        }

        var b = test;
        //直接调用函数,this 指向全局对象。
        b();
        //在函数外,this指向全局对象。
        console.log(this);
    </script>

this指向示例2

function test(){
            console.log(this);
        }

        var b = test;
        var arr = [b, test];
        arr[0]();

上面this指向arr对象。如果直接test() 调用,this指向window.

this指向示例3

var obj = {
            a: function() {
                console.log(this);
            },
            b: {
                x: 2342,
                func: function() {
                    console.log(this);
                }
            }
        };

        obj.a();  this 指向obj
        var b = obj.a;
        b(); this 指向window

        obj.b.func(); this 指向b

说到底,一切的关键是谁在调用,this就指向谁。

构造函数

对象中的属性,如果是一个函数,也称该属性为对象的方法

普通创建对象的方法

 // 创建一个用户
        function createUser(name, age, gender) {
            return {
                // 下面是语法糖简写形式,真正应该写的话是 name:name,age:age,gender:gender,
                name,
                age,
                gender,
                sayHello() {
                    console.log(`我叫${this.name},年龄${this.age}岁,性别${this.gender}`);
                }
            };
        }

        var u1 = createUser("张三", 18, "男");
        u1.sayHello();
        

上面的代码需要注意一个问题就是当创建的对象的属性 name 和传入的形参的名字一致的时候,对象中可以将 name = name, 简写成 name,.里面的 sayHello() 也是正确的写法

用于创建对象的函数

用函数创建对象,可以减少繁琐的对象创建流程

  1. 函数返回一个对象
  2. 构造函数:构造函数专门用于创建对象
new 函数名(参数);

如果使用上面的格式创建对象,则该函数叫做构造函数。

  1. 函数名使用大驼峰命名法
  2. 构造函数内部,会自动创建一个新对象,this指向新创建的对象,并且自动返回新对象
  3. 构造函数中如果出现返回值,如果返回的是原始类型,则直接忽略;如果返回的是引用类型,则使用返回的结果
  4. 所有的对象,最终都是通过构造函数创建的

下面是实际的一个例子

 // 创建一个用户
        function User(name, age, gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
            this.sayHello = function() {
                console.log(`我叫${this.name},年龄${this.age}岁,性别${this.gender}`);
            }
        }

        var u1 = new User("张三", 18, "男");
        u1.sayHello();

需要注意的是,如果上面对象的创建不是通过 new User("张三", 18, "男"); 来创建的,比方说是通过User("张三", 18, "男");来创建,先注意的一点是这里创建不了,如果想要正常创建的话,上面的对象,需要 return 一个对象出来,由于这里是构造函数,自动帮忙return一个对象出来了。

对于Array来说,


        // arr = [3, 5, 7, 2]; // 语法糖,这里的写法其实相当于上面的写法。

对于Object来说,

 //相当于
        // var obj = new Object();
        // obj.name = "asdf";
        // obj.age = 234;
        // obj.gender = "男";
        
// var obj = {
        //     name: "asdf",
        //     age: 234,
        //     gender: "男"
        // }; 语法糖,这里的写法其实相当于上面的写法。

new.target

该表达式在函数中使用,返回的是当前的构造函数,但是,如果该函数不是通过new调用的,则返回undefined

通常用于判断某个函数是否是通过new在调用。

上面的知识可以有一个用处,可以做到用户是否使用 new User 都返回一个对象出来。写法可以参照下面

 // 创建一个用户
        function User(name, age, gender) {

            var temp = function() {
                console.log(`我叫${this.name},年龄${this.age}岁,性别${this.gender}`);
            };

            if (new.target === User) {
                //正常的构造函数调用
                this.name = name;
                this.age = age;
                this.gender = gender;
                this.sayHello = temp;
            } else {
                return {
                    name,
                    age,
                    gender,
                    sayHello: temp
                }
            }
        }

        var u1 = new User("ss", 18, "女");

函数的本质

函数的本质就是对象。

某些教程中,将构造函数称之为构造器 所有的对象都是通过关键字new出来的,new 构造函数()

所有的函数,都是通过new Function创建。

举一个例子

 // function sum(a, b) {
        //     return a + b;
        // }
        
        下面的写法其实就相当于上面的写法。
        var sum = new Function("a", "b", "return a+b");
        console.log(typeof sum);
        console.log(sum(3, 5));

Function

由于函数本身就是对象,因此函数中,可以拥有各种属性。 也可以动态的添加各种方法和属性。

  function User(firstName, lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.fullName = this.firstName + " " + this.lastName;
        }
        User.abc = 123;
        User.test = function(){
            console.log("test")
        }

        User.test();

        console.log(User.abc);

包装类

JS为了增强原始类型的功能,为boolean、string、number分别创建了一个构造函数:

  1. Boolean
  2. String
  3. Number

如果语法上,将原始类型当作对象使用时(一般是在使用属性时),JS会自动在该位置利用对应的构造函数,创建对象来访问原始类型的属性。

类:在JS中,可以认为,类就是构造函数

成员属性(方法)、实例属性(方法):表示该属性是通过构造函数创建的对象调用的。 静态属性(方法)、类属性(方法):表示该属性是通过构造函数本身调用的。