04-函数

函数

JavaScript语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处.
JavaScript中有三种函数类型:函数声明,函数表达式和Function构造函数创建的函数。

  • 函数声明(FD)
    函数声明: function 函数名称 (参数:可选){ 函数体 }
    1. 有特定的名字
    2. 在进入上下文阶段创建
    3. 影响变量对象(在变量对象里)
    4. 可以在全局环境或函数体内声明(不能在表达式或一个代码块中声明)
    5. 可以在定义之前调用函数
    // 直接在全局上下文中
    function foo() {
      // 或者在一个函数的函数体内
      function innerFD() {}
    }
  • 函数表达式(FE)
    函数表达式:function 函数名称(可选)(参数:可选){ 函数体 }
    1. 名称可选
    2. 在代码执行阶段创建
    3. 不影响变量对象(不进入变量对象)
    4. 总处于表达式的位置
    5. 在定义之前不可调用,在定义阶段之后也不调用,只能在代码执行阶段可以调用
      如果没有函数名,肯定是函数表达式。
      如果有函数名,根据上下文来判断:
      如果函数在全局环境或函数体内,就是函数声明;如果是表达式的一部分,就是函数表达式。
      函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号
    function foo(){} // 函数声明,在全局环境中声明
    var bar = function foo(){}; // 函数表达式,是赋值表达式的一部分
    new function bar(){}; // 函数表达式,因为它是new运算符的一部分
    (function(){
       function bar(){} // 函数声明,在函数体中声明
    })();
    //圆括号、逗号、非等操作符可以将函数转换成函数表达式
    var foo = function _foo() {}; // 赋值运算符
    (function foo() {});// 圆括号(分组运算符),
    [function bar() {}];// 在数组初始化器内
    1, function baz() {};// 逗号运算符
    !function () {}(); // 逻辑运算符

函数表达式在在定义之前不可用,定义阶段之后也不可用,因为他不在变量对象中

    console.log(foo); // foo is not defined
    (function foo() {});
    console.log(foo);  // foo is not defined

函数表达式不存在变量对象中,可以结合能影响变量对象的变量(初始化为undefined),对其进行访问。

    console.log(foo); // undefined
    var foo = (function foo() {});
    console.log(foo);  // function foo() {}

采用函数表达式声明函数时,在function命令加上函数名称为命名函数表达式,该函数名只在函数体内有效,在函数体外无效。
这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。

    (function foo(bar) {
      if (bar) {
        console.log(1);
        return;
      }
      foo(true); // 1 可以调用
    })();
    // 在外部,不可以调用
    foo(); // foo is not defined
  • Function构造函数
    可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,其他参数都是函数的参数,如果只有一个参数,该参数就是函数体。Function构造函数可以不使用new命令,返回结果完全一样。
    var add = new Function(
      'x',
      'y',
      'return x + y'
    );
    // 等同于
    function add(x, y) {
      return x + y;
    }
  • 函数作用域
    作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
    在函数外部声明的变量就是全局变量,它可以在函数内部读取。
    在函数内部定义的变量,外部无法读取,称为“局部变量”。
    对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明(if、for等),一律都是全局变量。
    函数本身也是一个值,也有自己的作用域(保存在内部属性[[scope]]中),它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
// 函数在全局环境声明,所以它的作用域绑定全局作用域
    var a = 1;
    var x = function () {
      console.log(a);
    };
    function f() {
      var a = 2;
      x();
    }
    f() // 1
// x.[[scope]] = globalContext.VO
// 函数x在调用时:x.scope = [AO,x.[[scope]]] = [AO,globalContext.VO]
// 查找变量a,x.AO中没有a,从作用域链中查找,在globalContext.VO找到a=1

// 函数在另一个函数内部声明,它的作用域绑定外层函数的作用域
    function foo() {
      var x = 1;
      function bar() {
        console.log(x);
      }
      return bar;
    }
    var x = 2;
    var f = foo();
    f() // 1
// foo在全局环境创建:foo.[[scope]] = globalContext.VO
// 函数bar在函数foo内部创建:bar.[[scope]] = foo.scope
// foo调用时:foo.scope = [AO,foo.[[scope]]] = [AO,globalContext.VO]
// bar调用时:bar.scope = [AO,bar.[[scope]]] = [AO,foo.AO,globalContext.VO]
// bar内部代码执行,查找变量x,bar.AO中没有x,从作用域链找查找,在foo.AO中找x=1;
// 这种一个函数访问了另一个函数内的变量的机制就是闭包。
  • 函数参数
    函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递,在函数体内修改参数值,不会影响到函数外部。
    var p = 2;
    function f(p) {
      p = 3;
    }
    f(p);
    p // 2

函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(传入函数的原始值的地址),在函数内部修改参数,将会影响到原始值。
如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

    var obj = {p: 1};
    function f(o) {
      o.p = 2;
    }
    f(obj);
    obj.p // 2
// 对o的修改都会反映在obj身上。
// 但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj。
    var obj = [1, 2, 3];
    function f(o){
      o = [4, 5, 6]; // 将传入的引用指向另外的值
    }
    f(obj);
    console.log(obj); // [1, 2, 3]
