JavaScript 忍者秘籍笔记——函数是根基

第三章 函数是根基

函数的独特之处

函数是第一型(first-class)对象

对象在 javascript 中有如下功能:

  • 可以通过字面量进行创建。
  • 可以赋值给变量、数组或其他对象的属性。
  • 可以作为参数传递给函数。
  • 可以作为函数的返回值进行返回。
  • 可以拥有动态创建并赋值的属性。

在 javascript 中,函数拥有全部这些功能。除了可以像其它对象类型一样使用外,函数还可以被调用。这些调用通常以异步方式进行。

浏览器的事件轮询

桌面应用程序(GUI)大多采用如下方式:

  1. 创建用户界面。
  2. 进入轮询,等待事件触发。
  3. 调用事件的处理程序(监听器[listener])。

浏览器编程唯一的不同就是:代码不负责事件轮询和事件派发,而是浏览器处理。

我们的职责是为浏览器中发生的各种时间建立事件的处理程序(handler)。这些事件在触发时被放置在一个事件队列(先进先出列表[FIFO])中,然后浏览器将调用已经为这些事件建立好的处理程序(handler)。因为这些事件发生的时间和顺序都是不可预知的,所以事件处理函数的调用也是异步的。

浏览器的事件轮询是单线程的。每个事件都是按照在队列中所放置的顺序来处理的。这就是所谓的FIFO(先进先出)列表,或者一个使用古老定时器的筒仓(silo)。每个事件都在自己的生命周期内进行处理,所有其他事件必须等到这个事件处理结束后才能继续处理。执行过程如图所示:

浏览器在单线程中处理事件轮询,并处理每个事件自身的简化视图

浏览器把事件放到队列上的机制是在事件轮询模型之外。确定事件何时发生并把它们放到事件队列上的过程所处的线程,并不参与事件本身的处理。

回调的概念

定义一个函数,以便其它一些代码在适当的时候调用它。

函数声明

声明一个函数时,该名称在整个函数声明范围内时有效的。此外,如果一个命名函数声明在顶层,window 对象上的同名属性则会引用到该函数。

所有的函数都有一个 name 属性,该属性保存的是该函数名称的字符串。没有名称的函数也仍然有 name 属性,只是该属性值为空字符串

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>证明函数声明相关内容</title>
    <style>
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }

        // 声明一个命名函数,该名称在当前作用域有效,并隐式在window上添加一个同名属性
        function isNimble(){return true;}
        // 判断window属性是确定的
        assert(typeof window.isNimble === "function", "isNimble() defined");
        // 判断函数的 name 属性
        assert(isNimble.name === "isNimble", "isNimble() has a name");

        // 创建一个匿名函数,并赋值给canFly变量
        var canFly = function(){return true;};
        assert(typeof window.canFly === "function", "canFly() defined");
        assert(canFly.name === "", "canFly() has no name");

        // 创建一个匿名函数并引用到window的一个属性上
        window.isDeadly = function(){return true;};
        assert(typeof window.isDeadly === "function", "isDeadly() defined");

        // 在outer函数内定义一个inner函数,测试该inner()在其定义之前和之后都可以访问到,并且没有创建全局的inner()
        function outer(){
            assert(typeof inner === "function", "inner() in scope before declaration");
            function inner(){}
            assert(typeof inner === "function", "inner() in scope after declaration");
            assert(window.inner === undefined, "inner() not in global scope");
        }

        // outer()可以在全局作用域内访问到,而inner()则不可以
        outer();
        assert(window.inner === undefined, "inner() still not in global scope");

        // 这里声明的函数名无效,真正起到控制作用的是变量名
        window.wieldsSword = function swingsSword(){return true;};
        assert(window.wieldsSword.name === 'swingsSword', "wieldsSword's real name is swingsSword");
    </script>
</body>
</html>

作用域和函数

