• Javascript笔记(七)之函数、闭包、生成器、箭头函数


    一、函数(function)

    1、函数声明与调用

    1.1、函数的调用

    函数是定义一次但可以多次调用或执行任意多次的一段JavaScript代码。函数可以有参数,可以没有参数。
    参数:函数声明时候的参数,叫做形式参数,函数调用时候,传入的参数叫做实际参数;

    1.2、函数的声明的三种形式;

    函数的声明形式有如下几种,使用function关键字声明或者使用Function构造函数来声明;

    // 形式一
    function 函数名([形式参数列表]){
    // 函数体
    }
    // 形式二(命名函数表达式)
    var 函数名 = function ([形式参数列表]){
    // 函数体
    }
    var 函数名1 = function 函数名2 ([形式参数列表]){
    // 函数体
    }
    // 形式三: 使用Function构造函数(不推荐使用,函数体字符串可能会阻止JavaScript引擎优化)
    new Function (arg1, arg2, ... argN, functionBody)
    var 函数名1 = new Function (arg1, arg2, ... argN, functionBody);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    以上形式只是声明,并不会执行,只有调用的时候才会执行;

    1.3、函数的调用

    函数的调用非常简单,在JavaScript中,有一个特性,函数调用可以在函数的定义之前,这种行为被称做"声明提前",这里只作用于形式一创建的函数,通过变量引用匿名函数的形式可以避免此种行为;
    函数调用的形式如下:函数名 ([实参列表]);

    foo(); // return 'foo'
    bar(); // 会导致错误;TypeError: bar is not a function
    function foo(){
        return 'foo';
    }
    var bar = function(){
        return 'bar';
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2、函数返回值return

    任何函数都可以使用return语句来返回函数的执行结果,如果没有显示使用return 去返回一个值话,函数的返回值为undefined
    return有两个作用:

    1. 向调用处返回函数的执行结果;
    2. 终止函数向下继续执行;
    var a = foo();   // 接收函数返回值;
    console.log(a); // 控制台输出函数返回值;
    // 这种情况和return ;语句返回的结果都一样,是undefined
    function foo(){
       console.log('11');
    }
    /**执行结果
    11
    undefined
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3、arguments对象(关键字)

    ECMAScript函数不介意传递多少参数,也不会因为函数不统一而出错,实际上函数题可以通过arguments对象来接收传递进来的参数;该对象只在函数内部起作用,并且永远指向该函数调用者传入的所有参数

    3.1、arguments对象
    3.1、arguments对象简介(兼容IE6)

    arguments对象可以看作数组(类似Array,但是不是一个Array),我们可以使用下标的形式来访问参数内容,使用arguments.length属性可以取得,传入参数的长度,但是除此之外没其他Array属性;

    function foo(){
       console.log(arguments.length);
       console.log(typeof arguments);
       console.log(Object.prototype.toString.call(arguments).replace(/^\[object\s|\]$/g, ''));
       for(var key in arguments){
        console.log(key);
       }
    }
    foo(1,2,3,4);
    /**
    4
    object
    Arguments
    0
    1
    2
    3
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们可以使用如下方法将arguments对象转化为数组(如下内容,我们将讲到具体到对象到时候在详细讲解);

    var args = Array.prototype.slice.call(arguments);
    var args = [].slice.call(arguments);
    
    // ES2015(ES6)
    const args = Array.from(arguments);
    var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    3.2、arguments对象的属性

    arguments对象提供有如下几种属性;

    属性含义是否废弃兼容性
    arguments.callee指向当前执行的函数兼容IE6
    arguments.caller指向调用当前函数的函数兼容IE6
    arguments.length指向传递给当前函数的参数数量兼容IE6
    arguments.[@@iterator]返回一个新的Array迭代器对象,该对象包含参数中每个索引的值兼容IE6

    注意:

    1. 在严格模式下,arguments对象已与过往不同, arguments.[@@iterator]不再与函数的实际形参之间共享,同时caller属性也被移除;
    2. 在严格模式下,ES5禁止使用arguments.callee(),当一个函数必须调用自身的时候,避免使用arguments.callee()的时候,要么使用给函数表达式一个名字,要么使用一个函数声明;

    既然如此,那么我们就详细讲解一下arguments.callee属性(重点掌握)和arguments.[@@iterator](了解);

    1、为什么需要arguments.callee属性呢?
    早期JavaScript不允许使用命名函数表达式,处于这样的原因,不能创建一个递归函数表达式;

    // 如下语法是可以的
    function factorial (n) {
        return !(n > 1) ? 1 : factorial(n - 1) * n;
    }
    // 但是如果我们使用匿名函数的形式,我们改怎么递归呢?
    function (n) {
        return !(n > 1) ? 1 : /* what goes here? */ (n - 1) * n;
    }
     // 于是,为了解决这个问题,引入`arguments.callee`属性,然后我们就可以使用如下的方法来递归
    function (n) {
        return !(n > 1) ? 1 :arguments.callee(n - 1) * n;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    同时引入arguments.callee也可以消除递归调用这种紧密耦合第现象,但是这样的解决方案,也带来了一些问题;
    1、arguments是个很昂贵的操作,它是一个很大的对象,每次对贵调用都需要重新创建,影响现代浏览器的性能,还会影响过闭包
    2、arugments.callee递归调用会获取到一个不同的this值;

    // 在浏览器中全局对象为window对象,而在node中不是,node没有window对象,node之中,顶层对象为global对象;
    // 形式一:使用arguments.callee
    var global = this;
    console.log(global);
    var sillyFunction = function (recursed) {
        if (!recursed) { return arguments.callee(true); }
        if (this !== global) {
            console.log(this);
            console.log("This is: " + this);
        } else {
            console.log("This is the global");
        }
    }
    sillyFunction(); //window对象,Arguments对象
    
    // 形式二:不使用arugments.callee
    var global = this;
    
    console.log(global);
    var sillyFunction = function (recursed) {
        if (!recursed) { return sillyFunction(true); }
        if (this !== global) {
            console.log(this);
            console.log("This is: " + this);
        } else {
            console.log("This is the global");
        }
    }
    
    sillyFunction();/window对象,"This is the global"
    
    // 形式三:如果采用一下方式调用arguments.callee,则this为global对象
    var foo = function (bar) {
        var tempFun;
        if (!bar) { 
            tempFun=arguments.callee;
            return tempFun(true);
        }
        console.log(this);      //输出global对象
    }
    foo();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    3.2、reset参数(剩余参数)

    事实上,这个reset参数类似与Java之中的可变参数的思想,它实际上就是一个数组。JavaScript本身就可以接受无限多个参数,但是我们如果需要指定从第三个参数开始的参数,使用arguments就非常不合理,我们需要从下标2的位置开始,于是我们引入了reset参数.

    // 语法形式如下
    function resetArguments(...reset){
        console.log(Object.prototype.toString.call(reset).replace(/^\[object\s|\]$/g, ''));
    }
    resetArguments(1,2,3,4); // Array
    
    // 形式一:
    function resetArguments1(a,b,...reset){
        for(var value of reset){
            console.log(value,);
        }
    }
    resetArguments1(1,2,3,4,5); // 3,4,5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    3.3、参数默认值(Default function parameters)
    3.3.1、ES6之前默认值

    有时候,我们需要给函数的入参设定默认值的时候,例如分页参数pageNo,pageSize等参数;在ES6语法标准出现之前,如果我们要实现参数默认值,不能直接把默认值写在参数的位置,必须采用如下的方式

    // 形式一
    function testDefaultArguments1(pageSize,pageNo){
        pageSize = pageSize||1;
        pageNo = pageNo || 5;
        console.log(pageSize,pageNo);
    }
    
    // 形式二
    function testDefaultArguments2(pageSize,pageNo){
        pageSize = typeof pageSize === "undefined"?1:pageSize;
        pageNo = typeof pageNo === "undefined"?5:pageNo;
        console.log(pageSize,pageNo);
    }
    
    // 形式三:由于null、undefined、NaN等都会被转换false
    function testDefaultArguments3(pageSize,pageNo){
        pageSize = pageSize?pageSize:1;
        pageNo = pageNo?pageNo:5;
        console.log(pageSize,pageNo);
    }
    
    // 形式四:使用arguments对象
    function testDefaultArguments4(){
        pageSize = typeof arguments[0]!=="undefined"?arguments[0]:1;
        pageNo = typeof arguments[1]!=="undefined"?arguments[1]:5;
        console.log(pageSize,pageNo);
    }
    
    
    testDefaultArguments1(); // 1 , 5
    testDefaultArguments2(); // 1 , 5
    testDefaultArguments3(); // 1 , 5
    testDefaultArguments4(); // 1 , 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    3.3.2、ES6之后参数默认值(ES6语法,不兼容IE,兼容Edeg 14)

    在ES6之前,函数的参数是没有默认值设置的,或者说如果不传,其默认值为undefined;
    语法形式如下

    function [name]([param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]]) {
       statements
    }
    
    function testDefaultArguments(pageSize=5,pageNo=1){
        console.log(pageSize,pageNo);
    }
    testDefaultArguments(); // 5 , 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这种形式是最简单等形式之一,我们还可以用解构赋值等形式来获取函数参数,

    3.4、函数重载(网易面试曾问过)

    JavaScript没有函数重载的语法,如果你直接用其他语言的重载形式,不会提示语法错误,但是旧的会被新的覆盖,那么我们又要求采用重载等形式来写代码呢?我们可以使用arguments对象进行模拟重载,我们可以采用如下几种形式来模拟函数重载;
    重载的最重要的关注点就是函数参数的个数、参数的类型不同;

    示例一:这种形式如果功能复杂代码量较大,不利于维护和复用性;

    
    
    • 1

    5、作用域与let

    5.1、作用域与声明提前

    在其他语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称这种作用域叫做块级作用域(block scope),然而在早期的JavaScript之中没有块级作用域,取而代之的是函数作用域(function scope),变量在声明它们的函数体内以及函数体嵌套的任意函数体内都是具有定义的;
    此外,在函数外声明的变量为global对象的属性,在浏览器中,global对象就是window对象;

    5.2、let关键字(ES6语法)
    5.2.1、let的语法简介

    由于早期的JavaScript中没有块级作用域,所以在ES6的时候,引入了let关键字,该关键字的作用就是声明一个块级作用域的本地变量,并且可选的将其初始化为一个值;
    let语句的语法形式如下:

    let var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]];
    
    • 1

    范例一:作用域规则;
    范例二:简化内部函数代码;
    范例三:使用let可以进行封装私有属性,而不是使用闭包来创建私有接口;

    推荐使用let取代var,两者的语义相同,而且let没有副作用;

    5.2.2、let的暂存死区与错误
    5.2.3、letvar的区别

    1、作用域不同
    let允许你声明一个作用域被限制在块级中的变量,语句或表达式;
    var声明的变量只能是全局或整个函数块的;
    在程序或函数的顶层,let不会像var一样在全局对象上创建一个属性;
    2、

    5.3、作用域链

    每段JavaScript代码都有一个与之相关联的作用域链(scope chain),这个作用域链是一个对象列表或链表,这组对象定义了这段代码作用域链中的变量,当JavaScript查找变量的时候(这个搓成叫做变量解析,variable resolution),它会从链中的第一个对象开始查找,如果找到则直接使用这个属性的值,如果没有找到,则继续向下查找,依次类推,如果作用域上任何一个对象都没有这个属性,那么就会认为这段代码在作用域上不存在,就会抛出一个引用错误(ReferenceError)异常;

    在JavaScript中最顶层的代码的作用域是由一个全局对象开始,在不包含嵌套的函数体中,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个就是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象;当定一个函数的时候,它实际上保存一个作用域链,当调用该函数时,它会创建一个新的对象来存储它的局部变量,并将这个对象添加到作用域链上,同时创建一个更长的表示函数调用作用域的链;

    理解作用域链,对于理解with语句和闭包是很有帮助的;

    6、常量const(ES6语法)

    6.1、const的语法简介

    在ES6之前,我们没有使用常量的语法,即使是常量,我们也只能使用var来定义,但是这样依然会被别人修改,我们没有一个很好常量机制,于是ES6之中引入了const关键字;
    使用const声明一个只读的常量,一旦声明,常量的值就不会改变。
    其语法如下:

    const 常量名 = 值;
    
    • 1

    此时如果你试图改变常量的值,将会抛出TypeError;

    const PI = 3.1415;
    PI // 3.1415
    
    PI = 3;
    // TypeError: Assignment to constant variable.
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果你声明常量的时候,不初始化,将报错,抛出SyntaxError

    const foo;
    //SyntaxError: Missing initializer in const declaration
    
    • 1
    • 2

    const的作用域与let命令相同,只在声明所在的块级作用域内有效;
    const声明的常量也不能提升,存在暂时性死区,只能在声明后使用,否则抛出错误ReferenceError

    并且const声明的常量,也与let一样不可重复声明;

    6.2、const的本质
    6.3、constlet的建议

    letconst之间,建议优先使用const,尤其在全局环境,不应该设置变量,应该设置常量;
    const比较符合函数时编程思想,运算不改变值,只是新建值,而且这样有利于将来的分布式运算;
    使用const有利于提高程序的运行效率;

    7、function.name属性(ES6语法)

    在ES5非标准模式的,该属性不可配置,但是ES6可以配置;
    function.name属性返回函数声明的名称;

    function doSomething() { }
    doSomething.name;  // "doSomething" 
    // 构造函数形式创建的函数名
    (new Function).name; // "anonymous"
    
    // ES6新增:推断函数名称
    // 变量和方法可以从句法位置推断匿名函数代名称
    var f = function() {};
    var object = {
      someMethod: function() {}
    };
    
    console.log(f.name); // "f"
    console.log(object.someMethod.name); // "someMethod"
    
    var object = {
      someMethod: function object_someMethod() {}
    };
    
    console.log(object.someMethod.name); // "object_someMethod"
    try { object_someMethod } catch(e) { alert(e); }
    // ReferenceError: object_someMethod is not defined
    
    // 绑定函数的名称,function.bind()所创建的函数将会在函数的名称前加上“bound”;
    function foo() {}; 
    foo.bind({}).name; // "bound foo"
    
    // getters和setters的函数名:通过get和set防蚊器来存取属性的时候,"get"或"set"会出现在函数名称前
    // 关于setters和getters,我将放到JavaScript面向对象来讲解
    var o = { 
      get foo(){}, 
      set foo(x){} 
    }; 
    
    var descriptor = Object.getOwnPropertyDescriptor(o, "foo"); 
    descriptor.get.name; // "get foo" 
    descriptor.set.name; // "set foo";
    
    // 在ES6的语法之中,Symbol可以作为函数名称,那么方法的名字就是方括号加Symbol入参的字符串
    var sym1 = Symbol("foo"); 
    var sym2 = Symbol(); 
    var o = { 
      [sym1]: function(){}, 
      [sym2]: function(){} 
    }; 
    
    o[sym1].name; // "[foo]"
    o[sym2].name; // ""
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    该属性为只读属性,你不能直接更改它,但是你可以使用Object.defineProperty()来修改;

    由于类的创建可以使用functionclass关键字,我们获取类名称的时候,采用的obj.constructor.name来获取名称;

    function Foo() {}  // ES2015 Syntax: class Foo {}
    
    var fooInstance = new Foo();
    console.log(fooInstance.constructor.name); // logs "Foo"
    
    • 1
    • 2
    • 3
    • 4

    注意
    1、只有函数没有名为name的属性才会设置function.name。ES2015规定关键字static修饰的静态方法也被认为是类的属性,因此无法获取具有方法属性name()的几乎任何类的类名称;

    class Foo {
      constructor() {}
      static name() {}
    }
    
    var fooInstance = new Foo();
    console.log(fooInstance.constructor.name);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因此,你们依赖内置的Function.name属性来保持一个类的名称;

    2、使用JavaScript压缩工具的时候,应当注意这些工具有可能在构建时候更改函数的名称,导致你的程序出现并非所预期的结果;

    8、递归、尾调用与尾递归

    10、IIFE (Immediately Invokable Function Expressions)

    (function() {
        statements
    })();
    
    • 1
    • 2
    • 3

    大家先熟悉这种语法,我们在提到前端模块化与浏览器兼容的时候会再一次的提起这个东西;

    二、解构赋值(ES6语法)

    1、什么是解构赋值

    ES6允许按照一定模式,从数组或对象中取值,对变量进行赋值,这种被称为解构(Destructuring)。

    解构赋值是对赋值运算符的扩展。是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。在代码书写上简洁且易读,语义更加清晰明了,也方便了复杂对象中数据字段获取。

    2、解构赋值的使用语法

    2.1、模式匹配

    语法形式

    let [a,b,c] = [1,2,3];
    
    • 1
    2.2、不完全解构
    let [a=1,b]= []
    
    • 1

    不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组

    2.3、剩余运算符
    let [a, ...b] = [1,2,3]
    
    • 1
    2.4、对对象的解构赋值
    let person = { name: 'zhangsan', age: 20, sex: '男'};
    let {name, age, sex} = person;
    
    • 1
    • 2

    3、解构赋值可以使用过默认值

    let {a = 10, b = 5} = {a: 3};
    // a = 3; b = 5;
    
    
    let {c: aa = 10, d: bb = 5} = {c: 3};
    // aa = 3; bb = 5;
    
    
    var { message: msg = 'Something went wrong' } = {};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    三、闭包作用域

    四、生成器函数(generator)function*

    生成器函数是一种特殊类型的函数,它返回一个生成器对象,这个对象可以用来实现可迭代对象,生成器函数和普通函数的区别在于它使用特殊的语法来控制生成器对象的输出,从而实现按需生成值序列,避免一次性生成大量的值,减少内存的使用。

    function* generatorFunction(){
    	// 函数体
    }
    
    • 1
    • 2
    • 3

    在生成器函数中,我门需要使用 yield 语句产生一个值,从而将值传递给相应的迭代器对象。当执行到 yield 语句时,函数的状态将被保存,等待下一次迭代时继续执行。当函数执行结束时,生成器对象将不再产生值。

    function* generateSequence() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    const generator = generateSequence();
    
    console.log(generator.next().value); // 1
    console.log(generator.next().value); // 2
    console.log(generator.next().value); // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    五、箭头函数 Arrow Functions

    5.1、匿名函数与回调函数

    5.2、=>箭头函数(ES6语法)

    在函数式编程方法论的影响之下,ES6新增箭头函数的语法来简化匿名函数的写法。在有箭头函数以前,我们编写匿名函数过于复杂;

    function(){}
    
    • 1
    var person={
    	age:10,
    	getAget: ()->{
    		return age;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    箭头函数的好处:
    1、关于this作用域:箭头函数的this是定义函数的时候绑定,而不是执行函数的时候绑定的,实际原因是因为箭头函数跟没有没有自己的this

    5.3、双冒号运算符::(ES6语法)

    该运算自动将左边的对象,作为上下文环境,绑定到右边的函数上。使用它我们减少call、apply、bind的方法使用。

    例如

    run.bind(cat);
    cat::run;
    
    • 1
    • 2

    六、高阶函数

    6.1、map/reduce

    6.1.1、map
    6.1.2、reduce

    6.2、filter

    6.3、sort

    七、function.bind()

    注:部分代码引用自MDN文档

  • 相关阅读:
    python 注解, 装饰器@ 详解
    【CUDA】算子编译错误ATen/ATen.h: No such file or directory
    uniapp无感刷新token实现过程
    浅析JVM invokedynamic指令和Java Lambda语法|得物技术
    Python爬虫案例入门教程(纯小白向)——夜读书屋小说
    linux设置应用开机自启(通用:mysql、jar、nginx、solr...)
    海屯心理平台搭建系统模式
    C++设计模式-原型(Prototype)
    es elasticsearch 九 索引index 定制分词器 type结构后期弃用原因 定制动态映射 动态映射模板 零停机重建索引
    【es5】数据类型&语法整理
  • 原文地址:https://blog.csdn.net/fj_Author/article/details/82935330