// 在函数内部,形式参数o与实际参数obj存在一个赋值关系:o = obj
// 此时obj的引用就复制一份传递给o,对o的修改会反映到obj上
// 当对o重新赋值时,o的引用就修改了,不再指向obj,不会影响obj

某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。

    var a = 1;
    function f(p) {
      window[p] = 2;
    }
    f('a');
    a // 2
// 变量a本来是按值传递,但是写成window对象的属性,就达到了传址传递的效果。

如果有同名的参数,则取最后出现的那个值。

  • arguments对象
    arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
    arguments对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)。
    虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
    可以通过apply方法,把arguments作为参数传进去,这样就可以让arguments使用数组方法了。
    // 用于apply方法
    myfunction.apply(obj, arguments).
    // 使用与另一个数组合并
    Array.prototype.concat.apply([1,2,3], arguments)

将arguments转为真正的数组。

    var args = Array.prototype.slice.call(arguments);

arguments对象带有一个callee属性,返回它所对应的原函数。可以通过arguments.callee,达到调用函数自身的目的。

  • 立即调用函数表达式(IIFE)
    在Javascript中,一对圆括号()是一种运算符,跟在函数名之后,表示调用该函数。
    有时,我们需要在定义函数之后,立即调用该函数。这时,不能在函数的定义之后直接加上圆括号,这会产生语法错误。
    因为,function这个关键字即可以当作语句,也可以当作表达式。
    "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。
    为了避免解析上的歧义,JavaScript引擎规定,如果function关键字出现在行首,一律解释成语句。
    因此,JavaScript引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。
    // 语句
    function f() {}
    // 表达式
    var f = function f() {}
    // 函数定义后直接加(),会报错
    function(){ /* code */ }();// SyntaxError: Unexpected token (
    // 在函数定义后直接加(),再中()加如表达式,不会报错,但不会立即执行
    function foo() {
        console.log(1)
    }(1)
    // 因为这相当于在函数声明后,又声明了一个毫不相干的表达式,并不是调用函数

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”IIFE。

    (function(){ /* code */ }());
    // 或者
    (function(){ /* code */ })();
    //注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个IIFE,可能就会报错。

任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,new关键字也能达到这个效果。
通常情况下,只对匿名函数使用这种“立即执行函数表达式”。
它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是模仿块级作用域,可以封装一些外部无法读取的私有变量。

  • 闭包
    由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
    闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
// 利用闭包读取函数内部的变量
    var foo = {};
    (function init() {
        var x = 10;
        foo.bar = function () { //对外接口foo.bar
            console.log(++x);
        };
    })();
    foo.bar(); // 11
    foo.bar(); // 12
    console.log(x); // x is not defined 外部无法直接访问
    // 第一次调用foo.bar()时:
    // foo.bar().Scope: [AO, initContext.AO]
    // 查找x,在initContext.AO中找到x=10,前缀自增后,x=11,再输出
    // 第二次调用foo.bar()时,查找x=11,前缀自增后,x=12,再输出
    // 初始化函数表达式的名称(init)可省略。

    // 利用闭包保存状态
    var data = [];
    for (var i = 0; i < 3; i++) {
        data[i] = function () {
            console.log(i);
        }
    }
    data[0](); // 3
    data[1](); // 3
    data[2](); // 3
    // 修改后
    var data = [];
    for (var i = 0; i < 3; i++) {
        data[i] = (function (num) {
            return function() {
                console.log(num);
            };
        })(i);
    }
    data[0](); // 0
    data[1](); // 1
    data[2](); // 2
    //data[0]()执行时:data[0]Context.Scope: [AO, 匿名函数Context.AO,globalContext.VO]
    //data[0]Context.AO中没有num值,沿着作用域链从匿名函数Context.AO中查找,找到num=0

使用立即调用函数表达式来模仿块级作用域,创建私有变量和私有方法,使用闭包创建共有变量和方法。

    (function() {
        // 私有变量
        var age = 26;
        var name = 'Leo';
        // 私有方法
        function getAge() {
            return 'your age is' + age;
        }
        // 共有方法
        function getName() {
            return'your name is' + name;
        }
// 将引用保存在外部执行环境的变量中,形成闭包,防止该执行环境被垃圾回收
        window.getAge = getAge;
    })();

jQuery中模块与闭包

    // 使用函数自执行的方式创建模块
    (function(window, undefined) {
        // 声明jQuery构造函数
         var jQuery = function(name) {
            // 主动在构造函数中,返回一个jQuery实例
             return new jQuery.fn.init(name);
         }
        // 添加原型方法
         jQuery.prototype = jQuery.fn = {
             constructor: jQuery,
             init:function() { ... },
             css: function() { ... }
         }
         jQuery.fn.init.prototype = jQuery.fn;
        // 将jQuery改名为$,并将引用保存在window上,形成闭包,对外开发jQuery构造函数,这样就可以访问所有挂载在jQuery原型上的方法了
         window.jQuery = window.$ = jQuery;
     })(window);
    // 在使用时,我们直接执行了构造函数,因为在jQuery的构造函数中通过一些手段,返回的是jQuery的实例,所以我们就不用再每次用的时候在自己new了
    $('#div1');

《JavaScript高级程序设计》
《JavaScript 标准参考教程》
汤姆大叔-深入理解JavaScript系列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容