在 JavaScript 中,作用域是由 function 进行声明的,而不是代码块。声明的作用域创建于代码块,但不是终结于代码块。

  • 变量声明的作用域开始于声明的地方,结束于所在函数的结尾,于代码嵌套无关。
  • 命名函数的作用域是指声明该函数的整个函数范围,于代码嵌套无关。(机制提升)
  • 对于作用域声明,全局上下文就像一个包含页面所有代码的超大型函数。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>作用域断言测试</title>
    <style>
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }

        assert(true,"|------BEFORE OUTER ------|")
        assert(typeof outer == 'function', "outer() is in scope");
        assert(typeof inner === 'function', "inner() is scope");
        assert(typeof a === 'number', "a is in scope");
        assert(typeof b === 'number', "b is in scope");
        assert(typeof c === 'number', "c is in scope");
        assert(typeof d === 'number', "d is not in scope");
        assert(typeof e === 'number', "e is not in scope");

        function outer(){
            assert(true,"|------ INSIDE OUTER,BEFORE a ------|")
            assert(typeof outer == 'function', "outer() is in scope");
            assert(typeof inner === 'function', "inner() is scope");
            assert(typeof a === 'number', "a is in scope");
            assert(typeof b === 'number', "b is in scope");
            assert(typeof c === 'number', "c is in scope");
            assert(typeof d === 'number', "d is not in scope");
            assert(typeof e === 'number', "e is not in scope");

            var a = 1;

            assert(true,"|------ INSIDE OUTER,AFTER a ------|")
            assert(typeof outer == 'function', "outer() is in scope");
            assert(typeof inner === 'function', "inner() is scope");
            assert(typeof a === 'number', "a is in scope");
            assert(typeof b === 'number', "b is in scope");
            assert(typeof c === 'number', "c is in scope");
            assert(typeof d === 'number', "d is not in scope");
            assert(typeof e === 'number', "e is not in scope");

            function inner(){/******/}
            var b = 2;

            assert(true,"|------ INSIDE OUTER,AFTER inner() AND b ------|")
            assert(typeof outer == 'function', "outer() is in scope");
            assert(typeof inner === 'function', "inner() is scope");
            assert(typeof a === 'number', "a is in scope");
            assert(typeof b === 'number', "b is in scope");
            assert(typeof c === 'number', "c is in scope");
            assert(typeof d === 'number', "d is not in scope");
            assert(typeof e === 'number', "e is not in scope");

            if(a == 1){
                assert(true,"|------ INSIDE OUTER,INSIDE if AND INSIDE if ------|")
                assert(typeof outer == 'function', "outer() is in scope");
                assert(typeof inner === 'function', "inner() is scope");
                assert(typeof a === 'number', "a is in scope");
                assert(typeof b === 'number', "b is in scope");
                assert(typeof c === 'number', "c is in scope"); // 这里c 被初始化为undefined,所有断言会失败
                var c = 3;
                // 不能再let const 声明之前调用 assert 会报错
                let d=4; // 添加 ES6 的let
                const e=5; // 添加 ES6 的const

                assert(true,"|------ INSIDE OUTER,INSIDE if AND AFTER let const ------|")
                assert(typeof outer == 'function', "outer() is in scope");
                assert(typeof inner === 'function', "inner() is scope");
                assert(typeof a === 'number', "a is in scope");
                assert(typeof b === 'number', "b is in scope");
                assert(typeof c === 'number', "c is in scope");
                assert(typeof d === 'number', "d is not in scope");
                assert(typeof e === 'number', "e is not in scope");
            }

            assert(true,"|------ INSIDE OUTER,OUTSIDE if ------|")
            assert(typeof outer == 'function', "outer() is in scope");
            assert(typeof inner === 'function', "inner() is scope");
            assert(typeof a === 'number', "a is in scope");
            assert(typeof b === 'number', "b is in scope");
            assert(typeof c === 'number', "c is in scope");
            assert(typeof d === 'number', "d is not in scope");
            assert(typeof e === 'number', "e is not in scope");
        }
        outer();
        assert(true,"|------ INSIDE OUTER,OUTSIDE if ------|")
        assert(typeof outer == 'function', "outer() is in scope");
        assert(typeof inner === 'function', "inner() is scope");
        assert(typeof a === 'number', "a is in scope");
        assert(typeof b === 'number', "b is in scope");
        assert(typeof c === 'number', "c is in scope");
        assert(typeof d === 'number', "d is not in scope");
        assert(typeof e === 'number', "e is not in scope");
    </script>
</body>
</html>

函数调用

有四种不同的方式进行函数调用,每种方式都有细微的差别(主要区别在于如何定义每种调用类型的this):

  • 作为一个函数进行调用,是最简单的形式。
  • 作为一个方法进行调用,在对象上进行调用,支持面向对象编程。
  • 作为构造器进行调用,创建一个新对象。
  • 通过 apply()call()方法进行调用。

从参数到函数形参

  • 如果实际传递的参数数量大于函数声明的形参数量,超出的参数不会配给形参。
  • 如果声明的形参数量大于实际传递的参数数量,没有对应参数的形参会赋值为undefined。

所有的函数调用都会传递两个隐式参数:argumentsthis

  • arguments:是传递给函数的所有参数的一个集合,该集合有一个 length 属性。arguments 不是JS的数组,不能使用数组方法。
  • thisthis参数引用了与该函数调用进行隐式关联的一个对象,被称为函数上下文。它依赖于函数的调用方式,因此,this又称为调用上下文。

下面的代码中展示了作为函数调用和作为方法调用的区别:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>函数调用和方法调用的区别</title>
    <style>
        /*定义结果样式*/
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <!--显示测试结果-->
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }
        
        function creep(){return this;}
        assert(creep() === window, "Creeping in the window");

        var sneak = creep; // 创建变量引用creep
        assert(sneak() === window, "Sneaking in the window");

        var ninja1 = {
            skulk: creep // 创建属性引用creep
        };
        assert(ninja1.skulk() === ninja1, "The 1st ninja is skulking");

        var ninja2 = {
            skulk: creep
        };
        assert(ninja2.skulk() === ninja2, "The 2nd ninja is skulking");
    </script>
</body>
</html>

将函数作为构造器(constructor)进行调用,要在函数调用前使用 new 关键字。构造器调用时,会发生如下特殊行为:

  • 创建一个新的空对象。
  • 传递给构造器的对象时 this 参数,从而成为构造器的函数上下文。
  • 如果没有显示的返回值,新创建的对象则作为构造器的返回值进行返回。

构造器的目的是创建一个新对象并对其进行设置,然后将其作为构造器的返回值进行返回。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用构造器设置通用对象</title>
    <style>
        /*定义结果样式*/
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <!--显示测试结果-->
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }

        // 声明一个构造器,在函数上下文对象上创建一个skulk属性。该属性方法又返回了上下文自身
        function Ninja(){
            this.skulk = function(){return this;};
        }

        var ninja1 = new Ninja();
        var ninja2 = new Ninja();

        assert(ninja1.skulk() === ninja1, "The 1st ninja is skulking");
        assert(ninja2.skulk() === ninja2, "The 1st ninja is skulking");
    </script>
</body>
</html>

函数和方法的命名通常以动词开头,来描述它们所做的事情,并以小写字母开头。而构造器的命名通常是由一个描述所构造对象的名词来命名,并以大写字母开头。

JavaScript 的每个函数都有 apply()call()方法,使用任何一个方法,都可以显式指定任何一个对象作为其函数上下文。

函数作为第一型对象,可以向其它任何类型的对象一样,拥有属性和方法。

  • apply():接收两个参数,一个是作为函数上下文的对象,另一个是作为函数参数所组成的数组。例如:f.apply(o,[1,2,3]);
  • call():接收的参数包括作为函数上下文的对象和一个参数列表而不是单个数组。例如:f.call(o,1,2,3);
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用apply()和call()指定函数上下文</title>
    <style>
        /*定义结果样式*/
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <!--显示测试结果-->
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }
        
      function juggle(){
          var result = 0;
          for(var n=0; n<arguments.length; n++){
              result += arguments[n];
          }
          this.result = result; // 在上下文保存结果
      }

          var ninja1 = {};
          var ninja2 = {};
          
          juggle.apply(ninja1,[1,2,3,4]);
          juggle.call(ninja2,5,6,7,8);
          
          assert(ninja1.result === 10, "juggled via apply");
          assert(ninja2.result === 26, "juggled via call");
    </script>
</body>
</html>

函数式编程和命令式编程的区别在于思维层面:函数式程序的构建块而不是命令式语句。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>构建for-each函数演示函数上下文功能</title>
    <style>
        /*定义结果样式*/
        li.pass{color: green;}
        li.fail{color: red;}
    </style>
</head>
<body>
    <!--显示测试结果-->
    <ul id="results"></ul>

    <script>
        // 定义assert方法
        function assert(value,desc){
            var li = document.createElement('li');
            li.className = value ? "pass" : "fail";
            li.appendChild(document.createTextNode(desc));
            document.getElementById("results").appendChild(li);
        }
        
      function forEach(list,callback){
          for(var n=0; n<list.length; n++){
              callback.call(list[n],n);
          }
      }

      // 创建测试对象
      var weapons = ['shuriken','katana','nunchuncks'];

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

推荐阅读更多精彩内容